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:
- Implementing your own
BindingManager
deriving from BindingManagerBase
- 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)
<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:
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:
- Using the
System.CodeDom
namespace
- 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:
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:
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:
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:
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();
ilCtor.Emit(OpCodes.Ldarg_0);
ilCtor.Emit(OpCodes.Ldarg_1);
ilCtor.Emit(OpCodes.Stfld, mgrField);
ArrayList childFields = parse(root, typeBuilder);
initializeFields(childFields, ilCtor);
ilCtor.Emit(OpCodes.Ret);
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.
private void initializeFields(ArrayList childFields, ILGenerator ilCtor)
{
foreach(FieldBuilder field in childFields)
{
ConstructorInfo ctor =
field.FieldType.GetConstructor(new Type[] {GetType()});
ilCtor.Emit(OpCodes.Ldarg_0);
ilCtor.Emit(OpCodes.Ldarg_1);
ilCtor.Emit(OpCodes.Newobj, ctor);
ilCtor.Emit(OpCodes.Stfld, field);
}
}
This MSIL would read as the following C# statement:
_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.
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.
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);
ilGetMethod.Emit(OpCodes.Ldfld, mgrField);
ilGetMethod.Emit(OpCodes.Ldstr, xpath);
ilGetMethod.Emit(OpCodes.Call, _getCall);
ilGetMethod.Emit(OpCodes.Ret);
ILGenerator ilSetMethod = setMethod.GetILGenerator();
ilSetMethod.Emit(OpCodes.Ldarg_0);
ilSetMethod.Emit(OpCodes.Ldfld, mgrField);
ilSetMethod.Emit(OpCodes.Ldstr, xpath);
ilSetMethod.Emit(OpCodes.Ldarg_1);
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.
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:
xmlBindingManager.Filename = filename;
xmlBindingManager.Initialize();
xmlBindingManager.DataBind();