Introduction
I've been using the XSD.exe for quite a while to generate classes from XML Schema Definition (XSD) files. These classes are uniquely suited for serializing/deserializing XML data into an object model that a programmer can use to manipulate data. When paired with a middleware product that generates XML from database queries or stored procedures, this provides a powerful programming model where the programmer only needs to work in the object data space, while the middleware layer takes care of the object-relational mapping.
While developing the middleware product described above, I had a need to dynamically generate a simple GUI from the XSD which specified the structure of the XML data. The data would then be deserialized into the object model, and presented on the screen using data binding. While thinking about this, I realized that the .NET Framework ships with a control that already takes an object and displays its properties in an editable format: the PropertyGrid
.
Unlike other CodeProject articles that have used the PropertyGrid
for this purpose, this control displays the grid using the schema for formatting, loads the data using XML serialization, and edits the data using standard .NET methods that we're all familiar with from using VS2003/VS2005. The resulting control is very simple, and has very little code. All complexity and overhead is in generating the class structure; performance concerns can be addressed by an appropriate caching scheme, if so desired.
Background
This article draws inspiration from the following sources (CodeProject articles cited, where used):
In this article, I've removed error handling, and changed some of the code for simplicity. Also, this article is based on the .NET Framework 2.0, it will not work in the 1.1 framework.
Using the code
I've packaged the code into a control called XmlDataControl
, and a project that exercises the control. The control is derived from the PropertyGrid
.
Generating the GUI is centered around loading the schema into the control. The GUI is generated when you set the Schema
property of the XmlDataControl
(much the same way as the PropertyGrid
populates itself when you set the SelectedObject
). I've factored the class generation and CodeDom manipulation into a static class called XsdCodeGenerator
(easier to read, easier to change).
Once the XSD is loaded, you can load an XmlDocument
to be displayed and edited in the control by using the Document
property. Assigning the document deserializes it into the generated classes, accessing the document serializes it back into an XmlDocument
. As a side effect, it silently validates the XML, displaying only the elements of the XML that validate against the schema. For mode advanced validation and error handling, you can hook up the XmlValidatingReader
.
The PropertyGrid
behaves as you would expect; the properties of the classes are displayed and are editable. Enumerations and boolean values are shown in combo boxes. Arrays and collections can be edited using the collection editor.
Generating the Interface
Generate the CodeDom:
This is the code that does what Xsd.exe does:
CodeNamespace codeNamespace = new CodeNamespace("TestNameSpace");
XmlSchemas xmlSchemas = new XmlSchemas();
xmlSchemas.Add(schema);
XmlSchemaImporter schemaImporter =
new XmlSchemaImporter(xmlSchemas);
XmlCodeExporter codeExporter = new XmlCodeExporter(
codeNamespace,
new CodeCompileUnit(),
CodeGenerationOptions.GenerateProperties);
foreach (XmlSchemaElement element in schema.Elements.Values)
{
XmlTypeMapping map =
schemaImporter.ImportTypeMapping(element.QualifiedName);
codeExporter.ExportTypeMapping(map);
}
The new version of xsd.exe generates properties for each field when you use the CodeGenerationOptions.GenerateProperties
option. It has a few other new features related to data binding, but you can read from the documentation about those.
Modify the CodeDom
Modifying the CodeDom is not always necessary, but I wanted certain features:
- Convert arrays to generic collections for complex types, to make it easier to manage programmatically.
- Add an
ExpandableObjectConverter
to every class so you can expand it in the property grid.
- Rename
XmlAttribute
s to have an "@
" sign in front of them (no good reason for this, just wanted to see if it could be done).
- Add a custom collection editor to each collection class to give me more control over how they are displayed.
Aside from the first modification (arrays to collections), the modifications consist of adding attributes to classes and properties. This is good because we don't want to change the structure of the code generated because it has to validate against the schema we generated it from.
ConvertArraysToCollections(codeNamespace);
AddExpandableObjectConverter(codeNamespace);
RenameXmlAttributes(codeNamespace);
AddCustomCollectionEditor(codeNamespace);
Modifying the CodeDom is fairly simple, it's just a tree structure and you iterate through it looking for the things you'd like to change. The code is either self-explanatory, or tangential to this article. There are other resources on the net to help you manipulate the CodeDom.
Generate the Code:
Creating the code is not necessary, but allows us to see the results of manipulating the CodeDom. It will also allow us to see if any modifications we've made were successful. This helps during development, but in a released product, I would skip this step.
CodeCompileUnit compileUnit = new CodeCompileUnit();
compileUnit.Namespaces.Add(codeNamespace);
CodeDomProvider provider = CodeDomProvider.CreateProvider("CSharp");
#if DEBUG
StringWriter sw = new StringWriter();
CodeGeneratorOptions options = new CodeGeneratorOptions();
options.VerbatimOrder = true;
provider.GenerateCodeFromCompileUnit(compileUnit, sw, options);
m_codeString = sw.ToString();
#endif
Compile an Assembly:
Compiling the assembly is necessary since the next step is to load it and display the classes. The only trick here is adding a reference to the assembly where the custom collection editor resides (which is the same assembly as this code is in). Also notice that, I'm generating the assembly in memory, because for the purpose of this control, I don't want to keep the classes around.
CompilerParameters compilerParameters = new CompilerParameters();
compilerParameters.ReferencedAssemblies.Add("System.dll");
compilerParameters.ReferencedAssemblies.Add("mscorlib.dll");
compilerParameters.ReferencedAssemblies.Add("system.xml.dll");
compilerParameters.ReferencedAssemblies.Add(
Assembly.GetExecutingAssembly().Location);
compilerParameters.ReferencedAssemblies.Add("System.Drawing.dll");
compilerParameters.GenerateExecutable = false;
compilerParameters.GenerateInMemory = true;
CompilerResults results = provider.CompileAssemblyFromDom(
compilerParameters, new CodeCompileUnit[] { compileUnit });
Load the Generated Classes:
Load the generated assembly, and make a list of all the classes. The generated classes seem to be in the same order as specified in the code, which is also the same order as they were specified in the XSD. This means, the fist class is probably the first element in the XSD. If we set the SelectedObject
property of the PropertyGrid
to this class, it should display quite nicely.
Assembly assembly = results.CompiledAssembly;
Type[] exportedTypes = assembly.GetExportedTypes();
Loading and Viewing XML
Once the interface is generated, loading and viewing XML is just a matter of deserializing the XML into the created classes, using standard serialization techniques. However, since the UI doesn't actually have any specific knowledge about the types, and since the PropertyGrid
reflects on the classes to show the data, we don't have to do any casting.
private void LoadXmlDocument(XmlDocument doc)
{
Type type = this.SelectedObject.GetType();
object newObject = null;
XmlSerializer ser = new XmlSerializer(type);
using (StringReader reader = new StringReader(doc.OuterXml))
newObject = ser.Deserialize(reader);
this.SelectedObject = newObject;
ExpandAllGridItems();
}
The type of the object we are deserializing is contained in the SelectedObject
property of the PropertyGrid
. That selected object is in fact an "empty" instance of the generated class. We're just deserializing the XML into another instance of that class.
Serializing is essentially the reverse:
XmlDocument SaveXmlDocument()
{
Type type = this.SelectedObject.GetType();
StringWriter sw = new StringWriter();
XmlSerializer ser = new XmlSerializer(type);
using (XmlTextWriter writer = new XmlTextWriter(sw))
{
writer.Formatting = Formatting.Indented;
ser.Serialize(writer, this.SelectedObject);
}
XmlDocument doc = new XmlDocument();
doc.LoadXml(sw.ToString());
return;
}
In this method, the currently selected object in the PropertyGrid
is sent to the serializer, and is converted into an XML string. I reload the XML string into an XmlDocument
and return it.
Creating Instances
By default, we create a new instance of a generated class, but we don't create instances of properties that are not primitive types. The reason for this is that we might not want "blank" instances (or instances that have no data in them except for the defaults). If a user wants to create an instance, they can right click on the PropertyGrid
item they want to create the instance for, and choose "Create". Note that I have not yet provided a way to delete an instance, I'll leave that as an exercise for the reader (however, I can hint that we will destroy an instance in the same way as the CustomCollectionEditor
destroys a member of a collection).
Points of Interest
I've included a TestSchema with this project where I've tried to show the range of types, collections, arrays, and subtypes that this control can handle. I'm still developing the control, and I can still find XSDs that it cannot load. There are known issues with the XSD to .NET type mapping, and I'll find a way to deal with those in some future release.
Note that for .NET 2.0, if you add nullable="true"
to a schema element, .NET will wrap it in a nullable type. This is very handy for DateTime
, enum
and bool
for null database values.
Also note that arrays and collections can both be added and edited in the colleciton editor.
The string array has a special collection editor in .NET 2.0 that lets you edit the strings one at a time.
Once the ExpandableTypeConverter
is added, the PropertyGrid
works a little like a TreeListView
in that it can show you a tree view of the properties and the sub-properties on the left, and their values on the right.
The CustomCollectionEditor
was implemented using examples on CodeProject. I decided to implement my own because I wanted finer control over how it looked (like the title bar, the icon etc.).
Future Work
Some ideas I will be working on in the future:
- Fully dynamically generated GUI with data binding using standard Windows controls.
- TypeConverters to edit dates and times separately (the current control edits
DateTime
as Date
and not Time
).
- Gracefully handle any XSD.
- Develop a TypeConverter that can interpret a base64Encoded string as an image, or another binary type (i.e., a file) and offer the user some options for display and editing.
- Figure out why the
PropertyGrid
displays the properties in a completely different order than in the class and in the file.
- Dynamic specification of TypeConverters at run-time: allow the user a chance to specify what type converter to use to edit a specific item. For instance, a "flag" editor could be used to edit a byte to set the individual bits.
Known Bugs:
Editing a collection of the base64Binary
type currently does not work.
The base64Binary editor is interesting, it does not let you add data, just view the data that is there.
History
- March 1st 2006: -- Initial version.