The metadata attributes that we described in the previous section represent one piece of the architecture that enables declarative property persistence. The other piece consists of type converter classes that perform conversions to and from the given type to the String type and to other types. When a property is specified declaratively , as Width="100px" is, the page parser invokes an instance of a type converter class to convert the string value to the declared type of the property. For example, when parsing Width="100px" , the parser invokes the System.Web.UI.WebControls.UnitConverter to convert the string "100px" to the System.Web.UI.WebControls.Unit type, which can be assigned to the Width property. A type converter is a class that derives from the System.ComponentModel.TypeConverter class, which provides methods that perform conversions to and from other types to the given type. In the previous paragraph, we showed an example of how the page parser uses type converters. The page framework also uses type converters to perform view state serialization. And visual designers use type converters to display properties in the property browser and to perform design-time serialization. Primitive types and many other types in the .NET Framework class library have type converters associated with them. For example, the Boolean , Char , Enum , and Int32 types have corresponding BooleanConverter , CharConverter , EnumConverter , and Int32Converter classes associated with them. To associate a type converter with a type, you apply the TypeConverter Attribute to the type. The following code fragment associates the System.Web.UI. WebControls.UnitConverter with the System.Web.UI.WebControls.Unit type: [ TypeConverter(typeof(UnitConverter)) ] publicstructUnit{...} When a type converter is associated with a type, it is automatically available to properties of that type. If you want to associate a different type converter with a property of that type, you can override the property and specify another type converter via the TypeConverterAttribute . The following example shows how the WebControl class implements the BackColor property to associate a different type converter from the default ColorConverter associated with the Color type: [ TypeConverterAttribute(typeof(WebColorConverter)) ] publicvirtualColorBackColor{...} When the type of a property does not have a type converter associated with it, you can associate a type converter with the property by applying TypeConverterAttribute to the property. If you define custom types for properties, you must also implement type converters and associate them with the types. We'll next show how to implement a type converter class. Implementing a Type ConverterLet's define a few custom types and their associated type converters. We will use these types in the MapDemo example later in this section. The MapDemo control is a simple abstraction over the HTML <map> element that allows a page developer to specify circular and rectangular regions that are activated when the user clicks within an image. The custom types that we will define represent geometrical entities such as a point, circle, or rectangle. Figure 10-1 shows the custom types displayed in the property browser. Figure 10-1. The Circle and Rectangle properties of the MapDemo control displayed in the property browser
The MapDemo control exposes Circle and Rectangle properties whose types are the MapCircle and MapRectangle custom types: publicclassMapDemo:Image,IPostBackEventHandler{ publicMapCircleCircle{...} publicMapRectangleRectangle{...} } The properties of the MapCircle and MapRectangle classes are subproperties of the Circle and Rectangle properties of the MapDemo class: publicclassMapCircle{ publicMapPointOrigin{...} publicintRadius{...} } publicclassMapRectangle{ publicMapPointTopLeft{...} publicMapPointBottomRight{...} } Here's how a type converter comes into the picture. The MapCircle and MapRectangle custom types define properties of another custom type: MapPoint . When the MapDemo control is used declaratively on a page ”as shown in the following example ”the page parser uses a type converter to convert the specified string values of the subproperties into the corresponding MapPoint types: <msp:MapDemorunat="server"Circle-Origin="100,50"Circle-Radius="50"> <RectangleTopLeft="0,100"BottomRight="200,150"></Rectangle> </msp:MapDemo> In addition, the property browser uses type converters to enable editing of these properties, and the designer uses them to serialize these properties. Now let's look at the implementation of MapPoint and its associated type converter. MapPoint and MapPointConverterListing 10-1 contains the code for the MapPoint class. The type converter associated with the MapPoint type is shown in Listing 10-2. Listing 10-1 MapPoint.csusingSystem; usingSystem.ComponentModel; usingSystem.Globalization; namespaceMSPress.ServerControls{ [ TypeConverter(typeof(MapPointConverter)) ] publicclassMapPoint{ privateint_x; privateint_y; publicMapPoint():this(0,0){ } publicMapPoint(intx,inty){ _x=x; _y=y; } publicboolIsEmpty{ get{ return(_x==0&&_y==0); } } publicintX{ get{ return_x; } set{ _x=value; } } publicintY{ get{ return_y; } set{ _y=value; } } publicoverrideboolEquals(objectobj){ MapPointother=objasMapPoint; if(other!=null){ return(other.X==X)&&(other.Y==Y); } returnfalse; } publicoverrideintGetHashCode(){ return_x.GetHashCode()^_y.GetHashCode(); } publicoverridestringToString(){ returnToString(CultureInfo.CurrentCulture); } publicvirtualstringToString(CultureInfoculture){ returnTypeDescriptor.GetConverter(typeof(MapPoint)).ConvertToString(null,culture,this); } } } The MapPointConverter class in Listing 10-2 shows the essential aspects of implementing a type converter that is needed for a read-write property. Listing 10-2 MapPointConverter.csusingSystem; usingSystem.ComponentModel; usingSystem.ComponentModel.Design.Serialization; usingSystem.Globalization; usingSystem.Reflection; namespaceMSPress.ServerControls{ publicclassMapPointConverter:TypeConverter{ publicoverrideboolCanConvertFrom(ITypeDescriptorContextcontext,TypesourceType){ if(sourceType==typeof(string)){ returntrue; } returnbase.CanConvertFrom(context,sourceType); } publicoverrideboolCanConvertTo(ITypeDescriptorContextcontext,TypedestinationType){ if((destinationType==typeof(string)) (destinationType==typeof(InstanceDescriptor))){ returntrue; } returnbase.CanConvertTo(context,destinationType); } publicoverrideobjectConvertFrom(ITypeDescriptorContextcontext,CultureInfoculture, objectvalue){ if(value==null){ returnnewMapPoint(); } if(valueisstring){ strings=(string)value; if(s.Length==0){ returnnewMapPoint(); } string[]parts= s.Split(culture.TextInfo.ListSeparator[0]); if(parts.Length!=2){ thrownewArgumentException("InvalidMapPoint", "value"); } TypeConverterintConverter= TypeDescriptor.GetConverter(typeof(Int32)); returnnewMapPoint((int)intConverter.ConvertFromString(context,culture,parts[0]), (int)intConverter.ConvertFromString(context,culture,parts[1])); } returnbase.ConvertFrom(context,culture,value); } publicoverrideobjectConvertTo(ITypeDescriptorContextcontext,CultureInfoculture, objectvalue,TypedestinationType){ if(value!=null){ if(!(valueisMapPoint)){ thrownewArgumentException("InvalidMapPoint", "value"); } } if(destinationType==typeof(string)){ if(value==null){ returnString.Empty; } MapPointpoint=(MapPoint)value; TypeConverterintConverter= TypeDescriptor.GetConverter(typeof(Int32)); returnString.Join(culture.TextInfo.ListSeparator, newstring[]{ intConverter.ConvertToString(context,culture, point.X), intConverter.ConvertToString(context,culture, point.Y) }); } elseif(destinationType==typeof(InstanceDescriptor)){ if(value==null){ returnnull; } MemberInfomi=null; object[]args=null; MapPointpoint=(MapPoint)value; if(point.IsEmpty){ mi=typeof(MapPoint).GetConstructor(newType[0]); } else{ TypeintType=typeof(int); mi=typeof(MapPoint).GetConstructor(newType[]{intType,intType}); args=newobject[]{point.X,point.Y}; } if(mi!=null){ returnnewInstanceDescriptor(mi,args); } else{ returnnull; } } returnbase.ConvertTo(context,culture,value, destinationType); } } } MapPointConverter performs string-to-value conversions and generates an abstract representation of a constructor that creates an instance of the MapPoint type. It implements the following logic:
Type Converters for the MapCircle and MapRectangle TypesBefore looking at the MapCircle and MapRectangle types, we will define an abstract MapShape base class that expresses the common characteristics of a shape. We will also define a MapShapeConverter class, which derives from ExpandableObjectConverter and enables hierarchical UI for subproperties in the property browser. We will associate the MapShapeConverter with the MapShape base class via a type converter attribute. Listing 10-3 contains the code for the MapShape class. Listing 10-3 MapShape.csusingSystem; usingSystem.ComponentModel; usingSystem.Globalization; namespaceMSPress.ServerControls{ publicabstractclassMapShapeConverter: ExpandableObjectConverter{} [TypeConverter(typeof(MapShapeConverter))] publicabstractclassMapShape{ [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] publicabstractboolIsEmpty{ get; } [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] publicabstractstringMapCoordinates{ get; } [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] protectedabstractMapShapeNameMapShapeName{ get; } [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] publicstringShapeName{ get{ returnMapShapeName.ToString().ToLower(CultureInfo.InvariantCulture); } } publicabstractvoidLoadFromString(stringvalue); publicabstractstringSaveToString(); publicoverridestringToString(){ returnToString(CultureInfo.InvariantCulture); } publicvirtualstringToString(CultureInfoculture){ returnTypeDescriptor.GetConverter(GetType()).ConvertToString(null,culture,this); } } } Listing 10-4 shows the MapShapeName enumeration, which contains the shape names allowed by the HTML map specification: circle, rectangle, and polygon. Listing 10-4 MapShapeName.csusingSystem; namespaceMSPress.ServerControls{ publicenumMapShapeName{ Circle, Rectangle, Polygon } } The MapCircle and MapRectangle classes derive from the MapShape class. You'll find the complete code for the MapCircle and MapRectangle classes in this book's sample files. We have not defined a MapPolygon type, but you can extend the samples in this chapter by defining and using this type. The code fragments in Listing 10-5 show the definition of properties in the MapCircle and MapRectangle classes: Listing 10-5 Property definitions in the MapCircle and MapRectangle classes[ TypeConverter(typeof(MapCircleConverter)) ] publicclassMapCircle:MapShape{ [ DefaultValue(typeof(MapPoint),""), NotifyParentProperty(true) ] publicMapPointOrigin{ get{ return_origin; } set{ if(value==null){ thrownewArgumentNullException("value"); } _origin=value; } } [ DefaultValue(0), NotifyParentProperty(true) ] publicintRadius{ get{ return_radius; } set{ if(value<0){ thrownewArgumentOutOfRangeException("value"); } _radius=value; } } } [ TypeConverter(typeof(MapRectangleConverter)) ] publicclassMapRectangle:MapShape{ [ DefaultValue(typeof(MapPoint),""), NotifyParentProperty(true) ] publicMapPointBottomRight{ get{ return_bottomRight; } set{ if(value==null){ thrownewArgumentNullException("value"); } _bottomRight=value; } } [ DefaultValue(typeof(MapPoint),""), NotifyParentProperty(true) ] publicMapPointTopLeft{ get{ return_topLeft; } set{ if(value==null){ thrownewArgumentNullException("value"); } _topLeft=value; } } } We'll now examine the type converters for the MapCircle and MapRectangle types. When a server control defines properties of these types (and any other complex type), they should be defined as read-only properties because of state management, as we'll explain in the next section. When a property is read-only, a page parser does not require a type converter for the type of the property. However, a designer needs a type converter for a complex property that will be displayed in the property browser. The MapDemo control, which we'll look at shortly, illustrates this point. MapDemo contains the Circle property and the Rectangle property, whose respective types are MapCircle and MapRectangle . These properties are read-only, and property syntax such as Circle="100,50,50" is not allowed. Therefore, the page parser never has to convert from string representations to instances of the MapCircle or MapRectangle types. However, the designer needs type converters for these types because it uses them to display the string representations of the properties in the property browser. Figure 10-1 shows how the Circle and Rectangle properties of the MapDemo control are displayed in the property browser in Microsoft Visual Studio .NET. Listing 10-6 contains the code for the MapCircleConverter class. The implementation of MapRectangleConverter is similar to that of MapCircleConverter and is provided in the book's sample files. The type converter implementation in these cases is simpler than that of MapPointConverter because MapCircleConverter and MapRectangleConverter do not have to convert to an InstanceDescriptor . As we described earlier in the MapPointConverter example, an InstanceDescriptor is needed only if the type converter is used to generate code that creates an instance of the type, which is not needed for a read-only property. Listing 10-6 MapCircleConverter.csusingSystem; usingSystem.ComponentModel; usingSystem.ComponentModel.Design.Serialization; usingSystem.Globalization; usingSystem.Reflection; namespaceMSPress.ServerControls{ publicclassMapCircleConverter:MapShapeConverter{ publicoverrideboolCanConvertFrom(ITypeDescriptorContextcontext,TypesourceType){ if(sourceType==typeof(string)){ returntrue; } returnbase.CanConvertFrom(context,sourceType); } publicoverrideboolCanConvertTo(ITypeDescriptorContextcontext,TypedestinationType){ if(destinationType==typeof(string)){ returntrue; } returnbase.CanConvertTo(context,destinationType); } publicoverrideobjectConvertFrom(ITypeDescriptorContextcontext,CultureInfoculture, objectvalue){ if(value==null){ returnnewMapCircle(); } if(valueisstring){ strings=(string)value; if(s.Length==0){ returnnewMapCircle(); } string[]parts= s.Split(culture.TextInfo.ListSeparator[0]); if(parts.Length!=3){ thrownewArgumentException("InvalidMapCircle", "value"); } TypeConverterintConverter= TypeDescriptor.GetConverter(typeof(Int32)); returnnewMapCircle(newMapPoint((int)intConverter.ConvertFromString(context,culture,parts[0]), (int)intConverter.ConvertFromString(context, culture,parts[1])), (int)intConverter.ConvertFromString(context, culture,parts[2])); } returnbase.ConvertFrom(context,culture,value); } publicoverrideobjectConvertTo(ITypeDescriptorContextcontext,CultureInfoculture, objectvalue,TypedestinationType){ if(value!=null){ if(!(valueisMapCircle)){ thrownewArgumentException("InvalidMapCircle", "value"); } } if(destinationType==typeof(string)){ if(value==null){ returnString.Empty; } MapCirclecircle=(MapCircle)value; if(circle.IsEmpty){ returnString.Empty; } TypeConverterintConverter= TypeDescriptor.GetConverter(typeof(Int32)); returnString.Join(culture.TextInfo.ListSeparator, newstring[]{ intConverter.ConvertToString(context,culture, circle.Origin.X), intConverter.ConvertToString(context,culture, circle.Origin.Y), intConverter.ConvertToString(context,culture, circle.Radius) }); } returnbase.ConvertTo(context,culture,value, destinationType); } } } Putting It Together ”The MapDemo ExampleWe'll now use the MapPoint , MapCircle , and MapRectangle custom types and their associated type converters to implement a MapDemo control. The MapDemo control allows a page developer to create a circular region and a rectangular region (hot spots) in an image that cause postback to the server when a user clicks inside them. MapDemo is a trimmed -down version of the ImageMap control that we will implement later in this chapter. Figure 10-2 shows a page that uses the MapDemo control. Figure 10-2. MapDemoTest.aspx viewed in a browser. The rendered image has an associated HTML map with two hot spots.
The MapDemo control, shown in Listing 10-7, derives from the ASP.NET Image control and adds to it the functionality to render an image map. Listing 10-7 MapDemo.csusingSystem; usingSystem.ComponentModel; usingSystem.Web.UI; usingSystem.Web.UI.WebControls; namespaceMSPress.ServerControls{ [ DefaultEvent("MapClick"), DefaultProperty("Rectangle") ] publicclassMapDemo:Image,IPostBackEventHandler{ privatestaticreadonlyobjectEventMapClick=newobject(); privateconststringMapCircleArg="circle"; privateconststringMapRectangleArg="rectangle"; privateMapCircle_circle; privateMapRectangle_rectangle; privatebool_hasHotSpots; [ Category("Shape"), DefaultValue(typeof(MapCircle),""), Description("Thelocationofthecircularhotspot"), DesignerSerializationVisibility(DesignerSerializationVisibility.Content), NotifyParentProperty(true) ] publicMapCircleCircle{ get{ if(_circle==null){ _circle=newMapCircle(); } return_circle; } } [ Category("Shape"), DefaultValue(""), Description("Thelocationoftherectangularhotspot"), DesignerSerializationVisibility(DesignerSerializationVisibility.Content), PersistenceMode(PersistenceMode.InnerProperty), NotifyParentProperty(true) ] publicMapRectangleRectangle{ get{ if(_rectangle==null){ _rectangle=newMapRectangle(); } return_rectangle; } } [ Category("Action"), Description("Raisedwhenahotspotisclicked") ] publiceventMapClickEventHandlerMapClick{ add{ Events.AddHandler(EventMapClick,value); } remove{ Events.RemoveHandler(EventMapClick,value); } } protectedoverridevoidAddAttributesToRender(HtmlTextWriterwriter){ base.AddAttributesToRender(writer); if(_hasHotSpots){ writer.AddAttribute("UseMap","#ImageMap"+ClientID, false); } } protectedvirtualvoidOnMapClick(MapClickEventArgse){ MapClickEventHandlermapClickHandler= (MapClickEventHandler)Events[EventMapClick]; if(mapClickHandler!=null){ mapClickHandler(this,e); } } protectedoverridevoidRender(HtmlTextWriterwriter){ if(((_circle!=null)&&(_circle.IsEmpty==false)) ((_rectangle!=null)&& (_rectangle.IsEmpty==false))){ _hasHotSpots=true; } base.Render(writer); if(_hasHotSpots){ writer.AddAttribute(HtmlTextWriterAttribute.Name, "ImageMap"+ClientID); writer.RenderBeginTag(HtmlTextWriterTag.Map); if((_circle!=null)&&(_circle.IsEmpty==false)){ writer.AddAttribute("shape",_circle.ShapeName, false); writer.AddAttribute("coords", _circle.MapCoordinates); writer.AddAttribute(HtmlTextWriterAttribute.Href, Page.GetPostBackClientHyperlink(this, MapCircleArg)); writer.AddAttribute(HtmlTextWriterAttribute.Title, "CircularHotspot",false); writer.RenderBeginTag("area"); writer.RenderEndTag(); } if((_rectangle!=null)&& (_rectangle.IsEmpty==false)){ writer.AddAttribute("shape",_rectangle.ShapeName, false); writer.AddAttribute("coords", _rectangle.MapCoordinates,false); writer.AddAttribute(HtmlTextWriterAttribute.Href, Page.GetPostBackClientHyperlink(this, MapRectangleArg)); writer.AddAttribute(HtmlTextWriterAttribute.Title, "RectangularHotspot",false); writer.RenderBeginTag("area"); writer.RenderEndTag(); } writer.RenderEndTag();//Map } } #regionCustomStateManagementImplementation protectedoverridevoidLoadViewState(objectsavedState){ base.LoadViewState(savedState); strings=null; s=(string)ViewState["Circle"]; if(s!=null){ Circle.LoadFromString(s); } s=(string)ViewState["Rectangle"]; if(s!=null){ Rectangle.LoadFromString(s); } } protectedoverrideobjectSaveViewState(){ stringcurrentState=null; stringinitialState=null; if(_circle!=null){ currentState=_circle.SaveToString(); initialState=(string)ViewState["Circle"]; if(currentState.Equals(initialState)==false){ ViewState["Circle"]=currentState; } } if(_rectangle!=null){ currentState=_rectangle.SaveToString(); initialState=(string)ViewState["Rectangle"]; if(currentState.Equals(initialState)==false){ ViewState["Rectangle"]=currentState; } } returnbase.SaveViewState(); } protectedoverridevoidTrackViewState(){ if(_circle!=null){ ViewState["Circle"]=_circle.SaveToString(); } if(_rectangle!=null){ ViewState["Rectangle"]=_rectangle.SaveToString(); } base.TrackViewState(); } #endregionCustomStateManagementImplementation #regionImplementationofIPostBackEventHandler voidIPostBackEventHandler.RaisePostBackEvent(stringeventArg){ strings=string.Empty; if(eventArg!=null){ if(eventArg.Equals(MapCircleArg)) s=MapCircleArg; elseif(eventArg.Equals(MapRectangleArg)) s=MapRectangleArg; } OnMapClick(newMapClickEventArgs(s)); } #endregionImplementationofIPostBackEventHandler } } Listings 10-8 and 10-9 contain the code for the MapClickEventArgs and MapClickEventHandler classes associated with the MapClick event. Listing 10-8 MapClickEventArgs.csusingSystem; namespaceMSPress.ServerControls{ publicclassMapClickEventArgs:EventArgs{ privatestring_action; publicMapClickEventArgs(stringaction){ _action=action; } publicstringAction{ get{ return_action; } } } } Listing 10-9 MapClickEventHandler.csusingSystem; namespaceMSPress.ServerControls{ publicdelegatevoidMapClickEventHandler(objectsender, MapClickEventArgse); } MapDemo illustrates the following points:
Listing 10-10 shows the MapDemoTest.aspx page that uses the MapDemo control. The Circle property is persisted on the control's tag by using the hyphenated syntax for subproperties, and the Rectangle property is persisted as an inner property. Figure 10-2 shows the page accessed in a browser. Listing 10-10 MapDemoTest.aspx<%@PageLanguage="C#"%> <%@RegisterTagPrefix="msp"Namespace="MSPress.ServerControls" Assembly="MSPress.ServerControls"%> <html> <head> <scriptrunat="server"> voidmapDemo1_MapClick(objectsender,MapClickEventArgse){ label1.Text= string.Format("Youclickedthe{0}.",e.Action); } </script> </head> <body> <formrunat="server"> <br> Clickashape: <msp:MapDemoImageUrl="Shapes.gif"ImageAlign="Middle" runat="server"OnMapClick="mapDemo1_MapClick"id="mapDemo1" Circle-Origin="100,50"Circle-Radius="50"> <RectangleBottomRight="200,150"TopLeft="0,100"></Rectangle> </msp:MapDemo> <asp:LabelFont-Size="10pt"Font-Bold="true"id="label1" runat="server"/> </form> </body> </html> If you view the source for the page rendered in your browser, you will see the <map> element in the HTML rendered by the MapDemo instance: <imgid="mapDemo1"src="/BookWeb/Chapter10/Shapes.gif" align="Middle"border="0"UseMap="#ImageMapmapDemo1"/> <mapname="ImageMapmapDemo1"> <areashape="circle"coords="100,50,50" href="javascript:__doPostBack('mapDemo1','circle')" title="CircularHotSpot"> </area> <areashape="rectangle"coords="0,100,200,150" href="javascript:__doPostBack('mapDemo1','rectangle')" title="RectangularHotSpot"> </area> </map> |