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:
[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:
[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.
[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:
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...
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...
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:
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.)
protected override void OnInit(EventArgs e)
{
InitializeControls();
base.OnInit (e);
}
private void InitializeControls()
{
FileListGrid.AutoGenerateColumns = false;
FileListGrid.ShowHeader = true;
etc.
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:
protected override void CreateChildControls()
{
BuildControlHeirarchy();
...
...
}
private void BuildControlHeirarchy()
{
Controls.Add( BorderTable );
BorderTable.CellSpacing = 0;
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:
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
.
protected override void Render(HtmlTextWriter output)
{
if( ! Visible )
return;
if( FileListGrid.Items.Count == 0 )
FileListGrid.ShowHeader = false;
output.Write( "<link href='" + CssFile + "'
type='text/css' rel='stylesheet'>" );
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:
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:
[Browsable(false)]
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:
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...
DeleteBtn.Click += new EventHandler(DeleteBtn_Click);
... and doing our custom action...
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:
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!