Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Declaratively Populating A Property Grid

0.00/5 (No votes)
23 Sep 2004 1  
Runtime class generation to declaratively populate a property grid.

Sample Image - propertyGrid.png

Introduction

I've been looking at how to program a property grid declaratively, mainly because I don't want to create hard-coded classes just so I can use a property grid for some configuration settings which will most likely require changes as the project evolves. So I looked at what's involved in managing a property grid. This article illustrates the following:

  • declarative programming of a property grid
  • binding the container data to a control
  • firing an event when a property changes
  • serializing/deserializing the container

This article uses the code previously discussed in the following articles:

How Does It Work?

The PropertyGrid provides a SelectedObject method which you set an instance which you'd like to use the PropertyGrid to expose public properties for editing. Obviously, this requires that a class exists along with an instance of that class, so that not only can the PropertyGrid determine the public properties but also the property values can be updated when the user makes changes. Approaching this problem declaratively requires that a class be constructed, compiled, and instantiated at runtime. This MxContainer class does exactly this--using declarative XML and the MycroXaml (or MyXaml) parser, it instantiates a class based on attributes defined in the markup.

One of the features of the PropertyGrid is that you can decorate the property methods with attributes that the PropertyGrid uses to alter the behavior of the property when displayed. The MxContainer class provides the MxProperty class whose instances are added to the MxProperties collection. The MxProperty class supports the following property decorators:

  • Category
  • Description
  • ReadOnly
  • DefaultValue
  • DefaultProperty (class decorator)

Besides handling basic types like string, DateTime, Font, Color, and bool, the PropertyGrid also knows how to display the members of an enumeration. The MxContainer class supports enumerations by adding MxEnum instances to its MxEnumerations collections. These generate enumerations in the emitted code, which in turn can be applied as a property type.

The MxContainer class also implements an OnValueChanged event for each MxProperty instance. This event, when assigned to a handler, is automatically converted to a specific OnXxxChanged event, where "Xxx" is the specific property name.

The Declarative Markup

The above screenshot is created with the following declarative XML:

<?xml version="1.0" encoding="utf-8"?>
<MycroXaml Name="Form"
  xmlns:wf="System.Windows.Forms, System.Windows.Forms, Version=1.0.5000.0,
           Culture=neutral, PublicKeyToken=b77a5c561934e089"
  xmlns:mc="MycroXaml.MxContainer, MycroXaml.MxContainer">

  <wf:Form Name="AppMainForm" StartPosition="CenterScreen"
      ClientSize="600, 400" Text="Declarative Property Grid Demo">

    <mc:MxContainer Name="Container" DefaultProperty="OutputDirectory">
      <mc:MxEnumerations>
        <mc:MxEnum Name="MyEnum" Values="Executable, Library, Console"/>
      </mc:MxEnumerations>
      <mc:MxProperties>
        <mc:MxProperty Name="Font" Type="Font"
          Category="Editor" Description="The text editor font."
                   InitialValue="new Font(&quot;MS Sans Serif&quot;, 10)"/>

        <mc:MxProperty Name="Color" Type="Color" InitialValue="Color.Green"
            Category="Editor" Description="The text editor color."/>

        <mc:MxProperty Name="OutputDirectory" Type="string"
               InitialValue="&quot;c:\\test&quot;"
               DefaultValue="&quot;c:\\test&quot;"
            Category="Project Settings" Description="Output directory."
                      OnValueChanged="OnOutputDirectoryChanged"/>

        <mc:MxProperty Name="Exclude" Type="bool"
            Category="Project Settings"
            Description="Exclude this project from the build."
            InitialValue="true" DefaultValue="true"/>

        <mc:MxProperty Name="OutputType" Type="MyEnum"
               Category="Project Settings" Description="Project output type."
               DefaultValue="MyEnum.Executable"/>

        <mc:MxProperty Name="CreatedOn" Type="DateTime"
               InitialValue="DateTime.Today" ReadOnly="true"
            Category="Project Information" Description="Project start date."/>

        <mc:MxProperty Name="LastModifiedOn" Type="DateTime"
               InitialValue="DateTime.Today" ReadOnly="true"
               Category="Project Information"
               Description="Project last changed date."/>

      </mc:MxProperties>
    </mc:MxContainer>

    <wf:Controls>
      <wf:PropertyGrid Name="PropertyGrid" Dock="Fill"
             SelectedObject="{Container}" HelpBackColor="LightSteelBlue"/>
      <wf:TextBox Name="TextBox" Dock="Left" Text="Foobar" Multiline="true"
             Width="300" BorderStyle="Fixed3D">
        <wf:DataBindings>
          <mc:DataBinding PropertyName="ForeColor" DataSource="{Container}"
                 DataMember="Color"/>
          <mc:DataBinding PropertyName="Font" DataSource="{Container}"
                 DataMember="Font"/>
        </wf:DataBindings>
      </wf:TextBox>
      <wf:Panel Dock="Top" Height="40">
        <wf:Controls>
          <wf:Button Text="Serialize" Location="10, 10" Size="80, 25"
                        FlatStyle="System" Click="OnSerialize"/>
          <wf:Button Text="Deserialize" Location="100, 10" Size="80, 25"
                        FlatStyle="System" Click="OnDeserialize"/>
        </wf:Controls>
      </wf:Panel>
    </wf:Controls>
  </wf:Form> 
