Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / XML

MBG XML to Class Generator

5.00/5 (5 votes)
3 Nov 2010CPOL3 min read 28.1K   1.4K  
Code generator for creating XML serializable classes from scanning the XML file itself.

Capture.PNG

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:

  1. Class Entity (to hold data relating to the XML elements that will become classes)
  2. Class Property (related to the XML attributes and some elements)
  3. 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 TreeNodes). When the user selects a file to load, the code will pass the file name to XmlTreeView.ParseXml() and that begins the magic:

C#
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

    // Find names of all sub elements
    IEnumerable<string> distinctSubElementNames = 
      element.FindAllPossibleElements().Select(
      x => x.Name.LocalName).Distinct();
    foreach (string subElementName in distinctSubElementNames)
    {
        // Find first element with the specified name
        //XElement subElement = element.Elements(subElementName).First();
        XElement subElement = 
          element.FindAllPossibleElements().Where(
          x => x.Name.LocalName == subElementName).First();

        // If this is a property, then continue to next child element
        if (subElement.IsPropertyElement())
        {
            continue;
        }
        // If this element is an element in a collection
        if (subElement.IsCollectionElement())
        {
            newClass.IsCollection = true;
            newClass.CollectionType = subElementName.ToPascal();
            newClass.XmlName = subElementName;
            continue;
            //This is a collection already (only one type of child element)
        }
        else if (subElement.HasEqualSiblings())
        {
            string subClassName = subElement.Name.LocalName.ToPascal();
            newClass.CollectionProperties.Add(new CollectionProperty
            {
                CollectionName = subClassName + "Collection",
                //PropertyName = subClassName + 
                // (subElement.Name.LocalName.EndsWith("s") ? "es" : "s"),
                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();

        //Determine if element is a property of parent or is a child class
        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 +
                            // (subClass.CollectionType.EndsWith("s") ? "es" : "s"),
                            Name = subClass.CollectionType.Pluralize(),
                            IsXmlAttribute = false,
                            ReturnType = PropertyType.Custom,
                            CustomType = subClass.CollectionType + "Collection",
                            XmlName = subElement.Name.LocalName,
                            XmlItemName = subClass.XmlName
                        });
                    }
                    else
                    {
                        //If Is A Collection Property Already
                        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

C#
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("}"); // End Namespace

    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;//Empty for elements only (not attributes)
        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))
                    {
                        // Apply XmlArray / XmlArrayItem attributes
                        xmlAttribute = string.Format(Constants.XML_COLLECTION_FORMAT, 
                                       property.XmlName, property.XmlItemName);
                    }
                    else
                    {
                        // Use XmlItemName in XmlElement name
                        xmlAttribute = string.Format(Constants.XML_ELEMENT_FORMAT, 
                                                     property.XmlItemName);
                    }
                }
                else
                {
                    // Apply XmlElement attribute
                    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

  1. Click the Load button on the toolbar, select your XML file, and click OK.
  2. Change anything you need to change (class names, return types, etc.).
  3. 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!!!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)