Click here to Skip to main content
15,867,290 members
Articles / Programming Languages / C#
Article

Simple databinding with XML - A GUI for configuration files

Rate me:
Please Sign up or sign in to vote.
4.81/5 (29 votes)
2 Feb 20049 min read 129.5K   1.3K   78   22
This article is about using simple databinding on XML documents by using strongly typed XML documents generated at runtime, taking advantage of System.Reflection.Emit and MSIL.

Sample Image - xmlbindingmanager.gif

Contents

Introduction

With the introduction of the .NET platform, Microsoft advices us developers to use XML files to configure our applications. Personally, I prefer this approach over the Windows registry or database as it perfectly supports the XCopy deployment. However there is one serious drawback: if you mess up the XML file - by let's say a missing or invalid closing tag - your whole application will stop working because of an XML parser exception. To avoid such mistakes - and of course enhance the configuration experience for the end user - you should provide a graphical interface.

Windows forms come with the built-in feature of simple databinding, allowing you to bind properties of objects to your input controls so you don't have to code these endless assignments between controls and objects any longer. This is achieved by a BindingManager (derived from BindingManagerBase) with two implementations coming out of the box: CurrencyManager (which is used for complex binding and binds ILists, IListSources and IBindingLists) and PropertyManager (which is used for simple binding and binds object properties). None of those is suitable to bind a configuration file to form controls.

And then there is the famous XmlDataDocument class to synchronize XML documents with datasets (which can be bound by the CurrencyManager) but creating an instance of a XmlDataDocument only works with kind of "tabular" XML.

This article intends to:

  • explain and show the approach of generating objects at runtime to wrap XML files in objects (XmlWrapper)
  • provide a component to enable databinding with the newly generated objects (XmlBindingManager)
  • provide design time support so you can actually use it in a plug & play fashion (PropertyExplorer)

This article does not intend to implement an XML editor where you can add or delete nodes. It is thought of as a convenient way to edit configuration data.

How to read the code

To help you understand the code (especially the scope of methods and variables), here are the relevant coding conventions I follow:

  • private fields start with a "_" prefix followed by lowercase
  • private methods start with lowercase
  • public or protected methods and properties (there are no non-private fields) start with uppercase

For clarity's sake, I omitted helper functions in this article where I thought that the implementation is rather trivial and the method name tells the story anyway (which a method name should always do ;-)). You can look them up in the source code though.

Design alternatives

There are two possible ways to achieve an automated databinding between WinForms controls and XML elements and attributes - at least these are the two that came to my mind:

  1. Implementing your own BindingManager deriving from BindingManagerBase
  2. Reusing the PropertyManager with a strongly typed XmlDocument

We will follow the second alternative - mainly because I think that we could use a strongly typed XmlDocument in a variety of scenarios while the use of a custom BindingManager would be very limited.

A strongly typed XmlDocument

By strongly typed XmlDocument, I refer to an object (representing the root node of the XML) which exposes all attributes and elements through properties. Let me just illustrate this by the following example:

Sample app.config (or web.config)

XML
<configuration>
    <appSettings>
        <add key="SomeSetting" value="This is the value of SomeSetting" />
    </appSettings>
</configuration>

A strongly typed XmlDocument should expose the value for "SomeSetting" by this code:

C#
Console.WriteLine(Configuration.AppSettings.SomeSetting.Value);

This means we will have to define types at runtime to wrap the underlying XML.

Creating classes on-the-fly

There are two common ways to create types at runtime:

  1. Using the System.CodeDom namespace
  2. Emitting MSIL code

As advanced as it sounds to write MSIL code, I still prefer this option as using CodeDom is a very verbose activity (have you ever tried to write a type like this?). Also, I figured it won't be that much MSIL anyway.

The XmlWrapper

Accessing the values of elements and attributes of an XML document is actually straight forward when using XPath syntax. I won't go into detail about XPath and its possibilities as I assume you have at least heard of it.

As the XmlWrapper will wrap an XmlDocument (actually it wraps an XmlNode to allow partially wrapping), we start out with an overloaded constructor:

C#
public XmlWrapper(XmlNode node)
{
    _node = node;
    Initialize();
}

To avoid writing too much MSIL and to be able to change the access implementation in the rather intuitive C# environment, we then write a generic getter and setter method which both takes an XPath expression as an argument and have to be public because they are called by the generated object properties:

C#
public string GetValue(string xpath) 
{
    XmlNode setting = _node.SelectSingleNode(xpath);
    return (setting == null) ? string.Empty : setting.InnerText; 
}


public void SetValue(string xpath, string value) 
{
    XmlNode setting = _node.SelectSingleNode(xpath);
    setting.InnerText = value;
}

The next step is to actually parse the XML and create the classes. Obviously, we will need to apply a recursive algorithm to step through the child nodes. We start out with initializing the assembly, defining a namespace (which will be the namespace of the XmlWrapper) and so on:

C#
public void Initialize() 
{
    AppDomain domain = Thread.GetDomain();
    _assemblyName = new AssemblyName();
    _assemblyName.Name = _node.LocalName;

    _assemblyBuilder = domain.DefineDynamicAssembly(_assemblyName, 
                                   AssemblyBuilderAccess.RunAndSave);
    _moduleBuilder = _assemblyBuilder.DefineDynamicModule(_assemblyName.Name 
                                   + "Module", _assemblyName.Name + ".dll");

    _namespace = GetType().Namespace;
    _getCall = GetType().GetMethod("GetValue");
    _setCall = GetType().GetMethod("SetValue");

    parseNode(_node);
}

At the end, we call the parseNode method with the root node of the XML. The parsing obeys to the following rules:

  • if the NodeType is NodeType.Text, we have reached a leaf (there are no more child elements or attributes)
  • for every other NodeType, a new type within the current type will be defined and a property for the new type will be added to the current type
  • each generated object property will be initialized with a new instance to avoid NullReferenceExceptions
  • each generated string property will call the provided getter/setter with an individual XPath expression
  • each generated class gets a private field _bindingManager - which will hold the instance of the XmlWrapper - and a constructor taking the XmlWrapper as an argument

For those of you who are interested in the MSIL code, I put some comments next to the statements:

C#
private void parseNode(XmlNode root) 
{
    string typeName = getTypeName(root, "");
    TypeBuilder typeBuilder = _moduleBuilder.DefineType(typeName, 
                                            TypeAttributes.Public);
    FieldBuilder mgrField = typeBuilder.DefineField("_bindingManager", 
                                 GetType(), FieldAttributes.Private);
    ConstructorBuilder cb = 
        typeBuilder.DefineConstructor(MethodAttributes.Public, 
        CallingConventions.Standard, new Type[] {GetType()});

    ILGenerator ilCtor = cb.GetILGenerator();
    //put yourself on the stack
    ilCtor.Emit(OpCodes.Ldarg_0);
    //define next statement as first argument of constructor
    ilCtor.Emit(OpCodes.Ldarg_1);
    //store the value of _bindingManager as first argument on the stack
    ilCtor.Emit(OpCodes.Stfld, mgrField);

    ArrayList childFields = parse(root, typeBuilder);
    initializeFields(childFields, ilCtor);

    ilCtor.Emit(OpCodes.Ret);                //end constructor

    Type type = typeBuilder.CreateType();

    _boundObject = Activator.CreateInstance(type, new object[] {this});
}

We first get the name for this new type including the correct (nested) namespace. Then we get an instance of TypeBuilder which will emit the new type, define a field _bindingManager to store a reference to the XmlWrapper and define the constructor. We then enter the recursive loop which will parse the node. Finally, we initialize all non string fields, create the type and store an instance of it in the private field _boundObject which represents the strongly typed XmlDocument.

As you can see, the ILGenerator for the constructor is finished only after the complete parsing and a call to initializeFields(). The reason is that we want to initialize all object properties with instances. The intitializeFields() method takes therefore a reference to the constructor builder as an argument.

C#
private void initializeFields(ArrayList childFields, ILGenerator ilCtor)
{
    foreach(FieldBuilder field in childFields) 
    {
        ConstructorInfo ctor = 
          field.FieldType.GetConstructor(new Type[] {GetType()});
        //put yourself on the stack
        ilCtor.Emit(OpCodes.Ldarg_0);
        //define next statement as value for the field
        ilCtor.Emit(OpCodes.Ldarg_1);
        //get a new instance using the constructor taking an XmlWrapper
        ilCtor.Emit(OpCodes.Newobj, ctor);
        //store instance in the field
        ilCtor.Emit(OpCodes.Stfld, field);
    }
}

This MSIL would read as the following C# statement:

C#
_someSetting = new SomeSetting(this);