</MycroXaml>

The salient features of the above markup are:

  • support for enumerations
  • the construction of a runtime container
  • defining attribute decorators for the constructed class' properties
  • use of "inline" code to initialize values
  • data binding to alter TextBox properties automatically

Implementation

The core of the work is done by constructing a runtime class and instantiating it. The implementation is actually rather mundane--iterating through several collections to create the C# code, then invoking the compiler class to compile it. I didn't use Reflection.Emit because I'm not familiar with it and I like to inspect the constructed code visually for errors.

MxContainer

MxContainer constructs a class based on the XML definition. Note the following code:

sb.Append("\tpublic class "+className+" : MxDataContainer\r\n");

Notice how this class is derived from MxDataContainer. This is so that the application can interact with the container through Set/GetValue methods since the application, at compile time, has no type information regarding the runtime constructed class. These and other methods support the automatic transfer of data from the control to the container and from the container to the control. Some of the code in this class can probably leverage data binding better than I have done, but this implementation does work.

Runtime Compiler

Once the class has been constructed, it is compiled:

RunTimeCompiler rtc=new RunTimeCompiler();
ArrayList refs=new ArrayList();
refs.Add("System.dll");
refs.Add("System.Data.dll");
refs.Add("System.Drawing.dll");
refs.Add("MycroXaml.MxContainer.dll");
Assembly assembly=rtc.Compile(refs, "C#", sb.ToString(), String.Empty);
Trace.Assert(assembly != null, "Compiler errors");
refObj=(MxDataContainer)
  Activator.CreateInstance(assembly.GetModules(false)[0].GetTypes()[0]);

The problem here is anticipating the kinds of property types that the programmer might be using. Ideally, an assemblies collection should be added to the container so that the referenced assemblies can be defined declaratively rather than hard-coded. However, I've found that for all practical purposes, the above implementation works just fine, so I've never changed it!

The OnValueChanged Event

In the markup, each MxProperty supports an OnValueChanged event which can be wired up to an event handler. However, you can't use the same OnValueChanged event for each property in the constructed class. Rather, each property needs its own OnXxxChanged event handler, and the event assigned in the MxProperty tag has to be re-assigned to the constructed property event handler. This is done with:

EventInfo ei=refObj.GetType().GetEvent("On"+prop.Name+"Changed");
// there can be only one delegate

Delegate srcDlgt=prop.ValueChangedDelegates[0];
Delegate dlgt=Delegate.CreateDelegate(ei.EventHandlerType, srcDlgt.Target,
              srcDlgt.Method.Name);
ei.AddEventHandler(refObj, dlgt);

where "prop" is an MxProperty instance and ValueChangedDelegates is a property of MxProperty, returning the invocation list for the OnValueChanged event:

public Delegate[] ValueChangedDelegates
{
  get {return OnValueChanged.GetInvocationList();}
}

We're only interested in the first instance of the invocation list, since only one instance can be assigned declaratively. The above code thus illustrates how to copy an event handler from one event to another.

DataBinding

Databinding has to be done with a helper class because the Binding class does not support a default constructor. So, the DataBinding helper class looks like this:

public class DataBinding : ISupportInitialize, MycroXaml.Parser.IMycroXaml
{
  protected string propertyName;
  protected object dataSource;
  protected string dataMember;
  protected ControlBindingsCollection cbc;
  protected Binding binding;

  public String PropertyName
  {
    get{return propertyName;}
    set {propertyName=value;}
  }

  public object DataSource
  {
    get {return dataSource;}
    set {dataSource=value;}
  }

  public String DataMember
  {
    get {return dataMember;}
    set {dataMember=value;}
  }

  public void Initialize(object parent)
  {
    cbc=parent as ControlBindingsCollection;
  }

