Step by step guide for developing custom server control with custom collection

This guide is step by step instruction to create custom web control. The describe control contains complex property (Collection of classes) that the user can edit using custom type editor. Collection data will be preserving by control as control sub Html tags. This guide also contains most common issues relating to development such a control. The control that we about to develop name is commands and it part of NWAF framework that I'm working on. This control task is to let users to set commands to server control events laid down on web page. Commands has a property holding collection of classes that containing control name, event name and command name set to specify event. The user can add command to control events by using custom editor. Our custom editor show controls on page in hierarchal view. Choosing a control show it server side events and while selecting given event the user can set command name for that event. Controls event commands persist as inner HTML under commands control tags. The HTML output of our control should look like:

<cc1:Commands id="Commands2" style="Z-INDEX: 105; LEFT: 24px; POSITION: absolute; TOP: 107px" runat="server" Height="2px" Width="15px">
    <cc1:InnerCommand EventName="Click" ControlID="Button1" CommandName="a"></cc1:InnerCommand>
    <cc1:InnerCommand EventName="Command" ControlID="Button1" CommandName="b"></cc1:InnerCommand>
    <cc1:InnerCommand EventName="SelectedIndexChanged" ControlID="DropDownList1" CommandName="natty"></cc1:InnerCommand>
    <cc1:InnerCommand EventName="TextChanged" ControlID="TextBox1" CommandName="roni"></cc1:InnerCommand>
</cc1:Commands>

The overall class design of that controls is:
InnerCommand – Represent single user set of command name to control event. holds three members ControlName, EventName and Command name.
InnerCommands – Represent collection of user commands set of page controls. Inherit from Collection base and supply collection of command settings.
Commands – Custom control class.
Editor – Inherits from UITypeEditor. Override EditValue to load custom editor from.
VisualEditor – Custom editor form.

We will start from data aspect of the control. InnerCommand represent single entry of collection data. InnerCommand is regular classes exposing three public properties. for every property that we want to be part in control persistence we need to add NotifyParentProperty attribute setting to true. NotifyParentProperty bubble notification message to WebControl that use properties mark with NotifyParentProperty attribute in persistence process. Using the describe notification WebControl is marked as dirty in the designer. ASP.NET uses this data to serialize complex properties data as HTML elements.

public class InnerCommand
{
  private string m_ControlID;
  private string m_EventName;
  private string m_CommandName;
  public InnerCommand()
   {}
  public InnerCommand(string ControlID,string EventName,string CommandName)
  {
   m_ControlID = ControlID;
   m_EventName = EventName;
   m_CommandName = CommandName;
  }
  [NotifyParentProperty(true)]
  public string ControlID
  {
   get
   {
    return m_ControlID;
   }
   set
   {
    m_ControlID = value;
   }
  }
  [NotifyParentProperty(true)]
  public string EventName
  {
   get
   {
    return m_EventName;
   }
   set
   {
    m_EventName = value;
   }
   }
  [NotifyParentProperty(true)]
  public string CommandName
  {
   get
   {
    return m_CommandName;
   }
   set
   {
    m_CommandName = value;
   }
  }
}

InnerCommands Inherits from CollectionBase and holds the InnerCommand objects already defined by user. CollectionBase supplies basic functionality for collections implementation which let ASP.NET treat InnerCommands as collection. Pay attention to the implementation of class indexer and IndexOf method. This implementation prevents the annoying "Ambiguous match found " message as describe in KB-823194. I choose control name and event name as unique string key for each collection item.

public class InnerCommands : CollectionBase
{
  public CommandControl.InnerCommand this[object index]
  {
   get
   {
    return (InnerCommand) this.List[IndexOf(index)];
   }
   set
   {
    this.List[IndexOf(index)] = value;
   }
  }

  public void Add(CommandControl.InnerCommand Tab)
  {
   this.List.Add(Tab);
  }

  public void Insert(int index, CommandControl.InnerCommand item)
  {
   this.List.Insert(index,item);
  }

  public void Remove(CommandControl.InnerCommand Tab)
  {
   List.Remove(Tab);
  }

  public bool Contains(CommandControl.InnerCommand Tab)
  {
   return this.List.Contains(Tab);
  }

