Programming Microsoft Web Forms (Pro Developer)

 

Custom Server Controls

With the addition of the rich design-time support added in Visual Studio 2005, are there still reasons to use something other than a user control when you want to use the same bit of code and markup on multiple pages? Read on.

User controls can do almost anything you would like to do with a control. There are a few exceptions, however. For instance, if you are distributing a control beyond just a small group of developers, a user control, with multiple files required, might not be ideal. In addition, the developer experience is similar to that of the built-in controls, but not exactly the same. If your control must be located in the Toolbox rather than dragged from Solution Explorer, you need a custom server control.

Another significant issue with user controls is that some portion of the control is exposed to any developer who might be using it. Specifically, the .ascx file, which contains the markup for the user control, is usually available to a developer who is using the control for modification. This might not always be a good thing.

Note 

ASP.NET 2.0 can compile the markup in the .aspx and .ascx files, leaving behind only placeholder files.

A custom server control allows you to create controls that do not operate almost like the builtin controls, but exactly like them.

Building and Installing the Default WebCustomControl Control

It is best to create a separate project to contain all of your custom server controls so that they can easily be deployed to multiple Web sites. To contain the custom server controls for this chapter, I created another new project named CustomControlsLib, using the Class Library template, as shown in Figure 6-10. To create a project, rather than a Web site, point to New on the File menu, and then select Project, rather than Web Site.

Figure 6-10: Creating a project in Visual Studio for custom server controls

After clicking OK, Class1.cs opens in the editor. This class is not required, so it can be deleted by opening Solution Explorer, right-clicking Class1.cs, and selecting Delete from the context menu.

To create a custom server control, right-click the project in Solution Explorer, select Add from the context menu, and then select New Item from the menu that appears. In the Add New Item dialog box, select Web Custom Control from the list of installed templates, as shown in Figure 6-11.

Figure 6-11: Creating a new Web custom control in Visual Studio

A default custom control is created, named WebCustomControl1. The simple default code for the control inherits from the WebControl class, as shown in Listing 6-9.

Listing 6-9: WebCustomControl1.cs, Created by Visual Studio

using System; using System.Collections.Generic; using System.ComponentModel; using System.Text; using System.Web.UI; using System.Web.UI.WebControls; namespace CustomControlsLib { [DefaultProperty("Text")] [ToolboxData("<{0}:WebCustomControl1 runat=server></{0}:WebCustomControl1>")] public class WebCustomControl1 : WebControl { private string text; [Bindable(true)] [Category("Appearance")] [DefaultValue("")] public string Text { get { return text; } set { text = value; } } protected override void Render(HtmlTextWriter output) { output.Write(Text); } } }

After the standard using lines is a namespace declaration. A namespace is a way to ensure that names of classes are unique. After the namespace declaration and just above the class declaration are a few lines that set attributes for the class.

Note 

Attributes in .NET provide a declarative way to annotate or describe specific elements of code. Attributes can be relevant to classes, methods, or properties. At compile time, metadata describing the attributes specified are stored in the resulting executable code.

The DefaultProperty attribute specifies the default property for the component, which is the property that has focus when the Properties window is opened. The ToolboxData attribute describes how the control will be named when placed on a form.

After the attributes is the class declaration, where the code specifies that the WebCustomControl1 class inherits from the WebControl class. In Chapter 2, "A Multitude of Controls," Table 2-6 described the important properties of the WebControl class.

The WebCustomControl1 class contains a single private data member, named text. After text is declared, a property, somewhat confusingly named Text (note the uppercase T) is declared, which also has attributes. The first attribute is Bindable, and true is passed in as the argument, setting this attribute as bindable by default. The Bindable attribute is used at design time, and if it is set to true, two-way binding is supported. Two-way data binding in ASP.NET 2.0 allows changes to the underlying data to be pushed back. Two-way data binding uses the Bind method in places where one-way binding uses the Eval method. The Bind method can be used only in GridView, DetailsView, and FormView controls. Even if the Bindable attribute is set to false, the property can be used for data binding, although you should not rely on property change notifications being raised. The second attribute of the Text property is Category, which is used to place the property in a particular category when the properties of the control are viewed in category order. Finally, the DefaultValue attribute allows the developer of the control to supply a default value for the property. Visual Studio supplies an empty string ("") as the default value. The get and set methods of the Text property are very straightforward, simply returning or setting the text private data member.

The final member of the default WebCustomControl1 class created by Visual Studio is an override of the Render method. The Render method has a single parameter, an HtmlTextWriter object. This object contains a large number of methods to emit markup for the control. In the example provided by Visual Studio, the Write method is used to write the contents of the Text property to an output stream to be sent to the client for rendering on the client browser.

After you build the CustomControlLib project, you can switch back to the CustomControl project and add the newly created control to the Toolbox. If you right-click the control in the Toolbox and click the Choose Items option, the Choose Toolbox Items dialog box appears, as shown in Figure 6-12.

Figure 6-12: The Choose Toolbox Items dialog box in Visual Studio

To add the new control, click Browse, and then find the folder where the dynamic-link library (DLL) containing the control is located (in the bin\debug folder under the project folder). Select the .dll file, and the Choose Toolbox Items dialog box appears as shown in Figure 6-13. The first of the controls found in the .dll file is highlighted and selected.

Figure 6-13: The Choose Toolbox Items dialog box in Visual Studio, with the new control selected

After you add the control to the Toolbox, it appears as shown in Figure 6-14.

Figure 6-14: The Toolbox with the new control added

To test the new control, I created a new Web Form in the CustomControl Web site named TestCustomControl.aspx. After changing the title of the page from the default, I switched to Design view and dragged an instance of the new WebCustomControl1 control from the Toolbox onto the form. The control in Design view appeared as shown in Figure 6-15.

Figure 6-15: The WebCustomControl1 control dropped onto TestCustomControl.aspx

The design-time support in Visual Studio is very powerful. If a property on your custom server control is changed, the design surface immediately changes. Setting the Text property in the Properties window, for example, immediately changes the appearance of the control in Design view. Figure 6-16 shows the control in Design view after the Text property was changed to "Hello World!"

Figure 6-16: The WebCustomControl1 control after changing the Text property in the Properties window

A more practical use for a custom control might be to create a label with some display attribute automatically applied. By changing the Render method, we could change the default appear- ance of the text rendered by the control. For instance, the control could be modified to display the text as an H1 header as follows.

protected override void Render(HtmlTextWriter output) { output.Write(string.Format("<H1>{0}</H1>", Text)); }

After this change is made, the page looks like Figure 6-17 when run.

Figure 6-17: The WebCustomControl1 control rendering the text as an H1 element

The example control here is very simple, but controls built by overriding the Render method can be as complex as you can imagine. You can provide HTML markup as simple or as intricate as you want. But there is yet another way to build custom server controls.

Building a Composite Control

Although creating very complex HTML is often a good way to create a custom server control, there is an alternative. For instance, imagine that you want to create a control that creates a drop-down list with a built-in required field validator. You could certainly do this by crafting JavaScript that allowed you to duplicate the functionality of a validator control, but a better solution would be to create a control that used the power of existing ASP.NET controls. Fortunately, a composite control is just that.

In addition to the Render method, the CreateChildControls method can also be overridden. By using this method, you can create the Web server controls that will, in the end, be rendered on the Web Form they are placed on, just as if the constituent controls had been placed on the form individually. I created a new file named RequiredTextBox.cs in the same way that I created WebCustomControl.cs, and then I modified the code to use the CreateChildControls method and deleted the override provided for the Render method. Listing 6-10 shows the code for a control that uses composition to create a single component that acts as a required text box.

Listing 6-10: RequiredTextBox.cs

using System; using System.Collections.Generic; using System.ComponentModel; using System.Text; using System.Web.UI; using System.Web.UI.WebControls; namespace CustomControlsLib { [DefaultProperty("Text")] [ToolboxData("<{0}:RequiredTextBox runat=server></{0}:RequiredTextBox>")] public class RequiredTextBox : WebControl, INamingContainer { private string text; private string errorMessage; private string validatorText; private string validationGroup; private System.Drawing.Color validatorColor; #region Properties [Bindable(true)] [Category("Appearance")] [DefaultValue("")] public string Text { get { return text; } set { text = value; } } [Bindable(false)] [Category("Validator")] public string ValidatorText { get { return validatorText; } set { validatorText = value; } } [Bindable(false)] [Category("Validator")] public System.Drawing.Color ValidatorColor { get { return validatorColor; } set { validatorColor = value; } } [Bindable(false)] [Category("Validator")] public string ErrorMessage { get { return errorMessage; } set { errorMessage = value; } } [Bindable(false)] [Category("Validator")] public string ValidationGroup { get { return validationGroup; } set { validationGroup = value; } } #endregion public RequiredTextBox() { this.ErrorMessage = "*"; this.ValidatorText = "*"; this.ValidatorColor = System.Drawing.Color.Red; } protected override void CreateChildControls() { System.Web.UI.WebControls.TextBox tb; System.Web.UI.WebControls.RequiredFieldValidator val; tb = new TextBox(); tb.ID = this.UniqueID + "_TextBox"; tb.Text = Text; val = new RequiredFieldValidator(); val.ControlToValidate = tb.ID; val.ErrorMessage = this.ErrorMessage; val.Text = this.ValidatorText; val.ForeColor = this.ValidatorColor; val.ID = this.UniqueID + "_Validator"; val.Display = ValidatorDisplay.Dynamic; val.ValidationGroup = this.ValidationGroup; Controls.Add(tb); Controls.Add(new LiteralControl("&nbsp;")); Controls.Add(val); } } }

This code contains the same using lines and namespaces that the code in Listing 6-9 contains. The class, RequiredTextBox, inherits from the WebControl class, but notice that it also implements the INamingContainer interface. INamingContainer is an interesting interface: it has no members, and it is used only as a marker interface. When a control implements the INamingContainer interface, a namespace is created to ensure that all of the IDs of all child controls are unique.

Note 

This code uses the #region and #endregion directives to enclose the properties. This allows you to easily collapse and hide the section of the source code that defines the properties in Visual Studio. When you are reviewing a section of the source, the ability to hide the sections of the code that you are not looking at often makes it easier to quickly scan large blocks of code.

The properties declared for the RequiredTextBox control are as follows:

All properties are supported by private data members that have the same names (except that the first character of the private data members is lowercase (C# is case-sensitive). The constructor for the RequiredTextBox control then sets default values for some of the properties.

The CreateChildControls method is next in the code. This method first declares the two controls that make up the composite control, a TextBox control and a RequiredFieldValidator control. Next, properties of the control are set. The only properties set on the TextBox control are the Text prop- erty and the ID property. For the ID property, I used the UniqueID property of the underlying WebControl and appended "_TextBox". Similarly, I set the ID property of the RequiredFieldValidator control to the UniqueID property of the underlying WebControl and appended "_Validator". The rest of the properties of the RequiredFieldValidator control are self explanatory.

Finally, after the controls were created and their properties set, I had to actually add them to the control. By using the Add method of the Controls collection property of the RequiredTextBox control, I first added the TextBox control, then a dynamically created Literal control containing a single space to provide spacing between the two controls, and finally the RequiredFieldValidator control.

When the CustomControlsLib project was rebuilt and re-added to the Toolbox, the RequiredTextBox control was added to the Toolbox along with the WebCustomControl1 control. To test the control, I created a new page named TestRequiredTextBox.aspx. When I dragged the RequiredTextBox control and a button onto the page, the page appeared as shown in Figure 6-18.

Figure 6-18: The RequiredTextBox control, placed on a form in Visual Studio

Notice the tiny box to the left of the button. That is the RequiredTextBox control not exactly what we expected. The problem is that the control is not being rendered at design time. Fortunately, there is a solution.

The System.Web.UI.Design namespace contains a class called ControlDesigner that improves the appearance of controls at design time. To take advantage of this improved design-time appearance, we have to create a class that inherits from the ControlDesigner class and overrides one member method of the class, GetDesignTimeHtml.

To create a class that customizes the design-time appearance of the RequiredTextBox control, you must add a reference to the CustomControlsLib project. In Solution Explorer, right-click the project, and then select Add Reference. In the resulting dialog box, scroll down and select System.Design.

Tip 

Note that the namespace is System.Web.UI.Design, but the file that contains it is named System.Design.dll.

Several changes to the source are required to allow the new designer class to work. First, a few new using lines must be added.

using System.Web.UI.Design; using System.IO; using System.ComponentModel.Design;

Next, the class to handle the design-time appearance of the control must be added. This class is shown in Listing 6-11.

Listing 6-11: The RequiredTextBoxDesigner Class, to Be Added to RequiredTextBox.cs

class RequiredTextBoxDesigner : ControlDesigner { public override string GetDesignTimeHtml() { RequiredTextBox rtb = (RequiredTextBox)Component; StringWriter sw = new StringWriter(); HtmlTextWriter tw = new HtmlTextWriter(sw); Literal placeholder = new Literal(); placeholder.Text = rtb.ID; placeholder.RenderControl(tw); return sw.ToString(); } }

The code writes a very simple bit of text, the ID of the control. The HTML can be as complex as you want it to be. Next, we'll add the Designer attribute to the requiredTextBox class, as shown here.

[Designer(typeof(RequiredTextBoxDesigner),typeof(IDesigner))]

The Designer attribute requires that you specify either the name of the designer and base type as a string or the exact types. Specifying the exact types seems a little cleaner. After these changes are made, the CustomControlsLib project should be recompiled, and the controls re-added to the Toolbox in the CustomControls Web site. Now, a RequiredTextBox control appears as shown in Figure 6-19 when added to the form.

Figure 6-19: The RequiredTextBox control, placed on a form after adding a class descending from the ControlDesigner class

The difference is more than cosmetic. Before adding design-time support, selecting the control to adjust its properties was difficult at best. When the page is run, if you click the button without adding text to the text box, the page appears as shown in Figure 6-20.

Figure 6-20: Testing the RequiredTextBox control

The RequiredFieldValidator control that is a part of this composite control is fired whenever the text box that is also part of the control is left empty and the user navigates away from the control or submits the form.

Creating a Control That Mixes Client and Server Code

Another common task involved with creating custom server controls is the addition of clientside code. Although you could add a control individually and then add the client script each time it is required, creating a control and standardizing the way that client script is added is a much cleaner solution.

Imagine that you have a system that must handle percentages. The users involved might enter a percentage as 0.35, meaning 35 percent, or they might enter 35, also meaning 35 percent. Handling this in a global way by using a control is much cleaner than adding the JavaScript code to each individual page that requires this processing. Using client-side script would ensure that the percentages would be visually corrected to fit a standard format as the user's cursor left the text box.

The best way to create a solution such as this is to create a new control that inherits from the TextBox control rather than the WebControl control. The advantage of inheriting from the TextBox control directly is that all the heavy lifting is already done. All of the properties of the TextBox control are available, as well as all of the data-handling capabilities. To create this control, I first added a new item to the CustomControlsLib project and, again, selected Web Custom Control from the list of templates. For this example, I changed the base control class from WebControl to TextBox. I removed most of the existing code that Visual Studio provided as a template.

To see how simple it is to create a client-enabled custom server control by using a class that already provides most of what you need (in this case, the TextBox control), take a look at Listing 6-12.

Listing 6-12: TextBoxFixPercent.cs

using System; using System.Collections.Generic; using System.ComponentModel; using System.Text; using System.Web.UI; using System.Web.UI.WebControls; namespace CustomControlsLib { [DefaultProperty("Text")] [ToolboxData("<{0}:TextBoxFixPercent runat=server></{0}:TextBoxFixPercent>")] public class TextBoxFixPercent : System.Web.UI.WebControls.TextBox { protected override void Render(HtmlTextWriter output) { base.Render(output); } protected override void OnPreRender(EventArgs e) { StringBuilder script = new StringBuilder(); script.Append(" <script language='Javascript'> "); script.Append(" function AdjustPercentages(sourceTextBox) "); script.Append(" { "); script.Append( " var inputtedNumber = sourceTextBox.value; "); script.Append(" if (!isNaN( parseFloat(inputtedNumber))) "); script.Append(" { "); script.Append( " inputtedNumber = Number(inputtedNumber); "); script.Append( " if (inputtedNumber < 1 && inputtedNumber != 0 " + " && inputtedNumber > -1) "); script.Append(" { "); script.Append(" inputtedNumber = inputtedNumber * 100; "); script.Append( " inputtedNumber = inputtedNumber.toFixed(2); "); script.Append(" } "); script.Append(" else "); script.Append(" { "); script.Append( " inputtedNumber = inputtedNumber.toFixed(2); "); script.Append(" } "); script.Append(" sourceTextBox.value = inputtedNumber; "); script.Append(" } "); script.Append(" } </script> "); Page.ClientScript.RegisterClientScriptBlock(typeof(Page), "FixPercent", script.ToString()); string func= string.Format("AdjustPercentages({0})",this.ClientID); this.Attributes.Add("OnBlur", func); } } }

The bulk of the code is involved in writing the client script to the browser. Rather than directly writing out the JavaScript to a literal control or any similar operation, the Page class has a ClientScript member that is of type ClientScriptManager. This ClientScriptManager class has several methods that write out client script, including the RegisterClientScriptBlock method, which is used in this code. Other methods in the ClientScriptManager class that register script can be used to register an include for a script file or for use in a particular event, such as the form's submission. Using the ClientScriptManager class ensures that a script is sent to the browser only once.

When you create a control, knowing exactly when the client script is written is critical. You might think that the Render method would be a logical place to have the script written to the client browser. This, however, does not work, because the Render method takes place too late in the cycle. Fortunately, the OnPreRender event is available. The script block is built up in a StringBuilder object called script.

Note 

Using the StringBuilder class is much more efficient than just concatenating strings by using the plus operator. Each time a string is concatenated, a new string is created in memory. All these strings can cause surprisingly large amounts of memory use. When you use a StringBuilder object, the Append method allows you to append the supplied string to an internal buffer in the StringBuilder class, so the number of strings created is greatly reduced. In production code, I'd use a separate .js file and load that, but, for example purposes, embedding the JavaScript in code makes for a somewhat easier to understand example.

After I called the RegisterClientScriptBlock method, I created a string named func that contained the code to call the method in the script block (named AdjustPercentages) and pass in the current control by ClientID. Note that I also had to ensure that the JavaScript function was called at an appropriate time. The best client-side event to use is the OnBlur event, which fires as the user leaves the edit control. I added an OnBlur attribute by calling the Add method of the Attributes collection. Attributes is a property of the underlying TextBox class from which TextBoxFixPercent is derived.

In the CustomControls project, I added a new page, named TestTextBoxFixPercent.aspx. I re-added the file created in CustomControlsLib to the Toolbox in the CustomControls Web site. Then I added a Button control to the page. When I ran the page, I entered a number (0.35) into the TextBoxFixPercent control. Figure 6-21 shows what the page looked like before I pressed the Tab key.

Figure 6-21: The TextBoxFixPercent control before pressing the Tab key

When the cursor exited the control, the number was reformatted, as shown in Figure 6-22.

Figure 6-22: The TextBoxFixPercent control after exiting the control

When TestTextBoxFixPercent.aspx is run and you view the source of the page, the following markup is emitted for the TextBoxFixPercent control.

<input name="TextBoxFixPercent1" type="text" OnBlur="AdjustPercentages(TextBoxFixPercent1)" />

Again, this example is a relatively simple control. This technique, however, should allow you to create controls that inherit from existing controls as well as to add client-side scripting support as required.

 

Категории