A downloadable file for this article is available
here.
The sample downloadable solution (zip) is entirely developed using Visual Studio.NET 2003 Enterprise Architect on Windows Server 2003 Standard Edition. But, I am confident that it would work with other versions of Windows (which support .NET 1.1) as well.
Introduction
Everyone knows that ASP.NET already contains a flexible Table control to design round cornered boxes with ease. But, what if we wanted to convert that “table” to a button, which supports “postback” and other stuff? That forces us to examine the development of a new “dynamically expandable round cornered button.”
In my sense, “dynamically expandable” means the button has to expand (either vertically or horizontally) based on the content we provide to the button. It should be supported even at design-time. “Round cornered” means the button should accept different graphic images at all sides and corners to make it look like it is “round cornered.” And finally, it should support “posback”, “viewstate” and other stuff.
In my previous articles, I already introduced how to design and implement our own Custom Server Control from the scratch. If you are quite new to the concept of Custom Controls in ASP.NET, I suggest you to go through my previous articles before going through this article. This will also be similar to my “round cornered box control” article (within the same series) in several aspects, but with some modifications.
Even though I could use System.Web.UI.Control as the base class for my button control, I selected System.Web.UI.WebControls.WebControl as the base class, just because of the convenience and ease. Apart from all of these, we will also look into “<EditorAttribute>” available in ASP.NET.
System.Web.UI.Control class has only few rendering methods which could be overridden. This gives us less flexibility in developing the custom control, when we compare with the rendering methods available in System.Web.UI.WebControls.WebControl class. Of course, some of the most important properties (like width, height, font, and so on) of the WebControl class get inherited to our control.
In this scenario, I even exposed nine properties to fix border (rounded) images for our control. Before talking too much about the control, let us go through the implementation first. We will create a Visual Studio.NET 2003 solution throughout this article with more than one project.
Exposing properties for every side (including corners) of button
As you know, every side of the box needs to be provided with some image (even the corners). The following are the properties which handle all of those images (paths of those images).
Property ImageTopLeftURL() As String
Property ImageTopMiddleURL() As String
Property ImageTopRightURL() As String
Property ImageMiddleLeftURL() As String
Property ImageMiddleMiddleURL() As String
Property ImageMiddleRightURL() As String
Property ImageBottomLeftURL() As String
Property ImageBottomMiddleURL() As String
Property ImageBottomRightURL() As String
I hope all of the above are simple to read and understand. I declared each of those properties with an attribute at the top as follows:
<EditorAttribute(GetType(System.Web.UI.Design.UrlEditor), GetType
(System.Drawing.Design.UITypeEditor))>
The above is the most exciting statement used with all the previous properties. The “EditorAttribute” is mainly helpful for the Visual Studio.NET designer to facilitate an editor for the value being provided to that property (similar to the “Font” property).
In this case, we attach a dialog box (Visual Studio.NET supported open dialog) for the property, to facilitate the developer in selecting a respective image from folders within the web application hierarchy. If we don’t specify the above attribute, the developer needs to remember and type the entire path of the image within the property window of Visual Studio.NET designer, as opposed to simply selecting the image file.
Starting with Table
The default “Render” method (when not overridden by us) usually calls the “RenderBeginTag” before calling any other methods. Let us examine the “RenderBeginTag” method in my control step by step:
MyBase.AddAttributesToRender (writer)
writer.AddAttribute(HtmlTextWriterAttribute.Border,
"0px")
writer.AddAttribute(HtmlTextWriterAttribute.Cellpadding,
"0px")
writer.AddAttribute(HtmlTextWriterAttribute.Cellspacing,
"0px")
writer.AddStyleAttribute("text-Align", "center")
writer.AddStyleAttribute("vertical-Align", "middle")
writer.AddAttribute(HtmlTextWriterAttribute.Onclick,
Page.GetPostBackEventReference(Me, String.Empty))
writer.AddStyleAttribute("cursor", "hand")
writer.RenderBeginTag(HtmlTextWriterTag.Table)
Within the above method, I am trying to call the “MyBase.AddAttributesToRender(Writer)” statement to add any design-time attributes to my control. This is a must when we use the control with the Visual Studio.NET designer. You can also override with any other attributes you want (as shown in my previous articles of the same series). From all the above statements, the conclusion would be something like this: I am trying to render a “<TABLE>” tag with some attributes. Note that the tags have not yet been closed!
One of the most important statements to focus on from the above is:
writer.AddAttribute(HtmlTextWriterAttribute.Onclick,
Page.GetPostBackEventReference(Me, String.Empty))
That is the main trick I played to convert the “table” to “button.” Once the above statement is executed, it now supports a post back when clicked!
Starting simply with “<TABLE>” is not enough. We need to frame a 3x3 set of cells within the table. I call the first row of cells the header, second row of cells the body (or content) and third row the footer for better understanding.
In our next section, we shall see how to implement the header.
Creating the header
The header part mainly contains three cells (top left, top middle, top right). We need to create a new row with three cells and fill these cells with their respective images. The following is the next code fragment (continuation from the above code fragment within the same “RenderBeginTag” method).
writer.RenderBeginTag(HtmlTextWriterTag.Tr)
'top left
writer.AddAttribute(HtmlTextWriterAttribute.Width, "0px")
writer.AddAttribute(HtmlTextWriterAttribute.Height,
"0px")
writer.RenderBeginTag(HtmlTextWriterTag.Td)
If Me.ImageTopLeftURL.Trim.Length > 0 Then
writer.AddAttribute(HtmlTextWriterAttribute.Src,
Me.ImageTopLeftURL.Trim)
writer.AddAttribute(HtmlTextWriterAttribute.Border,
"0px")
writer.RenderBeginTag(HtmlTextWriterTag.Img)
writer.RenderEndTag()
End If
writer.RenderEndTag() 'td
The above code fragment starts a single row (first row in the table) and creates the first cell (top left cell). Once the image specification (path of the image) for the cell is provided, I embed it into the cell using the “<img>” tag. By default, I fixed the width to “0px” allowing it to fit automatically based on the image size.
'top middle
writer.AddAttribute(HtmlTextWriterAttribute.Width,
"100%")
writer.AddAttribute(HtmlTextWriterAttribute.Height,
"0px")
If Me.ImageTopMiddleURL.Trim.Length > 0 Then
writer.AddStyleAttribute
(HtmlTextWriterStyle.BackgroundImage, "url(" &
Me.ImageTopMiddleURL.Trim & ")")
writer.AddStyleAttribute("background-repeat",
"repeat-x")
End If
writer.RenderBeginTag(HtmlTextWriterTag.Td)
writer.RenderEndTag() 'td
As the middle cell may expand according to the content, I set it up so that the image (for the top middle cell) would be set as the background image. I also added a style attribute which makes it repeat when expanded. And finally, the following code fragment is for the last cell (top right).
'top right
writer.AddAttribute(HtmlTextWriterAttribute.Width, "0px")
writer.AddAttribute(HtmlTextWriterAttribute.Height,
"0px")
writer.RenderBeginTag(HtmlTextWriterTag.Td)
If Me.ImageTopRightURL.Trim.Length > 0 Then
writer.AddAttribute(HtmlTextWriterAttribute.Src,
Me.ImageTopRightURL.Trim)
writer.AddAttribute(HtmlTextWriterAttribute.Border,
"0px")
writer.RenderBeginTag(HtmlTextWriterTag.Img)
writer.RenderEndTag()
End If
writer.RenderEndTag() 'td
writer.RenderEndTag() 'tr
The above is very similar to the first cell (top left) and I don’t think that I need to explain it much. But the important issue is that I closed the header row with the last statement.
Creating the body (content)
Creating the body would also be very similar to the header. But, we need to play a trick to allow the developer to write his own message during the rendering of the control. This is the only complex issue to handle in our control.
Let us first add the first cell of the row (Middle left) using the following code fragment (a continuation from the above code fragment within the same “RenderBeginTag” method).
writer.RenderBeginTag(HtmlTextWriterTag.Tr)
'middle left
writer.AddAttribute(HtmlTextWriterAttribute.Width, "0px")
writer.AddAttribute(HtmlTextWriterAttribute.Height,
"0px")
If Me.ImageMiddleLeftURL.Trim.Length > 0 Then
writer.AddStyleAttribute(HtmlTextWriterStyle.BackgroundImage, "url(" &
Me.ImageMiddleLeftURL.Trim & ")")
writer.AddStyleAttribute("background-repeat",
"repeat-y")
End If
writer.RenderBeginTag(HtmlTextWriterTag.Td)
writer.RenderEndTag() 'td
The above completes the creation of the first cell. Now we create the second cell without closing it. I just open the middle cell with “<td>” tag and don’t close it at all. This allows the developer to write his own content according to his needs. Then who will close the cell? The next section will explain that.
'middle middle (center)
writer.AddAttribute(HtmlTextWriterAttribute.Width,
"100%")
writer.AddAttribute(HtmlTextWriterAttribute.Height,
"100%")
If Me.ImageMiddleMiddleURL.Trim.Length > 0 Then
writer.AddStyleAttribute(HtmlTextWriterStyle.BackgroundImage, "url(" &
Me.ImageMiddleMiddleURL.Trim & ")")
End If
writer.RenderBeginTag(HtmlTextWriterTag.Td)
You can look at the last statement. It only opens “<TD>” and does not close it yet.
Closing the body (content) and creating the footer
In the previous section, we already opened the center cell. But we didn’t close yet. In this section we are going to close it.
writer.RenderEndTag() 'td
'middle right
writer.AddAttribute(HtmlTextWriterAttribute.Width, "0px")
writer.AddAttribute(HtmlTextWriterAttribute.Height,
"0px")
If Me.ImageMiddleRightURL.Trim.Length > 0 Then
writer.AddStyleAttribute(HtmlTextWriterStyle.BackgroundImage, "url(" &
Me.ImageMiddleRightURL.Trim & ")")
writer.AddStyleAttribute("background-repeat",
"repeat-y")
End If
writer.RenderBeginTag(HtmlTextWriterTag.Td)
writer.RenderEndTag() 'td
writer.RenderEndTag() 'tr
writer.RenderEndTag() 'table
The above is quite straightforward for closing the previously opened “<TD>” tag. But the above code is not part of the “RenderBeginTag” method. Instead, it is from the “RenderEndTag” method. We further proceed to create the footer row by adding the same type of code (with very few modifications) available in the “Creating the Header” section to the above code fragment. As the coding is quite similar, you can get it from the downloadable.
Everything up until now is quite okay. How did I implement the trick to get the “developer specified content” into my control? This is from the following code fragment.
Protected Overrides Sub RenderContents(ByVal writer As
System.Web.UI.HtmlTextWriter)
writer.Write(Text)
End Sub
Even though it is only three lines, it should be considered the heart of the control. In other words, when the control is getting rendered, it first executes the “RenderBeginTag” method followed by the “RenderContents” method and finally “RenderEndTag”. For a detailed explanation of “rendering” issues, I suggest you to refer my previous articles of the same series.
How did I handle the postbacks?
Another extension to the same control is the following code fragment. Try obrserving it.
Protected Overrides Sub Render(ByVal writer As
System.Web.UI.HtmlTextWriter)
If Not Page Is Nothing Then
Page.VerifyRenderingInServerForm(Me)
End If
MyBase.Render(writer)
End Sub
The above implementation of the “Render” method ensures that the placement of your control should be within the “<FORM>” tag itself. It will automatically raise an error if not placed within the “<FORM>” tag. You should also observe the second statement, “MyBase.Render(writer)”. It asks to proceed with rendering as usual without considering any other issue.
The next question would be, how did I implement the “OnClick” event within my control? The following statements at various locations made it possible:
Public Event Click As EventHandler
Public Sub RaisePostBackEvent(ByVal eventArgument As String)
Implements System.Web.UI.IPostBackEventHandler.RaisePostBackEvent
OnClick(EventArgs.Empty)
End Sub
Protected Overridable Sub OnClick(ByVal e As EventArgs)
RaiseEvent Click(Me, e)
End Sub
It is the concept of both eventing and delegating. The above statements made it possible to implement ‘postback’ing together with delegating. I suggest you go through OOPS in .NET, if you are not familiar with events and delegates.
Even though I started emitting exclusive HTML using “writer” methods, you can also do the same by extending your control from an already existing “table” web control. If you directly extend it from a “table” web control, you can work with an almost .NET based ASP.NET framework model to add table rows and cells, which looks convenient and even readable. But then we need to understand a bit about the “rendering” issues of child controls within the parent control.
I leave it to the developers to further enhance the same control. The areas of improving the same control would include eventing, JavaScript for mouse hovers, data-binding, and so forth. Good luck.
Any comments, suggestions, bugs, errors, feedback etc. are highly appreciated at jag_chat@yahoo.com.