ONDotNet.com    
 Published on ONDotNet.com (http://www.ondotnet.com/)
 See this if you're having trouble printing code examples


O'Reilly Book Excerpts: ASP.NET in a Nutshell

User Controls and Custom Server Controls, Part 2

Related Reading

ASP.NET in a Nutshell
By G. Andrew Duthie, Matthew MacDonald

by Matthew MacDonald and G. Andrew Duthie

Editor's note: Part 2 in this series of excerpts from ASP.NET in a Nutshell focuses on ASP.NET custom server controls.


Custom Server Controls

For the reasons cited earlier in the chapter, user controls are not always the ideal choice for reuse. They tend to be very good for quickly reusing existing user interface elements and code, but are not especially useful for developing reusable building blocks for multiple web applications. This is where custom server controls come in.

A custom server control is, in its essence, a class that derives from either the Control or WebControl class of the System.Web.UI namespace, or from one of the classes that derive from these controls. Custom server controls can be used in your ASP.NET Web Forms pages in very much the same way you use the built-in server controls that come with ASP.NET. There are two primary categories of custom server controls:

Rendered controls
Rendered controls consist largely of custom rendering of the text, tags, and any other output you desire, which may be combined with the rendered output of any base class your control is derived from. Rendered controls override the Render method of the control they derive from. This method is called automatically by the page containing the control when it's time for the control output to be displayed.

Compositional controls
Compositional controls get their name from the fact that they are composed of existing controls whose rendered output forms the UI of the custom control. Compositional controls create their constituent controls by overriding the CreateChildControls method of the control they derive from. This method, like the Render method, is automatically called by ASP.NET at the appropriate time.

When designing a new custom server control, you need to consider some issues to decide which type of control to create:

In This Series

User Controls and Custom Server Controls, Part 4
In this final installment in a series of excerpts from ASP.NET in a Nutshell, learn how to share ASP.NET controls across applications.

User Controls and Custom Server Controls, Part 3
Part 3 of this four-part excerpt from ASP .NET in a Nutshell covers custom server contols.

User Controls and Custom Server Controls, Part 1
In part one from this series of book excerpts from ASP.NET in a Nutshell, get an overview on ASP.NET controls, and learn about ASP.NET user controls.

Note that by default, custom server controls expose all public members of the class from which they are derived. This exposure is important to consider when designing a control for use by other developers if you want to limit the customizations they can make. For instance, you might not want developers to change the font size of your control. In such a case, you should avoid deriving from a control that exposes that property.

Rendered Controls

Perhaps the best way to understand the process of creating a rendered custom server control is to see one. Example 6-4 shows a class written in Visual Basic .NET that implements a custom navigation control with the same functionality as the Nav.ascx user control discussed earlier in this chapter. Unlike the user control, which has the linked pages and images hardcoded into the control itself, the custom control in Example 6-4 gets this information from an XML file.

Example 6-4: NavBar.vb


Imports Microsoft.VisualBasic
Imports System
Imports System.Data
Imports System.Drawing
Imports System.IO
Imports System.Text
Imports System.Web
Imports System.Web.UI
Imports System.Web.UI.WebControls

Namespace aspnetian

Public Class NavBar 
   Inherits Panel

   Private NavDS As DataSet
   Private _showDividers As Boolean = True

   Public Property ShowDividers(  ) As Boolean
      Get
         Return _showDividers
      End Get
      Set
         _showDividers = value
      End Set
   End Property
  
   Sub NavBar_Load(sender As Object, e As EventArgs) Handles MyBase.Load

      LoadData(  )

   End Sub
  
   Protected Overrides Sub Render(Writer As HtmlTextWriter)
  
      Dim NavDR As DataRow
      Dim RowNum As Integer = 1
      Dim SB As StringBuilder
  
      MyBase.RenderBeginTag(Writer)
      MyBase.RenderContents(Writer)
  
      Writer.Write("<hr width='80%'>" & vbCrLf)
  
      For Each NavDR In NavDS.Tables(0).Rows
  
         SB = new StringBuilder(  )
         SB.Append(vbTab)
         SB.Append("<a href=""")
         SB.Append(NavDR("url"))
         SB.Append(""" onmouseover=""")
         SB.Append("img")
         SB.Append(RowNum.ToString(  ))
         SB.Append(".src='")
         SB.Append(NavDR("moimageUrl"))
         SB.Append("';""")
         SB.Append(" onmouseout=""")
         SB.Append("img")
         SB.Append(RowNum.ToString(  ))
         SB.Append(".src='")
         SB.Append(NavDR("imageUrl"))
         SB.Append("';""")
         SB.Append(" target='")
         SB.Append(NavDR("targetFrame"))
         SB.Append("'>")
         SB.Append(vbCrLf)
         SB.Append(vbTab)
         SB.Append(vbTab)
         SB.Append("<img border='0' align='absMiddle' alt='")
         SB.Append(NavDR("text"))
         SB.Append("' src='")
         SB.Append(NavDR("imageUrl"))
         SB.Append("' id='")
         SB.Append("img")
         SB.Append(RowNum.ToString(  ))
         SB.Append("' name='")
         SB.Append("img")
         SB.Append(RowNum.ToString(  ))
         SB.Append("'></a>")
         SB.Append(vbTab)
         SB.Append("<a href=""")
         SB.Append(NavDR("url"))
         SB.Append(""" onmouseover=""")
         SB.Append("img")
         SB.Append(RowNum.ToString(  ))
         SB.Append(".src='")
         SB.Append(NavDR("moimageUrl"))
         SB.Append("';""")
         SB.Append(" onmouseout=""")
         SB.Append("img")
         SB.Append(RowNum.ToString(  ))
         SB.Append(".src='")
         SB.Append(NavDR("imageUrl"))
         SB.Append("';""")
         SB.Append(" target='")
         SB.Append(NavDR("targetFrame"))
         SB.Append("'>")
         SB.Append(NavDR("text"))
         SB.Append("</a>")
         SB.Append(vbCrLf)
         If _showDividers = True Then
            SB.Append("<hr width='80%'>")
         Else
            SB.Append("<br/><br/>")
         End If
         SB.Append(vbCrLf)
         Writer.Write(SB.ToString(  ))
  
         RowNum += 1
  
      Next
  
      MyBase.RenderEndTag(Writer)
  
   End Sub
  
   Protected Sub LoadData(  )
  
      NavDS = New DataSet(  )
  
      Try
         NavDS.ReadXml(Page.Server.MapPath("NavBar.xml"))
      Catch fnfEx As FileNotFoundException
         CreateBlankFile(  )
         Dim Html As String
         Html = "<br>No NavBar.xml file was found, so one was " & _
            "created for you. Follow the directions in the file " & _
            "to populate the required fields and, if desired, " & _
            "the optional fields."
         Me.Controls.Add(New LiteralControl(Html))
      End Try
  
   End Sub   
  
   Public Sub CreateBlankFile(  )
      'Code to create a blank XML file with the fields used by
      '   the control. This code is included as a part of the file
      '   NavBar.vb, included with the sample files for the book.
   End Sub
  
End Class
  
End Namespace

The real meat of the NavBar control begins with the class declaration, which uses the Inherits keyword to declare that the control derives from the Panel control. This gives the control the ability to show a background color, to be hidden or shown as a unit, and to display the contents of its begin and end tags as part of the control.

Next, a couple of local member variables are declared. The location of the declaration is important, since these members need to be accessible to any procedure in the control. A property procedure is then added for the ShowDividers property, which will determine whether the control renders a horizontal line between each node of the control.

In the NavBar_Load method, which handles the Load event for the control (fired automatically by ASP.NET), the LoadData method is called to load the NavBar data from the XML file associated with the control.

Skipping over the Render method temporarily, the LoadData method creates a new instance of the ADO.NET DataSet class and calls its ReadXml method to read the data from the XML file. If no file exists, the LoadData method calls another method (CreateBlankFile) to create a blank XML file with the correct format for use by the developer consuming the control. This technique not only deals gracefully with an error condition; it provides an easier starting point for the developer using the control. Note that the CreateBlankFile method is declared as public, which means it can be called deliberately to create a blank file, if desired.

Last, but certainly not least, the overridden Render method, which is called automatically at runtime when the control is created, iterates through the first (and only) table in the dataset and uses an instance of the StringBuilder class to build the HTML output to render. Once the desired output has been built, the method uses the HtmlTextWriter passed to it by ASP.NET to write the output to the client browser. Note that prior to looping through the rows in the dataset, the render method calls the RenderBeginTag and RenderContents methods of the base Panel control. This renders the opening <div> tag that is the client-side representation of the Panel control, plus anything contained within the opening and closing tags of the NavBar control. Once all the rows have been iterated and their output sent to the browser, the RenderEndTag method is called to send the closing </div> tag to the browser.

You can compile the code in Example 6-4 by using the following single-line command (which can alternatively be placed in a batch file):

vbc /t:library /out:bin\NavBar.dll /r:System.dll,System.Data.dll,
System.Drawing.dll,System.Web.dll,System.Xml.dll NavBar.vb

The preceding command requires that you create a bin subdirectory under the directory from which the command is launched and that you register the path to the Visual Basic compiler in your PATH environment variable. If you have not registered this path, you will need to provide the full path to the Visual Basic .NET compiler (by default, this path is %windir%\Microsoft.NET\Framework\%version%).

Example 6-5 shows the XML file used to populate the control, Example 6-6 shows the code necessary to use the NavBar control in a Web Forms page, and Figure 6-1 shows the output of this page.

Example 6-5: NavBar.xml


<navBar>
    <!-- node field describes a single node of the control -->
    <node>
        <!-- Required Fields -->
        <!-- url field should contain the absolute or relative 
           URL to link to -->
        <url>NavBarClient.aspx</url>
        <!-- text field should contain the descriptive text for 
           this node -->
        <text>NavBar Client</text>
        <!-- End Required Fields -->
        <!-- Optional Fields -->
        <!-- imageUrl field should contain the absolute or relative 
           URL for an image to be displayed in front of the link -->
        <imageUrl>node.jpg</imageUrl>
        <!-- moimageUrl field should contain the absolute or 
           relative URL for an image to be displayed in front of 
           the link on mouseover -->
        <moImageUrl>node_rev.jpg</moImageUrl>
        <!-- targetFrame field should contain one of the following: 
           _blank, _parent, _self, _top  -->
        <targetFrame>_self</targetFrame>
        <!-- End Optional Fields -->
    </node>
    <node>
        <url>UCClient.aspx</url>
        <text>User Control Client</text>
        <imageUrl>alt_node.jpg</imageUrl>
        <moImageUrl>alt_node_rev.jpg</moImageUrl>
        <targetFrame>_self</targetFrame>
    </node>
    <node>
        <url>BlogClient.aspx</url>
        <text>Blog Client</text>
        <imageUrl>node.jpg</imageUrl>
        <moImageUrl>node_rev.jpg</moImageUrl>
        <targetFrame>
        </targetFrame>
    </node>
    <node>
        <url>BlogAdd.aspx</url>
        <text>Add New Blog</text>
        <imageUrl>alt_node.jpg</imageUrl>
        <moImageUrl>alt_node_rev.jpg</moImageUrl>
        <targetFrame>
        </targetFrame>
    </node>
</navBar>

Example 6-6: NavBarClient.aspx



<%@ Page Language="vb" %>
<%@ Register TagPrefix="aspnetian" Namespace="aspnetian" 
   Assembly="NavBar" %>
<html>
<head>
   <script runat="server">
      Sub Page_Load(  )
         'NB1.CreateBlankFile(  )
      End Sub
   </script>
</head>
<body>
   <table border="1" width="100%" cellpadding="20" cellspacing="0">
      <tr>
         <td align="center" width="150">
            <img src="aspnetian.jpg"/>
         </td>
         <td align="center">
            <h1>NavBar Control Client Page<h1>
         </td>
      </tr>
      <tr>
         <td width="150">
            <form runat="server">
               <aspnetian:NavBar id="NB1" 
                  showdividers="False" runat="server">
                  <strong>Navigation Bar</strong>
                  <br/>
               </aspnetian:NavBar>
            </form>
         </td>
         <td>
            This is where page content might be placed
            <br/><br/><br/><br/><br/><br/><br/><br/><br/>
         </td>
      </tr>
   </table>
</body>
</html>
Figure 6-1. NavBarClient.aspx output

Compositional Controls

As mentioned earlier in the chapter, compositional controls render their output by combining appropriate controls within the CreateChildControls method, which is overridden in the custom control.

Example 6-7 shows the C# code for a compositional control that provides simple functionality for a blog (which is short for web log). The control has two modes: Add and Display. The mode is determined by the internal member _mode, which can be accessed by the public Mode property.

Like the NavBar control created in the previous example, the class definition for the Blog control specifies that the class derives from the Panel control (using C#'s : syntax), and also implements the INamingContainer interface. The INamingContainer interface contains no members, so there's nothing to actually implement. It's simply used to tell the ASP.NET runtime to provide a separate naming scope for controls contained within the custom control. This helps avoid the possibility of naming conflicts at runtime.

Also like the NavBar control, the Blog control uses an XML file to store the individual Blog entries. The example uses the same method of retrieving the data, namely creating a dataset and calling its ReadXml method, passing in the name of the XML file.

In addition to declaring the _mode member variable and the BlogDS dataset, the example declares two Textbox controls (which will be used when adding a new blog entry) and two more string member variables (_addRedirect and _email).

The code in Example 6-7 then creates public property accessors for all three string variables. The Mode property determines whether the control displays existing blogs or displays fields for creating a new blog. The AddRedirect property takes the URL for a page to redirect to when a new blog is added. The Email property takes an email address to link to in each new blog field.

Next, the program overrides the OnInit method of the derived control to handle the Init event when it is called by the runtime. In this event handler, you call the LoadData method, which, like the same method in the NavBar control, loads the data from the XML file or, if no file exists, creates a blank file. It then calls the OnInit method of the base class to ensure that necessary initialization work is taken care of.

Next is the overridden CreateChildControls method. Like the Render method, this method is called automatically by the ASP.NET runtime when the page is instantiated on the server. Unlike the Render method, you don't want to call the CreateChildControls method of the base class, or you'll create a loop in which this method calls itself recursively (and the ASP.NET process will hang). In the CreateChildControls method, you check the value of the _mode member variable and call either the DisplayBlogs method or the NewBlog method, depending on the value of _mode. Note that this value is set by default to display, so if the property is not set, the control will be in display mode. Also note that the example uses the ToLower method of the String class to ensure that either uppercase or lowercase attribute values work properly.

The DisplayBlogs method iterates through the data returned in the dataset and instantiates controls to display this data. We use an if statement to determine whether more than one entry in a row has the same date. If so, we display only a single date header for the group of entries with the same date. We add an HtmlAnchor control to each entry to facilitate the readers' ability to bookmark the URL for a given entry. Then we write out the entry itself and add a contact email address and a link to the specific entry at the end of each entry.

Example 6-7: Blog.cs


using System;
using System.Data;
using System.Drawing;
using System.IO;
using System.Web;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
  
namespace aspnetian
{
  
public class Blog:Panel, INamingContainer
{
  
   protected DataSet BlogDS;
   protected TextBox TitleTB;
   protected TextBox BlogText;
  
   private string _addRedirect;
   private string _email;
   private string _mode = "display";
  
   public string AddRedirect
   {
      get
      {
         return this._addRedirect;
      }
      set
      {
         this._addRedirect = value;
      }
   }
  
   public string Email
   {
      get
      {
         return this._email;
      }
      set
      {
         this._email = value;
      }
   }
  
   public string Mode
   {
      get
      {
         return this._mode;
      }
      set
      {
         this._mode = value;
      }
   }
  
   protected override void OnInit(EventArgs e)
   {
      LoadData(  );
      base.OnInit(e);
   }
  
   protected override void CreateChildControls(  )
   {
      if (this._mode.ToLower(  ) != "add")
      {
         DisplayBlogs(  );
      }
      else
      {
         NewBlog(  );
      }
   }
  
   protected void LoadData(  )
   {
      BlogDS = new DataSet(  );
  
      try
      {
         BlogDS.ReadXml(Page.Server.MapPath("Blog.xml"));
      }
      catch (FileNotFoundException fnfEx)
      {
         CreateBlankFile(  );
         LoadData(  );
      }
   }
  
   protected void DisplayBlogs(  )
   {
      DateTime BlogDate;
      DateTime CurrentDate = new DateTime(  );
  
      DataRowCollection BlogRows = BlogDS.Tables[0].Rows;
      foreach (DataRow BlogDR in BlogRows)
      {
         string BDate = BlogDR["date"].ToString(  );
         BlogDate = new DateTime(Convert.ToInt32(BDate.Substring(4, 4)), 
            Convert.ToInt32(BDate.Substring(0, 2)),
            Convert.ToInt32(BDate.Substring(2, 2)));
  
         if (CurrentDate != BlogDate)
         {
            Label Date = new Label(  );
            Date.Text = BlogDate.ToLongDateString(  );
            Date.Font.Size = FontUnit.Large;
            Date.Font.Bold = true;
            this.Controls.Add(Date);
            this.Controls.Add(new LiteralControl("<br/><br/>"));
            CurrentDate = BlogDate;
         }
  
         HtmlAnchor Anchor = new HtmlAnchor(  );
         Anchor.Name = "#" + BlogDR["anchorID"].ToString(  );
         this.Controls.Add(Anchor);
  
         Label Title = new Label(  );
         Title.Text = BlogDR["title"].ToString(  );
         Title.Font.Size = FontUnit.Larger;
         Title.Font.Bold = true;
         this.Controls.Add(Title);
         
         this.Controls.Add(new LiteralControl("<p>"));
         LiteralControl BlogText = new LiteralControl("<div>" +
            BlogDR["text"].ToString(  ) + "</div>");
         this.Controls.Add(BlogText);
         this.Controls.Add(new LiteralControl("</p>"));
  
         HyperLink Email = new HyperLink(  );
         Email.NavigateUrl = "mailto:" + BlogDR["email"].ToString(  );
         Email.Text = "E-mail me";
         this.Controls.Add(Email);
  
         this.Controls.Add(new LiteralControl(" | "));
  
         HyperLink AnchorLink = new HyperLink(  );
         AnchorLink.NavigateUrl = Page.Request.Url.ToString(  ) + "#" +
            BlogDR["anchorID"].ToString(  );
         AnchorLink.Text = "Link";
         this.Controls.Add(AnchorLink);
  
         this.Controls.Add(new LiteralControl("<hr width='100%'/><br/>"));
      }
   }
  
   protected void NewBlog(  )
   {
      Label Title = new Label(  );
      Title.Text = "Create New Blog";
      Title.Font.Size = FontUnit.Larger;
      Title.Font.Bold = true;
      this.Controls.Add(Title);
  
      this.Controls.Add(new LiteralControl("<br/><br/>"));
  
      Label TitleLabel = new Label(  );
      TitleLabel.Text = "Title: ";
      TitleLabel.Font.Bold = true;
      this.Controls.Add(TitleLabel);
      TitleTB = new TextBox(  );
      this.Controls.Add(TitleTB);
  
      this.Controls.Add(new LiteralControl("<br/>"));
      
      Label BlogTextLabel = new Label(  );
      BlogTextLabel.Text = "Text: ";
      BlogTextLabel.Font.Bold = true;
      this.Controls.Add(BlogTextLabel);
      BlogText = new TextBox(  );
      BlogText.TextMode = TextBoxMode.MultiLine;
      BlogText.Rows = 10;
      BlogText.Columns = 40;
      this.Controls.Add(BlogText);      
  
      this.Controls.Add(new LiteralControl("<br/>"));
  
      Button Submit = new Button(  );
      Submit.Text = "Submit";
      Submit.Click += new EventHandler(this.Submit_Click);
      this.Controls.Add(Submit);
   }
  
   protected void Submit_Click(object sender, EventArgs e)
   {
      EnsureChildControls(  );
      AddBlog(  );
   }
  
   protected void AddBlog(  )
   {
      DataRow NewBlogDR;
      NewBlogDR = BlogDS.Tables[0].NewRow(  );
      NewBlogDR["date"] = FormatDate(DateTime.Today);
      NewBlogDR["title"] = TitleTB.Text;
      NewBlogDR["text"] = BlogText.Text;
      NewBlogDR["anchorID"] = Guid.NewGuid().ToString(  );
      NewBlogDR["email"] = _email;
      BlogDS.Tables[0].Rows.InsertAt(NewBlogDR, 0);
      BlogDS.WriteXml(Page.Server.MapPath("Blog.xml"));
      Page.Response.Redirect(_addRedirect);
   }
  
   protected string FormatDate(DateTime dt)
   {
      string retString;
  
      retString = String.Format("{0:D2}", dt.Month);
      retString += String.Format("{0:D2}", dt.Day);
      retString += String.Format("{0:D2}", dt.Year);
      return retString;
   }
  
   protected void CreateBlankFile(  )
   {
      // code to create new file...omitted to conserve space
   }
  
} // closing bracket for class declaration
  
} // closing bracket for namespace declaration

Figure 6-2. Output of BlogClient.aspx

Displaying the blog entries is only half the battle. While it would certainly be possible to edit the XML file directly in order to add a new blog entry, it makes much more sense to make this a feature of the control. This is what the NewBlog method does. In the NewBlog method, we instantiate Label and TextBox controls for data entry and a Button control to submit the new blog entry. When the Button is clicked, the Submit_Click event handler method is called when the control is re-created on the server. The Submit_Click event handler, in turn, calls the AddBlog method to insert a new row into the BlogDS dataset and then writes the contents of the dataset back to the underlying XML file. Before using the control, of course, we'll need to compile it and place it in the application's bin directory. The following snippet can be used to compile the control:

csc /t:library /out:bin\blog.dll /r:system.dll,system.data.dll,
system.xml.dll,system.web.dll blog.cs

Example 6-8 shows the ASP.NET code necessary to instantiate the Blog control programmatically. Note the use of the PlaceHolder control to precisely locate the Blog control output. For this code to work correctly, the compiled assembly containing the Blog control must reside in the application's bin subdirectory.Figure 6-2 shows the output of the control when used in the client page shown in Example 6-8.

Example 6-8: BlogClient.aspx


<%@ Page Language="vb" debug="true" %>
<%@ Register TagPrefix="aspnetian" Namespace="aspnetian" 
   Assembly="NavBar" %>
<html>
<head>
   <script runat="server">
      Sub Page_Load(  )
         Dim Blog1 As New Blog(  )
         PH.Controls.Add(Blog1)
      End Sub
   </script>
</head>
<body>
   <form runat="server">
   <table border="1" width="100%" cellpadding="20" cellspacing="0">
      <tr>
         <td align="center" width="150">
            <img src="aspnetian.jpg"/>
         </td>
         <td align="center">
            <h1>Blog Display Page<h1>
         </td>
      </tr>
      <tr>
         <td width="150" valign="top">
            <aspnetian:NavBar id="NB1" runat="server">
               <strong>Navigation Bar</strong>
               <br/>
            </aspnetian:NavBar>
         </td>
         <td>
            <asp:placeholder id="PH" runat="server"/>
         </td>
      </tr>
   </table>
   </form>
</body>
</html>

Example 6-9 shows the code necessary to instantiate the control declaratively. The example uses the TagPrefix aspnetian2 because both the NavBar control and the Blog control use the same namespace, but are compiled into separate assemblies (which means that using the same TagPrefix for both would result in an error).

Example 6-9: BlogAdd.aspx


<%@ Page Language="vb" debug="true" %>
<%@ Register TagPrefix="aspnetian" Namespace="aspnetian" 
    Assembly="NavBar" %>
<%@ Register TagPrefix="aspnetian2" Namespace="aspnetian" 
    Assembly="Blog" %>
<html>
<head>
   <script runat="server">
      Sub Page_Load(  )
         'Uncomment the line below to explicitly create a blank
         '   XML file, then comment the line out again to run the control
         'NB1.CreateBlankFile(  )
      End Sub
   </script>
</head>
<body>
   <form runat="server">
   <table border="1" width="100%" cellpadding="20" cellspacing="0">
      <tr>
         <td align="center" width="150">
            <img src="aspnetian.jpg"/>
         </td>
         <td align="center">
            <h1>Blog Add Page<h1>
         </td>
      </tr>
      <tr>
         <td width="150" valign="top">
            <aspnetian:NavBar id="NB1" runat="server">
               <strong>Navigation Bar</strong>
               <br/>
            </aspnetian:NavBar>
         </td>
         <td>
            <aspnetian2:Blog id="Blog1" 
               mode="Add" 
               addredirect="BlogClient.aspx" 
               email="graymad@att.net" 
               runat="server"/>
         </td>
      </tr>
   </table>
   </form>
</body>
</html>

As you can see, whether the control is used programmatically or declaratively, the amount of code necessary to provide simple blogging functionality is made trivial by the use of a custom server control. Note that you can also have the same page use the Blog control in either Display or Add mode, depending on the user's actions, as explained in the following section.

In the next installment, learn more custom server controls, including added design-time support and more.

Matthew MacDonald is a developer, author, and educator in all things Visual Basic and .NET. He's worked with Visual Basic and ASP since their initial versions, and written over a dozen books on the subject, including The Book of VB .NET (No Starch Press) and Visual Basic 2005: A Developer's Notebook (O'Reilly). His web site is http://www.prosetech.com/.

G. Andrew Duthie is the founder and principal of Graymad Enterprises, Inc., which provides training and consulting in Microsoft Web development technologies.

ASP.NET in a Nutshell

Related Reading

ASP.NET in a Nutshell
By G. Andrew Duthie, Matthew MacDonald

Return to the .NET DevCenter.

Copyright © 2009 O'Reilly Media, Inc.