In the recursive loop we follow the same pattern, defining a new type for every node and initializing the fields with new instances.

C#
private ArrayList parse(XmlNode parentNode, TypeBuilder parentType) 
{
    ArrayList fields = new ArrayList();

    foreach(XmlNode node in parentNode.ChildNodes) 
    {
        if(node.NodeType == XmlNodeType.Text) return null;

        string typeName = getTypeName(node, parentType.FullName);

        if(_moduleBuilder.GetType(typeName) != null)
            throw new InvalidOperationException(typeName + 
            "already exists. You have probably messed up the TagSettingList!");

        TypeBuilder typeBuilder = _moduleBuilder.DefineType(typeName, 
                                                TypeAttributes.Public);
        FieldBuilder mgrField = 
          typeBuilder.DefineField("_bindingManager", 
          GetType(), FieldAttributes.Private);
        
        ConstructorBuilder cb = 
          typeBuilder.DefineConstructor(MethodAttributes.Public, 
          CallingConventions.Standard, new Type[] {GetType()});
        ILGenerator ilCtor = cb.GetILGenerator();
        ilCtor.Emit(OpCodes.Ldarg_0);
        ilCtor.Emit(OpCodes.Ldarg_1);
        ilCtor.Emit(OpCodes.Stfld, mgrField);

        foreach(XmlAttribute attribute in node.Attributes) 
        {
            string xpath = buildXPathForProperty(node, attribute);
            string propertyName = getClsCompliantName(attribute.Name);
            buildProperty(typeBuilder, propertyName, mgrField, xpath);
        }

        if(node.HasChildNodes) 
        {
            ArrayList childFields = parse(node, typeBuilder);
            if(childFields == null) 
                buildProperty(typeBuilder, "Text", 
                        mgrField, getXPathPath(node));
            else 
                initializeFields(childFields, ilCtor);
        }

        ilCtor.Emit(OpCodes.Ret);

        Type type = typeBuilder.CreateType();
        fields.Add(buildChildTypeProperty(type, parentType));
    }

    return fields;
}

