Export (0) Print
Expand All

Metadata-Driven User Interfaces

 

John deVadoss
Microsoft Corporation

July 2005

Applies to:
   Enterprise Architecture
   User interface (UI)

Summary: This article explains how to dynamically create Windows Forms from metadata, and to also execute the forms while allowing for the customization of client-side logic. (31 printed pages)

Contents

Introduction
Prerequisites
Design Elements
Design-Time Implementation
Conclusion

Introduction

Many business applications require the user interface (UI) to be extensible as the requirements vary from one customer to another and from one end user to another. Client-side business logic for the UI may also need customization based on individual user need. A screen layout for a user might be different from another user. This may include control position, visibility, and so forth. Or, there may be various UIs for various devices such as SmartPhones, Tablet devices, or PDAs. Client-side business logic customization also includes customizing validation rules, changing control properties, and other modifications. For example, a manager may have different options for deleting and moving files than a subordinate has.

There are many techniques for enabling business applications to be extensible or customizable. Most applications solve this problem by storing customizable items such as UI layout and client-side business logic as metadata in a repository. This metadata can then be interpreted by a run-time engine to display the screen to users and to execute the client-side business logic when the user performs an action on the screen.

The advantages of this approach are twofold. First, you do not need to redeploy components on the presentation layer as the customization is done in a central repository. Second, this requires only a very light client installation—that is, you only need to deploy the run-time engine to the client machine.

This article describes this technique using the Microsoft .NET Framework. To be specific, this article explains how to dynamically create Windows Forms from metadata, and to also execute the forms while allowing for the customization of client-side logic.

Prerequisites

  • Knowledge about writing Visual Studio Add-Ins
  • Use of the DTE object model

Design Elements

There are essentially three design elements to implementing metadata-driven User interfaces.

  • The first is to design the schema for the metadata and decide on the repository storage mechanism. Repository storage could be a relational database (RDBMS) such as SQL Server, or any other store such as XML file. In this article, we will be using an XML file as a repository for storing metadata.
  • After we decide the metadata schema, we implement a design time environment in which the user (usually a developer or customization person) can create and modify the metadata for a specific screen. In this article, we will piggyback on Microsoft Visual Studio Windows Forms Designer to implement our design-time environment.
  • Finally, we design and implement the runtime engine.

Repository Structure

First of all, we need to decide what should be stored in the metadata. As mentioned before, metadata should contain all of the customizable items. For instance, if the customer wants to customize the screen layout and client-side validation logic, then the metadata should contain information about the UI layout that includes the controls placed on the screen, their properties, and the position in which they are placed. Metadata should also contain the client-side business logic.

Here is a sample XML file that contains UI information:

<?xml version="1.0" encoding="utf-8"?>
<form xmlns:xsd=http://www.w3.org/2001/XMLSchema
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-
instance" name="Form1" xmlns="http://tempuri.org/
MetadataForm.xsd">
  <controls>
    <control type="System.Windows.Forms.Form">
      <position>
            <height>224</height>
            <width>300</width>
            <top>13</top>
            <left>13</left>
      </position>
      <controls>
           <control type="System.Windows.Forms.Button">
               <position>
                   <height>23</height>
                   <width>75</width>
                   <top>144</top>
                   <left>160</left>
               </position>
               <controls />
               <name>button2</name>
               <text>button2</text>
               <tabIndex>2</tabIndex>
               <event-handlers />
           </control>
           <control type="System.Windows.Forms.Button">
               <position>
                  <height>23</height>
                  <width>75</width>
                  <top>144</top>
                  <left>64</left>
              </position>
              <controls />
              <name>button1</name>
              <text>button1</text>
              <tabIndex>1</tabIndex>
              <event-handlers>
                   <eventInfo eventName=
"Click" eventHandler="button1_Click" />
                   <eventInfo eventName=
"EnabledChanged" eventHandler="button1_
EnabledChanged" />
              </event-handlers>
           </control>
           <control type="System.Windows.Forms.TabControl">
               <position>
                   <height>100</height>
                   <width>200</width>
                   <top>24</top>
                   <left>48</left>
               </position>
               <controls>
                    <control type="System.Windows.Forms.TabPage">
                          <position>
                                <height>74</height>
                                <width>192</width>
                                <top>22</top>
                                <left>4</left>
                          </position>
                    <controls>
                         <control type="System.Windows.Forms.Button">
                               <position>
                                    <height>23</height>
                                    <width>56</width>
                                    <top>24</top>
                                    <left>112</left>
                              </position>
                              <controls />
                              <name>button3</name>
                              <text>button3</text>
                              <tabIndex>2</tabIndex>
                              <event-handlers>
                                     <eventInfo eventName=
