Adding Design-Time Support to ASP.NET Controls
G. Andrew Duthie
Graymad Enterprises, Inc
October 2003
Applies to:
Microsoft® ASP.NET
Microsoft Visual Studio® .NET
Microsoft .NET Framework
Summary: Learn how to build controls that take advantage of Microsoft Visual Studio .NET's design-time support, which makes them as easy to use as the built-in controls that come with Microsoft ASP.NET. (36 printed pages)
MSDNSamples\
Download DesignTimeSupportSample.msi.
This article was adapted from ASP.NET in a Nutshell, 2nd edition (ISBN: 0596001169), by G. Andrew Duthie and Matthew MacDonald, published by O'Reilly & Associates, Inc., 2003.
Contents
Introduction
Types of Design-Time Support
The Blog Control Sample
Adding Design-Time Support
Summary
Code Listings
Introduction
Microsoft® ASP.NET provides developers with one of the most powerful new tools in Web development: server controls. Server controls make it possible to create responsive, robust Web applications in a fraction of the time that it would take to create the equivalent application in classic ASP.
One of the key reasons for the increased productivity enabled by ASP.NET server controls is the rich design-time support for server controls in the Microsoft Visual Studio® .NET development environment. Developers can drag server controls from the Visual Studio .NET toolbox onto a page, access their properties through the Properties window, and take advantage of Microsoft IntelliSense® statement completion, both in the Visual Studio HTML editor, as well as in code-behind classes for ASP.NET pages. These design-time features bring to Web development the rapid application development (RAD) tools that Microsoft Visual Basic® developers have enjoyed for years.
ASP.NET also offers developers the ability to further increase their productivity by building their own custom server controls to encapsulate reusable chunks of user-interface specific code, such as login or registration forms. While developers are beginning to become aware of the power of developing their own custom controls, many developers may not be aware that they can harness the power of Visual Studio's design-time support in their controls as well, making them as easy to use as the built-in controls that come with ASP.NET. In this article, we'll describe the types of design-time support provided by the Microsoft .NET Framework and Visual Studio .NET, and demonstrate how developers can build controls that take advantage of that support.
Types of Design-Time Support
There are 5 distinct areas of design-time support for server controls in Visual Studio .NET. They are:
- IntelliSense in codebehind classes
- Property browser support in Design view
- Toolbox support
- Property browser support in HTML view
- IntelliSense in the HTML editor
These areas of design-time support are provided by several different mechanisms. IntelliSense in codebehind is enabled by the IDE reading the metadata for your control to determine the properties and methods exposed by your control, as well as their types and parameters. You do not need to do anything to enable IntelliSense in a code-behind class beyond authoring and compiling your control, and placing its assembly in the bin subdirectory of the application from which it will be used.
Property browser support in the Design view of the Visual Studio .NET editor is provided through two means: the type associated with a property and/or any Metadata Attributes associated with the property. You add Metadata Attributes (from here on, we'll just refer to them as attributes) to your code, for example, to identify the category of the property, provide a description for the property, and specify a preferred editor, if desired. Properties with certain types, such as System.Drawing.Color, are automatically mapped to the appropriate editor in Visual Studio .NET.
IntelliSense and Property browser support in the HTML view of Visual Studio .NET are provided through the use of an XSD schema that describes the types associated with the control, and which uses text decorations called Visual Studio Annotations to specify preferred editor and other preferences for the control.
Finally, you can provide support for dragging and dropping your control from the Visual Studio .NET toolbox through a combination of attributes and a custom bitmap with specific properties.
The Blog Control Sample
The control that we'll use to demonstrate the design-time features in Visual Studio .NET is called the Blog control, and is shown in Listing 1 at the end of this article. The control provides simple Web log functionality using XML as a storage medium. A Web log—or blog, as they're commonly called—is essentially a Web page where a person posts regular observations or commentary about their lives, the world, politics, or anything else on their mind. Blog entries are added through a Web browser.
The Blog control is fairly straightforward, and uses control composition to render output to the browser. In compositional controls, the CreateChildControls method (which is called automatically by the ASP.NET runtime) is overridden, and in that method, we create the controls that will constitute the UI of the custom control, and add them to the Controls collection of the control. In addition, the control contains logic for displaying and adding blogs, as well as for creating an XML blog storage file if none exists. The control exposes several public properties that developers will set at design-time, including the URL for the page that the control will redirect to when a new blog is added, the email address to be associated with new blogs, the mode of the control (display or add), and the color of the separator line between each blog entry. Figure 1 shows the blog control in action. The Add Blog hyperlinks are provided by ASP.NET Hyperlink controls, and are separate from the Blog control. The code for BlogClient.aspx is shown in Listing 2. The codebehind class for BlogClient.aspx is shown in Listing 3, and provides the logic for changing the blog mode when the "Add Blog" link is clicked.
Figure 1. The Blog control at runtime
Figure 2 shows the appearance of the basic Blog control at design time. Note that the properties are listed, but not categorized.
Figure 2. The Blog control at design time
Adding Design-Time Support
While it's fairly simple to use the Blog control in a Web forms page, it's still not 100% intuitive. For example, without documentation, there's no way for someone using the Blog control to know that the only appropriate values for the Mode property are Display or Add. Without explicitly telling developers using the control about the Add mode, it would be difficult for them to discover and make use of this mode on their own.
For developers using Visual Studio .NET (or another IDE that supports IntelliSense), you can solve this problem by adding design-time support to the control. This is done using a combination of the techniques described earlier in this article. Part of the challenge of providing design-time support for custom server controls is the variety of techniques necessary to fully support design-time functionality in a custom control. The simplest, requiring no additional coding, is IntelliSense statement completion in codebehind, which is shown in Figure 3 for BlogClient.aspx.vb.
Figure 3. IntelliSense in code-behind
Unfortunately, automatic support for statement completion does not extend to the Design or HTML views when editing Web forms pages, nor does Visual Studio provide built-in support for viewing and editing properties in the Property browser without some additional work in your control. To complicate things further, one technique is necessary for supporting IntelliSense in the Property browser and Design view of the Web forms editor, while another is necessary for supporting it in the HTML view of the Web forms editor.
The technique required for supporting property browsing in Design view uses attributes to inform Visual Studio .NET about how to handle the properties. Supporting statement completion and property browsing in HTML view requires creating a custom XSD schema that describes the types in your control. We'll discuss both these techniques in the next sections.
Design View and Metadata Attributes
Visual Studio .NET provides rich support for designing and modifying controls visually using drag-and-drop techniques, as well as tools, such as the Property browser, and related designers, such as the color picker. Support for these tools is provided by a series of attributes that you can add to your control. These attributes tell the Visual Studio IDE whether to display any properties your control exposes in the Properties browser, what type the properties are, and which designer should be used to set the properties' values.
For the version of the control that will provide design-time support, we'll make a copy of the control file Blog.vb named Blog_DT.vb, and make our modifications to this file. This allows us to create a design-time version of the control, and still have the original control for comparison.
To support editing of the AddRedirect property in the Property browser, we would add the following attributes before the Property procedure, as shown in the following code snippet:
<Browsable(True), _ Category("Behavior"), _ Description("URL to which the page should " & _ "redirect after successful submission of a " & _ "new Blog entry."), _ Editor("System.Web.UI.Design.UrlEditor", _ GetType(UITypeEditor))> _ Public Property AddRedirect() As String 'Property procedure code End Property
These attribute declarations allow the property to be displayed in the Property browser, set the desired category for the property (when properties are sorted by category), provide a description of the property, and tell Visual Studio .NET to use the UrlEditor class to edit the property's value, as shown in Figure 4.
Figure 4. Property support in Design view
The attribute syntax shown in this section is for Visual Basic .NET. In Visual Basic .NET, attributes are declared with the following syntax:
<AttributeName(AttributeParams)>
In C#, attributes take the form:
[AttributeName(AttributeParams)]
Visual Basic .NET requires that the attribute declaration appear on the same line as the member it's modifying, so it's usually a good idea to follow the attribute with a Visual Basic line continuation character to improve readability:
<AttributeName(AttributeParams)> _ Public Membername()
In both C# and Visual Basic, you can declare multiple attributes within a single set of [ ] or <> brackets by separating multiple attributes with commas, although in Visual Basic .NET, if they appear on separate lines, you must use the Visual Basic line continuation character to continue the attributes as a single statement.
Adding Toolbox Support
In addition to setting attributes at the property level, you can also set certain attributes at the class and assembly levels. For example, you can use the assembly-level attribute TagPrefix to specify the tag prefix to use for any controls contained in the assembly. Visual Studio .NET then automatically inserts this tag prefix when you add an instance of the control to a Web forms page from the Visual Studio toolbox. The following code snippet shows the syntax for the TagPrefix attribute. This attribute should be placed within the class module that defines the control, but outside the class and namespace declarations (note that in Visual Basic .NET projects, the namespace is defined at the project level, so you do not need to worry about placing the Assembly attributes outside of the Namespace declaration). In the following attribute the first parameter of the TagPrefix attribute is the namespace of the control, and the second is the text you wish to use for the tag prefix itself.
<Assembly: TagPrefix("BlogControl", "BlogControl")>
To complete the integration of a control in the Visual Studio .NET environment, add the ToolBoxData attribute (which tells Visual Studio .NET your preferred tag name for controls inserted from the toolbox) to the class that implements the control:
<ToolboxData("<{0}:Blog_DT runat=server></{0}:Blog_DT>")> _ Public Class Blog_DT Inherits Panel Implements INamingContainer 'control implementation End Class
When the control is inserted into a page from the toolbox, the{0}
placeholders will be replaced by the tag prefix specified by the TagPrefix attribute, while the rest of the text will be inserted literally.
You can also provide your own custom icon for the control to be displayed in the toolbox. To do so, create a 16 x 16 pixel bitmap (the color of the lower left pixel is used as the transparency color) with the same name as the class containing the control (in other words, classname.bmp). Add the bitmap to the project using the Add Existing Item command, and use the property browser to set its Build Action to "Embedded Resource" as shown in Figure 5.
Figure 5. Setting the Build Action
Once compiled, the control will support automatic insertion of the @Register directive, tag prefix, and tag name for the Blog control when the control is added to a page from the toolbox, and will display the custom icon in the toolbox, as shown in Figure 6. To add the control to the Visual Studio .NET toolbox, follow these simple steps:
- In Design view, select the Web forms tab of the Visual Studio .NET toolbox.
- Right-click anywhere in the tab and select Add/Remove Items (Customize Toolbox in Visual Studio .NET 2002).
- Select the .NET Framework Components tab, and then click Browse.
- Browse to the location of the compiled control assembly, select it, and click Open.
- Click OK.
Figure 6. Custom control in the Toolbox
Once the control has been added to the toolbox, you can add it to a Web forms page by either double-clicking the control, or dragging it from the Toolbox onto the Web forms page. In either case, Visual Studio .NET will automatically insert the correct @Register directive, including setting the TagPrefix based on the assembly-level attribute, and will also create a set of tags for the control with the tag name specified in the ToolBoxData attribute.
Adding a Designer
As written, the Blog control will not have any visible interface in the Design view of the Web forms editor. This can make it more difficult to select the control on the page, and also may make it more difficult to understand what the control will look like at runtime. To correct this, we can add support for a designer that will render HTML at design time that approximates the look of the Blog control at runtime. Note that you can also create designers that completely reproduce the runtime output of a control, but this is more involved and beyond the scope of this article.
All server control designers derive from the class System.Web.UI.Design.ControlDesigner, which exposes a number of methods you can override to provide design-time rendering for your control. The following code simply overrides the GetDesignTimeHtml method to return some simple HTML to be displayed at design time. Note that the example shows the entire designer class for the Blog control, which you can simply add to the existing Blog_DT.vb class file.
Public Class BlogDesigner Inherits ControlDesigner Public Overrides Function GetDesignTimeHtml() As String Return "<h1>Blog</h1><hr/><hr/>" End Function End Class
To tie this designer into the Blog_DT class, we use the Designer attribute, as shown in the following snippet. Note that this code also adds a Description attribute that describes what the control does.
<Description("Simple Blog control. Supports display " & _ "of Web log / news items from an XML file."), _ Designer("BlogControl.BlogDesigner"), _ ToolboxData("<{0}:Blog_DT runat=server></{0}:Blog_DT>")> _ Public Class Blog_DT Inherits Panel Implements INamingContainer
As you can see, the BlogDesigner class is extremely simple, but it adds a lot to the control's design-time appearance on a Web forms page, as shown in Figure 7.
Figure 7. Adding design-time rendering
Listing 4 shows the code for the Blog control, updated with attributes to enable design-time support for the control in Design view and the Property browser. Note that the example adds several using directives to import the namespaces needed to support the attributes and designer classes we've used. The new listing also adds an enumeration to be used for the value of the Mode property.
HTML View Support: Custom Schemas and Visual Studio Annotations
As much as the attributes described in the previous section help in providing support for the Blog control at design-time, they're missing one important piece: IntelliSense support for adding tags and attributes in the HTML view of the Web forms editor. For developers who are more comfortable working in HTML rather than in WYSIWYG style, this is a significant oversight.
Since the HTML view of the Web forms editor uses XSD schemas to determine what elements and attributes to make available in a Web forms page, in order to correct the oversight, we need to provide an XSD schema that describes the Blog control and the attributes that it supports. Optionally, we can add annotations to the schema that tell Visual Studio .NET about the various elements and how we'd like them to behave.
Listing 5 contains the portion of the XSD schema specific to the Blog control. The actual schema file (which is available in the sample code for this article) also contains type definitions for the Panel control, from which the Blog_DT control is derived, as well as other necessary attribute and type definitions. These definitions were copied from the asp.xsd schema file created for the built-in ASP.NET server controls.
Note that you should never modify the asp.xsd schema file directly, but rather copy any necessary type or attribute definitions to your custom schema file. While this may seem redundant, if you edit asp.xsd directly and a later installation or service pack for the .NET Framework overwrites this file, your custom schema entries would be lost.
In Listing 5, note the targetNamespace and xmlns attributes on the root schema element, which define the XML namespace for the control's schema. The value of the targetNamespace and xmlns attributes will also be used as an attribute in your Web forms page to "wire up" the schema. The <xsd:element> tag defines the root Blog_DT element. The <xsd:complexType> tag defines the attributes for the Blog_DT element, which includes the Web control attributes referenced by the <xsd:attributeGroup> tag. Finally, the <xsd:simpleType> tag defines the enumeration for the BlogMode type used as one of the attributes for the Blog_DT element.
Note that Listing 5 uses the vs:builder annotation to tell Visual Studio .NET to use the URL builder for the AddRedirect attribute and the Color builder for the SeparatorColor attribute. The vs:builder annotation is one of a number of annotations available to modify schemas. The most commonly-used annotations are listed in Table 1.
Table 1. Common Visual Studio .NET Annotations
Annotation | Purpose | Valid Values |
---|---|---|
vs:absolutepositioning | Used at the root <schema> element to determine whether Visual Studio may insert style attributes for positioning. | true or false |
vs:blockformatted | Indicates whether leading white space may be added to the element during automatic formatting. | true or false |
vs:builder | Specifies the builder to be used for editing the related property's value. | color, style, or url |
vs:deprecated | Allows a related property to be marked as "deprecated," which prevents it from showing up in the Properties browser and in statement completion. | true or false |
vs:empty | Used at the element level to indicate that Visual Studio .NET should use single tag syntax for the related tag (no end tag). | true or false |
vs:friendlyname | Used at the root level to provide a display name for the schema. | |
vs:iscasesensitive | Used at the root level, specifies whether Visual Studio .NET will treat the related tags in a case-sensitive manner. | true or false |
vs:ishtmlschema | Used at the root level, specifies whether the schema is an HTML document schema. | true or false |
vs:nonbrowseable | Used at the attribute level, specifies that the attribute should not appear in statement completion. | true or false |
vs:readonly | Used at the attribute level, specifies that the attribute may not be modified in the Properties window. | true or false |
vs:requireattributequotes | Used at the root level, specifies that the attribute values must have quotes. | true or false |
Once you've built your XSD schema, save it to the same location as the asp.xsd file (which defaults to C:\Program Files\Microsoft Visual Studio .NET 2003\Common7\Packages\schemas\xml\ for Visual Studio .NET 2003).
To allow Visual Studio .NET to read your custom schema, you'll need to add an xmlns attribute to the <body> tag of the page in which you wish to use the schema, as shown in the following snippet:
<body xmlns:BlogControl="urn:http://www.aspnetian.com/schemas">
Notice that this code uses the BlogControl prefix with the xmlns attribute to specify that the schema is for controls prefixed with the BlogControl tag prefix, which, you may recall, is set up for us by the TagPrefix attribute (described in the previous section on "Metadata attributes"). The value of the xmlns attribute should be the same as the targetNamespace attribute defined at the root of the schema.
Once you've wired up your schema via the xmlns attribute, you should be able to type an opening "<" character, and have the Blog control appear as one of the options for statement completion, as shown in Figure 8. At this point, you should also get statement completion for the defined properties as well, including the allowed values for the Mode property, and the builders specified by the annotations in the XSD file.
Figure 8. Statement completion in HTML view
Summary
In this article, we've seen the design-time support available in Visual Studio .NET for ASP.NET server controls, and demonstrated how developers can take advantage of this support in their own custom controls. While it's relatively straightforward to add design-time support to your controls, it does require mastering several different techniques to fully take advantage of these features. One area that is particularly lacking is that of wiring up custom XSD schemas to the page. At the time of this writing, there is no built-in support for automatically adding the xmlns attribute necessary for connecting the page to the XSD schema for the control. As a result, it is still necessary to add this attribute manually. Hopefully in future releases of Visual Studio .NET, this process will be automated.
The sample code for this article contains a Visual Studio .NET project for both the basic and design-time support versions of the Blog control, as well as a client project that demonstrates the use of each control. To run the BlogControlClient project, you will need to create a new virtual directory in IIS named BlogControlClient, and map it to the location on your hard drive where you save the BlogControlClient project folder.
My thanks to Rob Caron from the Microsoft Visual Studio .NET team for his assistance in working out the process of creating and wiring up the custom XSD schema.
About the author
G. Andrew Duthie is the founder and principal of Graymad Enterprises, Inc., providing training and consulting in Microsoft Web development technologies. Andrew has been developing multi-tier Web applications since the introduction of Active Server Pages. He has written numerous books on ASP.NET including: Microsoft ASP.NET Step By Step, Microsoft ASP.NET Programming with Microsoft Visual Basic, and ASP.NET in a Nutshell. Andrew is a frequent speaker at events including Software Development, the Dev-Connections family of conferences, Microsoft Developer Days, and VSLive! He also speaks at .NET user groups as a member of the International .NET Association (INETA) Speaker's Bureau. You can find out more about Andrew at his company's Web site, Graymad Enterprises, Inc..
This article was adapted from ASP.NET in a Nutshell, 2nd edition (ISBN: 0596001169), by G. Andrew Duthie and Matthew MacDonald, published by O'Reilly & Associates, Inc., 2003.
Code Listings
Listing 1. Blog.vb
'supports Color structure Imports System.Drawing 'supports StreamWriter type Imports System.IO Imports System.Web.UI 'supports use of HTML Controls Imports System.Web.UI.HtmlControls 'supports use of Web Controls Imports System.Web.UI.WebControls Public Class Blog Inherits Panel Implements INamingContainer Protected BlogDS As DataSet Protected TitleTB As TextBox Protected BlogText As TextBox Private _addRedirect As String Private _email As String Private _mode As String Private _separatorColor As Color = Color.Black Public Property AddRedirect() As String Get Return Me._addRedirect End Get Set(ByVal Value As String) Me._addRedirect = Value End Set End Property Public Property Email() As String Get Return Me._email End Get Set(ByVal Value As String) Me._email = Value End Set End Property Public Property Mode() As String Get Return Me._mode End Get Set(ByVal Value As String) Me._mode = Value End Set End Property Public Property SeparatorColor() As Color Get Return Me._separatorColor End Get Set(ByVal Value As Color) Me._separatorColor = Value End Set End Property Protected Overrides Sub OnInit(ByVal e As EventArgs) LoadData() MyBase.OnInit(e) End Sub Protected Overrides Sub CreateChildControls() If Not Me._mode = "Add" Then DisplayBlogs() Else NewBlog() End If End Sub Protected Sub LoadData() BlogDS = New DataSet() Try BlogDS.ReadXml(Page.Server.MapPath("Blog.xml")) Catch fnfEx As FileNotFoundException CreateBlankFile() LoadData() End Try End Sub Protected Sub DisplayBlogs() Dim BlogDate As DateTime Dim CurrentDate As DateTime = New DateTime() Dim BlogRows As DataRowCollection = _ BlogDS.Tables(0).Rows Dim BlogDR As DataRow For Each BlogDR In BlogRows Dim BDate As String = 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 Not CurrentDate = BlogDate Then Dim TempDate As Label = New Label() TempDate.Text = BlogDate.ToLongDateString() TempDate.Font.Size = FontUnit.Large TempDate.Font.Bold = True Me.Controls.Add(TempDate) Me.Controls.Add _ (New LiteralControl("<br/><br/>")) CurrentDate = BlogDate End If Dim Anchor As HtmlAnchor = New HtmlAnchor() Anchor.Name = "#" & BlogDR("anchorID").ToString() Me.Controls.Add(Anchor) Dim Title As Label = New Label() Title.Text = BlogDR("title").ToString() Title.Font.Size = FontUnit.Larger Title.Font.Bold = True Me.Controls.Add(Title) Me.Controls.Add(New LiteralControl("<p>")) Dim BlogText As LiteralControl = _ New LiteralControl("<div>" & _ BlogDR("text").ToString() & "</div>") Me.Controls.Add(BlogText) Me.Controls.Add(New LiteralControl("</p>")) Dim Email As HyperLink = New HyperLink() Email.NavigateUrl = "mailto:" & _ BlogDR("email").ToString() Email.Text = "E-mail me" Me.Controls.Add(Email) Me.Controls.Add(New LiteralControl(" | ")) Dim AnchorLink As HyperLink = New HyperLink() AnchorLink.NavigateUrl = _ Page.Request.Url.ToString() & "#" & _ BlogDR("anchorID").ToString() AnchorLink.Text = "Link" Me.Controls.Add(AnchorLink) Me.Controls.Add(New _ LiteralControl("<hr color='" & _ ColorTranslator.ToHtml(_separatorColor) & _ "' width='100%'/><br/>")) Next End Sub Protected Sub NewBlog() Dim Title As Label = New Label() Title.Text = "Create New Blog" Title.Font.Size = FontUnit.Larger Title.Font.Bold = True Me.Controls.Add(Title) Me.Controls.Add(New LiteralControl("<br/><br/>")) Dim TitleLabel As Label = New Label() TitleLabel.Text = "Title: " TitleLabel.Font.Bold = True Me.Controls.Add(TitleLabel) TitleTB = New TextBox() Me.Controls.Add(TitleTB) Me.Controls.Add(New LiteralControl("<br/>")) Dim BlogTextLabel As Label = New Label() BlogTextLabel.Text = "Text: " BlogTextLabel.Font.Bold = True Me.Controls.Add(BlogTextLabel) BlogText = New TextBox() BlogText.TextMode = TextBoxMode.MultiLine BlogText.Rows = 10 BlogText.Columns = 40 Me.Controls.Add(BlogText) Me.Controls.Add(New LiteralControl("<br/>")) Dim Submit As Button = New Button() Submit.Text = "Submit" AddHandler Submit.Click, AddressOf Me.Submit_Click Me.Controls.Add(Submit) End Sub Protected Sub Submit_Click(ByVal Sender As Object, _ ByVal e As EventArgs) EnsureChildControls() AddBlog() End Sub Protected Sub AddBlog() Dim NewBlogDR As DataRow 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) End Sub Protected Function FormatDate(ByVal dt As DateTime) _ As String Dim retString As String retString = String.Format("{0:D2}", dt.Month) retString &= String.Format("{0:D2}", dt.Day) retString &= String.Format("{0:D2}", dt.Year) Return retString End Function Public Sub CreateBlankFile() Dim NewXml As StreamWriter = _ File.CreateText(Page.Server.MapPath("Blog.xml")) NewXml.WriteLine("<blogs>") NewXml.WriteLine _ (" <!-- blog field describes a single blog -->") NewXml.WriteLine(" <blog>") NewXml.WriteLine(" <!-- date field contains" & _ " the creation date of the blog -->") NewXml.WriteLine(" <date>" & _ FormatDate(DateTime.Today) & "</date>") NewXml.WriteLine _ (" <title>Temporary Blog</title>") NewXml.WriteLine(" <!-- text field " & _ "should contain the blog text, including any " & _ "desired HTML tags -->") NewXml.WriteLine(" <text>This entry " & _ "indicates that the file blog.xml was not " & _ "found. A default version of this file has " & _ "been created for you. You can modify the " & _ "fields in this file as desired. If you set " & _ "the Blog control to add mode (add the " & _ "attribute mode='add' to the control's " & _ "declaration), the control will " & _ "automatically populate the XML file when " & _ "you submit the form.</text>") NewXml.WriteLine(" <!-- anchorID field " & _ "will be autopopulated by the control -->") NewXml.WriteLine(" <anchorID></anchorID>") NewXml.WriteLine(" <!-- email field should" & _ " contain the email address for feedback -->") NewXml.WriteLine(" <email>change this to a " & _ "valid email address</email>") NewXml.WriteLine(" </blog>") NewXml.WriteLine("</blogs>") NewXml.Close() End Sub End Class
Listing 2. BlogClient.aspx
<%@ Register TagPrefix="cc1" Namespace="BlogControl" Assembly="BlogControl" %> <%@ Page Language="vb" AutoEventWireup="false" Codebehind="BlogClient.aspx.vb" Inherits="BlogControlClient.WebForm1"%> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> <head> <title>Blog Client</title> </head> <body> <form id=Form1 method=post runat="server"> <p><asp:hyperlink id=Link1 navigateurl="BlogClient.aspx?mode=add" runat="server">Add Blog</asp:hyperlink></p> <cc1:blog id=Blog1 Email="andrew@graymad.com" AddRedirect="BlogClient.aspx" SeparatorColor="LawnGreen" runat="server"></cc1:blog> <p><asp:hyperlink id=Link2 navigateurl="BlogClient.aspx?mode=add" runat="server">Add Blog</asp:hyperlink></p> </form> </body> </html>
Listing 3. BlogClient.aspx.vb
Imports BlogControl Public Class WebForm1 Inherits System.Web.UI.Page Protected WithEvents Link1 As _ System.Web.UI.WebControls.HyperLink Protected WithEvents Link2 As _ System.Web.UI.WebControls.HyperLink Protected WithEvents Blog1 As BlogControl.Blog Private Sub Page_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load If Request.QueryString("mode") = "add" Then Blog1.Mode = "Add" Link1.Visible = False Link2.Visible = False Else Blog1.Mode = "Display" Link1.Visible = True Link2.Visible = True End If End Sub End Class
Listing 4. Blog_DT.vb
'supports design-time attributes Imports System.ComponentModel 'supports Color structure Imports System.Drawing 'supports UITypeEditor type Imports System.Drawing.Design 'supports StreamWriter type Imports System.IO Imports System.Web.UI 'supports ControlDesigner type ' note that you must add a reference to the ' Assembly System.Design to import this namespace Imports System.Web.UI.Design 'supports use of HTML Controls Imports System.Web.UI.HtmlControls 'supports use of Web Controls Imports System.Web.UI.WebControls <Assembly: TagPrefix("BlogControl", "BlogControl")> Public Enum BlogMode Add Display End Enum <Description("Simple Blog control. Supports display " & _ "of Web log / news items from an XML file."), _ Designer("BlogControl.BlogDesigner"), _ ToolboxData("<{0}:Blog_DT runat=server></{0}:Blog_DT>")> _ Public Class Blog_DT Inherits Panel Implements INamingContainer Protected BlogDS As DataSet Protected TitleTB As TextBox Protected BlogText As TextBox Private _addRedirect As String Private _email As String Private _mode As BlogMode Private _separatorColor As Color = Color.Black <Browsable(True), _ Category("Behavior"), _ Description("URL to which the page should " & _ "redirect after successful submission of a " & _ "new Blog entry."), _ Editor("System.Web.UI.Design.UrlEditor", _ GetType(UITypeEditor))> _ Public Property AddRedirect() As String Get Return Me._addRedirect End Get Set(ByVal Value As String) Me._addRedirect = Value End Set End Property <Browsable(True), _ Category("Behavior"), _ Description("Email address the control will use " & _ "for listing in new Blog entries.")> _ Public Property Email() As String Get Return Me._email End Get Set(ByVal Value As String) Me._email = Value End Set End Property <Browsable(True), _ Category("Behavior"), _ Description("Controls whether existing Blogs are " & _ "displayed, or fields for creating a new Blog " & _ "entry.")> _ Public Property Mode() As BlogMode Get Return Me._mode End Get Set(ByVal Value As BlogMode) Me._mode = Value End Set End Property <Browsable(True), _ Category("Appearance"), _ Description("Controls the color of the line that " & _ "separates Blog entries when in display mode.")> _ Public Property SeparatorColor() As Color Get Return Me._separatorColor End Get Set(ByVal Value As Color) Me._separatorColor = Value End Set End Property Protected Overrides Sub OnInit(ByVal e As EventArgs) LoadData() MyBase.OnInit(e) End Sub Protected Overrides Sub CreateChildControls() If Not Me._mode = BlogMode.Add Then DisplayBlogs() Else NewBlog() End If End Sub Protected Sub LoadData() BlogDS = New DataSet() Try BlogDS.ReadXml(Page.Server.MapPath("Blog.xml")) Catch fnfEx As FileNotFoundException CreateBlankFile() LoadData() End Try End Sub Protected Sub DisplayBlogs() Dim BlogDate As DateTime Dim CurrentDate As DateTime = New DateTime() Dim BlogRows As DataRowCollection = _ BlogDS.Tables(0).Rows Dim BlogDR As DataRow For Each BlogDR In BlogRows Dim BDate As String = 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 Not CurrentDate = BlogDate Then Dim TempDate As Label = New Label() TempDate.Text = BlogDate.ToLongDateString() TempDate.Font.Size = FontUnit.Large TempDate.Font.Bold = True Me.Controls.Add(TempDate) Me.Controls.Add _ (New LiteralControl("<br/><br/>")) CurrentDate = BlogDate End If Dim Anchor As HtmlAnchor = New HtmlAnchor() Anchor.Name = "#" + BlogDR("anchorID").ToString() Me.Controls.Add(Anchor) Dim Title As Label = New Label() Title.Text = BlogDR("title").ToString() Title.Font.Size = FontUnit.Larger Title.Font.Bold = True Me.Controls.Add(Title) Me.Controls.Add(New LiteralControl("<p>")) Dim BlogText As LiteralControl = _ New LiteralControl("<div>" & _ BlogDR("text").ToString() & "</div>") Me.Controls.Add(BlogText) Me.Controls.Add(New LiteralControl("</p>")) Dim Email As HyperLink = New HyperLink() Email.NavigateUrl = "mailto:" & _ BlogDR("email").ToString() Email.Text = "E-mail me" Me.Controls.Add(Email) Me.Controls.Add(New LiteralControl(" | ")) Dim AnchorLink As HyperLink = New HyperLink() AnchorLink.NavigateUrl = _ Page.Request.Url.ToString() & "#" & _ BlogDR("anchorID").ToString() AnchorLink.Text = "Link" Me.Controls.Add(AnchorLink) Me.Controls.Add _ (New LiteralControl("<hr color='" & _ ColorTranslator.ToHtml(_separatorColor) & _ "' width='100%'/><br/>")) Next End Sub Protected Sub NewBlog() Dim Title As Label = New Label() Title.Text = "Create New Blog" Title.Font.Size = FontUnit.Larger Title.Font.Bold = True Me.Controls.Add(Title) Me.Controls.Add(New LiteralControl("<br/><br/>")) Dim TitleLabel As Label = New Label() TitleLabel.Text = "Title: " TitleLabel.Font.Bold = True Me.Controls.Add(TitleLabel) TitleTB = New TextBox() Me.Controls.Add(TitleTB) Me.Controls.Add(New LiteralControl("<br/>")) Dim BlogTextLabel As Label = New Label() BlogTextLabel.Text = "Text: " BlogTextLabel.Font.Bold = True Me.Controls.Add(BlogTextLabel) BlogText = New TextBox() BlogText.TextMode = TextBoxMode.MultiLine BlogText.Rows = 10 BlogText.Columns = 40 Me.Controls.Add(BlogText) Me.Controls.Add(New LiteralControl("<br/>")) Dim Submit As Button = New Button() Submit.Text = "Submit" AddHandler Submit.Click, AddressOf Me.Submit_Click Me.Controls.Add(Submit) End Sub Protected Sub Submit_Click(ByVal Sender As Object, _ ByVal e As EventArgs) EnsureChildControls() AddBlog() End Sub Protected Sub AddBlog() Dim NewBlogDR As DataRow 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) End Sub Protected Function FormatDate(ByVal dt As DateTime) As String Dim retString As String retString = String.Format("{0:D2}", dt.Month) retString &= String.Format("{0:D2}", dt.Day) retString &= String.Format("{0:D2}", dt.Year) Return retString End Function Public Sub CreateBlankFile() Dim NewXml As StreamWriter = _ File.CreateText(Page.Server.MapPath("Blog.xml")) NewXml.WriteLine("<blogs>") NewXml.WriteLine _ (" <!-- blog field describes a single blog -->") NewXml.WriteLine(" <blog>") NewXml.WriteLine(" <!-- date field contains" & _ " the creation date of the blog -->") NewXml.WriteLine(" <date>" & _ FormatDate(DateTime.Today) & "</date>") NewXml.WriteLine _ (" <title>Temporary Blog</title>") NewXml.WriteLine(" <!-- text field " & _ "should contain the blog text, including any " & _ "desired HTML tags -->") NewXml.WriteLine(" <text>This entry " & _ "indicates that the file blog.xml was not " & _ "found. A default version of this file has " & _ "been created for you. You can modify the " & _ "fields in this file as desired. If you set " & _ "the Blog control to add mode (add the " & _ "attribute mode='add' to the control's " & _ "declaration), the control will " & _ "automatically populate the XML file when " & _ "you submit the form.</text>") NewXml.WriteLine(" <!-- anchorID field " & _ "will be autopopulated by the control -->") NewXml.WriteLine(" <anchorID></anchorID>") NewXml.WriteLine(" <!-- email field should" & _ " contain the email address for feedback -->") NewXml.WriteLine(" <email>change this to a " & _ "valid email address</email>") NewXml.WriteLine(" </blog>") NewXml.WriteLine("</blogs>") NewXml.Close() End Sub End Class Public Class BlogDesigner Inherits ControlDesigner Public Overrides Function GetDesignTimeHtml() As String Return "<h1>Blog</h1><hr/><hr/>" End Function End Class
Listing 5. Blog.xsd
<?xml version="1.0" encoding="utf-8" ?> <xsd:schema targetNamespace="urn:http://www.aspnetian.com/schemas" elementFormDefault="qualified" xmlns="urn:http://www.aspnetian.com/schemas" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:vs="https://schemas.microsoft.com/Visual-Studio-Intellisense" vs:friendlyname="Blog Control Schema" vs:ishtmlschema="false" vs:iscasesensitive="false" vs:requireattributequotes="true" > <xsd:annotation> <xsd:documentation> Blog Control schema. </xsd:documentation> </xsd:annotation> <xsd:element name="Blog_DT" type="BlogDef" /> <!-- <aspnetian:Blog> --> <xsd:complexType name="BlogDef"> <!-- <aspnetian:Blog>-specific attributes --> <xsd:attribute name="AddRedirect" type="xsd:string" vs:builder="url"/> <xsd:attribute name="Email" type="xsd:string"/> <xsd:attribute name="Mode" type="BlogMode"/> <xsd:attribute name="SeparatorColor" type="xsd:string" vs:builder="color"/> <!-- <asp:Panel>-specific attributes --> <xsd:attribute name="BackImageUrl" type="xsd:anyURI" /> <xsd:attribute name="HorizontalAlign" type="HorizontalAlign" /> <xsd:attribute name="Wrap" type="xsd:boolean" /> <xsd:attribute name="Enabled" type="xsd:boolean" /> <xsd:attribute name="BorderWidth" type="ui4" /> <xsd:attribute name="BorderColor" type="xsd:string" vs:builder="color" /> <xsd:attribute name="BorderStyle" type="BorderStyle" /> <xsd:attributeGroup ref="WebControlAttributes" /> </xsd:complexType> <!-- DataTypes --> <xsd:simpleType name="BlogMode"> <xsd:restriction base="xsd:string"> <xsd:enumeration value="Add" /> <xsd:enumeration value="Display" /> </xsd:restriction> </xsd:simpleType> </xsd:schema>
This article was adapted from ASP.NET in a Nutshell, 2nd edition (ISBN: 0596001169), by G. Andrew Duthie and Matthew MacDonald, published by O'Reilly & Associates, Inc., 2003.