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

Generate Classes From Declarative Code

By , 7 Oct 2004
 

Sample Image - DeclarativeClassGenerator.png

Introduction

I find that I do a lot of object graph modeling directly in XML rather than implementing the classes first or drawing UML diagrams of the classes. The XML modeling approach lets me see the object graph in a more compact tree structure, and I can play with what properties each class should have, etc., until the declarative markup meets the design requirements. Once I have the markup completed, I end up having to code the underlying classes by hand. Over and over again. It finally dawned on me that a quick and dirty generator that would take my sample XML and generate the underlying classes would meet 90% of my needs and save me a lot of typing. That's what this utility does.

Requirements

The code utilizes the MycroXaml declarative parser to generate the GUI which you'll need to download separately if you want to compile the project.

Example

Let's say you have a very simple declarative element in your markup:

<ComplexNumber Real="12.34" Imaginary="45.67"/>

The generator creates the underlying class:

public class ComplexNumber
{
  protected double imaginary;
  protected double real;
  public double Imaginary
  {
    get {return imaginary;}
    set {imaginary=value;}
  }
  public double Real
  {
    get {return real;}
    set {real=value;}
  }
}

If the markup contains a more complicated object graph, such as:

<Database Name="MyDB">
  <Tables>
    <Table Name="Person">
      <Fields>
        <Field Name="ID" PrimaryKey="true"/>
        <Field Name="FirstName" Unique="false"/>
        <Field Name="LastName" Unique="false" Indexed="true"/>
      </Fields>
    </Table>
  </Tables>
</Database>

The generator creates all the classes and their properties:

public class Field
{
  protected bool unique;
  protected string name;
  protected bool primaryKey;
  protected bool indexed;
  public bool Unique
  {
    get {return unique;}
    set {unique=value;}
  }
  public string Name
  {
    get {return name;}
    set {name=value;}
  }
  public bool PrimaryKey
  {
    get {return primaryKey;}
    set {primaryKey=value;}
  }
  public bool Indexed
  {
    get {return indexed;}
    set {indexed=value;}
  }
}

public class Database
{
  protected string name;
  protected ArrayList tables;
  public string Name
  {
    get {return name;}
    set {name=value;}
  }
  public ArrayList Tables
  {
    get {return tables;}
  }
  public Database()
  {
    tables=new ArrayList();
  }
}

public class Table
{
  protected string name;
  protected ArrayList fields;
  public string Name
  {
    get {return name;}
    set {name=value;}
  }
  public ArrayList Fields
  {
    get {return fields;}
  }
  public Table()
  {
    fields=new ArrayList();
  }
}

These classes are now ready to be pasted into your application, and the declarative code can now be used to instantiate the object graph and assign property values.

Things Of Note

The above example highlights some of the features of the generator:

  • It follows the "class-property-class" schema for the declarative code.
  • It assumes that the "property" in the "class-property-class" schema is a collection and implements the property type as an ArrayList. It could also be an abstract class, for example, which is not supported by this generator.
  • If a property in the markup is assigned "true" or "false", the property type is a "bool".
  • If a property value contains only digits, the property type is an "int".
  • If a property value contains digits and a decimal point, the property type is a "double".
  • All other property types are assumed to be "string". Enumerations are not supported.
  • Collection properties only have a "get" method.
  • Collection properties are instantiated in the class constructor.

The Code

The code is quite trivial. Since some people like to see the code in the article, here it is:

using System;
using System.Collections;
using System.IO;
using System.Text;
using System.Windows.Forms;
using System.Xml;

using MycroXaml.Parser;

namespace MyXaml.ClassGenerator
{
  public sealed class ClassInfo
  {
    public string name;
    public Hashtable properties;
    public ArrayList collections;

    public ClassInfo(string name)
    {
      this.name=name;
      properties=new Hashtable();
      collections=new ArrayList();
    }
  }

  public class ClassGenerator
  {
    protected Parser p;
    protected Hashtable classInfoList;

    public static void Main()
    {
      new ClassGenerator();
    }

    public ClassGenerator()
    {
      classInfoList=new Hashtable();
      StreamReader sr=new StreamReader("ClassGen.mycroxaml");
      string text=sr.ReadToEnd();
      sr.Close();
      XmlDocument doc=new XmlDocument();
      doc.LoadXml(text);
      p=new Parser();
      Form form=(Form)p.Load(doc, "ClassGenerator", this);
      Application.Run(form);
    }

    public void OnGenerate(object sender, EventArgs e)
    {
      classInfoList.Clear();
      TextBox src=(TextBox)p.GetInstance("xml");
      TextBox dest=(TextBox)p.GetInstance("code");
    
      XmlDocument doc=new XmlDocument();
      try
      {
        doc.LoadXml(src.Text);
      }
      catch(Exception ex)
      {
        MessageBox.Show(ex.Message);
        return;
      }
      XmlNode root=doc.DocumentElement;
      ProcessClass(root);
      dest.Text=EmitCode();
    }

