Click here to Skip to main content
Click here to Skip to main content

XmlGridControl Editing XML Data in the PropertyGrid

, 3 Mar 2006
Rate this:
Please Sign up or sign in to vote.
Using an XML Schema Definition file, this program uses code generation and CodeDom manipulation to edit XML data in the PropertyGrid.

Sample Image - maximum width is 600 pixels

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 XmlAttributes 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.

    // convert arrays to collections
    ConvertArraysToCollections(codeNamespace);

    // add the ExpandableObjectConverter attribute
    AddExpandableObjectConverter(codeNamespace);

    // make XmlAttributes display with an "@": [DisplayName("@<name>")]
    RenameXmlAttributes(codeNamespace);

    // Make collections edit with a nicer collection editor
    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();

// references for 
//  System.CodeDom.Compiler, System.CodeDom, System.Diagnostics
compilerParameters.ReferencedAssemblies.Add("System.dll");
compilerParameters.ReferencedAssemblies.Add("mscorlib.dll");

// System.Xml
compilerParameters.ReferencedAssemblies.Add("system.xml.dll");

// reference to this assembly for the custom collection editor
compilerParameters.ReferencedAssemblies.Add(
        Assembly.GetExecutingAssembly().Location);
compilerParameters.ReferencedAssemblies.Add("System.Drawing.dll");

compilerParameters.GenerateExecutable = false;
compilerParameters.GenerateInMemory = true;
// generate the assembly in memory

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)
{
    // find the type that's currently selected
    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()
    {
        // find the type that's currently selected
        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.

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

About the Author

Michael Coyle
Architect Blue Toque Software
Canada Canada
I've been lead architect in several software companies. I've worked in the Justice and Public Safety area for the last 7 years, writing facial recognition, arrest and booking software and emergency management/GIS software. Prior to that I worked in the games industry with 3D animation.
 
Currently I'm working on some GIS/mapping software for outdoor enthusiasts. I intend to spin off portions of this into the open source community as time permits.
Follow on   Twitter   Google+   LinkedIn

Comments and Discussions

 
QuestionXML / XSD error PinmemberNijju29-Oct-10 2:15 
AnswerRe: XML / XSD error PinmemberMichael Coyle29-Oct-10 12:44 
Generaldelete an item from the treeview,... also copy and paste through the menu like "Create" menu option PinmemberRadian021-Aug-08 3:19 
GeneralSchema with choices Pinmembersm16200923-Nov-07 0:34 
GeneralRe: Schema with choices PinmemberMichael Coyle23-Nov-07 6:30 
GeneralRe: Schema with choices Pinmemberablas8327-Nov-07 21:55 
QuestionObjects created by Elements with SubstitutionGroup attributes PinmemberMr B3-Sep-07 0:08 
AnswerRe: Objects created by Elements with SubstitutionGroup attributes PinmemberMichael Coyle3-Sep-07 14:42 
GeneralVery useful PinmemberRenjith V.14-Jul-07 19:31 
GeneralCustom DisplayName in the XSD PinmemberSerban Lascu5-Jun-07 22:10 
GeneralRe: Custom DisplayName in the XSD PinmemberSerban Lascu6-Jun-07 2:48 
GeneralRe: Custom DisplayName in the XSD PinmemberMichael Coyle6-Jun-07 7:37 
GeneralUpdated XMLGridControl2 PinmemberMichael Coyle27-Apr-07 15:58 
GeneralRe: Updated XMLGridControl2 PinmemberGaza138-May-07 1:10 
GeneralRe: Updated XMLGridControl2 PinmemberMichael Coyle8-May-07 5:55 
AnswerRe: Updated XMLGridControl2 PinmemberMichael Coyle11-May-07 8:00 
GeneralRe: Updated XMLGridControl2 PinmemberJian123456730-Aug-07 5:34 
GeneralRe: Updated XMLGridControl2 PinmemberYumashin Alex26-Jan-09 2:05 
GeneralRe: Updated XMLGridControl2 PinmemberMichael Coyle26-Jan-09 5:06 
GeneralRe: Updated XMLGridControl2 PinprotectorMarc Clifton30-Jan-09 1:29 
GeneralRe: Updated XMLGridControl2 [modified] PinmemberMichael Coyle30-Jan-09 19:37 
GeneralAdding nodes recursively [modified] PinmemberLe Sourcier17-Apr-07 22:05 
GeneralRe: Adding nodes recursively PinmemberMichael Coyle18-Apr-07 8:08 
GeneralObject not expanded recrusively Pinmemberdani kenan17-Apr-07 7:18 
GeneralRe: Object not expanded recrusively PinmemberMichael Coyle17-Apr-07 7:53 

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

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

| Advertise | Privacy | Mobile
Web02 | 2.8.140721.1 | Last Updated 3 Mar 2006
Article Copyright 2006 by Michael Coyle
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid