Now that we've spent a time delving into an obscure portion of IE's Document Object Model (DOM), how does this apply to ASP.NET? Part of the power of ASP.NET is the ability to encapsulate a combination of client-side code and server-side code into a server control that is treated as a single logical unit. This unit can then be easily reused without rewriting the code every time we need that functionality. The rest of this chapter will show how to implement our HTML editor as a custom server control, and then how to use it. The basics of implementing custom server controls was introduced in Chapter 15. If you do not have any familiarity with custom server controls, you should read the portions of Chapter 15 that introduce custom server controls before you read the rest of this chapter. Control DesignBefore writing the custom control, we need to specify what we want the control to do and how we want to interact with it. Here is a list of characteristics that I thought the control should have in addition to supporting our basic task of implementing a client-side HTML editor:
That is the basics of what we want the control to do, so now let's examine these points in a little more detail. Data StorageDesigning the control so it can be used with any method of data storage is easy. You don't implement data storage in the control. Rather than having the control store data, the control can fire an event when a button is pressed indicating that the person editing data with the control is done and that the data should be saved. You could then either write the code to handle the data storage in the page hosting the control, or write other controls that inherit from this control and specialize the storage method. In this chapter, we will write the control with no data-storing capability and then show how to store data from the page hosting the control. Using the Control with Visual Studio .NETThe simplest way to get our control to look decent in Visual Studio .NET is to use one of the existing Web controls as a base class. This gives us the functionality of that base class as a starting point, to which we can add additional functionality as necessary. I prefer the simplicity of Panel controls, so we will use that as our base class. Using Style SheetsThe Panel control that we are using as our base class already exposes a property called CssClass. However, our control will have nested Panel controls to give the functionality we want, so we will expose properties to set the CssClass properties of the nested controls for additional flexibility. In our control, we will expose the MenuCssClass and EditorCssClass properties for this purpose. Multiple Controls on a PageThe biggest concern with multiple controls on a given page is naming conflicts. The naming conflicts of any controls contained within our control can be overcome by having our control inherit from the INamingContainer interface. This is a marker interface that indicates to the compiler that the names of any contained controls should be concatenated with the name of our control to ensure uniqueness. SimplicitySimplicity is ensured by limiting the functionality of our control. Adding extra features may increase the usability, but features that are added to allow the control to be used only in certain unique circumstances should be avoided. Those types of features should be added by inheriting from the control that we have already created to form a specialized control, not by trying to add them into the base control. The class handles the common needs. Any niche concerns can be handled by extending the class through inheritance or aggregation. Control ImplementationNow that we know the details of how an editor can be implemented on the client side in HTML, and we've defined the characteristics we want in the server control, it is time to get into the details of implementing the actual control. Public InterfaceThe first thing you should do is to define the public interface. The public interface consists of the public methods, properties, and events that the end user of the control will see. Because we are trying to keep this control simple, our public interface will also be simple. Table 19.2 shows the details of the public interface.
For this control, I wrote some private helper methods to reduce redundancy in the coding. These helper methods are shown in Table 19.3. The actual work of creating the control is done in the overridden CreateChildControls() method. To reduce complexity, this method calls a number of other private methods that each creates a portion of the control. Table 19.4 shows the methods that are used to create the control. The final methods in this control are the event handlers for the Update and Cancel buttons. These methods are shown in Table 19.5. In addition to event handlers, our control exposes two events to be used by pages or controls that host our control. Table 19.6 shows the events that are exposed by our control. The most difficult problem while writing this control was how to get the edited HTML back to the server. The HTML tag that is used as the HTML editor is a <div> tag. Because a <div> tag is a display type tag and not an input type tag, the data in the <div> tag is not automatically sent back to the server. I overcame this problem by placing a hidden input tag on the page, and copying the data from the innerHtml property of the <div> tag to the value property of the hidden input tag. This created a second problem because now I needed to have the Update button run some client-side script and do a post back to the server. The solution to this second problem was to use a CustomValidator control. If a validator control is not attached to a particular input control, it is called when the form is submitted. The CustomValidator was set to call the client-side script necessary to copy the edited HTML into the hidden input tag.
Listing 19.2 is the complete source of the custom control implementing our client-side HTML editor. Listing 19.2 HTML Editor Server Control HTML_Editor.VBImports System.ComponentModel Imports System.Web.UI Imports System.Web.UI.WebControls Imports System.Web.UI.HtmlControls <DefaultProperty("Text"), ToolboxData _ ("<{0}:HTML_EditorVB runat=server></{0}:HTML_EditorVB>")> _ Public Class HTML_EditorVB Inherits System.Web.UI.WebControls.Panel Implements INamingContainer Event Update(ByVal sender As Object, ByVal e As EventArgs) Event Cancel(ByVal sender As Object, ByVal e As EventArgs) Private m_objMenu As Panel Private m_objEditor As Panel Private m_objMenu2 As Panel Private m_objHidden As HtmlInputHidden ' Properties <Bindable(True), Category("Appearance"), DefaultValue("")> _ Property [Text]() As String Get If IsNothing(ViewState.Item("Text")) Then ViewState.Item("Text") = "" End If Return ViewState.Item("Text") End Get Set(ByVal Value As String) ViewState.Item("Text") = Value End Set End Property <Bindable(True), Category("Appearance"), DefaultValue("")> _ Property [MenuCssClass]() As String Get If IsNothing(ViewState.Item("MenuCssClass")) Then ViewState.Item("MenuCssClass") = "" End If Return ViewState.Item("MenuCssClass") End Get Set(ByVal Value As String) ViewState.Item("MenuCssClass") = Value End Set End Property <Bindable(True), Category("Appearance"), DefaultValue("")> _ Property [EditorCssClass]() As String Get If IsNothing(ViewState.Item("EditorCssClass")) Then ViewState.Item("EditorCssClass") = "" End If Return ViewState.Item("EditorCssClass") End Get Set(ByVal Value As String) ViewState.Item("EditorCssClass") = Value End Set End Property ' Methods Protected Overrides Sub CreateChildControls() CreateScripts() CreateMenu() CreateEditBody() CreateMenu2() End Sub Private Sub CreateScripts() Dim strScripts As String strScripts = vbCrLf & "<script>" & vbCrLf & _ "function HTMLListCommand(editor,command,list)" & _ vbCrLf & "{" & vbCrLf & _ "editor.focus();" & vbCrLf & _ "editor.document.execCommand(command,false," & _ "list.opetions(list.selectedIndex).value);" & _ vbCrLf & "list.selectedIndex=0;" & vbCrLf & "}" & _ vbCrLf & "function HTMLBtnCommand(editor,command)" _ & vbCrLf & "{" & vbCrLf & "editor.focus();" & _ vbCrLf & _ "editor.document.execCommand(command,false,null);" _ & vbCrLf & "}" & vbCrLf & "</script>" & vbCrLf Controls.Add(CreateLiteral(strScripts)) End Sub Private Sub CreateMenu() Dim arrayNames() As String Dim arrayValues() As String Dim strEditor As String Dim strHandler As String m_objMenu = CreatePanel("Menu", MenuCssClass) Controls.Add(m_objMenu) strEditor = UniqueID.Replace(":", "_") & "_Editor" With m_objMenu.Controls 'Paragraph Settings arrayNames = New String() _ { _ "Paragraph","Normal","Heading 1","Heading 2", _ "Heading 3","Heading 4","Heading 5", _ "Heading 6","Directory List","Pre-Formatted", _ "Address" _ } arrayValues = New String() _ { _ "", "Normal", "<h1>", "<h2>", "<h3>", "<h4>", _ "<h5>", "<h6>", "<dir>", "<ore>", _ "<address>" _ } strHandler = "HTMLListCommand(" & strEditor & _ ",'FormatBlock',this)" .Add(CreateList("ParagraphList", arrayNames, _ arrayValues, strHandler)) ' Font Names arrayNames = New String() _ { _ "Font","Arial","Arial Black","Comic Sans MS", _ "Courier New", "Georgia", "Impact", _ "Lucida Console", "Palatino Linotype", _ "Trebuchet MS", "Verdana" _ } strHandler = "HTMLListCommand(" & strEditor & _ ",'FontName',this)" .Add(CreateList("FontList", arrayNames, arrayNames, _ strHandler)) ' Font Size arrayNames = New String() {"Size", "1", "2", "3", _ "4", "5", "6", "7"} strHandler = "HTMLListCommand(" & strEditor & _ ",'FontSize',this)" .Add(CreateList("FontSizeList", arrayNames, _ arrayNames, strHandler)) ' Fore Color arrayNames = New String() {"Color", "Black", _ "Blue", "Green", "Orange", "Red", "White", _ "Yellow"} strHandler = "HTMLListCommand(" & strEditor & _ ",'ForeColor',this)" .Add(CreateList("ColorList", arrayNames, _ arrayNames, strHandler)) ' Bold strHandler = "HTMLBtnCommand(" & strEditor & _ ",'Bold')" .Add(CreateImageButton("Bold", _ "/HTML_Editor/images/bold.gif", "Bold", _ strHandler)) ' Italic strHandler = "HTMLBtnCommand(" & strEditor & _ ",'Italic')" .Add(CreateImageButton("Italic", _ "/HTML_Editor/images/italic.gif", "Italic", _ strHandler)) ' Justify Left strHandler = "HTMLBtnCommand(" & strEditor & _ ",'JustifyLeft')" .Add(CreateImageButton("JustifyLeft", _ "/HTML_Editor/images/justifyleft.gif", _ "Left Justify Text", strHandler)) ' Justify Center strHandler = "HTMLBtnCommand(" & strEditor & _ ",'JustifyCenter')" .Add(CreateImageButton("JusfifyCenter", _ "/HTML_Editor/images/justifycenter.gif", _ "Center Justify Text", strHandler)) ' Justify Right strHandler = "HTMLBtnCommand(" & strEditor & _ ",'JustifyRight')" .Add(CreateImageButton("JusfifyRight", _ "/HTML_Editor/images/justifyright.gif", _ "Right Justify Text", strHandler)) ' Indent strHandler = "HTMLBtnCommand(" & strEditor & _ ",'Indent')" .Add(CreateImageButton("Indent", _ "/HTML_Editor/images/indent.gif", _ "Indent Text", strHandler)) ' Outdent strHandler = "HTMLBtnCommand(" & strEditor & _ ",'Outdent')" .Add(CreateImageButton("Outdent", _ "/HTML_Editor/images/outdent.gif", _ "Outdent Text", strHandler)) ' Cut strHandler = "HTMLBtnCommand(" & strEditor & _ ",'Cut')" .Add(CreateImageButton("Cut", _ "/HTML_Editor/images/cut.gif", "Cut", _ strHandler)) ' Copy strHandler = "HTMLBtnCommand(" & strEditor & _ ",'Copy')" .Add(CreateImageButton("Copy", _ "/HTML_Editor/images/copy.gif", "Copy", _ strHandler)) ' Paste strHandler = "HTMLBtnCommand(" & strEditor & _ ",'Paste')" .Add(CreateImageButton("Paste", _ "/HTML_Editor/images/paste.gif", "Paste", _ strHandler)) ' Undo strHandler = "HTMLBtnCommand(" & strEditor & _ ",'Undo')" .Add(CreateImageButton("Undo", _ "/HTML_Editor/images/undo.gif", "Undo", _ strHandler)) ' Redo strHandler = "HTMLBtnCommand(" & strEditor & _ ",'Redo')" .Add(CreateImageButton("Redo", _ "/HTML_Editor/images/redo.gif", "Redo", _ strHandler)) End With End Sub Private Sub CreateEditBody() m_objEditor = CreatePanel("Editor", EditorCssClass) Controls.Add(m_objEditor) With m_objEditor .Controls.Add(CreateLiteral(Text)) .Attributes.Add("contentEditable", "True") End With End Sub Private Sub CreateMenu2() ' Create Panel for the Update/Cancel Buttons m_objMenu2 = CreatePanel("Menu2", MenuCssClass) Controls.Add(m_objMenu2) 'Create Update Button Dim objUpdate As Button objUpdate = CreateButton("Update", "Update", True) AddHandler objUpdate.Click, AddressOf OnUpdate_Click 'Create Cancel Button Dim objCancel As Button objCancel = CreateButton("Cancel", "Cancel", False) AddHandler objCancel.Click, AddressOf OnCancel_Click 'Create Hidden Field to return edited HTML m_objHidden = New HtmlInputHidden() m_objHidden.ID = "ctlHidden" m_objMenu2.Controls.Add(m_objHidden) 'Client Validation Name Dim strClientValidationName As String strClientValidationName = UniqueID.Replace(":", "_") & _ "_UpdateHtml" 'Create Custom Validator 'Used to trigger the script to copy the 'edited HTML into the hidden control Dim objValidator As New CustomValidator() With objValidator .ClientValidationFunction = strClientValidationName .EnableClientScript = True End With m_objMenu2.Controls.Add(objValidator) 'Create Client Validation Script Dim strHiddenName As String Dim strEditorName As String Dim strScript As String strHiddenName = m_objHidden.UniqueID.Replace(":", "_") strEditorName = m_objEditor.UniqueID.Replace(":", "_") strScript = vbCrLf & "<script>" & vbCrLf & _ "{" & vbCrLf & _ "args.IsValid=true;" & vbCrLf & _ "document.all['" & strHiddenName & _ "'].value=document.all['" & strEditorName & _ "'].innerHTML;" & vbCrLf & _ "}" & vbCrLf & "</script>" & vbCrLf m_objMenu2.Controls.Add(CreateLiteral(strScript)) End Sub ' Helper Functions Private Function CreateLiteral(ByVal strText As String) _ As LiteralControl Return New LiteralControl(strText) End Function Private Function CreatePanel(ByVal strID As String, _ ByVal strCssClass As String) As Panel Dim objPanel As New Panel() With objPanel .ID = strID .CssClass = strCssClass End With Return objPanel End Function Private Function CreateButton(ByVal strID As String, ByVal _ strText As String, ByVal bCausesValidation As Boolean) _ As Button Dim objButton As New Button() With objButton .ID = strID .Text = strText .CausesValidation = bCausesValidation End With Return objButton End Function Private Function CreateImageButton(ByVal strID As String, _ ByVal strImageUrl As String, ByVal strToolTip As String _ , ByVal strOnClick As String) As ImageButton Dim objImageButton As New ImageButton() With objImageButton .ID = strID .ImageUrl = strImageUrl .ToolTip = strToolTip .Attributes.Add("OnClick", "strOnClick") .CausesValidation = False End With Return objImageButton End Function Private Function CreateList(ByVal strID As String, ByVal _ arrayNames() As String, ByVal arrayValues() As String, _ ByVal strOnChange As String) As DropDownList Dim objList As New DropDownList() Dim objItem As ListItem Dim i As Int32 If arrayNames.Length <> arrayValues.Length Then Throw New Exception( _ "arrayNames and arrayValues must be the same length") End If With objList .ID = strID .Attributes.Add("OnChange", strOnChange) For i = 0 To arrayNames.Length - 1 objItem = New ListItem() objItem.Text = arrayNames.GetValue(i) objItem.Value = arrayValues.GetValue(i) .Items.Add(objItem) Next .Items(0).Selected = True End With Return objList End Function ' Event Handlers Private Sub OnUpdate_Click(ByVal sender As Object, ByVal e _ As EventArgs) Text = m_objHidden.Value With m_objEditor.Controls .Clear() .Add(CreateLiteral(Text)) End With RaiseEvent Update(Me, e) End Sub Private Sub OnCancel_Click(ByVal sender As Object, ByVal e _ As EventArgs) RaiseEvent Cancel(Me, e) End Sub End Class To actually use the control, you need to place the images for the image buttons in the /HTML_Editor/images folder of the Web server. These images can be downloaded from www.ASPNET-Solutions.com. |