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):
Code generation (see Willem Fourie's excellent article, the CodeXS tool, and the MSDN article it is based on)
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.
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.
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.
privatevoid 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.
Hi, XmlGridControl is a great xml editor... but i can't get to delete an item from the treeview. let's say i have something like this: -Person -Name -Last Name -Middle Name -First Name -Age
and i want to delete the "Middle Name" node through the menu. this menu will be --------- |create | |delete | |copy | |paste | --------- when i choose "delete" the result should be: -Person -Name -Last Name -First Name -Age i tried to do this but i couldn't make it to work. i'm new to c# so i ask you to help me a little, looking forward to hearing from you, Radian
I've just stumbled across this control and it looks great. What I was wondering is do you have any plans to support schemas with choices? I have a fairly complex schema from which the output Xml can be made up from a number of different elements but unfortunately the control doesn't seem to support this.
When you add a choice element into the schema, the base code generator in the .NET 2.0 framework cannot decide at code-generation time what elements to create. It does generated code, it just puts a class with an "XmlElement" property to capture the XML elements in any deser9ialized document.
This illustrates the point that schema structures are a superset of the types of structures that can be expressed in code, or in other words, you can express structures in schema language that cannot be represented in code.
One strategy is to make a schema that represents the kind of document you're actually expecting. In some circles this is known as a "restriction" (or expansion) schema. The XML Document you're expecting may conform to the schema with the "choice" elements, but it can also conform to a more restrictive (or less restrictive) schema that you can generate code from. You just have to make sure that any document you generate validates against both schemas.
For reading on this you can (if you like) read about how it's done in the Global Justice XML Data Model
The short answer is that at the moment I don't know how to handle choice elements.
What I've done to solve it it's to not to define choices inside the XSD. I've used XSD polymophysm, what means that you can define abstract elements (that are your choices ) from which you can inherit in final (or not) elements (that are choices options).
.Net code generator will create a hierarchy of classes in your code mirroring what you defined in your XSD.
Then here's the magic: you can instantiate a property in your code that has an abstract type (the choice) in any of his child classes (the options).
You can also create your own type editor to choose between the classes that inherit of the parent class.
Firstly I would just like to say how useful your article has been. However, I have a slight problem with an XSD I am trying to use (which is supplied by one of our business partners), in that it is using Substitution groups. The code Generator Great artivle and Project I have an XSD that uses SubstitutionGroups. When the Code Generator is generating the Class that is containing a object containing a substitutionGroup, it creates a System.Object rather than the substitutionGroup class as defined in the XSD. Mmmm, Ok, I think it is probably worth giving an example. In the schema we have been given an XSD for an address as follows:
ukt000028 A container for address details. . telcoapisox:ServiceRequestOrder.sox Aggregate Active
The class that is created is as follows:
/// [System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "2.0.50727.42")] [System.SerializableAttribute()] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:uk.org.telcob2b/tML/ukt-CommonComponents-v5-0")] [System.Xml.Serialization.XmlRootAttribute(Namespace="urn:uk.org.telcob2b/tML/ukt-CommonComponents-v5-0", IsNullable=false)] public partial class Address {
/// [System.Xml.Serialization.XmlElementAttribute("BritishAddress", typeof(BritishAddress))] [System.Xml.Serialization.XmlElementAttribute("InternationalAddress", typeof(InternationalAddress))] [System.Xml.Serialization.XmlElementAttribute("NationalAddress", typeof(NationalAddress))] public object Item { get { return this.itemField; } set { this.itemField = value; } }
/// public AddressReferenceType AddressReference { get { return this.addressReferenceField; } set { this.addressReferenceField = value; } } }
As you can see, in the Address class, the sub-class BritishAddress is declares as Item of class System.Object. This means that I can't create a BritishAddress in the XML, it just creates a blank "Item" element and I cannot create the class in the Property Grid. Is there an easy way to create the BritishAddress class? If not, do you think that this can be created by manipulating the CodeDOM? If so, although the "ReplaceAttribute" method is doing something similar, I need to find the Properties in the CodeDom which are substitution groups, then replace the System.Object with the BritishAddress class. Any ideas how I go about doing this? I assume that this is the only place where this can be done because the Property Grid needs the compiled code, i,e, there is not some neat trick that allows me to do this in the "Create" method of the PropertyGrid?
As I mentioned in the article and elsewhere, XSD defines a language space that is a superset of what can be represented by C#. The SustitutionGroup element in XSD is on the edge of what is representable by C#.
Consider how you would write code to do that the SubstitutionGroup does; you need to create an class or property that can be one and only one of N classes. IE: you cannot have a class with N properties that represent the N possibilities in the substitution group because there is the possibility in C# to initialize more than one of them, which when serialized would make invalid XML (which of the possible elements of the substitution group would be valid in this case). You also can't have a union-like structure (like in C and C++) because the union can be interpreted as either one of N classes.
However, the CodeDom has a way, and you've almost discovered it yourself. Read on!
In the generated code notice that there will be a class for each of the complex types that make up the SubstitutionGroup, in your case there should be a BritishAddress, InternationalAddress and a NationalAddress. And as you noticed, the Item property of the Address class is an object.
Simply test the Item property for which class it contains:
if (myAddress.Item is BritishAddress) { // process BritishAddress BritishAddress l_BritishAddress = myAddress.Item as BritishAddress; } elseif (myAddress.Item is InternationalAddress ) { // see above } elseif (myAddress.Item is NationalAddress) { // see above }
The deserialization framework is smart enough to read the XML and create the right class, and assign it to the Item property. Conversely, the Serialization side is spart enought the generate the correct XMl if you assign one of the three classes to the Item property. It will generate an error on Serialization if you assign any other class.
I would like that the names displayed in the PropertyGrid wouldn't have such limitations on their content (spaces, special characters). I understand that the "name" property can't contain spaces, that is why I would like to add a "displayname" property in the XSD. Can this be achieved?
Alright, I managed to have names displayed with spaces. I edited the XSD with Visual Studio 2005 and I could type "First Name" in the name. VS2005 automatically changes spaces into _x0020. XMLGridControl2 generates then a property line "[System.Xml.Serialization.XmlElementAttribute("First Name")]". I modified the code in RenameXmlAttributes.cs to add a property "[System.ComponentModel.DisplayName("First Name")]" and it works.
However, I have problems now creating a Combobox with the choices : "Choice n°1", "Choice n°2", "Choice n°3" (spaces and special characters are being ignored).
Second problem : How can I put a description which appears in the lower part of the grid?
XsdToClasses currently doesn't generate anything for the System.ComponentModel.Description attribute. I would, in the future, like to scoop the documentation nodes from the schema and put them into the generated code.
Since the tool continually overwrites the generated code, you don't have the option to just edit the code unless you copy and paste the code and remove the code generator when you are done with it. This is a different model of operation, one where you consider the schema to be static. You can use Xsd.exe to do this for you, or I could be convinced to code a custom tool (although other people have already done this).
The new control is much more robust (though, as always, it may not be as robust as you need if you're making an industrial-strength app). It still suffers from some significant shortcomings, one being that it can require quite a bit of CPU and memory to generate code on the fly, especially for a large and complex schema.
That being said, I ave tested it on portions of the Global Justice Xml Data Model, and although the code generation portion is slow, and the memory used is huge, it really does work.
In one instance of using this control (or a variant) I solved the speed issue by installing a cache for each schema I needed to use. You're welcome to try this technique on your own.
I just tested the download (both the source and the binaries), and they worked for me. I used the built-in Microsoft Windows XP zip engine to create the archives. Can you be more specific about the error? One other person reported trouble, but then told me that they worked it out.
I re-compressed the archive and uploaded it again.
Do you have a current link to the latest version? The link provided no longer works.
BTW, I've been testing the code generation from the code base in this article against the NIEM-Core XSD, and it looks like it generates the code fine, but there are errors trying to serialize the XML, so I'm looking at that.
Oh--by "generates the code fine", I had to add a test to not instantiate abstract classes.
I've also done some work with the NIEM-core; I've used a custom code generator (available here: http://www.bluetoque.ca/Downloads/XsdToClasses.1.0.14.zip) on portions (subsets) of both NIEM and GJXDM. Both crazy, overly detailed schemas.
My experience in general is that this technique works for some schemas, but schema language is a superset of can be expressed in c# so there will always be schemas that cannot be represented in code, and probably shouldn't. There is no need for the internal representation of data to be dictated by a committee of non-developers meeting in rooms with requirements that don;t have much to do with the real world.
My technique for dealing with the large complex schemas has been to treat them the way people treat databases: create a mapping between the schema model and the object model, and use that to abstract the data from the business layer. Basically, xslt becomes the analogue to nHibernate (or any other Object-relational model).
With your GUI I add a new "info_helico" entry, then the control creates it with an empty "data_helico" node. So I right click on "data_helico" and I create it. Unfortunatelly, the childs nodes appears but empty and without any structure...(it's very strange, if I try to create each one, the software crash and exit).
First of all, I've tryied to solve it by modifying my XSD, but it doesn't fix the problem. Then, I've tried to look at the code, but I've never used C# and I can't find what I want. Could you help me ? What I really want: when I create a new "info_helico" I would like the control to create all child recursively...
The objects are expanded properly where the word "properly" means "how the control was designed". I'm using the built-in method PropertyGrid.ExpandAllGridItems which must account for the behaviour you're seeing.
In the screen shot I manually expanded all the grid items to demonstrate the control.
If you want to expand all object recursively you may find this sort of code useful:
// Recursively expand all items from the selected item void ExpandItem(GridItem item) { item.Expanded = true; foreach (GridItem child in item.GridItems) ExpandItem(child, items); }
It just remains to walk through each top-level item and call this recursive method.
(Note I just wrote this here, and have not tested it).
I have a ObjectType that is returned by a Web Service and I need to allow de user to change object properties in a PropertieGrid.
The problem is that one of the properties of the object is also a ObjectType and can not be changed in the PropertieGrid because only displays the type of ObjectType in the property.