A Web User control enables you to build a new control from existing controls. By taking advantage of User controls, you can easily extend the ASP.NET Framework with your own custom controls.
Imagine, for example, that you need to display the same address form in multiple pages in a web application. The address form consists of several TextBox and Validation controls for entering address information. If you want to avoid declaring all the TextBox and Validation controls in multiple pages, you can wrap these controls inside a Web User control.
Anytime that you discover that you need to display the same user interface elements in multiple pages, you should consider wrapping the elements inside a User control. By taking advantage of User controls, you make your website easier to maintain and extend.
In this chapter, you learn how to build custom controls with User controls. It starts with the basics. You learn how to create a simple User control and expose properties and events from the User control.
You then examine how you can use AJAX with a User control. You learn how to modify the content displayed by a User control without posting the page that contains the User control back to the web server.
Finally, you learn how you can load User controls dynamically. You learn how to load a User control at runtime and inject the User control into a page. In the final section of this chapter, dynamically loaded User controls are used to build a multi-page wizard.
Let’s start by building a simple User control that randomly displays one image from a folder of images (see Figure 7.1). The code for the User control is contained in Listing 7.1.
Example 7.1. RandomImage.ascx
<%@ Control Language="VB" ClassName="RandomImage" %> <%@ Import Namespace="System.IO" %> <script runat="server"> Private Sub Page_Load() Dim imageToDisplay As String = GetRandomImage() imgRandom.ImageUrl = Path.Combine("~/Images", imageToDisplay) lblRandom.Text = imageToDisplay End Sub Private Function GetRandomImage() As String Dim rnd As New Random() Dim images() As String = Directory.GetFiles(MapPath("~/Images"), "*.jpg") Dim imageToDisplay As String = images(rnd.Next(images.Length)) Return Path.GetFileName(imageToDisplay) End Function </script> <asp:Image id="imgRandom" Width="300px" Runat="server" /> <br /> <asp:Label id="lblRandom" Runat="server" />
You create a new User control in Visual Web Developer by selecting Website, Add New Item, and selecting the Web User control item.
The file in Listing 7.1 closely resembles a standard ASP.NET page. Like a standard ASP.NET page, the User control contains a Page_Load()
event handler. Also, the User control contains standard controls such as the ASP.NET Image and Label controls.
User controls are closely related to ASP.NET pages. Both the UserControl
class and the Page
class derive from the base TemplateControl
class. Because they derive from the same base class, they share many of the same methods, properties, and events.
The important difference between an ASP.NET page and a User control is that a User control is something you can declare in an ASP.NET page. When you build a User control, you are building a custom control.
Notice that the file in Listing 7.1 ends with the .ascx
extension. You cannot request this file directly from a web browser. To use the RandomImage
User control, you must declare the control in an ASP.NET page.
The page in Listing 7.2 contains the RandomImage
User control. When you open the page, a random image is displayed.
Example 7.2. ShowRandomImage.aspx
<%@ Page Language="VB" %> <%@ Register TagPrefix="user" TagName="RandomImage" Src="~/UserControls/RandomImage.ascx" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Show RandomImage</title> </head> <body> <form id="form1" runat="server"> <div> <user:RandomImage ID="RandomImage1" Runat="server" /> </div> </form> </body> </html>
Before you can use a web User control in a page, you must register it. The page in Listing 7.2 includes a <%@ Register %>
directive that contains the following three attributes:
TagPrefix
—. Indicates the namespace that you want to associate with the User control for the current page. You can use any string that you want.
TagName
—. Indicates the name that you want to associate with the User control for the current page. You can use any string that you want.
Src
—. Indicates the virtual path to the User control (the path to the .ascx
file)
The RandomImage
User control is declared in the body of the page. It looks like this:
<user:RandomImage ID="RandomImage1" Runat="Server" />
Notice that the declaration of the User control uses the TagPrefix
and TagName
specified in the <%@ Register %>
directive. Furthermore, notice that you provide a User control with both an ID
and a Runat
attribute, just as you would for any standard ASP.NET control.
You can add a User control to a page in Visual Web Developer simply by dragging the User control from the Solution Explorer window onto the Design surface. The <%@ Register %>
directive is automatically added to the source of the page.
As an alternative to registering a User control in each page in which you need to use it by using the <%@ Register %>
directive, you can register a User control once for an entire application. You can register a User control in an application’s web configuration file.
For example, the web configuration file in Listing 7.3 registers the RandomImage
control for the application.
After you register a User control in the web configuration file, you can simply declare the User control in any page. For example, the page in Listing 7.4 contains an instance of the RandomImage
User control, but it does not include the <%@ Register %>
directive.
Example 7.4. ShowAppRegister.aspx
<%@ Page Language="VB" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Show Application Register</title> </head> <body> <form id="form1" runat="server"> <div> <user:RandomImage ID="RandomImage1" Runat="Server" /> </div> </form> </body> </html>
You need to be aware of one important limitation when registering a User control in the web configuration file. A User control cannot be located in the same folder as a page that uses it. For that reason, you should create all your User controls in a subfolder (I typically create a UserControls subfolder for each of my applications).
The RandomImage User control always displays an image from the Images folder. It would be nice if you could specify the name of the folder that contains the images so that you could use different folder paths in different applications. You can do this by exposing a property from the RandomImage
User control.
The modified RandomImage
control in Listing 7.5, named PropertyRandomImage
, exposes a property named ImageFolderPath
.
Example 7.5. PropertyRandomImage.ascx
<%@ Control Language="VB" ClassName="PropertyRandomImage" %> <%@ Import Namespace="System.IO" %> <script runat="server"> Private _imageFolderPath As String = "~/Images" Public Property ImageFolderPath() As String Get Return _imageFolderPath End Get Set(ByVal Value As String) _imageFolderPath = value End Set End Property Private Sub Page_Load() Dim imageToDisplay As String = GetRandomImage() imgRandom.ImageUrl = Path.Combine(_imageFolderPath, imageToDisplay) lblRandom.Text = imageToDisplay End Sub Private Function GetRandomImage() As String Dim rnd As New Random() Dim images() As String = Directory.GetFiles(MapPath("~/Images"), "*.jpg") Dim imageToDisplay As String = images(rnd.Next(images.Length)) Return Path.GetFileName(imageToDisplay) End Function </script> <asp:Image id="imgRandom" Width="300px" Runat="server" /> <br /> <asp:Label id="lblRandom" Runat="server" />
After you expose a property in a User control, you can set the property either declaratively or programmatically. The page in Listing 7.6 sets the ImageFolderPath
property declaratively.
Example 7.6. ShowDeclarative.aspx
<%@ Page Language="VB" %> <%@ Register TagPrefix="user" TagName="PropertyRandomImage" Src="~/PropertyRandomImage.ascx" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Show Declarative</title> </head> <body> <form id="form1" runat="server"> <div> <user:PropertyRandomImage ID="PropertyRandomImage1" ImageFolderPath="~/Images2" Runat="server" /> </div> </form> </body> </html>
Notice that the PropertyRandomImage
User control in Listing 7.6 includes an ImageFolderPath
property. When you request the page, the random images are retrieved from the Images2 folder.
Any properties that you add to a User control appear in both Intellisense and the Property window.
The page in Listing 7.7 demonstrates how you can set the ImageFolderPath
programmatically.
Example 7.7. ShowProgrammatic.aspx
<%@ Page Language="VB" %> <%@ Register TagPrefix="user" TagName="PropertyRandomImage" Src="~/PropertyRandomImage.ascx" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <script runat="server"> Protected Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs) PropertyRandomImage1.ImageFolderPath = "~/Images2" End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Show Programmatic</title> </head> <body> <form id="form1" runat="server"> <div> <user:PropertyRandomImage ID="PropertyRandomImage1" Runat="server" /> </div> </form> </body> </html>
The page in Listing 7.7 includes a Page_Load()
event handler. This handler programmatically sets the ImageFolderPath
to the value Images2
.
You can expose custom events from a User control. After you expose the event, you can handle the event in the page that contains the User control.
Exposing events is useful when you need to pass information up to the containing page. Imagine, for example, that you want to create a custom tab strip with a User control. When a user clicks a tab, you want to change the content displayed in the page (see Figure 7.2).
The User control in Listing 7.8 contains the code for a simple tab strip.
Example 7.8. TabStrip.ascx
<%@ Control Language="VB" ClassName="TabStrip" %> <%@ Import Namespace="System.Collections.Generic" %> <script runat="server"> Public Event TabClick As EventHandler ''' <summary> ''' The index of the selected tab ''' </summary> Public ReadOnly Property SelectedIndex() As Integer Get Return dlstTabStrip.SelectedIndex End Get End Property ''' <summary> ''' Create the tabs ''' </summary> Private Sub Page_Load() If Not Page.IsPostBack Then ' Create the tabs Dim tabs As New List(Of String)() tabs.Add("Products") tabs.Add("Services") tabs.Add("About") ' Bind tabs to the DataList dlstTabStrip.DataSource = tabs dlstTabStrip.DataBind() ' Select first tab dlstTabStrip.SelectedIndex = 0 End If End Sub ''' <summary> ''' This method executes when a user clicks a tab ''' </summary> Protected Sub dlstTabStrip_SelectedIndexChanged(ByVal sender As Object, ByVal e As EventArgs) RaiseEvent TabClick(Me, EventArgs.Empty) End Sub </script> <asp:DataList id="dlstTabStrip" RepeatDirection="Horizontal" OnSelectedIndexChanged="dlstTabStrip_SelectedIndexChanged" CssClass="tabs" ItemStyle-CssClass="tab" SelectedItemStyle-CssClass="selectedTab" Runat="server"> <ItemTemplate> <asp:LinkButton id="lnkTab" Text='<%# Container.DataItem %>' CommandName="Select" Runat="server" /> </ItemTemplate> </asp:DataList>
The tab strip is created with the help of a DataList
control. The DataList
control displays links for each of the items created in the Page_Load()
event handler.
Notice that the TabStrip
control exposes an event named TabClick
. This event is raised in the dlstTabStrip_SelectedIndexChanged()
event handler when a user clicks a tab.
The page in Listing 7.9 uses the TabStrip
control to display different content depending on the tab selected.
Example 7.9. ShowTabStrip.aspx
<%@ Page Language="VB" %> <%@ Register TagPrefix="user" TagName="TabStrip" Src="~/TabStrip.ascx" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <script runat="server"> Protected Sub TabStrip1_TabClick(ByVal sender As Object, ByVal e As EventArgs) MultiView1.ActiveViewIndex = TabStrip1.SelectedIndex End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <style type="text/css"> html { background-color:silver; font:14px Georgia,Serif; } .tabs a { color:blue; text-decoration:none; font:14px Arial,Sans-Serif; } .tab { background-color:#eeeeee; padding:5px; border:Solid 1px black; border-bottom:none; } .selectedTab { background-color:white; padding:5px; border:Solid 1px black; border-bottom:none; } .views { background-color:white; width:400px; border:Solid 1px black; padding:10px; } </style> <title>Show TabStrip</title> </head> <body> <form id="form1" runat="server"> <div> <user:TabStrip ID="TabStrip1" OnTabClick="TabStrip1_TabClick" Runat="Server" /> <div class="views"> <asp:MultiView id="MultiView1" ActiveViewIndex="0" Runat="server"> <asp:View ID="Products" runat="server"> <h1>Products</h1> We sell a variety of useful products... </asp:View> <asp:View ID="Services" runat="server"> <h1>Services</h1> We offer a number of services... </asp:View> <asp:View ID="About" runat="server"> <h1>About</h1> We were the first company to offer products and services... </asp:View> </asp:MultiView> </div> </div> </form> </body> </html>
The page in Listing 7.9 includes an event handler for the TabStrip
control’s TabClick
event. When you click a tab, the index of the selected tab is retrieved from the tab strip, and the View
control with the matching index is displayed.
You can add a TabClick
event handler to the TabStrip
control by selecting the TabStrip
control from the top-left drop-down list and selecting the TabClick
event from the top-right drop-down list.
The ASP.NET Framework includes a Menu control that you can use to create both tabstrips and pop-up menus. This control is discussed in Chapter 4, “Using the Rich Controls,” and Chapter 17, “Using the Navigation Controls.”
Let’s end this section by creating a generally useful Web User control. We’ll build an AddressForm User control that you can reuse in multiple pages or reuse multiple times in a single page (see Figure 7.3).
The AddressForm
User control is contained in Listing 7.10.
Example 7.10. AddressForm.ascx
<%@ Control Language="VB" ClassName="AddressForm" %> <script runat="server"> Public Property Title() As String Get Return ltlTitle.Text End Get Set (ByVal Value As String) ltlTitle.Text = value End Set End Property Public Property Street() As String Get Return txtStreet.Text End Get Set (ByVal Value As String) txtStreet.Text = value End Set End Property Public Property City() As String Get Return txtCity.Text End Get Set (ByVal Value As String) txtCity.Text = value End Set End Property Public Property State() As String Get Return txtState.Text End Get Set (ByVal Value As String) txtState.Text = value End Set End Property Public Property PostalCode() As String Get Return txtPostalCode.Text End Get Set (ByVal Value As String) txtPostalCode.Text = value End Set End Property </script> <fieldset> <legend> <asp:Literal ID="ltlTitle" Text="Address Form" runat="server" /> </legend> <div class="addressLabel"> <asp:Label ID="lblStreet" Text="Street:" AssociatedControlID="txtStreet" Runat="server" /> </div> <div class="addressField"> <asp:TextBox ID="txtStreet" Runat="server" /> <asp:RequiredFieldValidator ID="reqStreet" Text="(required)" ControlToValidate="txtStreet" Runat="server" /> </div> <br class="clear" /> <div class="addressLabel"> <asp:Label ID="lblCity" Text="City:" AssociatedControlID="txtCity" Runat="server" /> </div> <div class="addressField"> <asp:TextBox ID="txtCity" Runat="server" /> <asp:RequiredFieldValidator ID="reqCity" Text="(required)" ControlToValidate="txtCity" Runat="server" /> </div> <br class="clear" /> <div class="addressLabel"> <asp:Label ID="lblState" Text="State:" AssociatedControlID="txtState" Runat="server" /> </div> <div class="addressField"> <asp:TextBox ID="txtState" Runat="server" /> <asp:RequiredFieldValidator ID="reqState" Text="(required)" ControlToValidate="txtState" Runat="server" /> </div> <br class="clear" /> <div class="addressLabel"> <asp:Label ID="lblPostalCode" Text="Postal Code:" AssociatedControlID="txtPostalCode" Runat="server" /> </div> <div class="addressField"> <asp:TextBox ID="txtPostalCode" Runat="server" /> <asp:RequiredFieldValidator ID="RequiredFieldValidator1" Text="(required)" ControlToValidate="txtPostalCode" Runat="server" /> </div> <br class="clear" /> </fieldset>
The AddressForm
control contains form controls for entering your street, city, state, and postal code. Each of these fields is validated by a RequiredFieldValidator
control. Finally, the AddressForm
includes a Label that can be used to provide a title for the control.
The AddressForm
exposes all of its form fields with properties. The control includes public Street, City, State, and PostalCode property, which you can read from the containing page.
The page in Listing 7.11 illustrates how you can use the AddressForm
control in a page.
Example 7.11. Checkout.aspx
<%@ Page Language="VB" %> <%@ Register TagPrefix="user" TagName="AddressForm" Src="~/AddressForm.ascx" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <script runat="server"> Protected Sub btnSubmit_Click(ByVal sender As Object, ByVal e As EventArgs) ' Show Billing Address Form Results ltlResults.Text = "<br />Billing Street: " + AddressForm1.Street ltlResults.Text += "<br />Billing City: " + AddressForm1.City ltlResults.Text += "<br />Billing State: " + AddressForm1.State ltlResults.Text += "<br />Billing Postal Code: " + AddressForm1.PostalCode ltlResults.Text += "<br /><br />" ' Show Shipping Address Form Results ltlResults.Text += "<br />Shipping Street: " + AddressForm2.Street ltlResults.Text += "<br />Shipping City: " + AddressForm2.City ltlResults.Text += "<br />Shipping State: " + AddressForm2.State ltlResults.Text += "<br />Shipping Postal Code: " + AddressForm2.PostalCode End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <style type="text/css"> html { background-color:silver; font:14px Georgia,Serif; } .content { background-color:white; width:600px; margin:auto; padding:20px; } .addressLabel { float:left; width:100px; padding:5px; text-align:right; } .addressField { float:left; padding:5px; } .clear { clear:both; } </style> <title>Checkout</title> </head> <body> <form id="form1" runat="server"> <div class="content"> <user:AddressForm id="AddressForm1" Title="Billing Address" Runat="server" /> <br /> <user:AddressForm id="AddressForm2" Title="Shipping Address" Runat="server" /> <br /> <asp:Button ID="btnSubmit" Text="Submit Form" OnClick="btnSubmit_Click" Runat="server" /> <hr /> <asp:Literal id="ltlResults" Runat="server" /> </div> </form> </body> </html>
The page in Listing 7.11 contains two instances of the AddressForm
control: a Billing Address and Shipping Address. When you click the Button control, the address information is retrieved from the AddressForm
controls and displayed in a Literal control. (In a real application, you would grab the data and store it in a database.)
The AddressForm
User control does not use an HTML table to layout its controls. You should strive to avoid using tables except when displaying tabular information. Instead, Cascading Style Sheet rules are used to position the form elements. The page looks almost identical in Internet Explorer 6, Firefox 1.0, and Opera 8.0.
AJAX (Asynchronous JavaScript and XML) enables you to update content in a page without posting the page back to the server. Behind the scenes, AJAX uses the XMLHttp ActiveX component (in the case of Microsoft Internet Explorer) or the XMLHttpRequest intrinsic browser object (in the case of other browsers such as FireFox).
In the ASP.NET Framework, AJAX is referred to as client callbacks. To add AJAX support to a User control, you must implement the ICallBackEventHandler
interface and add the necessary JavaScript scripts to process the results of the AJAX call.
Here are the steps for implementing AJAX:
Create a client script for invoking the AJAX call. You can get this script with the Page.ClientScript.GetCallbackEventReference()
method.
Create server methods named RaiseCallbackEvent() and GetCallbackResult()
, which returns a string value from the server.
Create a client method that receives the value from the server RaiseCallbackEvent()
method and does something with the value.
For example, the User control in Listing 7.12 randomly displays one of three quotations. The quotation is updated automatically every 10 seconds (see Figure 7.4).
Example 7.12. RandomQuotation.ascx
<%@ Control Language="VB" ClassName="RandomQuotation" %> <%@ Implements Interface="System.Web.UI.ICallbackEventHandler" %> <%@ Import Namespace="System.Collections.Generic" %> <script runat="server"> Public Sub RaiseCallbackEvent(ByVal result As String) Implements ICallbackEventHandler.RaiseCallbackEvent End Sub Public Function GetCallbackResult() As String Implements ICallbackEventHandler.GetCallbackResult Dim quotes As New List(Of String)() quotes.Add("All paid jobs absorb and degrade the mind -- Aristotle") quotes.Add("No evil can happen to a good man, either in life or after death -- Plato") quotes.Add("The only good is knowledge and the only evil is ignorance -- Plato") Dim rnd As Random = New Random() Return quotes(rnd.Next(quotes.Count)) End Function Private Sub Page_Load() Dim callback As String = Page.ClientScript.GetCallbackEventReference(Me, Nothing, "UpdateQuote", Nothing, "CallbackError", True) Dim startupScript As String = String.Format("setInterval( ""{0}"", 5000 )", callback) Page.ClientScript.RegisterStartupScript(Me.GetType(), "RandomQuotation", startupScript, True) End Sub </script> <div id="divQuote" class="quote"> Random Quotation </div> <script type="text/javascript"> function UpdateQuote(result) { var divQuote = document.getElementById('divQuote'), divQuote.innerText = result; } function CallbackError(result) { alert( result ); } </script>
The Page_Load()
method in Listing 7.12 generates the client script for invoking the AJAX call. A reference to the script that makes the AJAX call is retrieved from the Page.ClientScript.GetCallbackEventReference()
method. The JavaScript setInterval()
method is used to execute this script every five seconds.
The User control implements the ICallbackEventHandler
interface. This interface has two methods that you must implement: RaiseCallbackEvent()
and GetCallbackResult()
. These two methods are called on the server in order. In Listing 7.12, the RaiseCallbackEvent()
does nothing and the GetCallbackResult()
method returns the random quotation to the client.
Finally, notice that the User control contains a client script block that contains two JavaScript functions. The first function, named UpdateQuote()
, displays the random quotation returned by the RaiseCallbackEvent()
in an HTML <div>
tag. The second method, named CallbackError()
, shows an alert dialog box when an error occurs during performance of the AJAX call.
The page in Listing 7.13 illustrates how you can use the RandomQuotation
User control. It contains the User control and it also displays the current time.
Example 7.13. ShowRandomQuotation.aspx
<%@ Page Language="VB" %> <%@ Register TagPrefix="user" TagName="RandomQuotation" Src="~/RandomQuotation.ascx" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <style type="text/css"> .quote { width:200px; padding:20px; border:Dotted 2px orange; background-color:#eeeeee; font:16px Georgia,Serif; } </style> <title>Show Random Quotation</title> </head> <body> <form id="form1" runat="server"> <div> <%= DateTime.Now %> <br /> <user:RandomQuotation id="RandomQuotation1" Runat="server" /> </div> </form> </body> </html>
Notice that the random quotation is updated, but that the time on the page does not change. Only the area of the page that contains the random quotation is updated.
You can dynamically load a User control at runtime and display it in a page. Imagine, for example, that you want to display different featured products randomly on the home page of your website. However, you want to display each featured product with a completely different layout. In that case, you can create a separate User control for each product and load one of the User controls randomly at runtime.
You load a User control with the Page.LoadControl()
method. This method returns an instance of the Control
class that you can add to a page. Typically, you add the User control to a PlaceHolder
control that you have declared on the page.
The PlaceHolder
control was designed to do absolutely nothing. It simply acts as a placeholder on the page where you can add other controls.
For example, the page in Listing 7.14 randomly loads one of the controls from the FeaturedProducts folder and adds the control to the page.
Example 7.14. ShowFeaturedProduct.aspx
<%@ Page Language="VB" %> <%@ Import Namespace="System.IO" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <script runat="server"> Const randomFolder As String = "~/FeaturedProducts" Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs) Dim featuredProductPath As String = GetRandomProductPath() Dim featuredProduct As Control = Page.LoadControl(featuredProductPath) PlaceHolder1.Controls.Add(featuredProduct) End Sub Private Function GetRandomProductPath() As String Dim rnd As New Random() Dim files() As String = Directory.GetFiles(MapPath(randomFolder),"*.ascx") Dim featuredProductPath As String = Path.GetFileName(files(rnd.Next(files.Length))) Return Path.Combine(randomFolder,featuredProductPath) End Function </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Show Featured Products</title> </head> <body> <form id="form1" runat="server"> <div> <asp:PlaceHolder id="PlaceHolder1" Runat="server" /> </div> </form> </body> </html>
When you load a User control with the Page.LoadControl()
method, the User control is returned as an instance of the System.Web.UI.Control
class. This means that if the User control includes any custom properties, the properties aren’t available when you dynamically load the User control.
If you dynamically load a User control, then you need to cast the control to the correct type before you can access any of the control’s custom properties. To get a reference to a User control’s type, you must use the <%@ Reference %>
directive.
For example, imagine that you need to create a form that displays different questions depending on the answers that a user provides for previous questions. In that case, you can dynamically load different User controls that contain the different sets of questions.
For example, the page in Listing 7.15 contains a survey form. The first question asks you whether you are currently using ASP Classic or ASP.NET. Depending on your answer, the remainder of the form displays different questions (see Figure 7.5).
Example 7.15. WebSurvey.aspx
<%@ Page Language="VB" %> <%@ Reference Control="~/ASPSurvey.ascx" %> <%@ Reference Control="~/ASPNetSurvey.ascx" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <script runat="server"> Private _survey As Control = Nothing Private Sub Page_Load() Select Case ddlLanguage.SelectedIndex Case 1 _survey = Page.LoadControl("ASPSurvey.ascx") Case 2 _survey = Page.LoadControl("ASPNetSurvey.ascx") End Select If Not IsNothing(_survey) Then PlaceHolder1.Controls.Add(_survey) End If End Sub Protected Sub btnSubmit_Click(ByVal sender As Object, ByVal e As EventArgs) Select Case ddlLanguage.SelectedIndex Case 1 Dim aspResults As ASPSurvey = CType(_survey, ASPSurvey) ltlResults.Text = "<h1>ASP Survey</h1>" ltlResults.Text += "<br />Know slow? " + aspResults.KnowSlow.ToString() ltlResults.Text += "<br />Know outdated? " + aspResults.KnowOutdated.ToString() Case 2 Dim aspNetResults As ASPNetSurvey = CType(_survey, ASPNetSurvey) ltlResults.Text = "<h1>ASP.NET Survey</h1>" ltlResults.Text += "<br />Know fast? " + aspNetResults.KnowFast.ToString() ltlResults.Text += "<br />Know newest? " + aspNetResults.KnowNewest.ToString() End Select End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <style type="text/css"> html { font:14px Arial,Sans-Serif; } </style> <title>Web Survey</title> </head> <body> <form id="form1" runat="server"> <div> <asp:Label id="lblLanguage" Text="What language do you use to develop Web applications?" Runat="server" /> <br /> <asp:DropDownList id="ddlLanguage" ToolTip="Web application language (reloads form)" AutoPostBack="true" Runat="server"> <asp:ListItem Text="Select Language" /> <asp:ListItem Text="ASP Classic" /> <asp:ListItem Text="ASP.NET" /> </asp:DropDownList> <br /><br /> <asp:PlaceHolder id="PlaceHolder1" Runat="server" /> <asp:Button id="btnSubmit" Text="Submit" OnClick="btnSubmit_Click" Runat="server" /> <hr /> <asp:Literal id="ltlResults" Runat="server" /> </div> </form> </body> </html>
The DropDownList
control in Listing 7.15 reloads the page automatically when you select a new option. You should never reload a page without warning the user because this can be very confusing for someone who is using an assistive device such as a screen reader. In Listing 7.15, a warning is added to the ToolTip
property of the DropDownList
control.
Depending on the user’s selection from the DropDownList
control, one of two User controls is loaded in the Page_Load()
event handler: the ASPSurvey.ascx
or the ASPNetSurvey.ascx
User control. These controls are contained in Listing 7.16 and Listing 7.17.
When you submit the survey form, the btnSubmit_Click()
method executes. This method casts the User control loaded in the form to the correct type. It casts the User control to either the ASPSurvey
or the ASPNetSurvey
type.
Notice that the page in Listing 7.15 includes two <%@ Reference %>
directives. These reference directives enable you to cast the User control to the correct type so that you can access custom properties of the control such as the KnowSlow
and KnowOutdated
properties.
Example 7.16. ASPSurvey.ascx
<%@ Control Language="VB" ClassName="ASPSurvey" %> <script runat="server"> Public ReadOnly Property KnowSlow() As Boolean Get Return chkSlow.Checked End Get End Property Public ReadOnly Property KnowOutdated() As Boolean Get Return chkOutdated.Checked End Get End Property </script> <asp:CheckBox id="chkSlow" Text="Did you know that ASP Classic is slow?" Runat="server" /> <br /><br /> <asp:CheckBox id="chkOutdated" Text="Did you know that ASP Classic is outdated?" Runat="server" /> <br /><br />
Example 7.17. ASPNetSurvey.ascx
<%@ Control Language="VB" ClassName="ASPNetSurvey" %> <script runat="server"> Public ReadOnly Property KnowFast() As Boolean Get Return chkFast.Checked End Get End Property Public ReadOnly Property KnowNewest() As Boolean Get Return chkNewest.Checked End Get End Property </script> <asp:CheckBox id="chkFast" Text="Did you know that ASP.NET is fast?" Runat="server" /> <br /><br /> <asp:CheckBox id="chkNewest" Text="Did you know that ASP.NET is the newest Microsoft technology for building Web applications?" Runat="server" /> <br /><br />
This final section discusses how you can create a multi-page wizard by dynamically loading different user controls into the same page. This is going to be a complicated sample, but it is a realistic sample of situations when you would want to load User controls dynamically (see Figure 7.6).
Imagine that you must create a form with 200 questions in it. Displaying all 200 questions to a user in a single form would be overwhelming. Instead, it makes more sense to break the form into multiple pages. Each page of questions can be represented with a User control.
First, you need to define an interface, named the IWizardStep
interface, which all the User controls will implement. An interface enables you to know, in advance, that a User control supports a particular set of properties or methods.
You need to add the interface in Listing 7.18 to your application’s App_Code folder. In Visual Web Developer, create the interface by selecting Website, Add New Item, and select Class. Visual Web Developer prompts you to create the App_Code folder.
The IWizardStep
interface is contained in Listing 7.18.
The interface in Listing 7.18 contains two methods: LoadStep()
and NextStep()
. The LoadStep()
method is called when a User control is first loaded. The NextStep()
method is called when the Next button is clicked in the wizard.
Notice that the NextStep()
method returns a Boolean value. If the NextStep()
method returns the value False
, then the user doesn’t advance to the next wizard step.
This wizard will consist of the three wizard steps contained in Listing 7.19, Listing 7.20, and Listing 7.21.
Example 7.19. WizardStepsStep1.ascx
<%@ Control Language="VB" ClassName="Step1" %> <%@ Implements Interface="IWizardStep" %> <script runat="server"> Public Sub LoadStep() Implements IWizardStep.LoadStep If Not IsNothing(Session("FirstName")) Then txtFirstName.Text = CStr(Session("FirstName")) End If If Not IsNothing(Session("LastName")) Then txtLastName.Text = CStr(Session("LastName")) End If End Sub Public Function NextStep() As Boolean Implements IWizardStep.NextStep If Page.IsValid Then Session("FirstName") = txtFirstName.Text Session("LastName") = txtLastName.Text Return True End If Return False End Function </script> <h1>Step 1</h1> <asp:Label id="lblFirstName" Text="First Name:" AssociatedControlID="txtFirstName" Runat="server" /> <asp:RequiredFieldValidator id="reqFirstName" Text="(required)" ControlToValidate="txtFirstName" Runat="server" /> <br /> <asp:TextBox id="txtFirstName" Runat="server" /> <br /><br /> <asp:Label id="lblLastName" Text="Last Name:" AssociatedControlID="txtLastName" Runat="server" /> <asp:RequiredFieldValidator id="reqLastName" Text="(required)" ControlToValidate="txtLastName" Runat="server" /> <br /> <asp:TextBox id="txtLastName" Runat="server" />
The wizard step in Listing 7.19 contains a simple form that contains Textbox controls for the user’s first and last name. Both TextBox controls are validated with RequiredFieldValidator
controls.
Notice that the User control in Listing 7.19 implements the IWizardStep
interface. It contains an <%@ Implements %>
directive at the top of the control.
The LoadStep()
method assigns values to the txtFirstName
and txtLastName
TextBox controls from Session state. The NextStep()
method grabs the values from the txtFirstName
and txtLastName
TextBox controls and assigns the values to Session state.
The second step of the Wizard is contained in Listing 7.20.
Example 7.20. WizardStepsStep2.ascx
<%@ Control Language="VB" ClassName="Step2" %> <%@ Implements Interface="IWizardStep" %> <script runat="server"> Public Sub LoadStep() Implements IWizardStep.LoadStep If Not IsNothing(Session("FavoriteColor")) Then txtFavoriteColor.Text = CStr(Session("FavoriteColor")) End If End Sub Public Function NextStep() As Boolean Implements IWizardStep.NextStep If Page.IsValid Then Session("FavoriteColor") = txtFavoriteColor.Text Return True End If Return False End Function </script> <h1>Step 2</h1> <asp:Label id="lblFavoriteColor" Text="Favorite Color:" AssociatedControlID="txtFavoriteColor" Runat="server" /> <asp:RequiredFieldValidator id="reqFavoriteColor" Text="(required)" ControlToValidate="txtFavoriteColor" Runat="server" /> <br /> <asp:TextBox id="txtFavoriteColor" Runat="server" />
The User control in Listing 7.20 also implements the IWizardStep
interface. In this step, the user enters a favorite color.
The final wizard step is contained in Listing 7.21.
Example 7.21. WizardStepsStep3.ascx
<%@ Control Language="VB" ClassName="Step3" %> <%@ Implements Interface="IWizardStep" %> <script runat="server"> Public Sub LoadStep() Implements IWizardStep.LoadStep lblFirstName.Text = CStr(Session("FirstName")) lblLastName.Text = CStr(Session("LastName")) lblFavoriteColor.Text = CStr(Session("FavoriteColor")) End Sub Public Function NextStep() As Boolean Implements IWizardStep.NextStep Return False End Function </script> <h1>Step 3</h1> First Name: <asp:Label id="lblFirstName" Runat="server" /> <br /> Last Name: <asp:Label id="lblLastName" Runat="server" /> <br /> Favorite Color: <asp:Label id="lblFavoriteColor" Runat="server" />
The wizard step in Listing 7.21 displays a summary of answers that the user has provided in the first two wizard steps (see Figure 7.7). Notice that it also implements the IWizardStep
interface. Because this is the final wizard step, the NextStep()
method always returns the value False
.
The page in Listing 7.22 contains the actual wizard. This page loads each of the wizard steps.
Example 7.22. Wizard.aspx
<%@ Page Language="VB" %> <%@ Import Namespace="System.Collections.Generic" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <script runat="server"> Private _wizardSteps As New List(Of String)() Private _currentStep As Control ''' <summary> ''' The current step in the Wizard ''' </summary> Public Property StepIndex() As Integer Get If (IsNothing(ViewState("StepIndex"))) Then Return 0 else Return CInt(ViewState("StepIndex")) End If End Get Set (ByVal Value As Integer) ViewState("StepIndex") = value End Set End Property ''' <summary> ''' Load the list of wizard steps and load ''' current step ''' </summary> Sub Page_Load() _wizardSteps.Add("~/WizardSteps/Step1.ascx") _wizardSteps.Add("~/WizardSteps/Step2.ascx") _wizardSteps.Add("~/WizardSteps/Step3.ascx") LoadWizardStep() End Sub ''' <summary> ''' Load the current wizard step ''' </summary> Private Sub LoadWizardStep() _currentStep = Page.LoadControl(_wizardSteps(StepIndex)) _currentStep.ID = "ctlWizardStep" plhWizardStep.Controls.Clear() plhWizardStep.Controls.Add(_currentStep) CType(_currentStep, IWizardStep).LoadStep() ltlStep.Text = String.Format("Step {0} of {1}", StepIndex + 1, _wizardSteps.Count) End Sub ''' <summary> ''' Disable the Previous and Next ''' buttons when appropriate ''' </summary> Sub Page_PreRender() btnPrevious.Enabled = StepIndex > 0 btnNext.Enabled = StepIndex < _wizardSteps.Count - 1 End Sub ''' <summary> ''' Execute the step's NextStep() method ''' and move to the next step ''' </summary> Sub btnNext_Click(ByVal sender As Object, ByVal s As EventArgs) Dim success As Boolean = CType(_currentStep, IWizardStep).NextStep() If success Then If (StepIndex < _wizardSteps.Count - 1) Then StepIndex = StepIndex + 1 LoadWizardStep() End If End If End Sub ''' <summary> ''' Move to the previous step ''' </summary> Sub btnPrevious_Click(ByVal sender As Object, ByVal e As EventArgs) If StepIndex > 0 Then StepIndex = StepIndex - 1 LoadWizardStep() End If End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <style type="text/css"> html { font:14px Georgia,Serif; } fieldset { display:block; width:600px; padding:20px; margin:10px; } </style> <title>Wizard</title> </head> <body> <form id="form1" runat="server"> <div> <asp:Label id="lblStepNumber" Runat="server" /> <fieldset> <legend><asp:Literal ID="ltlStep" runat="server" /></legend> <asp:PlaceHolder id="plhWizardStep" Runat="server" /> </fieldset> <asp:Button id="btnPrevious" Text="< Previous" CausesValidation="false" OnClick="btnPrevious_Click" Runat="server" /> <asp:Button id="btnNext" Text="Next >" OnClick="btnNext_Click" Runat="server" /> </div> </form> </body> </html>
The list of wizard steps is created in the Page_Load()
method. The path to each wizard step User control is added to a collection of wizard steps.
The StepIndex
property represents the index of the wizard step to display. Notice that the value of this property is stored in ViewState
so that the value is available across multiple page requests.
The current wizard step is loaded by the LoadWizardStep()
method. This method uses the StepIndex to grab the path to the current wizard step. Next, it uses the Page.LoadControl()
method to actually load the wizard step User control.
After the LoadWizardStep()
method loads the current wizard step, it calls the control’s LoadStep()
method and initializes the control.
The page also contains a Previous and Next button. When you click the Previous button, the btnPrevious_Click()
method is called and the StepIndex
is reduced by one. When you click the Next button, the btnNext_Click()
method is called.
The btnNext_Click()
method first calls the current wizard step’s NextStep()
method. If this method returns the value True
, then one is added to the StepIndex
property and the next wizard step is loaded. Otherwise, if the NextStep()
method returns false
, the next wizard step is not loaded.
In this chapter, you learned how to build custom controls by creating User controls. The first section covered the basics of User controls. You learned how to create a User control and register it both in a page and in a Web configuration file. You learned how to add custom properties and events to a User control.
You also explored the topic of AJAX and User controls. You learned how to update content in a User control without posting the page that contains the User control back to the web server.
Finally, you learned how to add User controls dynamically to a page. You learned how to use the <%@ Reference %>
directive to cast a User control to a particular type. You also learned how to create a multi-page wizard by dynamically loading a series of User controls.