January 2007 - Posts
It almost seems like a cruel joke. Innumerable WPF samples indicate that you can implement databinding in a particular way. However, doing so breaks designer support and you have to rearrange code to reflect the reality of current limitations with Cider. Let me explain what I mean.
When you create XAML markup you are declaratively creating objects with XML. Normally, the XAML tags will create controls like textboxes and labels, etc. But there is nothing to stop you from instantiating model objects in XAML. So, instead of writing
Person p = new Person();
in your code behind, you can instantiate a person like this
<Window.Resources>
<local:Person x:Key="myDataSource" Name="Joe"/>
or like this
<Window.Resources>
<ObjectDataProvider x:Key="myDataSource" ObjectType="{x:Type local:Person}" />
The XML namespaces (local and x above) have to be registered at the top of your file like this:
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:YourProjectNamespace"
You can then databind to the person object like this:
<TextBlock Text="{Binding Source={StaticResource myDataSource}, Path=Name}"/>
Make sure you put the Window.Resource element above the TextBlock element otherwise your datasource won't exist at the time you are databinding and you will get a null reference exception. Finally, can run the sample and it will execute perfectly. But don't venture into the designer! If you do, you get a nasty error in the error list:
Assembly '' was not found. The 'clr-namespace' URI refers to an assembly that is not referenced by the project.
There is no designer support for declaratively created model object in the current release that I know of. To get designer support for these types of pages, rip out the dynamically created object and change the codebehind to instantiate and object explicitly and bind up the object to the context.
public Window1()
{
InitializeComponent();
Parent p = new Parent();
this.DataContext = p;
}
I spent the better part of a day downloading and setting up the
new Orcas in anticipation of playing with .NET 3.0 and 3.5 (Greenbits). Unless you want to play with Entity Framework in its "Raw" format (no visual designer) stay away from this download - its problems are
detailed here. The bottom line is its not configured to do .NET 3.0 development. I just don't really see any reason why this CTP release was even attempted. Let's hope the February CTP is a lot better.
Next week I start a new project for one of my clients. Its a premier project for the company that will be using the latest .NET 3.0 technologies: Windows Presentation Foundation, Windows Workflow Foundation and Windows Communication Foundataion. The best part of it is that the team will be comprised of a small number (6) of incredibly smart developers - some of the best in the region. Architecturally speaking, the most interesting piece will be designing and implementing the Enterprise Service Bus.
Stay tuned throughout the year as I share through my blog any insights that I gain - at least anything that's not proprietary.
WCF doesn't always serialize custom collections of objects the way that you would like. For example, a Family may have a collection of Members.
public class SerializeObjectModel
{
[DataContract(Name = "Family")]
public class Family
{
[DataMember(Name = "Members")]
public FamilyMembers MembersList = new FamilyMembers();
}
public class FamilyMembers : List<string>
{
}
}If you load up the list with some data and serialize it through WCF (WCF uses the DataContractSerializer by default to serialize objects so you can simulate serialization in a test harness by calling the class directly)
Family f = new Family();
f.MembersList.Add("A");
f.MembersList.Add("B");
f.MembersList.Add("C");
f.MembersList.Add("D");
...
DataContractSerializer ser = new DataContractSerializer(typeof(Family));
ser.WriteObject(cwriter, f);
It will serialize like this:
<Family xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schema/
s.datacontract.org/2004/07/ObjectModel">
<Members xmlns:d2p1="http://schemas.microsoft.com/2003/10/Serialization/Arrays
">
<d2p1:string>A</d2p1:string>
<d2p1:string>B</d2p1:string>
<d2p1:string>C</d2p1:string>
<d2p1:string>D</d2p1:string>
</Members>
</Family>
That's fine. Now you add some additional members to the FamilyMembers class, maybe a list of family members that have been deleted fromt the list (granny passed away, RIP).
public class FamilyMembers : List <string>
{
//DKF: added this list
public List<string> DeletedList = new List<string>();
You run it through the WCF Serializer
Family f = new Family();
f.MembersList.Add("A");
...
//WANT TO GET THIS SERIALIZED TOO!
f.MembersList.DeletedList.Add("Dave Foderick");and you don't see the deleted list! It looks exactly like the previous output. If the deleted list never gets to the other end of the wire then they never get removed from the database.
To get the list to serialize the way we would like, we have to get control of how WCF serialized it. This post is the golden nugget that describes the precedence of how lists are serialized. You see that what is serializing our list is #6 which apparently iterates through the collection and spits out the members, ignoring anything else that may be of interest to us on the list iteself. The only hook into the serialization process that has higher prececedence and will allow us the flexibility (CollectionDataContractAttribute won't do!) is IXmlSerializable. So modify the collection class to implement that interface.
public class FamilyMembers : List<string> , IXmlSerializable
{
//DKF: added this list
public List<string> DeletedList = new List<string>();
#region IXmlSerializable Members
public System.Xml.Schema.XmlSchema GetSchema()
{
return null;
}
public void ReadXml(XmlReader reader)
{
throw new Exception("The method or operation is not implemented.");
}
public void WriteXml(XmlWriter writer)
{
//First, serialize all the items in the base list
writer.WriteStartElement("Items");
foreach (string item in this)
{
writer.WriteElementString("Member", item);
}
writer.WriteEndElement();
//now, serialize all the items in the deleted list
writer.WriteStartElement("DeletedItems");
foreach (string deletedItem in DeletedList)
{
writer.WriteElementString("Member", deletedItem);
}
writer.WriteEndElement();
}
More code to write for sure but victory is ours!
<Family xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schema
s.datacontract.org/2004/07/ObjectModel">
<Members>
<Items>
<Member>A</Member>
<Member>B</Member>
<Member>C</Member>
<Member>D</Member>
</Items>
<DeletedItems>
<Member>Dave Foderick</Member>
</DeletedItems>
</Members>
</Family>
To get this working right in a real app, you have to write the deserialization logic too. Here is a more realistic implementation (TDto is a generic parameter for a custom class that replaces string in the above samples).
#region IXmlSerializable Members
///
/// Get xml schema
/// In this case, we don't have any
///
///
public System.Xml.Schema.XmlSchema GetSchema()
{
return null;
}
private DataContractSerializer GetSerializer()
{
Type dtoType = typeof(TDto);
DataContractSerializer ser = new DataContractSerializer(dtoType);
return ser;
}
///
/// Deserialize the object
///
///
public void ReadXml(XmlReader reader)
{
DataContractSerializer ser = GetSerializer();
//read past the first element. This is the outermost element for the list
reader.ReadStartElement();
//read in the undeleted objects
ReadXmlObjects(reader, "Items", this);
//now read in the deleted list
ReadXmlObjects(reader, "DeletedItems", this._deletedList);
}
private string PrefixedElementName(XmlReader reader, string elementName)
{
string retval = reader.Prefix;
if (reader.Prefix.Length > 0)
{
retval += ":";
}
retval += elementName;
return retval;
}
///
/// read objects into list
///
///
///
///
private void ReadXmlObjects(XmlReader reader,string elementName, IList list)
{
DataContractSerializer ser = GetSerializer();
Type dtoType = typeof(TDto);
string prefixedCollection = PrefixedElementName(reader,elementName);
if (reader.IsStartElement(prefixedCollection))
{
reader.ReadStartElement();
//TODO: very tricky!
//element name must match classname. Is this always true???
//another strategy is to embed a count of the number of object to
//read inside the xml. Then this code can be a foreach
string prefixedClassName = PrefixedElementName(reader,dtoType.Name);
while (reader.IsStartElement(prefixedClassName))
{
TDto dto = (TDto)ser.ReadObject(reader);
list.Add(dto);
}
if (reader.IsStartElement() == false)
{
reader.ReadEndElement();
}
}
}
///
/// Serialize the object
///
///
public void WriteXml(System.Xml.XmlWriter writer)
{
//This is the WCF default Serializer
DataContractSerializer ser = new DataContractSerializer(typeof(TDto));
//First, serialize all the items in the base list
writer.WriteStartElement("Items");
foreach (TDto item in this)
{
ser.WriteObject(writer, item);
}
writer.WriteEndElement();
//ser.WriteObject(writer, this.Items);
//now, serialize all the items in the deleted list
writer.WriteStartElement("DeletedItems");
foreach (TDto deletedItem in _deletedList)
{
ser.WriteObject(writer, deletedItem);
}
//ser.WriteObject(writer, _deletedList);
writer.WriteEndElement();
}
#endregion
Download my test harness with sample code.
Many people (including myself) have had difficulties getting NHibernate to work with ASP.NET 2.0. The usual problem centers around how to get NHibernate to read in the configuration files for your entites and the problem is exacerbated by ASP.NET 2.0's new dynamic compilation model. I can tell you that it is possible to make it work and here is how I did it.
In ASP.NET 1.1 you could specify that configuration files, such as hbm.xml files, could be embedded resources which meant that they would get baked into the single dll produced from building your web site. You could let NHibernate know about all your entities by simply pointing it to the single dll
Configuration cfg = new Configuration();
cfg.AddAssembly("MySite.dll");
This technique also works in 2.0 if you use the Web Application Project compilation model because it works like 1.1 and builds a single dll and allows you to embed resources into that dll. But it won't work if you use the default Web Site Project compilation model or Express. Both of the latter use a dynamic compilation model where you can just copy .cs to the web site and it automatically compiles. And, as far as I know, there is no way to embed the xml configuration files into the resultant dll.
So to make it work in 2.0 here is the most direct solution. First, put all your hbm.xml files into the same directory on the server. This could be App_Code or App_Data or whevever you keep your entity classes (which should be somewhere under App_Code) or anywhere else on the web site. I put them in App_Code\Resources. Then, configure NHibernate with the AddXmlFile method giving it each and every configuration file that you have, like this:
Configuration cfg = new Configuration();
string basePath = System.Web.HttpContext.Current.Server.MapPath(@"~/App_Code/Resources/");
cfg.AddXmlFile(basePath+"Customer.hbm.xml");
cfg.AddXmlFile(basePath+"Order.hbm.xml");
Note that you have to add a line for each configuration file, which kinda sucks when you have to add more hbm.xml files to your project.
The last trick to get this working is to fix up the hbm.xml files themselves. Had you have been building a site to one specific dll (Web Project compilation, VS2003) then all your types live in that dll and it is easy to tell the xml file where the type lives. With dynamic compilation in ASP.NET 2.0 where does your entity type live? Well, if you put your entity class anywhere under the Add_Code directory it will be compiled into the App_Code.dll. Therefore, just make sure you place your entity classes somewhere under the App_Code directory and then modify your hbm.xml files to reference your entity types in App_Code.dll. Here is what my hbm.xml file starts with.
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.0">
<class name="Domain.Portfolio, App_Code" table="PortfolioHead">
...
Note that this is for NHibernate 1.0. Your mapping version may need to be something different. Use whatever is appropriate for your version of NHibernate. NHibernate and ASP.NET are now one with each other.
This topic was discussed on the NHibernate forum.