Introduction
MBG XML To Class Generator is a tool for creating a .NET (C#) class file from scanning an XML file. It generates all the required classes, as well as XML attributes for serialization purposes.
Background
On occasions, I have found myself confronted with an XML file from a customer or other source, and having to manually go through the file in order to create an entity in my code. Now, whilst it is a thoroughly good idea to make oneself familiar with the structure of the XML file in question, it is not fun to have to type up many classes and properties. And so, the XML to Class Generator was born.
Where to Start?
Well, we need some entities to hold data for us between scanning the XML file and creating the .cs file. So, let us define some of those. We will need:
- Class
Entity
(to hold data relating to the XML elements that will become classes) - Class
Property
(related to the XML attributes and some elements) - Collection property (there are reasons why I didn't just include an "
IsCollection
" property on the Property
class, which I won't go into now)
The class entity is the main entity and holds a collection of sub classes, properties and collection properties.
Parsing the XML
For parsing the XML, I decided to put this code inside a custom TreeView control: XmlTreeView
. I did this for the purposes of displaying the data and also having a convenient place to go get it when I need to generate it (I store the data in the Tag
properties of the TreeNode
s). When the user selects a file to load, the code will pass the file name to XmlTreeView.ParseXml()
and that begins the magic:
public void ParseXml(string fileName)
{
this.Nodes.Clear();
XDocument document = XDocument.Load(fileName);
Class mainClass = CreateClass(document.Root, true);
this.Nodes.Add(CreateNode(mainClass));
}
private Class CreateClass(XElement element, bool isMain)
{
Class newClass = new Class
{
Name = element.Name.LocalName.ToPascal(),
IsMain = isMain
};
newClass.ClassNameChanged +=
new Class.ClassNameChangedEventHandler(class_ClassNameChanged);
#region Collections
IEnumerable<string> distinctSubElementNames =
element.FindAllPossibleElements().Select(
x => x.Name.LocalName).Distinct();
foreach (string subElementName in distinctSubElementNames)
{
XElement subElement =
element.FindAllPossibleElements().Where(
x => x.Name.LocalName == subElementName).First();
if (subElement.IsPropertyElement())
{
continue;
}
if (subElement.IsCollectionElement())
{
newClass.IsCollection = true;
newClass.CollectionType = subElementName.ToPascal();
newClass.XmlName = subElementName;
continue;
}
else if (subElement.HasEqualSiblings())
{
string subClassName = subElement.Name.LocalName.ToPascal();
newClass.CollectionProperties.Add(new CollectionProperty
{
CollectionName = subClassName + "Collection",
PropertyName = subClassName.Pluralize(),
ClassName = subClassName,
XmlName = subClassName ==
subElement.Name.LocalName ? string.Empty : subElement.Name.LocalName
});
}
}
#endregion
#region Normal Properties
foreach (string attributeName in element.FindAllPossibleAttributes())
{
newClass.Properties.Add(new Property
{
Name = attributeName.ToPascal(),
ReturnType = PropertyType.String,
IsXmlAttribute = true,
XmlName = attributeName
});
}
#endregion
#region Sub Classes
var subEls = element.FindAllPossibleElements();
foreach (XElement subElement in subEls)
{
string subElementName = subElement.Name.LocalName.ToPascal();
int count = element.Elements(subElement.Name).Count();
if (subElement.IsPropertyElement())
{
Property existingProperty =
newClass.Properties.SingleOrDefault(x => x.Name == subElementName);
if (existingProperty == null)
{
newClass.Properties.Add(new Property
{
Name = subElementName,
ReturnType = PropertyType.String,
IsXmlAttribute = false,
XmlName = subElement.Name.LocalName
});
}
}
else
{
Class subClass =
newClass.SubClasses.SingleOrDefault(x => x.Name == subElementName);
if (subClass == null)
{
newClass.SubClasses.Add(CreateClass(subElement, false));
if (newClass.IsCollection && newClass.CollectionType == subElementName)
{
continue;
}
else
{
subClass = newClass.SubClasses.SingleOrDefault(
x => x.Name == subElementName);
if (subClass.IsCollection)
{
newClass.Properties.Add(new Property
{
Name = subClass.CollectionType.Pluralize(),
IsXmlAttribute = false,
ReturnType = PropertyType.Custom,
CustomType = subClass.CollectionType + "Collection",
XmlName = subElement.Name.LocalName,
XmlItemName = subClass.XmlName
});
}
else
{
CollectionProperty cp =
newClass.CollectionProperties.SingleOrDefault(
x => x.ClassName == subElementName);
if (cp != default(CollectionProperty))
{ continue; }
newClass.Properties.Add(new Property
{
Name = subElementName,
IsXmlAttribute = false,
ReturnType = PropertyType.Custom,
CustomType = subElementName,
XmlName = subElement.Name.LocalName
});
}
}
}
}
}
#endregion
return newClass;
}
private TreeNode CreateNode(Class mainClass)
{
TreeNode node = new TreeNode
{
Text = mainClass.Name,
Tag = mainClass
};
foreach (Class subClass in mainClass.SubClasses)
{
node.Nodes.Add(CreateNode(subClass));
}
return node;
}
Here, we are basically just scanning the document. When we hit an element, we create another class (most of the time) and we create a property for each attribute. That sounds pretty straightforward, but if it were as simple as that, the code wouldn't be quite as longwinded as it is. In fact, it started getting so complicated, I would often forget what was going on in there if I left it for too long a time. So, if you can pick it all up quickly, great! You're a superman. If not, just be happy that it works! The code is not foolproof, but it should do a good job most of the time, and only leave you to correct maybe a few lines (if that). I would take it further and fix it up some more, but I thought, hell with it, I'm doing this for free. =) He he.
Anyway, back to business... now that we have parsed the XML file, we can use the UI to change a couple of things and then hit the shiny green button... yes, that means "Go". For each property, you will find an attribute called "Return Type". This, as you may have guessed, relates to the property's return type. Because, by default, we can only detect strings; you will need to explicitly specify if you want a different data type for a particular property. Or you can leave it and change it after code generation is complete. Now you can click the button... ;-)
Code Generation
public static void Generate(Class mainClass, string fileName)
{
StringBuilder sbClasses = new StringBuilder(2000);
sbClasses.Append(Constants.USINGS);
sbClasses.Append(string.Format(
Constants.NAMESPACE_START_FORMAT, "MyNamespace"));
sbClasses.Append(GenerateClass(mainClass, ref sbClasses));
sbClasses.Append("}");
sbClasses.ToString().ToFile(fileName);
}
private static string GenerateClass(Class newClass, ref StringBuilder sbClasses)
{
if (newClass.IsMain && newClass.IsCollection)
{
throw new ArgumentException("The root class cannot be a collection!");
}
string classFormat = Constants.ROOT_CLASS_FORMAT;
if (!newClass.IsMain)
{
classFormat = newClass.IsCollection ?
Constants.COLLECTION_CLASS_FORMAT : Constants.CLASS_FORMAT;
}
StringBuilder sbProperties = new StringBuilder(2000);
StringBuilder sbConstructor = new StringBuilder(50);
#region Properties
foreach (Property property in newClass.Properties.OrderBy(p => p.Name))
{
string xmlAttribute = string.Empty;
if (property.IsXmlAttribute)
{
if (!string.IsNullOrEmpty(property.XmlName) &&
property.XmlName != property.Name)
{
xmlAttribute =
string.Format(Constants.XML_ATTRIBUTE_FORMAT, property.XmlName);
}
else { xmlAttribute = Constants.XML_ATTRIBUTE; }
}
else
{
if (!string.IsNullOrEmpty(property.XmlName) &&
property.XmlName != property.Name)
{
if (!string.IsNullOrEmpty(property.XmlItemName))
{
if (!string.IsNullOrEmpty(property.XmlName))
{
xmlAttribute = string.Format(Constants.XML_COLLECTION_FORMAT,
property.XmlName, property.XmlItemName);
}
else
{
xmlAttribute = string.Format(Constants.XML_ELEMENT_FORMAT,
property.XmlItemName);
}
}
else
{
xmlAttribute =
string.Format(Constants.XML_ELEMENT_FORMAT, property.XmlName);
}
}
}
sbProperties.Append(string.Format(
Constants.PROPERTY_FORMAT,
property.ReturnType == PropertyType.Custom
? property.CustomType
: property.ReturnType.ToString(),
property.Name,
xmlAttribute));
sbProperties.Append(Environment.NewLine);
if (property.ReturnType == PropertyType.Custom)
{
sbConstructor.Append(string.Format(
"{0} {1} = new {2}();",
Environment.NewLine,
property.Name,
property.CustomType));
}
}
#endregion
foreach (CollectionProperty collectionProperty in newClass.CollectionProperties)
{
sbProperties.Append(string.Format(
Constants.PROPERTY_FORMAT,
collectionProperty.CollectionName,
collectionProperty.PropertyName,
collectionProperty.RepresentsXmlNode
? string.Empty
: string.Format(
Constants.XML_ELEMENT_FORMAT,
string.IsNullOrEmpty(collectionProperty.XmlName)
? collectionProperty.ClassName
: collectionProperty.XmlName)));
sbConstructor.Append(string.Format(
"{0} {1} = new {2}();",
Environment.NewLine,
collectionProperty.PropertyName,
collectionProperty.CollectionName));
}
foreach (Class subClass in newClass.SubClasses)
{
sbClasses.Append(GenerateClass(subClass, ref sbClasses));
}
if (!newClass.CollectionProperties.IsNullOrEmpty())
{
StringBuilder sbCollectionClasses = new StringBuilder();
foreach (CollectionProperty collectionProperty in newClass.CollectionProperties)
{
sbCollectionClasses.Append(string.Format(
Constants.COLLECTION_CLASS_FORMAT, collectionProperty.ClassName));
sbCollectionClasses.Append(Environment.NewLine);
}
string itemClass = string.Format(classFormat,
newClass.IsCollection ? newClass.CollectionType : newClass.Name,
sbProperties.ToString(),
sbConstructor.ToString());
return string.Concat(sbCollectionClasses.ToString(),
Environment.NewLine, itemClass);
}
else
{
return string.Format(
classFormat,
newClass.IsCollection ? newClass.CollectionType : newClass.Name,
sbProperties.ToString(),
sbConstructor.ToString());
}
}
And here we are going through each Class entity, generating the classes, properties, etc., and outputting the class to a file.
Using the Tool
- Click the Load button on the toolbar, select your XML file, and click OK.
- Change anything you need to change (class names, return types, etc.).
- Click "Generate"; that's the big green button! :-D
Notes
This application makes use of the MBG Extensions Library and as such, includes helpful Load
/ Save
methods in the output classes for serializaing / dersializing. So, yeah... you don't even have to write any serialization code either; aren't you lucky? ;-) To build the source code, you will need to reference the MBG Extensions Library. You can get it from the binary file download, or from here: http://www.codeproject.com/KB/dotnet/MBGExtensionsLibrary.aspx.
Enjoy!!!