    protected void ProcessClass(XmlNode node)
    {
      string className=node.Name;
      ClassInfo ci;

      if (classInfoList.Contains(className))
      {
        ci=(ClassInfo)classInfoList[className];
      }
      else
      {
        ci=new ClassInfo(className);
        classInfoList[className]=ci;
      }

      foreach(XmlAttribute attr in node.Attributes)
      {
        string name=attr.Name;
        string val=attr.Value.ToLower();

        // assume a string type
        string ptype="string";

        // if this is a new field, figure out the type
        if (!ci.properties.Contains(name))
        {
          // may be boolean
          if ( (val=="true") || (val=="false") )
          {
            ptype="bool";
          }

          // if it starts with a digit, may be int or double
          if (Char.IsDigit(val, 0))
          {
            ptype="int";
            if (val.IndexOf(".") != -1)
            {
              ptype="double";
            }
          }
          ci.properties[name]=ptype;
        }

        foreach(XmlNode childNode in node.ChildNodes)
        {
          if (!ci.collections.Contains(childNode.Name))
          {
            ci.collections.Add(childNode.Name);
            foreach(XmlNode grandchildNode in childNode.ChildNodes)
            {
              ProcessClass(grandchildNode);
            }
          }
        }
      }
    }

    protected string EmitCode()
    {
      StringBuilder sb=new StringBuilder();

      foreach(DictionaryEntry entry in classInfoList)
      {
        ClassInfo ci=(ClassInfo)entry.Value;
        string cname=ci.name.ToUpper()[0] + ci.name.Substring(1);
        sb.Append("public class "+cname+"\r\n");
        sb.Append("{\r\n");

        // emit properties
        foreach(DictionaryEntry prop in ci.properties)
        {
          string pname=(string)prop.Key;
          string ptype=(string)prop.Value;
          pname=pname.ToLower()[0]+pname.Substring(1);
          sb.Append("\tprotected "+ptype+" "+pname+";\r\n");
        }
        // emit collections
        foreach(string collName in ci.collections)
        {
          string name=collName.ToLower()[0]+collName.Substring(1);
          sb.Append("\tprotected ArrayList "+name+";\r\n");
        }

        // emit get/set for properties
        foreach(DictionaryEntry prop in ci.properties)
        {
          string pname=(string)prop.Key;
          string ptype=(string)prop.Value;
          pname=pname.ToLower()[0]+pname.Substring(1);
          string sname=pname.ToUpper()[0]+pname.Substring(1);
          sb.Append("\tpublic "+ptype+" "+sname+"\r\n");
          sb.Append("\t{\r\n");
          sb.Append("\t\tget {return "+pname+";}\r\n");
          sb.Append("\t\tset {"+pname+"=value;}\r\n");
          sb.Append("\t}\r\n");
        }

        // emit get for collections
        foreach(string collName in ci.collections)
        {
          string name=collName.ToUpper()[0]+collName.Substring(1);
          string collNameSmall=collName.ToLower()[0]+collName.Substring(1);
          sb.Append("\tpublic ArrayList "+name+"\r\n");
          sb.Append("\t{\r\n");
          sb.Append("\t\tget {return "+collNameSmall+";}\r\n");
          sb.Append("\t}\r\n");
        }

        // If class has collections, create a constructor that
        // initializes the ArrayLists.
        if (ci.collections.Count > 0)
        {
          // emit constructor
          sb.Append("\tpublic "+cname+"()\r\n");
          sb.Append("\t{\r\n");
          // for each collection, initialize it in the constructor
          foreach(string collName in ci.collections)
          {
            string name=collName.ToLower()[0]+collName.Substring(1);
            sb.Append("\t\t"+name+"=new ArrayList();\r\n");
          }
          sb.Append("\t}\r\n");
        }
        sb.Append("}\r\n\r\n");
      }
      return sb.ToString();
    }
  }
}

Conclusion

I hope this little utility is helpful for all you declarative programmers out there!

License

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

About the Author

Marc Clifton
United States United States
Member
Marc is the creator of two open source projets, MyXaml, a declarative (XML) instantiation engine and the Advanced Unit Testing framework, and Interacx, a commercial n-tier RAD application suite.  Visit his website, www.marcclifton.com, where you will find many of his articles and his blog.
 
Marc lives in Philmont, NY.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralNice ideamemberPaul Talbot17 Oct '07 - 0:14 
It's not perfect but with a few hours spent under the covers this could be a great little application.
GeneralI Forgotmemberjosenfin28 Nov '06 - 2:41 

In my correction i forgot to write that the code isnt in the same bloc.
 
I think it's better to put my correction
not in the "foreach(XmlAttribute attr in node.Attributes)" bloc
but just after the "foreach(XmlAttribute attr in node.Attributes)" bloc.
 

Sigh | :sigh:
 
josenfin

