| User Controls |
| Many articles have been written about creating
and using user controls in ASP.Net. Most do a good job of describing how to
create and statically add controls and some even cover dynamically adding the
control. There seems to be a lack of information on some of the difficulties
you can experience when using user controls. I'll provide basic coverage of
creating and adding the control to a page statically, exposing properties and
raising events. I'll also be looking at the issues of managing dynamic
controls.
|
|
|
| The Basic Control |
|
I'm going to start with a menu type control
that provides several hyperlinks to various pages in a web site. The basic
control architecture is to allow data binding the list of menu items from the
control page. I will add a DataList of LinkButtons for our menu. I will also
keep the data binding simple by assuming we are binding to a DataSet or
DataTable. The result of our controls can be seen in Figure 1. |
| Figure 1. |
|
|
| Since a user control is placed inside a form
where I cannot directly control the layout, I should enforce the layout using
tables or other containers and not rely on flow layout. |
| One of the first issues you can encounter is
dependent upon how you handle the creation of the LinkButtons during data
binding. If I create an event handler for the ItemDataBound event of the
DataList I can certainly find the LinkButton control and set its text and
CommanArgument properties. I can also attach an event handler for the Command
event during this process. If I test this arrangement we notice that the
Command event handler never gets entered. I have to change the event wire up to
get the event to behave properly. I need to specify the name of the event
handler on the ascx page inside the LinkButton element. After I make this
slight correction, the event handler works as expected. If I declare all the
information using data binding expressions with the declarative OnCommand event
handler I do not have this issue. Listing 1 demonstrates the ItemDataBound
event handler code and Listing 2 demonstrates the alternative DataList with the
data binding expressions. |
| Listing 1. |
private void MenuList_ItemDataBound(object sender, DataListItemEventArgs e)
{
if( e.Item.ItemType == ListItemType.Item
|| e.Item.ItemType == ListItemType.AlternatingItem)
{
DataRowView drv = (DataRowView)e.Item.DataItem;
LinkButton lnk = (LinkButton)e.Item.FindControl("lnkNav");
lnk.Text = drv[displayField].ToString();
lnk.CommandArgument = drv[dataKeyField].ToString();
}
}
|
| Listing 2. |
<asp:DataList id="MenuList" runat="server" RepeatDirection="Horizontal" CellSpacing="10">
<ItemTemplate>
<asp:LinkButton ID="lnkNav" Runat="server" OnCommand="lnk_Command"
CommandArgument='<%# DataBinder.Eval(Container.DataItem, DataKeyField) %>'>
<%# DataBinder.Eval(Container.DataItem, DisplayField) %></asp:LinkButton>
</ItemTemplate>
</asp:DataList>
|
| Adding Events |
| Now that I have the event handlers in my menu
control I need to propagate these to the parent. I must first decide how I want
to send the value back to the parent. The two options I consider are setting a
read only field on the menu control, or creating our own event arguments. I have
decided on the latter method for this example.
|
| I create our event arguments, called
ItemSelectedEventArgs, by inheriting from EventArgs and adding a Key property
to pass the key value back to the parent. The code for our event arguments can
be seen in Listing 3. |
| Listing 3. |
public class ItemSelectedEventArgs : EventArgs
{
private string key;
public string Key
{
get{ return key; }
}
public ItemSelectedEventArgs(string key)
{
this.key = key;
}
}
|
| To complete our event, I need to create a
delegate. A delegate is a type-safe method reference. This allows the parent to
provide the method to call when the event is raised. I will call our delegate
ItemSelectedDelegate and our event will be called ItemSelected. In following
the standard approach for events, I create a method called OnItemCreated which
determines if there are any subscribers to the event and calls the delegate if
there are. I call this method from the event handler for our LinkButtons.
Listing 4 shows the related code sections for the delegate and event. |
| Listing 4. |
public delegate void ItemSelectedDelegate(object sender, ItemSelectedEventArgs e);
...
public class MenuControl : System.Web.UI.UserControl
{
protected System.Web.UI.WebControls.DataList MenuList;
protected string displayField;
protected string dataKeyField;
public event ItemSelectedDelegate ItemSelected;
...
protected void lnk_Command(object sender, CommandEventArgs e)
{
ItemSelectedEventArgs selArgs = new ItemSelectedEventArgs(e.CommandArgument.ToString());
OnItemSelected(selArgs);
}
protected void OnItemSelected(ItemSelectedEventArgs e)
{
if( ItemSelected != null )
ItemSelected(this, e);
}
...
|
| Now that I have the event, I can simply add an
event handler form our main page. The method for retrieving user data from a
control follows the same pattern, expose the data field as a property and allow
the client to retrieve it. Figure 2 shows the result of the event on our form.
|
| Figure 2. |
|
|
| Adding Controls Dynamically |
| But what if I don't know which controls I will
need at design time? Do I code in every possible control and write code to show
the control I want and hide the rest? Then what happens if I have to add yet
another control? As you can see this situation could very easily get out of
control. Fortunately ASP.Net allows us to dynamically load controls at run time
by using the Page.LoadControl method. |
| The LoadControl method takes the path to our
user control and returns a type of control. For this to be useful I need to
cast the Control to our specific type. I can also extend this by using
polymorphism through a base class or interface on our control. This would allow
the most flexibility in extending our solution. |
| For our example I will keep the code simple.
I'll be creating a series of controls that displays the item selected from the
menu and allows the user to input some text. The first thing I need to do is
create a control container for the dynamic control. I also need to determine
which control to create based on the user selection in the menu. This is a
simple switch statement but provides us with a factory for creating our user
controls. I'll be using a div as a control container for our dynamic controls.
Figure 3 displays the results of a selected item with the dynamic control. Each
time I select a new menu item, I will dynamically create and add the control to
our container. |
| Figure 3. |
|
|
| Now I want to perform an action when the user
clicks a button on our main form. I simply add a button to the form and add an
event handler and I should be all set. In this case I'll simply change the text
of the button so the event can be seen. I first click on one of the menu items
to dynamically load the control. This can be seen in Figure 4. I then click the
button and my control is gone. This can be seen in Figure 5. This control
disappearing act was caused by the control not being reloaded on the page.
During the post back, the page has no idea about this new control. To overcome
this I have to somehow maintain the state of the page. To do this, I'll add the
key value that I get from the menu control into the view state when I create
the control. I'll also have to add some code on the page load event to recreate
the control on every request. The page load and the control creation code can
be seen in Listing 5. Now the control will be added at every request. |
| Figure 4. |
|
|
| Figure 5. |
|
|
| Listing 4. |
private void Page_Load(object sender, System.EventArgs e)
{
if( ! IsPostBack )
{
MainMenu.DataSource = GetDataTable();
MainMenu.DataKeyField = "ItemId";
MainMenu.DisplayField = "Title";
MainMenu.DataBind();
}
else
{
object key = ViewState["ControlKey"];
if( key != null )
{
int keyValue = (int)key;
AddControl(keyValue);
}
}
}
...
private void MainMenu_ItemSelected(object sender, ItemSelectedEventArgs e)
{
this.lblSelectedItem.Text = "You have selected item " + e.Key;
int keyValue = int.Parse(e.Key);
AddControl(keyValue);
ViewState["ControlKey"] = keyValue;
}
...
private void AddControl(int keyValue)
{
CtrlContainer.Controls.Clear();
Control ctrl = null;
switch(keyValue)
{
case 1:
ctrl = Page.LoadControl("BookControl.ascx");
break;
case 2:
ctrl = Page.LoadControl("AuthorControl.ascx");
break;
case 3:
ctrl = Page.LoadControl("ProductControl.ascx");
break;
case 4:
ctrl = Page.LoadControl("CustomerControl.ascx");
break;
}
CtrlContainer.Controls.Add(ctrl);
}
|
|
Expanding the functionality of the control, I want to capture a button click
each one of our controls. I'll add a button and event handler to each of the
user controls. With this addition, the initial form after the click on the
control can be seen in Figure 6. This performs as expected in our new form. |
| Figure 6. |
|
|
| The next step is to actually capture some
input from the user. I'll add a drop down list to the form and have the
selected text become the label of the button. The results of this scenario can
be seen in Figure 8.
|
| Figure 7. |
|
|
| Summary |
| While the examples I have presented are
simple, they allow you to see how this could be extended into more complex
controls. Hopefully you'll be able to avoid some of the common issues people
have with using user controls.
|