Introduction
My background as a Geographer/GIS guru often involves developing web sites for interactive mapping on a variety of topics. One of our requirements include abilities to create vector graphics on the client side for functions such as zoom boxes, measurement tools, shapes (points, lines, polygons), and other things common to desktop Geographical Information Systems. Many of our clients have strict guidelines that include developing applications which do not require plug-ins and/or downloads. I have found VML to be a perfect solution for this requirement and have developed VML drawing controls to enable drag and drop tools for the creation of client side line, polyline, polygon, rectangle and round rectangle graphics to automate this task. In this article, we will take a look at the VMLControl
base class and then the VMLPolygon
web control as an example of these VML drawing controls.
* Please note, although there are no downloads or plug-ins required, an Internet Explorer web browser is.
Background
For more background information about VML, please see Simon Stewart's article Introduction to VML and Microsoft's VML Reference. I would also like to extend a special thanks to Sreenivas Vemulapalli whose articles on Using the Property Grid helped these controls take shape.
VML namespace reference: the use of VML requires the following tag usage to be included in the HTML document:
<HTML xmlns:v="urn:schemas-microsoft-com:vml">
<HEAD>
<style>v\:* { BEHAVIOR: url(#default#VML) }
</style>
</HEAD>
....
Using the code
These controls should be used like any other .NET web control and can be added to your project through the Add/Remove Toolbox Items... under the Tools menu in the Visual Studio IDE.
The VMLDrawingControls comprises of five shape control classes that are derived from the VMLControl
base class.
VMLLine
VMLPolyline
VMLPolygon
VMLRectangle
VMLRoundRectangle
(inherits from VMLRectangle
)
VMLControl
The MustInherit
VMLControl
base class provides the framework for the controls and includes properties for:
ID
ButtonType
[Button
| Image
]
ButtonText
ImageSrc
, ImageSrcMouseDown
, ImageSourceMouseOver
CursorStyle
, CustomCursor
, CustomCursorEnabled
BoundingDrawCanvas
The BoundingDrawCanvas
is the required property which holds the ID
of the image control that will be used as the bounding drawing canvas for the vector graphics. I used the AddAttributesToRender
overrides of the System.Web.UI.WebControls.WebControl
to warn the user of this requirement. Once the requirement has been met in the control's properties, the warning will go away.
Protected Overrides Sub AddAttributesToRender(ByVal _
output As System.Web.UI.HtmlTextWriter)
If Me.BoundingDrawCanvas = "" Or _
Me.BoundingDrawCanvas = "(none)" Then
output.Write("<BR>")
output.Write("<Font color='Red'>" & _
"<li>BoundingDrawCanvas Property Is Required</Font>")
Else
If Page.FindControl(Me.BoundingDrawCanvas) Is Nothing Then
output.Write("<BR>")
output.Write("<Font color='Red'>" & _
"<li>BoundingDrawCanvas Control ID" & _
" Cannot Be Found</Font>")
End If
End If
MyBase.AddAttributesToRender(output)
End Sub
* Code Source: VMLControl.vb
The following is the BoundingDrawCanvas
property from the VMLControl
base class that is used in all of the derived VML shape classes.
<Category("Appearance"), _
Description("The Control ID Of The Image Control" & _
" Which Defines The Bounding Draw Canvas"), _
TypeConverter(GetType(BoundingControlsConverter)), _
PersistenceMode(PersistenceMode.Attribute)> _
Public Property BoundingDrawCanvas() As String
Get
_BoundingDrawCanvas = _
CType(ViewState("VMLBoundingDrawCanvas"), String)
If _BoundingDrawCanvas Is Nothing Then
Return "(none)"
Else
Return _BoundingDrawCanvas
End If
End Get
Set(ByVal Value As String)
ViewState("VMLBoundingDrawCanvas") = Value
_BoundingDrawCanvas = Value
End Set
End Property
*Code Source: VMLControl.vb
I created a BoundingControlsConverter
for this property to list out the current image controls on the web form. Due to the nature of my work, I use the image web control (which contains my map image) as my drawing canvas. The GetStandardValues
override from the base class System.ComponentModel.TypeConverter
allows me to retrieve the IDs of the image controls for the property dropdown list. Using the Context.Container.Components
passed in, allows me to loop the components of the page to identify image controls to use for the canvas. A future modification will be to accommodate table cells, div
s, and other container tags.
Public Overloads Overrides Function GetStandardValues(ByVal context As _
System.ComponentModel.ITypeDescriptorContext) As _
System.ComponentModel.TypeConverter.StandardValuesCollection
Dim supportedCtrls As New ArrayList
Dim Component As IComponent
Dim wcImg As WebControls.Image
For Each Component In context.Container.Components
If TypeOf Component Is WebControls.Image Then
wcImg = CType(Component, WebControls.Image)
supportedCtrls.Add(wcImg.ID)
End If
Next
supportedCtrls.Sort()
Dim svc As New _
StandardValuesCollection(CType(supportedCtrls.ToArray(GetType(String)), _
String()))
Return svc
End Function
*Code Source: BoundingControlsConverter.vb
VML Styles
I created three VML style classes that were used as properties for the drawing control classes to define things like color, width, style, fill patterns, etc.
VMLLineStyle
VMLFillStyle
LineEndPointProperties
(provides From
and To
point styles)
For example, the VMLPolygon
control has the FillStyle
property of type VMLFillStyle
which defines the color, pattern, etc. for the polygon to be rendered. The ExpandableObjectConverter
was used to make the property expandable in the property grid similar to the Font
property of other controls.
<Category("VML Symbolization"), _
Description("Defines The VML Fill Style Properties To Be Used"), _
DesignerSerializationVisibility(DesignerSerializationVisibility.Content), _
TypeConverter(GetType(ExpandableObjectConverter))> _
Public Property FillStyle() As VMLFillStyle
Get
Return _FillStyle
End Get
Set(ByVal Value As VMLFillStyle)
_FillStyle = Value
End Set
End Property
*Code Source: VMLPolygon.vb
This is the VMLFillStyle
class:
Imports System.ComponentModel
Imports System.Web.UI
Imports System.Drawing.Design
<Serializable(), PersistenceMode(PersistenceMode.Attribute)> _
Public Class VMLFillStyle
Enum FillTypeEnum
Solid
Gradient
GradientRadial
Tile
Pattern
Frame
End Enum
Private _FillType As FillTypeEnum = FillTypeEnum.Solid
Private _FillColor As System.Drawing.Color = Nothing
Private _FillColor2 As System.Drawing.Color = Nothing
Private _FillOpacity As Int16 = 100
Private _FillOpacity2 As Int16 = 100
Sub New()
End Sub
<Category("Rectangle Fill Appearance"), _
Description("Defines The Type Of Fill To Apply"), _
NotifyParentProperty(True)> _
Public Property FillType() As FillTypeEnum
Get
Return _FillType
End Get
Set(ByVal Value As FillTypeEnum)
_FillType = Value
End Set
End Property
<Category("Rectangle Fill Appearance"), _
Description("Defines The Primary Fill Color"), _
NotifyParentProperty(True)> _
Property FillColor() As System.Drawing.Color
Get
Return _FillColor
End Get
Set(ByVal Value As System.Drawing.Color)
_FillColor = Value
End Set
End Property
<Category("Rectangle Fill Appearance"), _
Description("Defines The Secondary Fill Color"), _
NotifyParentProperty(True)> _
Property FillColor2() As System.Drawing.Color
Get
Return _FillColor2
End Get
Set(ByVal Value As System.Drawing.Color)
_FillColor2 = Value
End Set
End Property
<Category("Rectangle Fill Appearance"), _
Description("Defines The Opacity Of The Primary Color"), _
NotifyParentProperty(True)> _
Property FillOpacity() As Int16
Get
Return _FillOpacity
End Get
Set(ByVal Value As Int16)
If ((Value < 0 Or Value > 100)) Then
Throw New ArgumentException("The Opacity" & _
" Must Be Beteen 0 and 100")
End If
_FillOpacity = Value
End Set
End Property
<Category("Rectangle Fill Appearance"), _
Description("Defines The Opacity Of The Secondary Color"), _
NotifyParentProperty(True)> _
Property FillOpacity2() As Int16
Get
Return _FillOpacity2
End Get
Set(ByVal Value As Int16)
If ((Value < 0 Or Value > 100)) Then
Throw New ArgumentException("The Opacity2" & _
" Must Be Beteen 0 and 100")
End If
_FillOpacity2 = Value
End Set
End Property
<DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)> _
Public ReadOnly Property FillTag() As String
Get
Dim retFillTag As New System.Text.StringBuilder
retFillTag.Append("<v:fill ")
retFillTag.Append(" Type=" & Quote(Me.FillType.ToString))
retFillTag.Append(" Color=" & _
Quote(HelperFunctions.BuildRGBString(Me.FillColor)))
retFillTag.Append(" Color2=" & _
Quote(HelperFunctions.BuildRGBString(Me.FillColor2)))
retFillTag.Append(" Opacity=" & _
Quote(FillOpacity.ToString & "%"))
retFillTag.Append(" Opacity2=" & _
Quote(Me.FillOpacity2.ToString & "%"))
retFillTag.Append(" />")
Return retFillTag.ToString
End Get
End Property
<Description("Define The VML Fill Properties")> _
Overrides Function ToString() As String
Return "(VML Fill Style)"
End Function
End Class
*Code Source: VMLFillStyle.vb
A module named HelperFunctions
was created to extend these controls. The ReadJavascript
procedure was created to read the JavaScript source file to manipulate the VML graphics on the client side as a resource of the assembly and the contents written to the out-stream. Using this method, I can keep all of the JavaScript in a single .js file and modify it as needed and then simply rebuild the assembly. There is also a property in the VMLControl
base class which I added to reference an external .js file but never thoroughly implemented it.
Public Function ReadJavasript() As String
Dim jsStream As IO.Stream
jsStream = System.Reflection.Assembly.GetExecutingAssembly()._
GetManifestResourceStream("VMLDrawingControls" & _
".VMLClientJavascript.js")
Dim jsStreamReader As New StreamReader(jsStream)
Return jsStreamReader.ReadToEnd
End Function
*Code Source: HelperFunctions.vb
Protected Overrides Sub Render(ByVal output As _
System.Web.UI.HtmlTextWriter)
Page.RegisterStartupScript("VMLScript", _
"<script language=""javascript""> " & _
HelperFunctions.ReadJavasript & " </script>")
MyBase.Render(output)
End Sub
*Code Source: VMLPolygon.vb
VMLPolygon
The VMLPolygon
is an example of the VMLDrawingControls
. This controls inherits from the VMLControl
base class and has methods and properties to define the look and feel of the rendered polygon. As shown above, the control uses the VMLFillStyle
to define the fill properties and it all uses the VMLLineStyle
to define the outline properties of the polygon.
Imports System.web.UI.HtmlTextWriter
Imports System.Web.UI
Imports System.Web.UI.Design
Imports System.ComponentModel
Imports System.Drawing.Design
Imports System.Drawing
Imports System.Design
Imports System.Windows.Forms
<Designer(GetType(VMLControlDesigner)), _
ToolboxData("<{0}:VMLPolygon runat="server"></{0}:VMLPolygon>")> _
Public Class VMLPolygon
Inherits VMLControl
Implements INamingContainer
Sub New()
MyBase.ButtonText = "Draw Polygon"
End Sub
Private _LineStyle As New VMLLineStyle
Private _FillStyleEnabled As Boolean = False
Private _FillStyle As New VMLFillStyle
<Category("Appearance"), _
Description("Defines The Line Stroke Style"), _
DesignerSerializationVisibility(DesignerSerializationVisibility.Content), _
TypeConverter(GetType(ExpandableObjectConverter))> _
Public Property LineStyle() As VMLLineStyle
Get
Return _LineStyle
End Get
Set(ByVal Value As VMLLineStyle)
_LineStyle = Value
End Set
End Property
<Category("VML Symbolization"), _
Description("Enables The Use Of A FillStyle")> _
Public Property FillStyleEnabled() As Boolean
Get
Return _FillStyleEnabled
End Get
Set(ByVal Value As Boolean)
_FillStyleEnabled = Value
End Set
End Property
<Category("VML Symbolization"), _
Description("Defines The VML Fill Style Properties To Be Used"), _
DesignerSerializationVisibility(DesignerSerializationVisibility.Content), _
TypeConverter(GetType(ExpandableObjectConverter))> _
Public Property FillStyle() As VMLFillStyle
Get
Return _FillStyle
End Get
Set(ByVal Value As VMLFillStyle)
_FillStyle = Value
End Set
End Property
Protected Overrides Sub Render(ByVal output As _
System.Web.UI.HtmlTextWriter)
Page.RegisterStartupScript("VMLScript", _
"<script language=""javascript""> " & _
HelperFunctions.ReadJavasript & " </script>")
MyBase.Render(output)
End Sub
Protected Overrides Sub AddAttributesToRender(ByVal output _
As System.Web.UI.HtmlTextWriter)
Dim cursorString As String
If Me.CustomCursorEnabled Then
cursorString = "url(" & Me.CustomCursor.ToString.ToLower & ")"
Else
cursorString = Me.CursorStyle.ToString
End If
Select Case Me.ButtonType
Case ButtonTypeEnum.Button
output.AddAttribute(HtmlTextWriterAttribute.Type, "button")
output.AddAttribute(HtmlTextWriterAttribute.Onclick, _
"activate('" & Me.ID & "','" & Me.BoundingDrawCanvas & _
"','POLYGON','" & cursorString & "')")
output.AddAttribute(HtmlTextWriterAttribute.Value, Me.ButtonText)
Case ButtonTypeEnum.Image
output.AddAttribute(HtmlTextWriterAttribute.Type, "image")
output.AddAttribute(HtmlTextWriterAttribute.Onclick, _
"activate('" & Me.ID & "','" & Me.BoundingDrawCanvas & _
"','POLYGON','" & cursorString & "'); return false;")
output.AddAttribute(HtmlTextWriterAttribute.Src, _
Me.ImageSrc.Replace("\", "/"))
output.AddAttribute("onmouseout", "this.src=
Me.ImageSrc.Replace("\", "/") & "
If Not Me.ImageSrcMouseOver.Equals(String.Empty) Then
output.AddAttribute("onmouseover", "this.src='" & _
Me.ImageSrcMouseOver.Replace("\", "/") & "
End If
If Not Me.ImageSrcMouseDown.Equals(String.Empty) Then
output.AddAttribute("onmousedown", _
"return false; this.src='" & _
Me.ImageSrcMouseDown.Replace("\", "/") & "
End If
End Select
MyBase.AddAttributesToRender(output)
End Sub
Protected Overrides Sub RenderChildren(ByVal output As _
System.Web.UI.HtmlTextWriter)
Dim outTags As New System.Text.StringBuilder
outTags.Append("<v:polyline ID=" & Quote(Me.ID) & " ")
Dim cursorString As String
If Me.CustomCursorEnabled Then
cursorString = Me.CustomCursor
outTags.Append("style=" & DoubleQuoteChar & _
"z-index: 1002;cursor: url('" & _
cursorString & "');" & DoubleQuoteChar & " >")
Else
cursorString = Me.CursorStyle.ToString
outTags.Append("style='cursor: " & cursorString & ";z-index: 1002;'>")
End If
outTags.Append(Me.LineStyle.StrokeTag)
If Me.FillStyleEnabled Then
outTags.Append(Me.FillStyle.FillTag)
End If
outTags.Append("</v:polyline>")
output.Write(outTags.ToString)
End Sub
End Class
*Code Source: VMLPolygon.vb
Putting It All Together
With the properties assigned, we can use the RenderChildren
of our shape classes to write out the VML tags to the browser. The ID assigned in the control properties is used by the client-side script to dynamically modify the VML shape properties.
Protected Overrides Sub RenderChildren(ByVal output As System.Web.UI.HtmlTextWriter)
Dim outTags As New System.Text.StringBuilder
outTags.Append("<v:polyline ID=" & Quote(Me.ID) & " ")
Dim cursorString As String
If Me.CustomCursorEnabled Then
cursorString = Me.CustomCursor
outTags.Append("style=" & DoubleQuoteChar & _
"z-index: 1002;cursor: url('" & _
cursorString & "');" & DoubleQuoteChar & " >")
Else
cursorString = Me.CursorStyle.ToString
outTags.Append("style='cursor: " & _
cursorString & ";z-index: 1002;'>")
End If
outTags.Append(Me.LineStyle.StrokeTag)
If Me.FillStyleEnabled Then
outTags.Append(Me.FillStyle.FillTag)
End If
outTags.Append("</v:polyline>")
output.Write(outTags.ToString)
End Sub
*Code Source: VMLPolygon.vb
Points of Interest
One of the issues I ran into while creating these controls is the ability or inability to persist the control properties to the designer. I found the Visual Studio IDE to sometime not update or persist these properties during the building phases of these controls and seemed to resolve itself upon a restart of the IDE. I believe these problems to be resolved with newer releases of the IDE.
Future Enhancements
I hope to enhance these controls in the future to have a VML control for general shape, text, arc, oval, image and other VML elements. I would also like to implement a method for returning the coordinates of the shapes to the server for special processing needs.
History
- Original posting -- Dec 15, 2005