Evjen c26.tex V2 - 01/28/2008 3:48pm Page 1251 Chapter 26: User and Server Controls [Bindable(true)] [DefaultValue("")] public string Name { get { return _name;} set { _name = value;} } [Bindable(true)] [DefaultValue("")] public string Text { get { return _text;} set { _text = value;} } public override void DataBind() { CreateChildControls(); ChildControlsCreated = true; base.DataBind(); } protected override void CreateChildControls() { this.Controls.Clear(); _message = new Message(Name,Text); if (this.MessageTemplate == null) { this.MessageTemplate = new DefaultMessageTemplate(); } this.MessageTemplate.InstantiateIn(_message); Controls.Add(_message); } protected override void RenderContents(HtmlTextWriter writer) { EnsureChildControls(); ChildControlsCreated = true; base.RenderContents(writer); } } } To start to dissect this sample, first notice the MessageTemplate property. This property allows Visual Studio to understand that the control can contain a template, and allows it to display the IntelliSense for that template. The property has been marked with the PersistanceMode attribute indicating that the template control should be persisted as an inner property within the control’s tag in the ASPX page. Additionally, the property is marked with the TemplateContainer attribute, which helps ASP.NET figure out what type of template control this property represents. In this case, it’s the Message template control you created earlier. 1251 Evjen c26.tex V2 - 01/28/2008 3:48pm Page 1252 Chapter 26: User and Server Controls The container control exposes two public properties, Name and Text. These properties are used to popu- late the Name and Text properties of the Message control since that class does not allow developers to set the properties directly. Finally, the CreateChildControls method, called by the DataBind method, does most of the heavy lifting in this control. It creates a new Message object, passing the values of Name and Text as constructor values. Once the CreateChildControls method completes, the base DataBind operation comtinues to execute. This is important because that is where the evaluation of the Name and Text properties occurs, which allows you to insert these properties values into the template control. After the control and template are created, you can drop them onto a test Web page. Listing 26-37 shows how the control can be used to customize the display of the data. Listing 26-37: Adding a templated control to a Web page VB <%@ Page Language="VB" %> <%@ Register Assembly="WebControlLibrary1" Namespace="WebControlLibrary1" TagPrefix="cc1" %> <script runat="server"> Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Me.TemplatedControl1.DataBind() End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>Templated Web Controls</title> </head> <body> <form id="form1" runat="server"> <div> <cc1:TemplatedControl Name="John Doe" Text="Hello World!" ID="TemplatedControl1" runat="server"> <MessageTemplate>The user ’<%# Container.Name %>’ has a message for you: <br />"<%#Container.Text%>" </MessageTemplate> </cc1:TemplatedControl> </div> </form> </body> </html> C# <script runat="server"> protected void Page_Load(object sender, EventArgs e) { this.TemplatedControl1.DataBind(); } </script> As you can see in the listing, the < cc1:TemplatedControl > control contains a MessageTemplate within it, which has been customized to display the Name and Text values. Figure 26-16 shows this page after it has been rendered in the browser. 1252 Evjen c26.tex V2 - 01/28/2008 3:48pm Page 1253 Chapter 26: User and Server Controls Figure 26-16 One item to consider when creating templated controls is what happens if the developer does not include a template control inside of the container control. In the previous example, if you removed the Mes- sageTemplate control from the TemplateContainer, a NullReferenceException would occur when you tried to run your Web page because the container control’s MessageTemplate property would return a null value. In order to prevent this, you can include a default template class as part of the container control. An example of a default template is shown in Listing 26-38. Listing 26-38: Creating the templated control’s default template class VB Friend Class DefaultMessageTemplate Implements ITemplate Public Sub InstantiateIn(ByVal container As System.Web.UI.Control) _ Implements System.Web.UI.ITemplate.InstantiateIn Dim l As New Literal() l.Text="No MessageTemplate was included." container.Controls.Add(l) End Sub End Class C# internal sealed class DefaultMessageTemplate : ITemplate { public void InstantiateIn(Control container) { Literal l = new Literal(); l.Text="No MessageTemplate was included."; container.Controls.Add(l); } } Notice that the DefaultMessageTemplate implements the ITemplate interface. This interface requires that the InstantiateIn method be implemented, which we use to provide the default template content. To include the default template, simply add the class to the TemplatedControl class. You will also need to modify the CreateChildControls method to detect the null MessageTemplate and instead create an instance of and use the default template. 1253 Evjen c26.tex V2 - 01/28/2008 3:48pm Page 1254 Chapter 26: User and Server Controls VB If template = Nothing Then template = New DefaultMessageTemplate() End If C# if (template == null) { template = new DefaultMessageTemplate(); } Creating Control Design-Time Experiences So far in this chapter, you concentrated primarily on what gets rendered to the client’s browser, but the browser is not the only consumer of server controls. Visual Studio and the developer using a server control are also consumers, and you need to consider their experiences when using your control. Note that beginning with Visual Studio 2008, the Web Page Design Surface used to provide Web page designers with a WYSIWYG design experience has been completely rewritten. The design-surface, which in prior versions was derived from the core Internet Explorer rendering engine has been replaced by a completely independent and new rendering engine. This is good news for Web page developers because they are no longer subject to the quirks of IE rendering. If you have existing controls you should be sure to test them thoroughly on the new design-surface to ensure compatibility. From a control design perspective, all of the previous functionality has been retained. Therefore, any controls you have written to take advantage of design-time tools such as SmartTags or Designer regions should function normally on the new design surface. ASP.NET offers numerous improvements in the design-time experience you give to developers using your control. Some of these improvements require no additional coding, such as the WYSIWYG ren- dering of user controls and basic server controls; but for more complex scenarios, ASP.NET includes a number of tools that give the developer an outstanding design-time experience. When you write server controls, a priority should be to give the developer a design-time experience that closely replicates the runtime experience. This means altering the appearance of the control on the design surface in response to changes in control properties and the introduction of other server controls onto the design surface. Three main components are involved in creating the design-time behaviors of a server control: ❑ Type Converters ❑ Designers ❑ UI Type Editors Because a chapter can be written for each one of these topics, in this section I attempt to give you only an overview of each, how they tie into a control’s design-time behavior, and some simple examples of their use. Type Converters TypeConverter is a class that allows you to perform conversions between one type and another. Visual Studio uses type converters at design time to convert object property values to String types so that they 1254 Evjen c26.tex V2 - 01/28/2008 3:48pm Page 1255 Chapter 26: User and Server Controls can be displayed on the Property Browser, and it returns them to their original types when the developer changes the property. ASP.NET includes a wide variety of type converters you can use when creating your control’s design-time behavior. These range from converters that allow you to convert most number types, to converters that let you convert Fonts, Colors, DataTimes, and Guids. The easiest way to see what type converters are available to you in the .NET Framework is to search for types in the framework that derive from the TypeConverter class using the MSDN Library help. After you have found a type converter that you want to use on a control property, mark the property with a TypeConverter attribute, as shown in Listing 26-39. Listing 26-39: Applying the TypeConverter attribute to a property VB <Bindable(True)> _ <Category("Appearance")> _ <DefaultValue("")> _ <TypeConverter(GetType(GuidConverter))> _ Property BookId() As System.Guid Get Return _bookid End Get Set(ByVal Value As System.Guid) _bookid = Value End Set End Property C# [Bindable(true)] [Category("Appearance")] [DefaultValue("")] [TypeConverter(typeof(GuidConverter))] public Guid BookId { get { return _bookid; } set { _bookid = value; } } In this example, a property is exposed that accepts and returns an object of type Guid. The Property Browser cannot natively display a Guid object, so you convert the value to a string so that it can be displayed properly in the property browser. Marking the property with the TypeConverter attribute and, in this case, specifying the GuidConverter as the type converter you want to use, allows complex objects like a Guid to display properly in the Property Browser. 1255 Evjen c26.tex V2 - 01/28/2008 3:48pm Page 1256 Chapter 26: User and Server Controls Custom Type Converters It is also possible to create your own custom type converters if none of the in-box converters fit into your scenario. Type converters derive from the System.ComponentModel.TypeConverter class. Listing 26-40 shows a custom type converter that converts a custom object called Name to and from astring. Listing 26-40: Creating a custom type converter VB Imports System Imports System.ComponentModel Imports System.Globalization Public Class Name Private _first As String Private _last As String Public Sub New(ByVal first As String, ByVal last As String) _first = first _last = last End Sub Public Property First() As String Get Return _first End Get Set(ByVal value As String) _first = value End Set End Property Public Property Last() As String Get Return _last End Get Set(ByVal value As String) _last = value End Set End Property End Class Public Class NameConverter Inherits TypeConverter Public Overrides Function CanConvertFrom(ByVal context As _ ITypeDescriptorContext, ByVal sourceType As Type) As Boolean If (sourceType Is GetType(String)) Then Return True End If Return MyBase.CanConvertFrom(context, sourceType) End Function 1256 Evjen c26.tex V2 - 01/28/2008 3:48pm Page 1257 Chapter 26: User and Server Controls Public Overrides Function ConvertFrom( _ ByVal context As ITypeDescriptorContext, _ ByVal culture As CultureInfo, ByVal value As Object) As Object If (value Is GetType(String)) Then Dim v As String() = (CStr(value).Split(New [Char]() {" "c})) Return New Name(v(0), v(1)) End If Return MyBase.ConvertFrom(context, culture, value) End Function Public Overrides Function ConvertTo( _ ByVal context As ITypeDescriptorContext, _ ByVal culture As CultureInfo, ByVal value As Object, _ ByVal destinationType As Type) As Object If (destinationType Is GetType(String)) Then Return (CType(value, Name).First + " " + (CType(value, Name).Last)) End If Return MyBase.ConvertTo(context, culture, value, destinationType) End Function End Class C# using System; using System.ComponentModel; using System.Globalization; public class Name { private string _first; private string _last; public Name(string first, string last) { _first=first; _last=last; } public string First { get{ return _first;} set { _first = value;} } public string Last { get { return _last;} set { _last = value;} } } public class NameConverter : TypeConverter { public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { 1257 Evjen c26.tex V2 - 01/28/2008 3:48pm Page 1258 Chapter 26: User and Server Controls if (sourceType == typeof(string)) { return true; } return base.CanConvertFrom(context, sourceType); } public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string) { string[] v = ((string)value).Split(new char[] {’ ’}); return new Name(v[0],v[1]); } return base.ConvertFrom(context, culture, value); } public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) { if (destinationType == typeof(string)) { return ((Name)value).First + " " + ((Name)value).Last; } return base.ConvertTo(context, culture, value, destinationType); } } The NameConverter classoverridesthreemethods, CanConvertFrom , ConvertFrom ,and ConvertTo .The CanConvertFrom method allows you to control what types the converter can convert from. The ConvertFrom method converts the string representation back into a Name object, and ConvertTo converts the Name object into a string representation. After you have built your type converter, you can use it to mark properties in your control with the TypeConverter attribute, as you saw in Listing 26-35. Control Designers Controls that live on the Visual Studio design surface depend on control designers to create the design-time experience for the end user. Control designers, for both WinForms and ASP.NET, are classes that derive from the System.ComponentModel.Design.ComponentDesigner class. .NET provides an abstracted base class specifically for creating ASP.NET control designers called the System.Web.UI.Design .ControlDesigner . In order to access these classes you will need to add a reference to the System .Design.dll assembly to your project. .NET includes a number of in-box control designer classes that you can use when creating a custom control; but as you develop server controls, you see that .NET automatically applies a default designer. The designer it applies is based on the type of control you are creating. For instance, when you created your first TextBox control, Visual Studio used the ControlDesigner class to achieve the WYSIWYG design-time rendering of the text box. If you develop a server control derived from the ControlContainer class, .NET automatically use the ControlContainerDesigner class as the designer. 1258 Evjen c26.tex V2 - 01/28/2008 3:48pm Page 1259 Chapter 26: User and Server Controls You can also explicitly specify the designer you want to use to render your control at design time using the Designer attribute on your control’s class, as shown in Listing 26-41. Listing 26-41: Adding a Designer attribute to a control class VB <DefaultProperty("Text")> _ <ToolboxData("<{0}:WebCustomControl1 runat=server></{0}:WebCustomControl1>")> _ <Designer(GetType(System.Web.UI.Design.ControlDesigner))> _ Public Class WebCustomControl1 Inherits System.Web.UI.WebControls.WebControl C# [DefaultProperty("Text")] [ToolboxData("<{0}:WebCustomControl1 runat=server></{0}:WebCustomControl1>")] [Designer(typeof(System.Web.UI.Design.ControlDesigner))] public class WebCustomControl1 : WebControl Notice that the Designer attribute has been added to the WebCustomControl1 class. You have specified that the control should use the ControlDesigner class as its designer. Other in-box designers you could have specified are ❑ CompositeControlDesigner ❑ TemplatedControlDesigner ❑ DataSourceDesigner Each designer provides a specific design-time behavior for the control, and you can select one that is appropriate for the type of control you are creating. Design-Time Regions As you saw earlier, ASP.NET allows you to create server controls that consist of other server controls and text. In ASP.NET 1.0, a server control developer could use the ReadWriteControlDesigner class to enable the user of the server control to enter text or drop other server controls into a custom server control at design time. An example of this is the ASP.NET Panel control, which enables developers to add content to the panel at design time. Since ASP.NET 2.0, however, creating a control with this functionality has changed. The ReadWrite ControlDesigner class was marked as obsolete, and a new and improved way was included to allow the developer to create server controls that have design-time editable portions. The new technique, called designer regions, is an improvement over the ReadWriteControlDesigner in several ways. First, unlike the ReadWriteControlDesigner class, which allowed only a single editable area, designer regions enable you to create multiple, independent regions defined within a single control. Second, designer classes can now respond to events raised by a design region. This might be the designer drawing a control on the design surface or the user clicking an area of the control or entering or exiting a template edit mode. 1259 Evjen c26.tex V2 - 01/28/2008 3:48pm Page 1260 Chapter 26: User and Server Controls To show how you can use designer regions, create a container control to which you can apply a custom control designer (as shown in Listing 26-42). Listing 26-42: Creating a c omposite control with designer regions VB <Designer(GetType(MultiRegionControlDesigner))> _ <ToolboxData("<{0}:MultiRegionControl runat=server width=100%>" & _ "</{0}:MultiRegionControl > ") > _ Public Class MultiRegionControl Inherits CompositeControl ’ Define the templates that represent 2 views on the control Private _view1 As ITemplate Private _view2 As ITemplate ’ These properties are inner properties <PersistenceMode(PersistenceMode.InnerProperty), DefaultValue("") > _ Public Overridable Property View1() As ITemplate Get Return _view1 End Get Set(ByVal value As ITemplate) _view1 = value End Set End Property <PersistenceMode(PersistenceMode.InnerProperty), DefaultValue("") > _ Public Overridable Property View2() As ITemplate Get Return _view2 End Get Set(ByVal value As ITemplate) _view2 = value End Set End Property ’ The current view on the control; 0= view1, 1=view2, 2=all views Private _currentView As Int32 = 0 Public Property CurrentView() As Int32 Get Return _currentView End Get Set(ByVal value As Int32) _currentView = value End Set End Property Protected Overrides Sub CreateChildControls() MyBase.CreateChildControls() Controls.Clear() Dim template As ITemplate = View1 1260 . MessageTemplate and instead create an instance of and use the default template. 12 53 Evjen c26.tex V2 - 01/28/2008 3: 48pm Page 1 254 Chapter 26: User and Server Controls VB If template = Nothing Then template. shown in Listing 26 -38 . Listing 26 -38 : Creating the templated control’s default template class VB Friend Class DefaultMessageTemplate Implements ITemplate Public Sub InstantiateIn(ByVal container. design-surface, which in prior versions was derived from the core Internet Explorer rendering engine has been replaced by a completely independent and new rendering engine. This is good news