  //Collection IndexOf method
  public int IndexOf(object obj)
  {
   if (obj is int)
    return (int)obj;

   if (obj is string)
   {
    for (int i = 0; i < List.Count; i++)
     if (((InnerCommand)List[i]).ControlID + "_" + ((InnerCommand)List[i]).EventName == obj.ToString())
      return i;
     return -1;
   }
   else
   {
    throw new ArgumentException("Only a string or an integer is permitted for the indexer.");
   }
  }

  public void CopyTo(CommandControl.InnerCommand[] array, int index)
  {
   List.CopyTo(array, index);
  }

  public bool Contains(string key)
  {
   bool RV = false;
   IEnumerator oEnum = this.GetEnumerator();
   while(oEnum.MoveNext())
   {
    string inkey = ((InnerCommand)oEnum.Current).ControlID + "_" + ((InnerCommand)oEnum.Current).EventName;
    if (string.Compare(key,inkey,true) == 0)
     return true;
   }
   return RV;
  }
  public void Remove(string key)
  {
   IEnumerator oEnum = this.GetEnumerator();
   while(oEnum.MoveNext())
   {
    string inkey = ((InnerCommand)oEnum.Current).ControlID + "_" + ((InnerCommand)oEnum.Current).EventName;
    if (string.Compare(key,inkey,true) == 0)
     this.Remove((InnerCommand)oEnum.Current);
   }
  }
}

Editor Class : To show custom form as collection editor you need to create your class derived from UITypeEditor. Your class should override EditValue and UITypeEditorEditStyle. UITypeEditorEditStyle set the display mode or custom editor to modal dialog.

EditValue responsible to show custom collection editor, get the collection created by the user back and notify the control so the collection elements will be preserve as sub HTML elements of the control. Use provider GetService method to get IWindowsFormsEditorService. Use IWindowsFormsEditorService ShowDialog to show your custom form and to get back collection. In this sample I use the page constructor to set form internal data, but you can use methods to receive data from Editor Class.

Two things to notice here before you look at the code.
1) It might sound stupid but ensure that your page DialogResult property set to DialogResult.OK. Otherwise you won't see any HTML that persist your data. (Seen it happened!).
2) You have to use context.OnComponentChanged(); otherwise your control wont know that internal data changed and won't preserve collection data.

public class Editor : System.Drawing.Design.UITypeEditor {   public Editor() : base()
  {
  }

  public override object EditValue(System.ComponentModel.ITypeDescriptorContext context, IServiceProvider provider, object value)
  {
   if(context != null && context.Instance != null && provider != null)
   {
    System.Windows.Forms.Design.IWindowsFormsEditorService edSvc = (System.Windows.Forms.Design.IWindowsFormsEditorService)
    provider.GetService(typeof(System.Windows.Forms.Design.IWindowsFormsEditorService));
    if (edSvc != null)
    {
     System.Web.UI.Control oControl = (System.Web.UI.Control)context.Instance;
     VisualEditor form = new VisualEditor(oControl,(CommandControl.InnerCommands)value);

     System.Windows.Forms.DialogResult result = edSvc.ShowDialog(form);
     context.OnComponentChanging();
     if (result == System.Windows.Forms.DialogResult.OK)
     {
      value = form.Comands;
      context.OnComponentChanged();
     }
    }
    return value;
   }
   return base.EditValue (context, provider, value);
  }

  public override System.Drawing.Design.UITypeEditorEditStyle GetEditStyle(System.ComponentModel.ITypeDescriptorContext context)
  {
   if(context != null && context.Instance != null)
   {
    return System.Drawing.Design.UITypeEditorEditStyle.Modal;
   }
   return base.GetEditStyle (context);
  }
}

The form code is quiet strait forward. The page gets the current control command collection and the control instance. Using the current control instance I can go recursion through page controls to show all controls on the host page. Current control command collection is used to show command names already set to controls events.

public class VisualEditor : System.Windows.Forms.Form
{
  private System.ComponentModel.Container components = null;
  private System.Windows.Forms.TreeView treeView1;
  private System.Windows.Forms.ListBox listBox1;
  private System.Windows.Forms.TextBox txtCommand;
  private System.Windows.Forms.Label label1;
  private System.Windows.Forms.Label label2;
  private System.Windows.Forms.Label label3;
  private System.Windows.Forms.Button btnSave;
  private System.Web.UI.Control m_Control;
  private CommandControl.InnerCommands m_commands = new CommandControl.InnerCommands();