"Click" eventHandler="button3_Click" />
                              </event-handlers>
                         </control>
                         <control type="System.Windows.Forms.TextBox">
                                <position>
                                     <height>20</height>
                                     <width>64</width>
                                     <top>40</top>
                                     <left>16</left>
                                </position>
                                <controls />
                                <name>textBox2</name>
                                <text>textBox2</text>
                                <tabIndex>1</tabIndex>
                                <event-handlers />
                         </control>
                         <control type="System.Windows.Forms.TextBox">
                                <position>
                                      <height>20</height>
                                      <width>64</width>
                                      <top>8</top>
                                      <left>16</left>
                                </position>
                                <controls />
                                <name>textBox1</name>
                                <text>textBox1</text>
                                <tabIndex>0</tabIndex>
                                <event-handlers />
                         </control>
                   </controls>
                   <name>tabPage1</name>
                   <text>tabPage1</text>
                   <tabIndex>0</tabIndex>
                   <event-handlers />
               </control>
               <control type="System.Windows.Forms.TabPage">
                   <position>
                       <height>74</height>
                       <width>192</width>
                       <top>22</top>
                       <left>4</left>
                   </position>
                   <controls>
                         <control type="System.Windows.Forms.LinkLabel">
                              <position>
                                    <height>23</height>
                                    <width>120</width>
                                    <top>24</top>
                                    <left>24</left>
                              </position>
                              <controls />
                              <name>linkLabel1</name>
                              <text>linkLabel1</text>
                              <visible>false</visible>
                              <tabIndex>0</tabIndex>
                              <event-handlers>
                                         <eventInfo
 eventName="LinkClicked" eventHandler=
"linkLabel1_LinkClicked" />
                              </event-handlers>
                   </control>
              </controls>
              <name>tabPage2</name>
              <text>tabPage2</text>
              <visible>false</visible>
              <tabIndex>1</tabIndex>
              <event-handlers />
            </control>
          </controls>
          <name>tabControl1</name>
          <text />
          <tabIndex>0</tabIndex>
          <event-handlers />
        </control>
      </controls>
      <name>Form1</name>
      <text>Form1</text>
      <tabIndex>0</tabIndex>
      <event-handlers>
        <eventInfo eventName="Load" eventHandler="Form1_Load" />
      </event-handlers>
    </control>
  </controls>
  <script>
    <events eventHandler="linkLabel1_LinkClicked">
      <content>      private void linkLabel1_LinkClicked
(object sender, System.Windows.Forms.
LinkLabelLinkClickedEventArgs e)
      {
      
      }</content>
      <type>text</type>
    </events>
    <events eventHandler="Form1_Load">
      <content>      private void Form1_Load(object sender, System.EventArgs e)
      {
         textBox1.Text = " " ;
         textBox2.Text = " " ;
      }</content>
      <type>text</type>
    </events>
    <events eventHandler="button1_Click">
      <content>      private void button1_Click(object sender, System.EventArgs e)
      {
         textBox1.Text = "Button 1 Clicked" ;
      }</content>
      <type>text</type>
    </events>
    <events eventHandler="button1_EnabledChanged">
      <content>      private void button1_Enabled
Changed(object sender, System.EventArgs e)
      {
      
      }</content>
      <type>text</type>
    </events>
    <events eventHandler="button3_Click">
      <content>      private void button3_Click(object sender, System.EventArgs e)
      {
         Button3.Enabled = true ;
         textBox1.Text = "Button 3 Clicked" ;
      }</content>
      <type>text</type>
    </events>
  </script>
</form>

Let's look at the content of the XML. The form root tag implies that the metadata contains information regarding a screen or dialog box. Underneath the form element are the controls element and the script element.

The controls element contains information about the controls embedded inside the form and is the container for zero or more control elements. The control elements represent a control and contain its properties such as name of the control, relative position to place the control, caption text, and the tab order. The control element also has a collection of events and event-handlers for the screen element. If a control is a container of child controls, this information is held in the controls element. The control element has a type attribute whose value is the fully qualified common language runtime (CLR) type name of the control. This information is needed by the run-time engine to create the type at runtime.

The script element is the container node for the client-side business logic, holding the event handler name and the code definition for it. When the form is recreated, this code will be integrated into the code page for the form.

In the sample, we will be using XML serialization to convert repository XML into a concrete CLR type. This CLR type is generated using the xsd.exe tool. The XML Schema for the repository is given below.

<?xml version="1.0" encoding="utf-8" ?>                        
<xs:schema id="MetadataForm"
 targetNamespace=http://tempuri.org/MetadataForm.xsd
          elementFormDefault="qualified"
                        
   xmlns= http://tempuri.org/