The interesting parts here are buildXPathForProperty() and buildProperty() which emit the call to the getter/setter with the defined XPath. While buildXPathForProperty() is rather straight forward, buildProperty() deserves a closer look. When defining a property, you actually define get/set methods (which is usually taken care of by the C# compiler). We follow the conventions of the compiler using "get_PropertyName" and "set_PropertyName" but you could name them any way you like. We also apply the method attributes and special name by convention but again this is not mandatory for making this code work. After defining those two methods, we assign them as the get method and set method of the property.

C#
private void buildProperty(TypeBuilder typeBuilder, 
       string propertyName, FieldBuilder mgrField, string xpath)
{
    PropertyBuilder pb = typeBuilder.DefineProperty(propertyName, 
                    PropertyAttributes.None, typeof(string), null);
    MethodBuilder getMethod = typeBuilder.DefineMethod("get_" + propertyName, 
                    MethodAttributes.Public | MethodAttributes.HideBySig | 
                    MethodAttributes.SpecialName, 
                    typeof(string), null);
    MethodBuilder setMethod = typeBuilder.DefineMethod("set_" + propertyName, 
                    MethodAttributes.Public | MethodAttributes.HideBySig | 
                    MethodAttributes.SpecialName, 
                    null, new Type[] {typeof(string)});
    
    ILGenerator ilGetMethod = getMethod.GetILGenerator();
    ilGetMethod.Emit(OpCodes.Ldarg_0);
    //use the value of _bindingManager (=XmlWrapper instance)
    ilGetMethod.Emit(OpCodes.Ldfld, mgrField);
    //load the defined xpath on the stack
    ilGetMethod.Emit(OpCodes.Ldstr, xpath);
    //invoke the GetValue method on XmlWrapper
    ilGetMethod.Emit(OpCodes.Call, _getCall);
    ilGetMethod.Emit(OpCodes.Ret);

    ILGenerator ilSetMethod = setMethod.GetILGenerator();
    ilSetMethod.Emit(OpCodes.Ldarg_0);
    //use the value of _bindingManager (=XmlWrapper instance)
    ilSetMethod.Emit(OpCodes.Ldfld, mgrField);
    //load the defined xpath on the stack as the first ar
    ilSetMethod.Emit(OpCodes.Ldstr, xpath);
    //load the new value on the stack as the second argument
    ilSetMethod.Emit(OpCodes.Ldarg_1);
    //invoke the SetValue method on XmlWrapper
    ilSetMethod.Emit(OpCodes.Call, _setCall);
    ilSetMethod.Emit(OpCodes.Ret);

    pb.SetGetMethod(getMethod);
    pb.SetSetMethod(setMethod);
}

Additionally, the XmlWrapper offers some methods which encapsulate some reflection code. It is not really necessary, however, I would like to have reflection code rather hidden in a component than in a form or user control.

The XmlBindingManager

While the XmlWrapper encapsulates the underlying XmlNode, the XmlBindingManager provides the actual binding functionality. It does so by extending the properties of other controls by a XmlBinding property which maps a property of the control (usually the Text property) with a property of the XmlWrapper bound object.

As there are a lot of articles about design time support, I won't go into detail here. When using the XmlBindingManager at design time, you have to provide an XML file to bind to. However, as we just have seen that the underlying data source is actually an XmlNode, you could use this as well when implementing this component by code.

When parsing the XML node, the XmlWrapper will clash when finding two nodes with the same name within the same parent node because it would then try to generate two properties with the same name. As this will most likely happen when binding to an app.config file (<add key="SomeSetting" value="SomeValue" /><add key="AnotherSetting" value="AnotherValue" />), we will have to supply a resolver (although we could find a more sophisticated algorithm of generating property names). In this case, the resolver is simply a Hashtable (strongly typed for containing a key/tag pair). We will define by this that when finding, e.g. a <add ...> tag, the generator should use the key property for building the property name. The XmlBindingManager provides this through the TagSettings property.

The implementation of the XmlBindingManager is rather trivial as it only delegates to the parent BindingContext. All we have to do once we added the component to a form or user control is call the Initialize() and DataBind() methods. The Initialize() method sets up the XmlWrapper while the DataBind() method adds DataBinding to all the bound controls.

C#
public void Initialize() 
{
    _wrapper = new XmlWrapper();
    _xmlDoc = new XmlDocument();
    _xmlDoc.Load(_filename);
    
    if(_nodeName == null || _nodeName == "")
        _wrapper.Node = _xmlDoc.DocumentElement;
    else
        _wrapper.Node = _xmlDoc.SelectSingleNode("//" + _nodeName);

    if(_wrapper.Node == null)
      throw new ArgumentException(string.Format("{0} is not a valid node", 
      _nodeName));

    foreach(TagSetting setting in _tagSettings) 
        _wrapper.KeyMapping.Add(setting.XmlTag, setting);

    _wrapper.Initialize();
}

public void DataBind() 
{
    if(!DesignMode) 
    {
        foreach(XmlBinding binding in _bindingCollection.Values) 
        {
            binding.Control.DataBindings.Clear();
            binding.Control.DataBindings.Add(binding.BoundProperty, 
                         _wrapper.BoundObject, binding.XmlProperty);
        }
    }
}

The PropertyExplorer

Last thing to do is to offer some design time support by means of the PropertyExplorer which makes it a two-click solution to bind any control to the underlying data source. It is a simple treeview control showing all properties of the generated wrapper class. It will show up when you want to set the XmlProperty of the XmlDataBinding setting in the property grid. It is implemented as a drop-down designer but there is commented code to implement it as a popup designer if you choose so.

If you have any problems using the design time features of these components, try to unload all add-ins of the Visual Studio. It took me some time to find out why the TagSettings has not been serialized correctly into code just to find out that my version of CodeSmith obviously was occasionally disturbing. Once I had unloaded it, everything worked just fine.

Using the code

Just compile the source, add the XmlBindingManager to your toolbox and drag an instance on your form or user control. Select the XML file you want to bind to, click on a control you want to bind, look up the XmlDataBinding property for this control (it's in the XML category) and select the node from the PropertyExplorer. Of course, you can change to a different XML file at runtime. All you have to do is:

C#
//Set the new file
xmlBindingManager.Filename = filename;

//Initialize the XmlWrapper
xmlBindingManager.Initialize();

//Rebind the controls
xmlBindingManager.DataBind();

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here



Comments and Discussions

 
GeneralBindingManagerBase Pin
Member 37719524-Mar-04 2:08
Member 37719524-Mar-04 2:08 
GeneralRe: BindingManagerBase Pin
Girish J Jain31-May-06 17:10
Girish J Jain31-May-06 17:10 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.