Custom Web Controls Demystified, Part 2 Last issue I gave you a 'hit-the-ground-running' introduction to custom Web control development and showed you how to build a renered control and an inherited control. In this issue you'll complete your inherited control by adding styling, sizing capabilities, as well as instruct it how to raise events. Afterward you will jump into building the last control of the series, the EmailContact control, bringing together the previous two controls with some business functionality into a powrful composite Web control. In the first part of this article (CoDe Magazine, September/October 2005) you learned how to create an inherited Web control, as well as a fairly functional rendered Web control. In part two of this article you’ll learn three professional touches for your custom Web control. First, you’ll learn how to make all parts of your custom control resize correctly. Next, you’ll learn how to capture an event when the button is clicked or when text in the textbox changes. Finally, you’ll learn how to add basic styling. Control Sizing When you drop any Web control on a form, you’re probably used to sizing it by dragging one of its sizing points in whatever direction you want. The problem here is that the Web control you’re building consists of three HTML elements, and you want to be able to size each one individually to give the FormField control maximum usefulness. The built-in Width and Height property that the control has comes from the Control class that you are ultimately inheriting from, and it corresponds to the control as a whole. If you try to resize the control using these properties as it currently stands, nothing happens. This is because you have not added sizing attributes to the contained elements, so they stay exactly as-is. I’m going to do something later with these existing properties, but for now I want you to add two properties called CaptionWidth and ButtonWidth. For the purposes of this article, I’m only going to show you how to deal with widths, but the downloadable code contains code to handle heights as well. I left out a width property for the textbox for a reason, as I’ll explain later. | " | It’s a good idea to use a Case statement (switch in C#) as opposed to an If statement. This sets you up for any future enhancement to your control.
| " |
A property that handles height or width for an element is of type Unit. Here’s the code for the CaptionWidth property. In VB.NET: Public Property CaptionWidth() As Unit Get If CType( _ ViewState("CaptionWidth"), Object) _ Is Nothing Then Return Unit.Pixel(130) End If Return CType( _ ViewState("CaptionWidth"), Unit) End Get Set(ByVal Value As Unit) ViewState("CaptionWidth") = Value End Set End Property
In C#: public Unit CaptionWidth { get { if (((object) ViewState["CaptionWidth"]) == null) return Unit.Pixel(130); return ((Unit) ViewState["CaptionWidth"]); } set { ViewState["CaptionWidth"] = value; } }
As you can see, the same ViewState-oriented property technique is used here as described earlier in the article. The Unit object is serializable so it can be fully persisted in the ViewState variable. Now that you’ve added the new properties, you need to do something with them. Remember that earlier I taught you that you can use the AddAttribute method of the HtmlTextWriter object to add tag attributes to the upcoming RenderBeginTag call. That’s exactly how you’re going to set the Width attribute to the ‘span’ tag and ‘input’ tag for the button, with one minor difference I’ll explain in a minute. output.AddStyleAttribute( HtmlTextWriterStyle.Width, this.CaptionWidth.ToString());
And of course, you would have a similar line for the ButtonWidth property. Now you can set these properties individually to adjust the width of the caption and the button. As you realized, there’s one element of the Web control whose width I have not handled this way. In fact, there’s a reason I haven’t yet showed you how to handle this element. To make the control more programmer-friendly, I’m letting the textbox take the remaining width of the entire control; that is the total width of the Web control minus the width of the caption and the button (and don’t forget those two spaces you inserted between elements-calculated to be 10 pixels). The value that you’re going to use to set the width of the textbox will consist of the total width of the control (the Width property) minus the value of CaptionWidth, minus the value of ButtonWidth, and minus 10. The subtraction of the ButtonWidth value will depend on the setting of the ButtonVisible property and the number 10 accounts for the extra spaces rendered between the elements. I derived the number 10 by trial and error to see what looked best in the designer. You’ll use the calculated value to set the Width attribute of the textbox’s “input” tag. In VB.NET: Dim i_Width As Integer = _ CType(Me.Width.Value, Integer) - (CType(Me.CaptionWidth.Value, Integer) - 10)
If Me.ButtonVisible Then i_Width -= CType( _ Me.ButtonWidth.Value, Integer) End If
If i_Width < 20 Then i_Width = 20
output.AddStyleAttribute( _ HtmlTextWriterStyle.Width, i_Width.ToString())
output.RenderBeginTag(HtmlTextWriterTag.Input)
In C#: int i_Width = ((int)(this.Width.Value)) - ((int)(this.CaptionWidth.Value) - 10);
if(this.ButtonVisible) i_Width -= ((int)(this.ButtonWidth.Value));
if (i_Width < 20) i_Width = 20;
output.AddStyleAttribute( HtmlTextWriterStyle.Width, i_Width.ToString());
output.RenderBeginTag(HtmlTextWriterTag.Input);
Notice that you’re also ensuring a minimum total width of 20 for the control. In the downloadable code for the finished FormField control (Figure 1), I also handle the Width property to account for percentages as well as pixel entry as its value. This will become necessary when I get to the composite control later, but for now I’m not going to worry about it. Another thing to note is that you’re using a different method from the AddAttribute you used before. The AddStyleAttribute takes care of adding the property, not as an attribute to the tag, but as an attribute within the HTML style attribute. Later when I address styling, I’ll touch on this some more.  Figure 1: FormField control in action.If you try sizing this control on a Web Form now, you will see that the textbox stretches to the size of the entire control while leaving the caption and button the same width. Those elements will only be sized by setting their properties individually. Now that you can size the control properly, you’re going to add some actual functionality for handling postbacks and handling events. Events What good is having a button on a form if it doesn’t do anything? Since I’ve taken time to build a custom Web control with a button as part of its elements, I now want to give that button some functionality. In a rendered control, you accomplish this by implementing a couple of interfaces. The button does not need to do any data checking, instead, it simply needs to trigger a postback and raise an event in the Web Form’s code-behind class. In order to do this, I start by extending the control’s class to implement the IPostBackEventHandler interface. This interface defines only one method called RaisePostBackEvent which receives a string argument. This method will get fired when a postback is triggered by one of the elements in the control. In order to trigger a postback from the Web control, you need to do a couple of things. First, you must tell the button to trigger a page postback when a user clicks it. The HTML tag that you used to render the button is an “input” tag with a “type” attribute of “button.” Inherently, this HTML tag can only raise a client event in its “onclick” attribute; no problem, that’s exactly what you’re going to do. Once again you’re going to add another attribute to one of the HTML tags. This time it will be the “input” tag that gets rendered for the button element. The attribute you need to add to this tag is the “onclick” attribute whose value should contain Jscript code to execute when the user clicks the button. When the ASP.NET parser processes an ASPX Web Form to render to a browser, it also builds a Jscript function that handles the postback to the server. This function is normally called by any control that needs to trigger a postback. You don’t really need to know the name of this Jscript function because .NET provides a method call that will generate it (though if you view the source of any rendered ASPX page, you will see this function which is called “__doPostBack”). This is good in case the function name changes in future versions of .NET. The method that generates the Jscript call is called GetPostBackEventReference and it sits off the Page object, which incidentally is accessible from the control’s class. The two arguments you need to send to this method are the calling class (the control’s class) and an identifier that identifies the button element. This identifier is what gets sent into the RaisePostBackEvent method that was defined by the IPostBackEventHandler interface. As before, you add the new attribute to the button’s “input” tag before the “input” tag is rendered. In VB.NET: output.AddAttribute( _ HtmlTextWriterAttribute. _ Onclick, Page.GetPostBackEventReference( _ Me, "button"))
output.RenderBeginTag(HtmlTextWriterTag.Input)
In C#: output.AddAttribute( HtmlTextWriterAttribute.Onclick, Page.GetPostBackEventReference( this, "button")) ;
output.RenderBeginTag(HtmlTextWriterTag.Input);
As you can see, the word “button” is chosen for the identifier of the button element. This will get passed into the RaisePostBackEvent method when the page is postbacked. Your control is now ready to handle postbacks. Clicking the button will now call the RaisePostBackEvent method, sending the word “button” into its argument. The only problem is that you haven’t told this method to do anything yet, so let’s wire in an event to raise to the page. | " | You can create custom composite controls that contain other custom composite controls, thus creating a control tree. Remember however, that the deeper you get the more performance-heavy your control will get.
| " |
When you click on a regular button control on a Web Form, you trigger a Click event on the page’s code-behind class. You’re going to create a ButtonClick event that will get raised on the page’s code-behind class when the button on the FormField control gets pressed. Let’s start by declaring a ButtonClick event using the standard EventHandler delegate. In VB.NET: Public Event ButtonClick As EventHandler
In C#: public event EventHandler ButtonClick;
This is the event that will be raised in the RaisePostBackEvent method. To make sure that this event gets raised only when the button is pressed, you’ll need a condition-check against the value that was used when the button element was rendered. In VB.NET: Public Sub RaisePostBackEvent( _ ByVal eventArgument As String) Implements _ IPostBackEventHandler.RaisePostBackEvent Select Case eventArgument.ToLower() Case "button" RaiseEvent ButtonClick( _ Me, New EventArgs) End Select End Sub
In C#: public void RaisePostBackEvent( string eventArgument) { switch (eventArgument.ToLower()) { case "button" : if(this.ButtonClick != null) this.ButtonClick( this, new EventArgs()); break; } }
It’s a good idea to use a Case statement (switch in C#) as opposed to an If statement. This sets you up for any future enhancement to your control. Now that you have an event wired up to the button element, let’s make it the default event for the control. This will allow programmers that use your control to double-click on it while in design mode on a Web Form, and have the code-behind come up with the ButtonClick all coded up and ready to go. To do this you need to decorate the class declaration with the DefaultEvent attribute and send into its constructor, the string “ButtonClick” (code not shown). You’re not done with events yet. You need to create a TextChanged event to capture changes in the textbox, much like the one that comes with the regular Textbox Web control. This event is a bit different because upon the page postback, you’re going to need to check if the value in the textbox has changed before you raise it. You should know that you have to declare the event so go ahead and do that at the top of the class. In VB.NET: Public Event TextChanged As EventHandler
In C#: public event EventHandler TextChanged;
As you can see, this code will use the default EventHandler delegate as well. For neither of these two events do you need to create a new delegate and event argument object, so you’re fine with using the default one. In the case where you needed to send information to the event, you would use a custom event argument object and delegate as you would in any case where you’re using events. Now that the event is declared, you need to raise it somewhere. You need to implement an interface that will allow you to check posted values for elements in your control; it’s called IPostBackDataHandler and it implements two methods: LoadPostData and RaisePostDataChangedEvent. The LoadPostData method gets called during a postback and receives data from the elements on the Web Form. This data can be checked against properties in your control to check for changes or anything else that may be required. This is the essence behind the ability to check for text changes in the textbox. The code in the LoadPostData event will look at the data that was posted from the textbox and compare it against the value of the Text property, which may be different from that of the actual textbox on the form. In VB.NET: Public Function LoadPostData( _ ByVal postDataKey As String, _ ByVal postCollection As NameValueCollection) As Boolean _ Implements IPostBackDataHandler.LoadPostData
Dim s_CurrentValue As String = _ Me.Text Dim s_PostedValue As String = _ postCollection(Me.UniqueID & _ ":Field") Dim s_Button As String = _ postCollection(Me.UniqueID & _ ":Button") Dim b_ButtonClicked As Boolean = _ (Not s_Button Is Nothing AndAlso _ s_Button.Length <> 0)
If b_ButtonClicked Then Page.RegisterRequiresRaiseEvent(Me) End If
If (Not s_CurrentValue.Equals( _ s_PostedValue)) Then Me.Text = s_PostedValue Return True End If
Return False
End Function
In C#: public bool LoadPostData( string postDataKey, NameValueCollection postCollection) { string s_CurrentValue = this.Text; string s_PostedValue = postCollection[this.UniqueID]; string s_Button = postCollection[this.UniqueID + _ ":Button"]; bool b_ButtonClicked = (s_Button != null) && (s_Button.Length != 0);
if(b_ButtonClicked) Page.RegisterRequiresRaiseEvent(this);
if(!s_CurrentValue.Equals(s_PostedValue)) { this.Text = s_PostedValue; return true; }
return false; }
| & | | 
By: Miguel Castro Miguel is an architect with IDesign who specializes in architecture consulting and building .NET solutions. He is a Microsoft MVP and INETA speaker and has been a software developer for over 22 years. With a Microsoft background that goes all the way back to VB 1.0 (and QuickBasic in fact), Miguel jumped on .NET as soon as the first public Beta was released and has provided .NET solutions for clients around the country in a variety of industries. He considers himself to be a .NET Developer and Architect and has equal love for both VB and C#, and no tolerance for language bigotry. He’s spoken at numerous user groups around the country as well as developer conferences.
He’s the author of the CodeBreeze code-generator, which among things can be found on his Web site:
www.steelbluesolutions.com
Miguel currently lives in Lincoln Park, NJ with his wife Elena and his daughter Victoria.
subscriptions@infotekcg.com | Fast Facts | | In part 1 you learned how to build a custom rendered control. In Part 2 you’ll learn how to build a composite control. | |
|