MetadataForm.xsd                   
xmlns:mstns= http://tempuri.org/MetadataForm.xsd                     
   xmlns:xs="http://www.w3.org/2001/XMLSchema">
                     
<!-- Definition for MetadataForm  -->                     
   <xs:complexType name="MetadataForm">                     
      <xs:all>                              
         <xs:element name="controls" type=
"MetadataControls" minOccurs="1"
                                                       
maxOccurs="1" />                  
         <xs:element name="script" type=
"MetadataScript" minOccurs="1"
          maxOccurs="1" />                           
      </xs:all>                           
      <xs:attribute name="name" type="xs:string" use="required" />         
   </xs:complexType>                              <!-- Definition for MetadataControls  -->                     
   <xs:complexType name="MetadataControls">                  
      <xs:sequence>                           
         <xs:element name="control" type=
"MetadataControl" minOccurs="0"
          maxOccurs="unbounded"></xs:element>                     
      </xs:sequence>                           
   </xs:complexType>                           
            <!-- Definition for MetadataControl  -->
                           <xs:complexType name=
"MetadataControl">                        
<xs:all>                           
         <xs:element name="position" type="Position" />            
         <xs:element name="controls" type="MetadataControls" />         
         <xs:element name="name" type="xs:string" />            
         <xs:element name="text" type="xs:string" />            
         <xs:element name="visible" type="xs:boolean" default="true" />      
         <xs:element name="tabIndex" type="xs:int" />            
         <xs:element name="event-handlers" type="AllEvents" />         
      </xs:all>                           
      <xs:attribute name="type" type="xs:string" />               
   </xs:complexType>
                              
<!-- Definition for Events  -->
                              <xs:complexType name=
"AllEvents">                  
      <xs:sequence>                           
         <xs:element name="eventInfo" type=
"EventInfo" minOccurs ="0" maxOccurs    =
"unbounded" ></xs:element>                           
      </xs:sequence>                           
   </xs:complexType>                           
   <xs:complexType name="EventInfo">                     
         <xs:attribute name="eventName" type="xs:string"></xs:attribute>      
         <xs:attribute name="eventHandler" type="xs:string"></xs:attribute>      
   </xs:complexType>                              <!-- Definition for Position  -->                        
   <xs:complexType name="Position">                     
      <xs:sequence>                           
         <xs:element name="height" type="xs:int"></xs:element>         
         <xs:element name="width" type="xs:int"></xs:element>         
         <xs:element name="top" type="xs:int"></xs:element>         
         <xs:element name="left" type="xs:int"></xs:element>         
      </xs:sequence>                           
   </xs:complexType>                           
   <!-- Definition for MetadataScript  -->
                        <xs:complexType name=
"MetadataScript">                     
      <xs:sequence>                           
         <xs:element name="events" type=
"eventItem" minOccurs="0"       maxOccurs=
"unbounded" />                           
      </xs:sequence>                           
   </xs:complexType>
                              
<!-- Definition for contentType -->                           
<xs:simpleType name="contentType">                     
      <xs:restriction base="xs:string">                     
         <xs:enumeration value="text" />                  
         <xs:enumeration value="binary" />               
      </xs:restriction>
                              
</xs:simpleType>                              
<!-- Definition for eventItem -->
                              <xs:complexType name=
"eventItem">                  
      <xs:all>                              
         <xs:element name="content" type="xs:string" />            
         <xs:element name="type" type="contentType" />            
      </xs:all>                           
      <xs:attribute name="eventHandler" type="xs:string" />            
   </xs:complexType>                           
   <xs:element name="form" type="MetadataForm" />                  
</xs:schema>                                 

When we run the command XSD.exe /c on the above .xsd file, it creates the CLR type called MetadataForm. The source file for this type is located at $SOLUTIONDIR\MetadataLibrary\MetadataForm.xsd.

The target file is located at $SOLUTIONDIR\MetadataLibrary\MetadataForm.cs.

Here is how the MetadataForm class looks:

[System.Xml.Serialization.XmlTypeAttribute
(Namespace="http://tempuri.org/MetadataForm.xsd")]   
[System.Xml.Serialization.XmlRootAttribute
("form", Namespace="http://tempuri.org/MetadataForm.xsd",
    IsNullable=false)]                              
   public class MetadataForm                            
   {                                 
          /// <remarks/>                           
      [System.Xml.Serialization.XmlArrayItemAttribute
("control", IsNullable=false)]      
      public MetadataControl[] controls;                         
      /// <remarks/>                           
      [System.Xml.Serialization.XmlArrayItemAttribute
("events", IsNullable=false)]      
      public eventItem[] script;                            
      /// <remarks/>                           
      [System.Xml.Serialization.XmlAttributeAttribute()]               
      public string name;                        
                                    
      public MetadataForm ()                        
      {                              
         controls = null;                        
         script = null;                        
         name = null;                        
      }                              
      public MetadataForm(ArrayList ctrlList,
 ArrayList evtList, string formName)      
      {                                       
name = formName;                     
         controls = new MetadataControl[ctrlList.Count];            
         script = new eventItem[evtList.Count];               
                                    
         int cnt = 0;                        
         if(ctrlList.Count > 0)                     
         {                           
            foreach(object ctrl in ctrlList)               
            {                        
               controls[cnt] = (MetadataControl)ctrl;         
               cnt++;                     
            }                        
         }                           
         if(evtList.Count > 0)                     
         {                           
            cnt = 0;                                    foreach(object evt in evtList)               
            {                        
               script[cnt] = (eventItem)evt;            
               cnt++;                     
            }                        
         }                           
      }                              
   }                                 