  public object ReturnedObject
  {
    get {return binding;}
  }

  public void BeginInit()
  {
  }

  public virtual void EndInit()
  {
    try
    {
      binding=new Binding(propertyName, dataSource, dataMember);
      if (dataSource is MycroXaml.MxContainer.MxDataContainer)
      {
        ((MycroXaml.MxContainer.MxDataContainer)dataSource).Add(cbc.Control, binding);
      }

      // do manually, as the Add method is declared as a "new" method,

      // causing confusion on reflection.

      cbc.Add(binding);
    }
    catch(Exception e)
    {
      Trace.Fail(e.Message);
    }
  }
}

Notice how this class uses ISupportInitialize to instantiate the Binding class once all the property values have been assigned, and it also manually adds the resulting Binding instance to the ControlBindingsCollection. This cannot be done in the parser because the Add method is declared with a "new", overwriting the base class implementation. Since the method definitions are the same, acquiring the Add method using reflection results in ambiguous methods. This is something the parser cannot easily handle.

Serialization/Deserialization

A configuration class, runtime generated or not, is pretty useless unless you can save the configuration and restore it later. As discussed in my article on simple serialization, a runtime constructed class cannot take advantage of the XmlSerializer or BinaryFormatter classes since the constructed class' type information is not known at compile time. Serialization is thus done with the simple serializer/deserializer I previously wrote. After deserialization, the controls to which the container properties are bound have to be updated manually (at least, I've not figured out how to do it automatically, as it probably has to do with not supporting the correct OnXxxChanged construct--further investigation is needed). The lines:

propertyGrid.Refresh();
container.BeginEdit(); // rebind controls with the new values

cause an update of the PropertyGrid with the new values and update all data bound controls. (BeginEdit is probably not the best name for this method, but the intention is to ensure that the controls are all updated with the latest container values before the user begins editing those values.)

Wrapping It All Together

So, what does the code look like that actually constructs the above UI? How does it all come together?

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text;
using System.Windows.Forms;
using System.Xml;

using Mtc.SimpleSerializer;

using MycroXaml.Parser;
using MycroXaml.MxContainer;

namespace PropertyGridDemo
{
  public class Startup
  {
    protected MxDataContainer container;
    protected PropertyGrid propertyGrid;

    [STAThread]
    static void Main() 
    {
      new Startup();
    }

    public Startup()
    {
      Parser mp=new Parser();
      StreamReader sr;
      string text;
      XmlDocument doc;

      sr=new StreamReader("propertyGrid.xml");
      text=sr.ReadToEnd();
      sr.Close();
      doc=new XmlDocument();
      try
      {
        doc.LoadXml(text);
      }
      catch(Exception e)
      {
        Trace.Fail("Malformed xml:\r\n"+e.Message);
      }

      Form form=(Form)mp.Load(doc, "Form", this);
      container=(MxDataContainer)mp.GetInstance("Container");
      propertyGrid=(PropertyGrid)mp.GetInstance("PropertyGrid");
      form.ShowDialog();
    }

    public void OnSerialize(object sender, EventArgs e)
    {
      Serializer s=new Serializer();
      s.Start();
      s.Serialize(container);
      string text=s.Finish();
      StreamWriter sw=new StreamWriter("data.xml");
      sw.Write(text);
      sw.Close();
}

    public void OnDeserialize(object sender, EventArgs e)
    {
      StreamReader sr=new StreamReader("data.xml");
      string text=sr.ReadToEnd();
      sr.Close();
      Deserializer d=new Deserializer();
      d.Start(text);
      d.Deserialize(container, 0);
      propertyGrid.Refresh();
      container.BeginEdit(); // rebind controls with the new values

    }

    public void OnOutputDirectoryChanged(object sender,
                                ContainerEventArgs cea)
    {
      // some event handler activity

    }
  }
}

That's it! The MycroXaml parser handles the instantiation of the object graph, the MycroXaml.MxContainer assembly handles the runtime construction of the container, and the SimpleSerializer assembly handles serialization/deserialization of the container. The UI and property grid container have been moved over to declarative code, so all that the imperative code has to do is fire things up and handle the events.

Conclusion

I find that using a declarative approach to construct custom containers suitable for manipulation with a PropertyGrid is a simple and flexible approach. Combined with the ease of setting up data binding, event handling, and serialization, the classes presented here should save time by providing a general and flexible solution. Personally, I find that separating declarative program elements from imperative ones to be a very powerful mechanism to deal with the realities of software engineering--not enough design time, feature requests/changes during implementation, and the unknown territory (!) of maintenance and enhancements.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here