|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionI'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 Unlike other CodeProject articles that have used the BackgroundThis 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 codeI've packaged the code into a control called Generating the GUI is centered around loading the schema into the control. The GUI is generated when you set the Once the XSD is loaded, you can load an The Generating the InterfaceGenerate 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 Modify the CodeDomModifying the CodeDom is not always necessary, but I wanted certain features:
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 Assembly assembly = results.CompiledAssembly;
Type[] exportedTypes = assembly.GetExportedTypes();
Loading and Viewing XMLOnce 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 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 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 Creating InstancesBy 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 Points of InterestI'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 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 The Future WorkSome ideas I will be working on in the future:
Known Bugs:Editing a collection of the The base64Binary editor is interesting, it does not let you add data, just view the data that is there. History
| ||||||||||||||||||||