The MetadataForm class contains an array of MetadataControl objects and an array of eventItem objects along with the name of the form.

Following is the definition of the eventItem class:

/// <remarks/>                                 
[System.Xml.Serialization.XmlTypeAttribute
(Namespace="http://tempuri.org/MetadataForm.xsd")]   
   public class eventItem                            
   {                                     
      /// <remarks/>                           
      public string content;                            
      /// <remarks/>                           
      public contentType type;                            
      /// <remarks/>                           
      [System.Xml.Serialization.Xml
AttributeAttribute()]                     
public string eventHandler;                     
      public eventItem()                              {                              
         eventHandler = content = null;                  
         type = contentType.text ;                     
      }                                    
public eventItem(string eventHandler, string content,
 contentType cType )      
      {                              
         this.eventHandler = eventHandler;                  
         this.content = content;                     
         this.type = cType;                     
      }                              
   }                                       

Every eventItem object contains the name of the event handler for which code is being stored. The code itself is kept in the content element in text or binary form defined by contentType enum. The present code supports storing the content in text form only; however, the solution can be extended to support storing the code in binary form.

   /// <remarks/>
      public enum contentType {
   
   /// <remarks/>
    text,
    
       /// <remarks/>
              binary,
    }

The MetadataControl type is defined as:

/// <remarks/>                                 
[System.Xml.Serialization.XmlTypeAttribute
(Namespace=http://tempuri.org/MetadataForm.xsd   ]   
   public class MetadataControl                        
   {                                 
      /// <remarks/>                           
      public Position position;                        
      /// <remarks/>                           
      [System.Xml.Serialization.Xml
ArrayItemAttribute("control", IsNullable=false)]      
      public MetadataControl[] controls;                     
      /// <remarks/>                           
      public string name;                        
      /// <remarks/>                           
      public string text;                            
      /// <remarks/>                           
      [System.ComponentModel.DefaultValueAttribute(true)]            
      public bool visible = true;                        
      /// <remarks/>                           
      public int tabIndex;                        
      /// <remarks/>                           
      [System.Xml.Serialization.XmlArrayItemAttribute
("eventInfo", IsNullable=false)]   
      public EventInfo[] event-handlers;                     
      /// <remarks/>                           
      [System.Xml.Serialization.XmlAttributeAttribute()]               
      public string type;                        
                                    
      public MetadataControl()                        
      {                              
         position = new Position ();                     
         tabIndex = 0;                        
         type = name = text = null;                  
         visible = true;                        
         event-handlers = null;                     
         controls = null;      public MetadataControl(string name,
 string text, bool visible, int tabIndex,
   string  type,     int ht, int wd, int top,
 int left ,ArrayList evts )                     
      {                              
         this.name = name ;                     
         this.text = text ;                        
         this.visible = visible;                     
         this.tabIndex = tabIndex;                     
         this.type = type;                        
         position = new Position (ht,wd,top,left);               
         event-handlers = new EventInfo[evts.Count];            
                                             int cnt = 0;                        
         if(evts.Count > 0)                     
         {                           
            foreach(object evt in evts)                  
            {                        
               event-handlers[cnt] = (EventInfo)evt;         
               cnt++;                     
            }                        
         }                           
      }                              
   }                                 

Finally, the Position type is defined as:

/// <remarks/>                                 
[System.Xml.Serialization.XmlTypeAttribute
(Namespace=http://tempuri.org/MetadataForm.xsd   )]   
   public class Position                            
   {                                 
      /// <remarks/>                           
      public int height;                           
      /// <remarks/>                           
      public int width;                               
      /// <remarks/>                                 public int top;                               
      /// <remarks/>                           
      public int left;                           
                                          public Position()                           
      {                              
         height = width = top = left = 0;                  
      }                              
      public Position( int ht, int wd, int top, int left)               
      {                              
         height = ht;                        
         width = wd;                        
         this.top = top;                        
         this.left = left;                        
      }                              
   }                                              

The MetadataControl class holds the control information for all controls in the design form. The form's properties are held at the root and all other controls are part of its controls array. Each control's name, text, tab order, visibility, position, and type are collected. Also, if a control is a parent to some other controls, the child control's information is held in the controls array. For every control, the events it raises and the event handlers for them are kept in the event-handlers array. This is an array of EventInfo objects.

EventInfo type is defined as:

/// <remarks/>                                 
[System.Xml.Serialization.XmlTypeAttribute
(Namespace="http://tempuri.org/MetadataForm.xsd")]   
   public class EventInfo                            
   {                                     
      /// <remarks/>                           
      [System.Xml.Serialization.XmlAttributeAttribute()]               
      public string eventName;                            
      /// <remarks/>                           
      [System.Xml.Serialization.XmlAttributeAttribute()]               
      public string eventHandler;                     
      public EventInfo()                        
      {                              
         eventName = null;                     
         eventHandler = null ;                     
      }                              
      public EventInfo(string eventName, string eventHandler)            
      {                              
         this.eventName = eventName;                  
         this.eventHandler = eventHandler;                        }                              
   }                                 

Design-Time Implementation

We implement the design-time environment as a Visual Studio.Net Add-In component. Please refer to the article Custom Add-Ins Help You Maximize the Productivity of Visual Studio .NET for more information on how to develop an add-in.

Why Design-Time?

You need the design-time tool to provide a graphical interface for manipulating the raw XML repository store. This tool will be used in two stages:

  • During initial development of the screen.
  • During the customization phase.

The tool should provide a graphical designer to manipulate the UI layout, such as adding controls to the screen, manipulating control's properties, resizing the screen size, and so forth. It should allow the adding of events and event-handlers to the controls. From the user's perspective, this tool should abstract the metadata structure, where it is stored, and how it is stored.

Here we will not delve too deeply into the implementation of Visual Studio add-ins or client-side scripting. It is expected that you have a good understanding of how Visual Studio add-ins work. The source code for the sample demonstrates how we implemented our Visual Studio add-in.

Designer Implementation

Use the Windows Forms Designer to design the form layout and hook events to controls. The Visual Studio add-in programming model lets us access the window objects that are currently opened in the Visual Studio environment. It also lets us query whether the window is a designer window by checking if the window object is derived from an interface called IDesignerHost.

Once you install the add-in there will be a menu item for it under Tools.

ms954610.metadriveui_fig1(en-us,MSDN.10).gif

Figure 1. The add-in under the Tools menu

Upon initialization, the design time add-in component adds a set of menu items under Tools. Now, the Tools should look like this.

ms954610.metadriveui_fig2(en-us,MSDN.10).gif

Figure 2. The expanded Tools menu after add-in initialization

These are summarized in the following table.

Table 1. Summary of add-in-enabled menu items

Menu ItemAction
AttachObtains IDesignerHost interface pointer from the active document. The active document should be a windows forms designer.

Stores the IDesignerHost pointer in a private variable for further use.

NewCreates a new metadata document.
OpenOpens an existing metadata document from repository. Shows Open File dialog for letting the user choose the appropriate metadata document.
Show FormRuns the XML file form.
SaveSaves the metadata document.
DetachDetaches from the Windows Forms Designer.

Attach Menu Item

Opens a new Windows Forms project in C# and obtains an IDesignerHost handle for the active window object. This is saved in an internal variable. This also enables the other menu items.

Every designer surface provides certain services to monitor the activities occurring on it. In the present case, we get a reference to the IComponentChangeService to get information about the changes happening on the designer surface. We wrote event handlers for appropriate designer events.

internal void AttachNewWindowsFormHandle()
{
         //Open a new windows project
   applicationObject.Solution.AddFromTemplate
(@"C:\Program Files\Microsoft Visual Studio .NET
 2003\VC#\CSharpProjects\CSharpEXE.vsz",@"C:\"+"
FromCode","FromCode",true);
   AttachDesignerToForm();
}

internal void AttachDesignerToForm()
{
   if (applicationObject.ActiveDocument != null)
   {
      foreach (Window W in applicationObject.ActiveDocument.Windows)
      {
         designerHost = W.Object as IDesignerHost; 
      }
   }
   AttachDesignerEvent-handlers();
}

private void AttachDesignerEvent-handlers() 
{
   //Component change events.
   IComponentChangeService ccs =
 (IComponentChangeService)designerHost.GetService
(typeof(IComponentChangeService));
   if (ccs != null) 
   {
      ccs.ComponentChanged += new
 ComponentChangedEventHandler(this.OnComponentChanged);
   }
}

Detach Menu Item

Sets the designerHost object to null and unwires all the event handlers on the designer. Also disables all add-in menu items except Attach.

public void DetachFromDesigner()
{
   DetachDesignerEvent-handlers();
   this.designerHost = null;
}

private void DetachDesignerEvent-handlers() 
{
   IComponentChangeService ccs =
(IComponentChangeService)designerHost.GetService
(typeof(IComponentChangeService));
   if (ccs != null) 
   {
      ccs.ComponentChanged -= new
 ComponentChangedEventHandler(this.OnComponentChanged);
   }
}

New Menu Item

Adds a blank form to the current project and obtains a designer object for it. You can begin designing the form by dragging-and-dropping controls, attaching event handlers, setting control properties, and implementing the event handlers.

The only designer event captured is the ComponentChanged event. This is used to record the events raised by a control and the corresponding event handler names.

private void OnComponentChanged(object sender, ComponentChangedEventArgs cea) 
{
   if (cea.Member.GetType().Name.Equals("EventPropertyDescriptor"))
   {
      string ctrlName = ((Control)cea.Component).Name;
      EventInfo ei = new EventInfo(cea.Member.Name,cea.NewValue.ToString());
            
      ArrayList evtInfoList = new ArrayList();
      if (!ctrlEvtList.ContainsKey(ctrlName))
      {
         evtInfoList.Add(ei);
      }
      else
      {
         evtInfoList = (ArrayList)ctrlEvtList[ctrlName];
         if(!evtInfoList.Contains(ei))
         {
            evtInfoList.Add(ei);
         }
         ctrlEvtList.Remove(ctrlName);
      }
      ctrlEvtList.Add(ctrlName,evtInfoList);
   }
}

Save Menu Item

Initiates the following steps:

  1. Prompts you to provide a location and name for the XML file to be stored.
  2. Processes the current designer object recursively to collect information about all controls in it.
    • A MetadataControl object is created for each and every object in the designer. This stores properties such as name, text, tabIndex, location, size, type, and visibility.
    • If a control is a parent to other controls, information about these child controls is collected in the MetadataControl array member of the parent control.
    private MetadataControl GetTheControl(Control formCtrl)
    {
       MetadataControl newCtrl = new MetadataControl();
             
       newCtrl.name = formCtrl.Name;
       newCtrl.text = formCtrl.Text;
       newCtrl.position.height = formCtrl.Height;
       newCtrl.position.width = formCtrl.Width;
       newCtrl.position.top = formCtrl.Top;
       newCtrl.position.left = formCtrl.Left;
       newCtrl.tabIndex = formCtrl.TabIndex;
       newCtrl.type = formCtrl.GetType().ToString();
       newCtrl.visible = formCtrl.Visible;
       newCtrl.controls = new MetadataControl[formCtrl.Controls.Count];
       newCtrl.event-handlers = null;
             
       if(newCtrl.controls.Length > 0)
       {
          int cnt = 0;
          foreach (Control childCtrl in formCtrl.Controls)
          {
             newCtrl.controls[cnt] = GetTheControl(childCtrl);
             cnt++;
          }
       }
       return newCtrl;
    }
    
  3. The events raised by the control and the corresponding event handlers names are stored in the event-handlers member (of type EventInfo). This is updated in the Metadatacontrols member of the form.
    private void IntegrateCtrlWithEvents(MetadataControl[] arr)
    {
       foreach(MetadataControl ctrl in arr)
       {
          if(ctrlEvtList.ContainsKey(ctrl.name))
          {
             ArrayList ei = (ArrayList)ctrlEvtList[ctrl.name];
             ctrl.event-handlers = new EventInfo[ei.Count];
             ei.CopyTo(ctrl.event-handlers);
          }
          else
          {
             ctrl.event-handlers = new EventInfo[0];
          }
          if (ctrl.controls.Length > 0)
          {
             IntegrateCtrlWithEvents(ctrl.controls);
          }
       }
    }
    
    
  4. The code file for the form is parsed to get the event handler code. This is added to the script element of the MetadataForm.
          private ArrayList GetEventCode()
          {
             ArrayList evtCode = null;
             try
             {
                ProjectItem pi = applicationObject.ActiveDocument.ProjectItem;
                MetadataCodeSpace codespace = new MetadataCodeSpace(pi);
    
                string[] evtHdlrNames = GetOnlyEvtHandlers(ctrlEvtList.Values);
                evtCode = codespace.ExtractEventCode(evtHdlrNames);
             }
             catch(Exception e)
             {
                MessageBox.Show(e.Message,"Error !!!");
                return null;
             }
             return evtCode;
          } 
    
    public ArrayList ExtractEventCode(string[] evtHandlers)
    {
       eventItem tempEvt;
       // Parse code file for event code
       try
       {
          CodeElement celt = projectItem.FileCodeModel.CodeElements.Item(1);
          
          CodeNamespace cNamespace = (CodeNamespace)celt;
       
          CodeClass cClass = (CodeClass)(cNamespace.Members.Item(1)); 
    
          foreach (CodeElement ce in cClass.Members)
          {
             switch (ce.Kind)
             {
                case EnvDTE.vsCMElement.vsCMElementFunction:
                   
                   CodeFunction cf = (CodeFunction)ce;
                   //Chk whether its an eventhandler
                   if (foundEvtHandler(evtHandlers,cf.Name))
                   {
                      TextPoint startPt = cf.StartPoint;
                      TextPoint endPt = cf.EndPoint;   
                            
                      EditPoint editPt = startPt.CreateEditPoint();
                      tempEvt = new eventItem(cf.Name,editPt.GetLines(startPt.Line,endPt.Line+1),contentType.text);
                
                      evtCodeList.Add(tempEvt);
                   }
                   tempEvt = null;
                   break;
             }
          }
       }
       catch(Exception e)
       {
          string s = e.Message;
       }
       return evtCodeList;
    }
    
    
  5. Once all the form information is collected, a new MetadataForm object is created and serialized into XML. This XML is saved with the file name provided in Step 1.
       // Add all the controls to the form object
       activeForm = new MetadataForm(ctrls,evts,GetActiveFormName());
                      
       //activeFileName contains name of the file user has chosen
       StreamWriter sw = new StreamWriter(activeFileName);
     
       XmlSerializer writer = new XmlSerializer(typeof(MetadataForm));
       //Serialize the activeForm member variable
       writer.Serialize(sw, activeForm);
       sw.Close();
    
    

Runtime Implementation

Open Menu Item

Prompts you to provide the path to the XML file that you want to open in the designer. Once you provide this name the process of recreating the form from the XML file occurs.

Following are the steps involved in the process:

  1. Deserialize the XML file into the CLR type MetadataForm.
  2. Create a new blank form and set its properties based on the form control in the MetadataForm object.
  3. Create controls and place them appropriately on this form.
  4. Hook the event handlers to the controls on the form.
  5. Insert the event handler code read from the XML file in the code-behind file.

De-Serialization

As we have seen earlier, the first step is to de-serialize the repository file into MetadataForm type and store the created type into a member variable called activeForm. Here is the code snippet that does this.

//Create the serializer
XmlSerializer serializer = new XmlSerializer(typeof(MetadataForm));
 
FileStream stream = null;
 
try {
                       //Open the file
                       Stream
 = new FileStream(filename, FileMode.Open,
 FileAccess.Read);
 
                       //De-serialize the XML into CLR type
                       activeForm =
 (MetadataForm)serializer.Deserialize(stream);
 
                       stream.Close();
 
}
catch(Exception e) {
                       MessageBox.Show(e.Message);
}

Create a New Form

Adds a new blank form to the current project and initializes it with the form properties in MetadataForm object.

// Adding a new form to project
Project proj = applicationObject.Solution.Projects.Item(1);

ProjectItem pi = proj.ProjectItems.AddFromTemplate
(@"C:\Program Files\Microsoft Visual Studio .NET
 2003\VC#\CSharpProjectItems\CSharpAddWinFormWiz.vsz",
formName+".cs");

applicationObject.Windows.Item(formName+".cs [Design]").Activate();    

// Setting properties of the form
private void SetNewFormProperties(MetadataControl ctrl)
{
   ((Form)designerHost.RootComponent).Name=ctrl.name;
   ((Form)designerHost.RootComponent).Text=ctrl.text;
   ((Form)designerHost.RootComponent).Height=ctrl.position.height;
   ((Form)designerHost.RootComponent).Width=ctrl.position.width;
}

Creating Controls at Runtime

The concept of reflection is used to create controls at runtime. As we have seen in the repository discussion, the repository structure contains information about controls embedded in the form.

<control type="System.Windows.Forms.Button">
          <position>
               <height>23</height>
               <width>75</width>
               <top>192</top>
               <left>192</left>
          </position>
          <controls />
          <name>button1</name>
          <text>Submit</text>
          <tabIndex>0</tabIndex>
</control>

If we look at the XML fragment, the control tag contains a type attribute that contains the fully-qualified CLR type of the control. All we need to do at this point is to create the control using reflection. We use the Activator class to create an instance of the control. The code snippet that does this is shown below. This function takes the parent control (Windows.Forms.Control) and collection child controls (MetadataControl) as parameters.

private void AddControlsToActiveForm(MetadataControl[] ctrl,Control parent)
{
   try
   {
         foreach (MetadataControl child in ctrl) 
      {
         System.Type controltype = Type.GetType(child.type);
         //unknown control type. Handle this as an error
         if (controltype == null) continue; 
   
         //Create the control
         
         Control control = (Control)Activator.CreateInstance(controltype);
         control.Name = child.name;
         control.Text = child.text;
         control.Top = child.position.top;
         control.Left = child.position.left;
         control.Height = child.position.height;
         control.Width = child.position.width;
         control.Visible = true;
         control.TabIndex = child.tabIndex;
               
         parent.Container.Add(control);
               
         PropertyDescriptor parentProp =
 TypeDescriptor.GetProperties(control).Find
("Parent",true);
         parentProp.SetValue(control, parent );

         if (child.controls.Length > 0)
            AddControlsToActiveForm(child.controls,control);
      }
   }
   catch (Exception e)
   {
      MessageBox.Show(e.Message );
   }
}

  1. First we get the CLR type for a given type using Type.GetType that takes a string containing the fully qualified CLR type name as parameter.
  2. Then, we call the CreateInstance method of the Activator class to create an instance of the control and typecast it to the type control, which is the base class of all Windows Forms controls.
  3. Then we set the appropriate properties of the control.
  4. After creating the control, we add it to the controls collection of the parent control. If the child control has controls within it, we call the function recursively sending appropriate parameters.

Attaching Event-handlers to Controls

Each MetadataControl in the repository file maintains a collection of events it raises and their corresponding event handlers. This information is used to bind the control created at runtime to the event handler specified. The IEventBindingService of designerHost is used for this. Each control has a list of events it can raise. The event and the corresponding event handler are bound using the control's property list.

IEventBindingService eventservice =
 IEventBindingService)designerHost.GetService
(typeof(IEventBindingService));
if( eventservice != null )
{
   if((child.event-handlers != null) || (child.event-handlers.Length > 0))
   {
      EventInfo[] eiArr = child.event-handlers;
      foreach( EventInfo ei in eiArr)
      {
         EventDescriptor ed =
 TypeDescriptor.GetEvents(control).Find(ei.eventName,true);
         if( ed == null )
            break;
   
         PropertyDescriptor pd = eventservice.GetEventProperty(ed);
         if( pd == null )
            break;                
                
         pd.SetValue(control, ei.eventHandler);
      }
   }
}

Insert Event Handler Code

Finally, the event handler code is inserted into the code file of the form. All the event handler code for a form is present in the script element of the MetadataForm object. Using the CodeDom namespace classes we parse through the code space of the file and insert the event-handling code appropriately.

private void AddEventHandlerCode()
{
   eventItem[] eiArr = activeForm.script;

   ProjectItem pi = applicationObject.ActiveDocument.ProjectItem;
   CodeElement celt = pi.FileCodeModel.CodeElements.Item(1);
         
   CodeNamespace cNamespace = (CodeNamespace)celt;
         
   CodeClass cClass = (CodeClass)(cNamespace.Members.Item(1)); 
   foreach (CodeElement ce in cClass.Members)
   {
      switch (ce.Kind)
      {            
         case EnvDTE.vsCMElement.vsCMElementFunction:
         CodeFunction cf = (CodeFunction)ce;
         foreach(eventItem ei in eiArr)
         {
            if (cf.Name.Equals(ei.eventHandler))
            {
               EditPoint ep =
                                             
                                                  
cf.GetStartPoint(vsCMPart.vsCMPartBody)
.CreateEditPoint();
               ep.Insert(justCodeBlock(ei.content));
            }
         }
         break;
      }
   }
}

private string justCodeBlock(string fnCode)
{
              string tmpcode = fnCode.Substring(fnCode.IndexOf("{")+1);
              return tmpcode.Substring(0,tmpcode.Length-1);
}

Show Form Menu Item

The XML file form can be run through the add-in by invoking the ShowForm from the add-in menu. This runs an existing XML file that is already open in the designer, or else prompts you to provide the path to the XML file. It opens the XML file (same process as Open Menu) in the current project and runs it by building the solution and running it through the add-in code.

public void ShowForm()
{
    try
   {
      if( applicationObject.Solution.Projects.Item(1).ProjectItems.Count == 0 )
      {
         OnOpen();
      }
      BuildNRunSolution();
   }
   catch(Exception e)
   {
      MessageBox.Show(e.Message);
   }
}
private void BuildNRunSolution()
{
         applicationObject.Solution.SolutionBuild.Build(true);
         applicationObject.Solution.SolutionBuild.Run();
}

Conclusion

In this article, we discussed an approach to implementing repository or metadata-driven user interfaces using the .NET Framework. We also explained how to use the .NET Framework classes to create a customized forms designer, store the form as XML, load the XML file into the designer, modify the XML file, and then load and run the form.

Show:
© 2014 Microsoft