GeneralRe: I Forgotmemberjosenfin28 Nov '06 - 2:44 
protected void ProcessClass(XmlNode node)
{
string className=node.Name;
ClassInfo ci;
 
if (classInfoList.Contains(className))
{
ci=(ClassInfo)classInfoList[className];
}
else
{
ci=new ClassInfo(className);
classInfoList[className]=ci;
}
 
foreach(XmlAttribute attr in node.Attributes)
{
string name=attr.Name;
string val=attr.Value.ToLower();
 
// assume a string type
string ptype="string";
 
// if this is a new field, figure out the type
if (!ci.properties.Contains(name))
{
// may be boolean
if ( (val=="true") || (val=="false") )
{
ptype="bool";
}
 
// if it starts with a digit, may be int or double
if (Char.IsDigit(val, 0))
{
ptype="int";
if (val.IndexOf(".") != -1)
{
ptype="double";
}
}
ci.properties[name]=ptype;
}
}
 
foreach(XmlNode childNode in node.ChildNodes)
{
if (!ci.collections.Contains(childNode.Name))
{
ci.collections.Add(childNode.Name);
ProcessClass(childNode);
foreach(XmlNode grandchildNode in childNode.ChildNodes)
{
ProcessClass(grandchildNode);
}
}
}
}
 
josenfin

GeneralVery Interesting but....From Paris Near Opéramemberjosenfin27 Nov '06 - 22:53 
Hello from france and sorry for english.
 
I Think there is a problem.
It could be better if you add this line during the ProcessClass method.
because you lose few objects :
 
Your Code :
 
foreach(XmlNode childNode in node.ChildNodes)
{
if (!ci.collections.Contains(childNode.Name))
{
ci.collections.Add(childNode.Name);
foreach(XmlNode grandchildNode in childNode.ChildNodes)
{
ProcessClass(grandchildNode);
}
}
}
 
My Correction :
 
foreach(XmlNode childNode in node.ChildNodes)
{
if (!ci.collections.Contains(childNode.Name))
{
ci.collections.Add(childNode.Name);
// Here
ProcessClass(childNode);
// ----
foreach(XmlNode grandchildNode in childNode.ChildNodes)
{
ProcessClass(grandchildNode);
}
}
}
 
Very good idea. thanks a lot !

 
josenfin

GeneralGreat MindsmemberBoneSoft3 Jul '06 - 11:37 
I had a similar idea a while back. But I started with my own little language that could represent most .Net code constructs. But after dealing with a lot of XML that I didn't really want to parse through with XPath queries, I decided to do something similar to this. XSD.exe has always bothered me, I'm picky about code format, and I hate the public fields that it produces. So I built a tool to generate code from XML. Both approaches evolved into fairly robust solutions that I've packaged into one product.
 
Most of the time, I know almost exactly how I want my model to be, just dread typing all the properties etc... I don't want to blatantly plug my product, but I would encourage you to take a look at it since you are like-minded on the subject. Since you obviously approach modeling in a similar fashion, I'd really value your feed back. The link in my auto-sig will get you to my site.
 


Try code model generation tools at BoneSoft.com.
GeneralXSD.exememberx0n14 Oct '04 - 4:13 
What about using the visual studio included "XSD /classes" command line utility? It does pretty much the same thing (afaict) and also emits attributes to ensure round trip (de)serialization if you ever wanted to spit back the original XML? it understands pretty much all of schema, and you can use xml hints to change the output class formation too.
 
- Oisin
 

 

GeneralRe: XSD.exemembernickvh28 Apr '08 - 14:42 
i agree - why write/work on all this code when xsd will do most if not all of what you have here?
GeneralError running the examplememberTommaso Caldarola14 Oct '04 - 0:37 
I get this exception:
 
An unhandled exception of type 'System.Exception' occurred in system.dll
 
Additional information: 5, 5 is not a valid value for Int32.
 
bai
t
 

GeneralRe: Error running the examplememberMichael P Butler14 Oct '04 - 1:06 
At a guess. The code doesn't handle Italian regional settings for numbers. Marc's code is using '.' to determine whether a number is an int or a floating point number. but you are using a comma.
 
I suespect, code will be needed to determine regional settings and do the correct conversion.
 
Michael

CP Blog [^]

GeneralRe: Error running the examplememberTommaso Caldarola14 Oct '04 - 2:23 
I have added MyAxmlparser project to debug and the error occurs in the ProcessAttributes() method
 
if ( pvalue.StartsWith("{") && pvalue.EndsWith("}") )
{
//...
}
else
{
// it's string, so use a type converter.
TypeConverter tc=TypeDescriptor.GetConverter(pi.PropertyType);
if (tc.CanConvertFrom(typeof(string)))
{
object val=tc.ConvertFrom(pvalue); //<---- *** ERROR ***
 
tc is PointConverter and pvalue is "5, 5" namely the value of location.

 
bai
t

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

Permalink | Advertise | Privacy | Mobile
Web01 | 2.6.130523.1 | Last Updated 7 Oct 2004
Article Copyright 2004 by Marc Clifton
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid