Maximizing.ASP.dot.NET.Real.World.ObjectOriented.Development [Electronic resources]

Jeffrey Putz

نسخه متنی -صفحه : 146/ 83
نمايش فراداده

Building Your Own Server Control from Scratch

Let's build a simple text box control to demonstrate at a basic level how you might build your own control.

Using Visual Studio 2005, start a new class library project and call it SimpleTextBox. In the solution explorer of the new project, you'll need to add a reference to the System.Web.dll assembly because it's not included by default. To do this, right-click References and click Add Reference. Figure 9.1 shows the Add Reference dialog with the correct assembly selected. If you are planning to compile the class with the command line, be sure to use the reference switch and include System.Web.dll.

Figure 9.1. The Add Reference dialog.

Now we can begin writing the class. Start by adding a using (Imports in VB.NET) statement to include System.Web and any other namespaces you think you might need. Your class has to be in a namespace, so be sure to choose one that is unique. Finally, your class declaration should indicate that the class will inherit from one of three choices.

The first two choices are to inherit from System.Web.UI.Control or System.Web.UI.WebControls.WebControl. The primary difference between the two is that WebControl offers many visual properties, such as Width, Height, and Font. The third choice is to inherit from an existing class, as we did in our DropDownList example previously. We're keeping it really simple here, so we'll inherit from Control.

Because we're also planning to process postback data, we also need to implement the IPostBackDataHandler interface. That leaves our class looking something like Listing 9.5.

This simple example is similar to one in the SDK documentation that ships with the .NET Framework, listed under the overview for the IPostBackDataHandler interface. Our example has a few changes to reflect what one might consider to be best practices.

Listing 9.5. The start of our custom server control

C#

using System;
using System.Collections.Specialized;
using System.Text;
using System.Web;
using System.Web.UI;
namespace SimpleTextBox
{
public class MyTextBox : System.Web.UI.Control, IPostBackDataHandler
{
public MyTextBox()
{
}
}
}

VB.NET

Imports System
Imports System.Collections.Specialized
Imports System.Text
Imports System.Web
Imports System.Web.UI
Namespace SimpleTextBox
Public Class MyTextBox
Inherits System.Web.UI.Control
Implements IPostBackDataHandler
Public Sub New()
End Sub
End Class
End Namespace

Let's start with the parts we'll need to get text in and out and to render the HTML in the page. We need a property that can hold the text to be displayed in the text box, so we'll create one called, cleverly enough, Text. This property differs from those you've seen in other class examples because we will save its value in the ViewState collection instead of saving it to a private variable. Note that the get part of our property requires that we cast the value from ViewState into a string, just as we would when fetching a value from Session or Cache.

We also override Control's Render() method with our own in order to render the HTML to the browser. We're using a StringBuilder object to assemble a string that builds the HTML, including the control's name (which comes from the UniqueID property, a part of the Control base class) and the value of our text box, using HttpUtility.HtmlEncode() to escape characters that would otherwise break the HTML in the browser (such as quotes and "greater than" signs). Our two new members are shown in Listing 9.6.

Where the heck does this Render() method come from, and why do you have to use it? Remember that we're inheriting from Control as our base class. The documentation for Control says that the Render() method is where the code renders the actual HTML and inserts it into the page. Because we want to control that rendering, we override the method and do it ourselves because we're not satisfied with whatever the original Control class had in mind. As we mentioned earlier in the book, it doesn't matter that we don't know the specific implementation under the hood. We only care that the Control class is intended for rendering HTML to the browser.

Listing 9.6. Adding a property and overriding the Render() method in our control

C#

public string Text
{
get { return (string)ViewState["Text"]; }
set { ViewState["Text"] = value; }
}
protected override void Render(HtmlTextWriter writer)
{
StringBuilder builder = new StringBuilder();
builder.Append("<input type=\"text\" name=\");
builder.Append(this.UniqueID);
builder.Append("\" value=\");
builder.Append(HttpUtility.HtmlEncode(Text));
builder.Append("\" />");
writer.Write(builder.ToString());
}

VB.NET

Public Property Text() As String
Get
Return CStr(ViewState("Text"))
End Get
Set
ViewState("Text") = value
End Set
End Property
Protected Overrides Sub Render(writer As HtmlTextWriter)
Dim builder As New StringBuilder()
builder.Append("<input type="text" name="")
builder.Append(Me.UniqueID)
builder.Append("" value="")
builder.Append(HttpUtility.HtmlEncode(Text))
builder.Append("" />")
writer.Write(builder.ToString())
End Sub

If we temporarily removed the IPostBackDataHandler from the class declaration, it would compile, and we could put it in a page. We could even assign a value to the Text property, and it would appear in the text box. However, it wouldn't be very useful because so far, we have no way of reading the data it will contain on postback.

Implementing IPostBackDataHandler requires that we add two methods: LoadPostData(string, NameValueCollection) and RaisePostDataChangedEvent(). The first is used to process the data on postback by accessing its value from the returned form data, returning a Boolean value that indicates whether or not the data has changed, while the second is used to execute code if the control's state has been altered.

Confused yet? Don't be! There are a lot of interfaces that help you, as a server control author, to define the way a control will behave in the context of a Web form and its postback mechanism. Under no circumstance are we trying to cover every possibility here; we're only trying to give you a solid overview of how the entire process works.

Let's start with LoadPostData(). Its purpose is to compare the old value of the control's properties to those sent on postback. If the values have changed, we should assign the new values to our properties and make the method return true, signaling to ASP.NET that there has indeed been a change. This is easy because we only have one value to worry about. The two parameters, the string and NameValueCollection, give us access to the posted form data. We'll get the old value from the Text property and the new value from the form data posted back to us. If it changed, we'll update the Text property and return true; otherwise we'll do nothing and return false. Listing 9.7 shows the method added to our class.

Listing 9.7. The LoadPostData() implemented in our class

C#

public virtual bool LoadPostData(string postDataKey, NameValueCollection postCollection)
{
string oldValue = Text;
string newValue = postCollection[this.UniqueID];
if (oldValue == null || !oldValue.Equals(newValue))
{
Text = newValue;
return true;
}
return false;
}

VB.NET

Public Overridable Function LoadPostData(postDataKey As String, _
postCollection As NameValueCollection) As Boolean
Dim oldValue As String = Text
Dim newValue As String = postCollection(Me.UniqueID)
If oldValue Is Nothing Or Not oldValue.Equals(newValue) Then
Text = newValue
Return True
End If
Return False
End Function

Under the hood, ASP.NET will call RaisePostDataChangedEvent() if your LoadPostData() method returns true. RaisePostData ChangedEvent() doesn't actually need to do anything for the purpose of our demonstration. However, if we wanted to do things right and release a commercial-grade control, we would implement our own event and event handler, called TextChanged and OnTextChanged, respectively (using the appropriate naming conventions). Listing 9.8 shows how we would write our event handler and event; otherwise we would leave RaisePostDataChangedEvent() empty.

Listing 9.8. Implementing our own event and event handler

C#

public virtual void RaisePostDataChangedEvent()
{
OnTextChanged(EventArgs.Empty);
}
public event EventHandler TextChanged;
protected virtual void OnTextChanged(EventArgs e)
{
if (TextChanged != null) TextChanged(this, e);
}

VB.NET

Public Overridable Sub RaisePostDataChangedEvent()
OnTextChanged(EventArgs.Empty)
End Sub
Public Event TextChanged As EventHandler
Protected Overridable Sub OnTextChanged(e As EventArgs)
If Not (TextChanged Is Nothing) Then
TextChanged(Me, e)
End If
End Sub

What does it mean "to do it right?" Think back to our HttpModule example where we added an event handler to application events. We could do that because the authors of ASP.NET programmed those events and made them available to us. By adding the code in Listing 9.8, we're doing the same thing for anyone who wants to tie into our server control.

Our control is finished! Listing 9.9 shows the finished class as well as its use in a page that, on postback, copies the text from the text box to a LinkButton's text value. We removed the constructor because it's not used. Figure 9.2 shows what it looks like in the browser after a postback.

Listing 9.9. The finished control, showing its use in a page

C#

using System;
using System.Collections.Specialized;
using System.Text;
using System.Web;
using System.Web.UI;
namespace SimpleTextBox
{
public class MyTextBox : System.Web.UI.Control, IPostBackDataHandler
{
public string Text
{
get { return (string)ViewState["Text"]; }
set { ViewState["Text"] = value; }
}
protected override void Render(HtmlTextWriter writer)
{
StringBuilder builder = new StringBuilder();
builder.Append("<input type=\"text\" name=\");
builder.Append(this.UniqueID);
builder.Append("\" value=\");
builder.Append(HttpUtility.HtmlEncode(Text));
builder.Append("\" />");
writer.Write(builder.ToString());
}
public virtual bool LoadPostData(string postDataKey, NameValueCollection postCollection)
{
string oldValue = Text;
string newValue = postCollection[this.UniqueID];
if (oldValue == null || !oldValue.Equals(newValue))
{
Text = newValue;
return true;
}
return false;
}
public virtual void RaisePostDataChangedEvent()
{
OnTextChanged(EventArgs.Empty);
}
public event EventHandler TextChanged;
protected virtual void OnTextChanged(EventArgs e)
{
if (TextChanged != null) TextChanged(this, e);
}
}
}

VB.NET

Imports System
Imports System.Collections.Specialized
Imports System.Text
Imports System.Web
Imports System.Web.UI
Namespace SimpleTextBox
Public Class MyTextBox
Inherits System.Web.UI.Control
Implements IPostBackDataHandler
Public Property Text() As String
Get
Return CStr(ViewState("Text"))
End Get
Set
ViewState("Text") = value
End Set
End Property
Protected Overrides Sub Render(writer As HtmlTextWriter)
Dim builder As New StringBuilder()
builder.Append("<input type="text" name="")
builder.Append(Me.UniqueID)
builder.Append("" value="")
builder.Append(HttpUtility.HtmlEncode([Text]))
builder.Append("" />")
writer.Write(builder.ToString())
End Sub
Public Overridable Function LoadPostData(postDataKey As String, _
postCollection As NameValueCollection) As Boolean
Dim oldValue As String = Text
Dim newValue As String = postCollection(Me.UniqueID)
If oldValue Is Nothing Or Not oldValue.Equals(newValue) Then
Text = newValue
Return True
End If
Return False
End Function
Public Overridable Sub RaisePostDataChangedEvent()
OnTextChanged(EventArgs.Empty)
End Sub
Public Event TextChanged As EventHandler
Protected Overridable Sub OnTextChanged(e As EventArgs)
If Not (TextChanged Is Nothing) Then
TextChanged(Me, e)
End If
End Sub
End Class
End Namespace

.aspx page

<%@ Page Language="C#" Codefile="Default.aspx.cs" Inherits="Default_aspx" %>
<%@ Register Assembly="SimpleTextBox" Namespace="SimpleTextBox" TagPrefix="Sample" %>
<script runat="server">
void Page_Load(object sender, EventArgs e)
{
LinkButton1.Text = TextBoxTest.Text + " Click to postback!";
}
</script>
<l>
<body>
<form id="form1" runat="server">
<Sample:MyTextBox ID="TextBoxTest" Runat="Server" /><br />
<asp:LinkButton ID="LinkButton1" Runat="server">LinkButton</asp:LinkButton>
</form>
</body>
<l>

Figure 9.2. Our server control as seen in a browser