  public CommandControl.InnerCommands Comands
  {
   get
   {
    return m_commands;
   }
  }
  public VisualEditor(System.Web.UI.Control oControl, CommandControl.InnerCommands commands)
  {
   if (commands == null)
   {
    m_commands = new CommandControl.InnerCommands();
   }
   else
   {
    m_commands = commands;
   }
   m_Control = oControl;
   InitializeComponent();
  }
  public VisualEditor()
  {
   InitializeComponent();
  }

  protected override void Dispose( bool disposing )
  {
   if( disposing )
   {
    if(components != null)
    {
     components.Dispose();
    }
   }
   base.Dispose( disposing );
  }

#region Windows Form Designer generated code
  private void InitializeComponent()
  {
   this.treeView1 = new System.Windows.Forms.TreeView();
   this.listBox1 = new System.Windows.Forms.ListBox();
   this.txtCommand = new System.Windows.Forms.TextBox();
   this.label1 = new System.Windows.Forms.Label();
   this.label2 = new System.Windows.Forms.Label();
   this.label3 = new System.Windows.Forms.Label();
   this.btnSave = new System.Windows.Forms.Button();
   this.SuspendLayout();

   this.treeView1.ImageIndex = -1;
   this.treeView1.Location = new System.Drawing.Point(8, 40);
   this.treeView1.Name = "treeView1";
   this.treeView1.SelectedImageIndex = -1;
   this.treeView1.Size = new System.Drawing.Size(400, 240);
   this.treeView1.TabIndex = 0;
   this.treeView1.AfterSelect += new System.Windows.Forms.TreeViewEventHandler(this.treeView1_AfterSelect);
   this.listBox1.Location = new System.Drawing.Point(8, 320);
   this.listBox1.Name = "listBox1";
   this.listBox1.Size = new System.Drawing.Size(400, 108);
   this.listBox1.TabIndex = 1;
   this.listBox1.SelectedValueChanged += new System.EventHandler(this.listBox1_SelectedValueChanged);
   this.txtCommand.Location = new System.Drawing.Point(8, 464);
   this.txtCommand.Name = "txtCommand";
   this.txtCommand.Size = new System.Drawing.Size(400, 20);
   this.txtCommand.TabIndex = 2;
   this.txtCommand.Text = "";
   this.txtCommand.Leave += new System.EventHandler(this.txtCommand_Leave);
   this.label1.Location = new System.Drawing.Point(8, 8);
   this.label1.Name = "label1";
   this.label1.Size = new System.Drawing.Size(168, 24);
   this.label1.TabIndex = 3;
   this.label1.Text = "Controls:";
   this.label2.Location = new System.Drawing.Point(8, 296);
   this.label2.Name = "label2";
   this.label2.Size = new System.Drawing.Size(168, 24);
   this.label2.TabIndex = 4;
   this.label2.Text = "Events:";
   this.label3.Location = new System.Drawing.Point(8, 440);
   this.label3.Name = "label3";
   this.label3.Size = new System.Drawing.Size(168, 24);
   this.label3.TabIndex = 5;
   this.label3.Text = "Command:";
   this.btnSave.Location = new System.Drawing.Point(112, 496);
   this.btnSave.Name = "btnSave";
   this.btnSave.Size = new System.Drawing.Size(200, 24);
   this.btnSave.TabIndex = 6;
   this.btnSave.Text = "Save";
   this.btnSave.Click += new System.EventHandler(this.btnSave_Click);
   this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
   this.ClientSize = new System.Drawing.Size(416, 534);
   this.Controls.Add(this.btnSave);
   this.Controls.Add(this.label3);
   this.Controls.Add(this.label2);
   this.Controls.Add(this.label1);
   this.Controls.Add(this.txtCommand);
   this.Controls.Add(this.listBox1);
   this.Controls.Add(this.treeView1);
   this.Name = "VisualEditor";
   this.Text = "VisualEditor";
   this.Load += new System.EventHandler(this.VisualEditor_Load);
   this.ResumeLayout(false);
  }
#endregion

  private void VisualEditor_Load(object sender, System.EventArgs e)
  {
   System.Windows.Forms.TreeNode oNode = new System.Windows.Forms.TreeNode("Page Controls");
   this.treeView1.Nodes.Add(oNode);
   loadTree(oNode,m_Control.Parent);
  }

