Click here to Skip to main content
15,878,970 members
Articles / Web Development / ASP.NET
Article

Managing ViewState, Rendering and Events in Composite Custom Server Controls

Rate me:
Please Sign up or sign in to vote.
4.87/5 (62 votes)
29 Sep 200510 min read 215.1K   121   59
Creating child controls, rendering, wiring up events, and managing viewstate when building an advanced custom server control that embeds other server controls (composite control).

Image 1

Introduction

Recently I built a web file manager server control. The control embeds two other server controls - microsoft.web.ui.webcontrols.treeview and DataGrid - and has to handle rendering, events and ViewState for those controls. It took me a lot of trial and error to figure out how to wire everything up correctly. This article simply shares the lessons I learned in the hope that it will save you some time Googling for the answers.

Figuring out ToolboxData

Let's start with something simple. You know that the {0} in:

C#
[ToolboxData( "<{0}:ExiaWebFileManager runat= server>" )]

gets replaced with something to form the HTML tag. But what does it get replaced with? This drove me crazy until I figured it out, and until you do, dragging and dropping the control from the toolbox will yield weird results, like replacing {0} with "cc1". If you're like me, you've been wondering for a long time how to get rid of the "cc1" and in your frustration, you might have written something like:

C#
[ToolboxData( "<Exia:ExiaWebFileManager runat= server>" )]

Well, it turns out that it gets filled in from an assembly level attribute. Put an attribute like this somewhere outside your namespaces, such as in AssemblyInfo.cs.

C#
[assembly:TagPrefix( "Exia.Web.UI.WebControls", "Exia" )]

This tells Visual Studio that when you drop any control in the Exia.Web.UI.WebControls namespace onto a form, the tag prefix will be "Exia". Aside from the fact that your tags will be well formed, the <%register> directive will now get written out properly.

Another quickie... the Toolbox icon

What threw me off here was that it's not an icon, it's a bitmap. Create a 16x16 bitmap, include it in your project, set its Build Action to Embedded Resource and put an attribute like this above your server control class:

C#
ToolboxBitmapAttribute( typeof(ExiaWebFileManager), 
                                       "FileManager.bmp")

Preserving ViewState of embedded server controls

This drove me crazy and consumed three days of my time. What threw me off was searching Google and finding some code like this...

C#
private TreeView treeView
{
    get{ return (TreeView)ViewState["TreeView"]; }
    set{ ViewState["TreeView"] = value; }
}

...coupled with my own stupidity. The idea behind this code is that the custom control contains an embedded TreeView, and it makes sense then that the TreeView needs to be saved and restored on ViewState, right? That was my thinking anyway. Of course, you get an error saying the TreeView can't be serialized, and there are some more threads on Google about how it could be made serializable, that made me think I was on the right track.

Finally, just out of frustration, I tried the simplest thing possible, knowing it wouldn't work...

C#
private TreeView treeView = new TreeView;

...and whaddayaknow... it worked!

That's when it dawned on me that (of course, you idiot) no control is ever saved, just its ViewState. The control is re-instantiated from scratch on each postback, and the saved ViewState is applied to the new control.

Well, it almost worked...

Of course, the control needs to be managed in the context of the control hierarchy and rendered before its ViewState can be restored. My next big challenge was to figure out how CreateChildControls and Render are related to each other, and what was supposed to do what, and when, and with what? After wading through some seemingly overly complex examples on the web, I managed to boil it down to the following simple rules. I'm sure there are other ways to do it, but here's how it works for our control:

Declare and instantiate controls as private instance variables

Say your control has a table that contains a row, the row contains a cell, and the cell contains a Microsoft.Web.UI.WebControls.TreeView. Declare the elements as private instance variables of your control, like:

C#
class ... 
{
    private Table BorderTable = new Table();;
    private TableRow BorderTableRow = new TableRow();
    private TableCell BorderTableCell = new TableCell();
    private TreeView MyTreeView = new TreeView();
    etc.
...
...

Initialize the controls in the OnInit event

In the OnInit event, configure basic control properties, such as AutoGenerateColumns, ShowHeader etc., and wire up the events. (More on event wire-up later.)

C#
protected override void OnInit(EventArgs e)
{
    InitializeControls();
    base.OnInit (e);
}

private void InitializeControls()
{
    // Set basic properties of controls
    FileListGrid.AutoGenerateColumns = false;
    FileListGrid.ShowHeader = true;
    etc.

    // Wire up events.
    FileListGrid.ItemCommand += 
       new DataGridCommandEventHandler( FileListGrid_ItemCommand );
    FileUploader.DialogClosed += 
       new DialogOpenerClosedEvent(FileUploader_DialogClosed);
    DeleteBtn.Click += new EventHandler( DeleteBtn_Click );
    AddBtn.Click += new EventHandler( AddBtn_Click );
    MoveBtn.Click += new EventHandler( MoveBtn_Click );
    etc.
}

Assemble the controls in CreateChildControls

In the CreateChildControls method, put the controls together into a hierarchy. Most importantly, add the root control or controls to the Controls collection of the server control. At the same time, set any attributes that need to be set, like CSS class and so on:

C#
protected override void CreateChildControls()
{
    BuildControlHeirarchy();
    ...
    ...
}

private void BuildControlHeirarchy()
{
    // Add the root control to the 
    // Controls collection of
    // the server control. When this 
    // is done, the base.Render()
    // method will render the entire 
    // heirarchy for you and
    // manage the ViewState of any child 
    // controls in the heirarchy
    Controls.Add( BorderTable );
    BorderTable.CellSpacing = 0;
    
    // Build up the rest of the control heirarchy...
    BorderTable.Controls.Add( BorderTableRow );
    BorderTableRow.Controls.Add( BorderTableCell );
    BorderTableCell.CssClass = BorderCssClass;
    BorderTableCell.Controls.Add( MyTreeView );
    ...
    ...
    etc.
}

Do any custom rendering in the Render method

The minimum you have to do in the Render method is call Base.Render(). In fact, the minimum you have to do is not to override this method. The base Render method takes care of rendering the hierarchy of controls and child controls that you built up in CreateChildControls. You need to override Render to render special HTML that you can't easily build up using CreateChildControls. In our Render method, for instance, we write out a special style that fixes the grid header in a stationary position when the grid scrolls, like this:

C#
output.Write(@"<style type='text/css'>
    .DataGridFixedHeader { POSITION: relative; ; 
          TOP: expression(this.offsetParent.scrollTop - 2) }
    </style>" );

Another reason to override Render is for speed. Rendering HTML directly is faster than building up the control hierarchy. (But of course, if you render the HTML directly, you don't have access on the server to the objects rendered, so if you need to set up things like the objects' event handlers and so on, then you'll either need to build them up in CreateChildControls or know quite a bit about wiring up HTML to events with GetPostBackEventReference(), and other complex stuff.)

If you do override, just don't forget to call base.Render(), otherwise the control hierarchy you had built up in CreateChildControls will never get rendered:

Here's what our complete Render method looks like. As you can see, it does very little other than call base.Render.

C#
protected override void Render(HtmlTextWriter output)
{
    if( ! Visible )
        return;

    if( FileListGrid.Items.Count == 0 )
        FileListGrid.ShowHeader = false;
    
    // Write out the html to include the CSS file
    output.Write( "<link href='" + CssFile + "' 
             type='text/css' rel='stylesheet'>" );

    // Write out the CSS style for the 
    // fixed datagrid header. 
    output.Write( @"
            <style type='text/css'>
            .DataGridFixedHeader { POSITION: relative; ; 
               TOP: expression(this.offsetParent.scrollTop - 2) }
            </style>" );

    base.Render( output );            
}

Create grid columns in CreateChildControls

If your control embeds a DataGrid or other type of server control, when should you create the grid columns? Do they get saved with ViewState?

No, the columns are not saved with ViewState. They're child controls of the grid control in the same sense that table cells are child controls of table rows. So you create them at the same time you create other controls in the hierarchy, in CreateChildControls.

Bind the data in CreateChildControls

The data gets bound in CreateChildControls, but not on postback. Here's what our complete CreateChildControls method looks like:

C#
protected override void CreateChildControls()
{
    ConfigureFileGridColumns();
    BuildControlHeirarchy();

    if( ! Page.IsPostBack )
        BindData();

    base.CreateChildControls();
}

Managing the ViewState

Now comes the great part. It turns out that if everything is set up correctly, there's almost nothing that you need to manually manage with ViewState. The embedded server controls will automatically manage their own ViewState. The only time you need to be concerned about ViewState is for managing control-specific information that needs to be round-tripped. For instance, in our file manager control, we call ViewState to keep track of the following things:

  • The currently selected file in the file list;
  • The currently selected directory in the directory tree;
  • The active command if there's a command pending, such as a directory move;

Here's how our code for keeping track of the currently selected file looks:

C#
[Browsable(false)]  // Suppress display in the 
                    // Properties browser in 
                    // Visual Studio
public string SelectedFile
{
    get
    {
        if( ViewState["SelectedFile"] == null )
            ViewState["SelectedFile" ] = "";

        return (string)ViewState["SelectedFile"];
    }
    set
    {
        ViewState["SelectedFile"] = value; 
    }
}

All very straightforward stuff. So to summarize regarding ViewState, as long as the controls are assembled into the control hierarchy in CreateChildControls and they're rendered in the base render method, the managing of ViewState for embedded controls is all taken care of and you only have to do any custom ViewState management specific to your control.

Wiring up events

Our control emulates the Windows file explorer, with a TreeView displaying directories and a grid displaying the files. When a user clicks on a file, we need to perform some custom actions. In order to make this work, we need to trap the ItemCommand event of the embedded DataGrid that constitutes our file list.

You would think this would be pretty straightforward. When you initialize the DataGrid (see above) you just wire up the event like this:

C#
FileListGrid.ItemCommand += 
  new DataGridCommandEventHandler(FileListGrid_ItemCommand);

There's just one problem. The event handler never gets called. So you Google a day or two away, and finally in desperation, or just out of luck (maybe you should have bought that book on server controls after all) you try implementing INamingContainer. And bam, the events work. That's when you realize that if the control doesn't have its own ID namespace in the control's hierarchy, the events can't wire up properly. So don't forget to implement INamingContainer if you want to trap events of embedded controls.

What about IPostbackEventHandler?

While we're on the subject of implementing, what about implementing IPostbackEventHandler. Do you have to implement that in a custom control? The answer is that you need to implement IPostbackEventHandler if you want your control to be event-aware. In other words, a control that implements IPostbackEventHandler will receive the event that is raised when the user interacts with the control. If the control doesn't implement IPostbackEventHandler, the control will be ignored when the event is fired, and the control's parent, or the next control up the hierarchy that implemented IPostbackEventHander, will receive the event.

If this is as clear as mud, here's an example. Our custom control contains embedded buttons. Because those buttons are ASP.NET buttons, they implement IPostbackEventHandler. That is to say, when a user clicks an ASP.NET button, the event is sent to the ASP.NET button, which has a chance to respond to it. In the case of the ASP.NET button, the control does indeed respond to it. The response is to raise the Click event, which a developer, like you and I, can trap in our code.

So, in our custom control, when our embedded buttons get rendered, the fact that they already implement IPostbackEventHandler means they will trap the user's click, and we therefore don't have to implement IPostbackEventHandler in our custom control. We simply need to take advantage of the fact that the button has trapped the event for us and raised the Click event. We do this by wiring the Click event to our desired action, as we saw above...

C#
DeleteBtn.Click += new EventHandler(DeleteBtn_Click);

... and doing our custom action...

C#
private void DeleteBtn_Click(object sender, 
                              System.EventArgs e)
{
    if( !IsRootSelected() )
    {    
        Delete();
    }
}

So when would you need to implement IPostbackEventHandler? You would need to implement IPostbackEventHandler if you want your control to respond to events, and the component or HTML that's generating the JavaScript to cause a postback does not itself implement IPostbackEventHandler. Let's say for instance that instead of adding an ASP.NET button control to the control hierarchy, you simply wrote the HTML directly to the response stream, like this:

C#
protected override void Render(HtmlTextWriter output)
{ 
  output.Write(
    "<a href='" + GetPostbackEventReference() + "'>Click me</a>) 
}

In this case, when the href is clicked, the reference to your control will be made through GetPostbackEventReference and the fact that your control implements INamingContainer, but the event won't actually be sent to your control to handle unless it implements IPostbackEventHandler.

Conclusion

Developing custom controls that embed other custom controls can be tricky until you understand the mechanisms of control instantiation, rendering and event wire up. The available documentation in general doesn't seem to provide clear answers and easily followed guidelines. In developing our first major commercial custom control, we worked through a number of issues and in the end, were pleased to find that if things are organized, the .NET Framework not only makes sense, but does a lot of the plumbing for you, like managing ViewState and sending events along the correct path.

This article has not attempted to provide a comprehensive guide to control development, but rather a simple reflection on our own experience and some examples of how we got things working well. I hope it saves you some time in your own custom control development.

Additional help

I'm very interested in helping anyone who wants to build a custom control. If you require more examples or source code, although I can't supply the full source code for the commercial control, I can certainly supply large pieces of it. Just let me know where you're having problems and I'll be glad to try to help as best as I can!

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


Written By
President Exia Corp.
Canada Canada
I grew up in a small town outside Montreal, Canada, where I grew to appreciate the Francophone culture, the hospitality of rural Quebec and the awesome skiing of the Eastern Townships. I studied Computer Science at the University of Waterloo, Classical Guitar at the University of Toronto, Electrical Engineering at the University of Ottawa, and earned an MBA at Queen's University.

I'm a partner in Exia Corp, developers of The Exia Process for better software project management.

Comments and Discussions

 
QuestionMultiple custom controls on page Pin
Dubravko B28-Oct-15 22:37
professionalDubravko B28-Oct-15 22:37 
QuestionData isn't being saved on UpDate Pin
Lord_Nick695-Oct-15 7:41
Lord_Nick695-Oct-15 7:41 
GeneralVery Useful Pin
Jhonathan Izquierdo6-Jun-13 6:19
Jhonathan Izquierdo6-Jun-13 6:19 
GeneralMy vote of 1 Pin
Ahmed_Said14-Jan-12 12:40
Ahmed_Said14-Jan-12 12:40 
Generalbase.CreateChildControls() Pin
Roy Oliver21-Oct-10 6:48
Roy Oliver21-Oct-10 6:48 
GeneralNeed Code Pin
Ratz Pilz19-Mar-10 4:10
Ratz Pilz19-Mar-10 4:10 
GeneralWeb File Manage Control Code needed Pin
deldin19-Mar-10 3:02
deldin19-Mar-10 3:02 
GeneralRe: Web File Manage Control Code needed Pin
deldin21-Mar-10 19:04
deldin21-Mar-10 19:04 
Generalawesome article. Pin
tzeis19-Feb-10 16:23
tzeis19-Feb-10 16:23 
QuestionCan i get some code on event wire up Pin
jometrv12-Jan-10 1:39
jometrv12-Jan-10 1:39 
QuestionExcellent, could you send me the code please? Pin
Member 227777225-Nov-09 22:40
Member 227777225-Nov-09 22:40 
GeneralThanks for posting this article Pin
rclabo6-May-09 11:40
rclabo6-May-09 11:40 
GeneralCode Please Pin
Shaka91328-Apr-09 10:33
Shaka91328-Apr-09 10:33 
GeneralMy vote of 1 Pin
kckn4fun19-Apr-09 6:54
kckn4fun19-Apr-09 6:54 
GeneralAwesome Pin
drummr852-Apr-09 10:58
drummr852-Apr-09 10:58 
GeneralCode please Pin
Tici29-Mar-09 21:50
Tici29-Mar-09 21:50 
GeneralCode Please Pin
aureolin13-Feb-09 13:22
aureolin13-Feb-09 13:22 
GeneralCode Please Pin
btwestmo6-Feb-09 9:22
btwestmo6-Feb-09 9:22 
GeneralPls send me the code. Pin
Member 20450427-Jan-09 10:17
Member 20450427-Jan-09 10:17 
QuestionSource code Pin
Dreyola10-Dec-08 1:01
Dreyola10-Dec-08 1:01 
GeneralThank you! Pin
mikeg181-Dec-08 15:24
mikeg181-Dec-08 15:24 
Questionplease could i have the code too? Pin
kindaska7421-Oct-08 5:36
kindaska7421-Oct-08 5:36 
GeneralSource Code request Pin
mwyrsch13-Oct-08 7:26
mwyrsch13-Oct-08 7:26 
GeneralPlease send me the source code Pin
Hasan_Abbas28-Sep-08 18:27
Hasan_Abbas28-Sep-08 18:27 
GeneralRe: Please send me the source code Pin
Nigel Shaw14-Mar-12 2:50
Nigel Shaw14-Mar-12 2:50 

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

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.