  private void loadTree(System.Windows.Forms.TreeNode oNode, System.Web.UI.Control control)
  {
   //System.Windows.Forms.MessageBox.Show(control.ClientID);
   foreach (System.Web.UI.Control o in control.Controls)
    {
    if ( o.ID != "")
    {
     System.Windows.Forms.TreeNode oNod = new System.Windows.Forms.TreeNode(o.ClientID);
     oNode.Nodes.Add(oNod);
     loadTree(oNod,o);
    }
   }
  }

  private void treeView1_AfterSelect(object sender, System.Windows.Forms.TreeViewEventArgs e)
  {
   this.listBox1.Items.Clear();
   System.Web.UI.Control oControl = m_Control.Parent.FindControl(e.Node.Text);
   if (oControl != null)
   {
    System.Type oType = oControl.GetType();
    System.Reflection.EventInfo[] oEI = oType.GetEvents();
    for(int i = 0; i < oEI.Length ; i++)
    {
     this.listBox1.Items.Add( oEI[i].Name);
    }
   }
  }

  private void btnSave_Click(object sender, System.EventArgs e)
  {
   this.DialogResult = DialogResult.OK;
   this.Close();
  }

  private void txtCommand_Leave(object sender, System.EventArgs e)
  {
   if (((TextBox)sender).Text != "" )
   {
    if( m_commands.Contains(this.treeView1.SelectedNode.Text + "_" + listBox1.SelectedItem))
    {
     m_commands[this.treeView1.SelectedNode.Text + "_" + listBox1.SelectedItem].CommandName = ((TextBox)sender).Text;
    }
    else
    {
     CommandControl.InnerCommand oCommand = new CommandControl.InnerCommand(this.treeView1.SelectedNode.Text,(string)listBox1.SelectedItem,((TextBox)sender).Text);
     m_commands.Add(oCommand);
    }
   }
  }

  private void listBox1_SelectedValueChanged(object sender, System.EventArgs e)
  {
   if (this.treeView1.SelectedNode != null && ((ListBox)sender).SelectedItem != null)
   {
    if( m_commands.Contains(this.treeView1.SelectedNode.Text + "_" + (string)((ListBox)sender).SelectedItem))
    {
     txtCommand.Text = m_commands[this.treeView1.SelectedNode.Text + "_" + (string)((ListBox)sender).SelectedItem].CommandName ;
    }
    else
    {
     txtCommand.Text = "";
    }
   }
   else
   {
    txtCommand.Text = "";
   }
  }
}
(See page designer in full source code available to download.)

What we left with is the control itself. The control inherits from WebControl and implements collection property CommandsList. CommandsList (read only) just return  instance of InnerCommnds holding Control collection internal data. Beside CommandsList this control implement Render method to render Collection Data as XML data inside hidden field.

Preserving is done here just by using attributes. The control class should be decorated with ParseChildren and PersistChildren attributes if your control class inherits from Control. Books and documentation suggest that if your class inherits from WebControl those attributes already applied to webControl and you don’t need to use them. I strongly recommend declaring those attributes since I saw cases where persistence doesn't work or errors raised if those attributes are missing. PersistChildren should be set to false so the control persist content as property and not as inner controls. ParseChildren(true) tells page parser to parse data as property within control tags.

Collection property should be decorated with the following attributes:
DesignerSerializationVisibility: this attribute should be set to DesignerSerializationVisibility.Content. DesignerSerializationVisibility.Content tells the designer that their sub-properties should be serialized.
PersistenceMode: PersistenceMode imply to the parser to persist property as inner property. to persist an inner default property correctly PersistenceMode must be set to PersistenceMode.InnerDefaultProperty. if you won't use PersistenceMode.InnerDefaultProperty page parser might add tags with property name encapsulating collection item tags:

<CommandsList>
    <cc1:InnerCommand EventName="SelectedIndexChanged" ControlID="DropDownList1" CommandName="sdfsd"></cc1:InnerCommand>
<CommandsList/>

And you'll get the following parser error: "Parser error: 'collectionItems' must have items of type 'CollectionItem'. 'PropertName' is of type System.Web.UI.HtmlControls.HtmlGenericControl"
Editor: editor attribute tells property window that the decorate property got its own implementation of editing property value. Editor constructor takes two parameters. The first is the type of the custom editor. The second is the type that your editor derived from.

[ToolboxData("<{0}:Commands runat='server'></{0}:Commands>")
,ParseChildren(true,"CommandsList"),
PersistChildren(false)
]
public class Commands : System.Web.UI.WebControls.WebControl
{
  private InnerCommands m_InnerCommands;

  public Commands()
  {
  }

  [Editor (typeof(CommandsEditor.Editor),typeof(System.Drawing.Design.UITypeEditor))]
  [PersistenceMode(PersistenceMode.InnerDefaultProperty),
  DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
  public InnerCommands CommandsList
  {
   get
   {
    if (m_InnerCommands == null)
    {
     m_InnerCommands = new InnerCommands();
    }
    return m_InnerCommands;
   }
  }

  protected override void Render(HtmlTextWriter writer)
  {
   System.Text.StringBuilder oSB = new System.Text.StringBuilder();
   // translate CommandControl.InnerCommands to string
   oSB.Append("<commands>");
   foreach( CommandControl.InnerCommand oCmd in m_InnerCommands)
   {
    oSB.Append("<control ");
    oSB.Append(" name=\"");
    oSB.Append(oCmd.CommandName);
    oSB.Append("\">");

    oSB.Append("<event ");
    oSB.Append(" name=\"");
    oSB.Append(oCmd.EventName);
    oSB.Append("\"");
    oSB.Append(" command=\"");
    oSB.Append(oCmd.CommandName);
    oSB.Append("\"");
    oSB.Append(" />");

    oSB.Append("</control>");

   }
   oSB.Append("</commands>");
   string strCommand = "";
   if (HttpContext.Current != null)
   {
    strCommand = HttpContext.Current.Server.HtmlEncode( oSB.ToString());
   }
   else
   {
    strCommand = oSB.ToString();
   }
   strCommand = "<input type=\"hidden\" name=\"__NWAFCOMMANDS\" value=\"" + strCommand + "\"/>";
   if (this.Site.DesignMode)
   {
    writer.Write("Select Commands property </br> to select commands </br> for controls events.");
   }
   else
   {
    writer.Write(strCommand);
   }
   base.Render (writer);
  }
}
Complete code available for download.

16 Comments

  • Cut and past to new browser. geocities block it.

  • problem with the link

  • Can't download the source!!!

  • Link problem fixed.

  • Hi Natty.
    Congratulations for the post, it's great. Now please help me with one thing. I need to modify the collection of subcontrols of my control, inside a SmartTag instead of a Custom Editor. I can't figure out how to do that.

    I can create smart tags properly. But in your example you call context.OnComponentChanged(), that renders the control's markup, inside the EditValue method that's from UITypeEditor. When I'm inside a custom Form I don't have access to this context. I tried to pass ControlDesigner all the way up to the Form so that I can call onComponentChanged. Everything that happens is that my Form closes but no data is actually rendered to the aspx.

    Any idea?
    thanks a lot

  • Hey...
    With this article you summarize a bunch of of the more important views!
    ! Fast to browse and inclusive of helpful
    advise!
    Many thanks for posting Step by step guide for developing custom server control with custom collection...

  • Cheers for quality content in your write-up Step by step guide for developing custom server control with custom collection.

    .
    Thanks again...

  • Useful piece of writing, you always come up with the ideal resources & Step by step guide for developing custom server control with custom
    collection is absolutely no exception to this rule.
    ..

  • Hello
    Your piece Step by step guide for developing custom server control with custom collection
    is interesting & well thought out, I want to return to view your
    posts..!

  • Thanks for some interesting good tips in your blog post Step by step guide for developing custom
    server control with custom collection.
    Thanks again.

  • Hi
    Your opinions Step by step guide for developing custom server control with custom collection is good as well as well thought out, I shall
    be back to read through new blogs!

  • =Hi,
    On this blog post you sum up some of the more important tips!
    ! Fast to browse and full of effective ideas!!
    Thanks for sharing Step by step guide for developing custom server control with
    custom collection!

  • Particularly great blog post Step by step guide for developing custom server control with custom collection.
    .. Always keep writing!!

  • Hey just wanted to give you a brief heads up and let you know a few of the pictures
    aren't loading correctly. I'm not sure why but I think its a linking issue.
    I've tried it in two different browsers and both show the same outcome.

  • Great info again! I am looking forward for more updates;)

  • It's very easy to find out any matter on net as compared to textbooks, as I found this post at this site.

Comments have been disabled for this content.