LANGUAGES: VB.NET | HTML
ASP.NET VERSIONS: 1.x
Bar Graphs to Go
Creating a Custom Graph Control from Scratch
In the previous Inherit and Extend article you saw how to inherit from existing controls
and extend them to meet your needs. This is a valuable technique, but sometimes
custom functionality is needed that simply isn't addressed at all by the built-in
.NET Web controls. Charts and graphs are a good example of this. A basic bar graph
is a control that can be used and reused to spruce up most any Web site. And colorful
charts not only please the eye, they are an effective way to communicate important
information or fun facts.
Weighing Your Options
To create graphs so beautiful that they defy words you usually need to use the System.Drawing namespace, be an amazing
artist, and have plenty of development time on your hands. But it doesn't have to
be that complicated. Basic HTML provides everything that's needed to display simple,
attractive bar graphs that will connect with users. With that in mind, the System.Drawing
namespace will be left for a future article.
Now it's time to determine the best way to go about generating the HTML that's needed
for the bar graph. As I mentioned, inheriting from another control isn't an option,
because there's no single suitable control to extend. However, a set of label controls
could be up to the task, with each configured to varying widths and background colors
to represent the bars. Figure 1 contains the VB.NET code for a
basic control that uses this technique. The data is hard-coded to keep things simple
for now; by the end of the article the control will be much more practical.
Imports
System.ComponentModel
Imports
System.Web.UI
Imports
System.Web.UI.WebControls
<DefaultProperty("Text"),
ToolboxData("<{0}:BarGraphA " _
+ "runat=server></{0}:BarGraphA>")>
_
Public
Class BarGraphA
Inherits System.Web.UI.Control
Protected
Overrides Sub Render(ByVal
output _
As HtmlTextWriter)
Dim lbl As New WebControls.Label()
Dim lf As New LiteralControl("<br>")
'Output graph title
lbl.Text = "Projected Output:"
lbl.RenderControl(output)
lf.RenderControl(output)
'Output first bar
lbl.BackColor = Drawing.Color.Gray
lbl.ForeColor = Drawing.Color.Yellow
lbl.Width = New
Unit("10%")
lbl.Text = "2004"
lbl.RenderControl(output)
lf.RenderControl(output)
'Output second bar
lbl = New WebControls.Label()
lbl.BackColor = Drawing.Color.Black
lbl.ForeColor = Drawing.Color.Aqua
lbl.Width = New
Unit("40%")
lbl.Text = "2005"
lbl.RenderControl(output)
lf.RenderControl(output)
'Output third bar
lbl = New WebControls.Label()
lbl.BackColor = Drawing.Color.Blue
lbl.ForeColor = Drawing.Color.Black
lbl.Width = New
Unit("80%")
lbl.Text = "2006"
lbl.RenderControl(output)
lf.RenderControl(output)
End
Sub
End
Class
Figure 1: Although not the most efficient code
ever written, this shows that a bar graph can be created with surprisingly little
effort.
The example in Figure 1 starts
by importing some namespaces that will be used frequently throughout the code. Then
a couple of basic attributes are set so the control will display properly when placed
in the toolbox. This class inherits from System.Web.UI.Control, which supplies
the basic functionality needed for any simple control, including the Render
method that is overridden to output the bar graph. When developing controls you'll
be more likely to inherit from an existing control or from System.Web.UI.WebControls.WebControl
(which, in turn, inherits from System.Web.UI.Control.) Although System.Web.UI.WebControls.WebControl
provides more default functionality than System.Web.UI.Control, the majority
of those properties aren't needed for this bar graph control, and therefore would
only detract from the focus of this article.
The example code in Figure 1 then goes on to declare
an object variable that will contain a label, which will be reused for the display
of each bar. It also declares a LiteralControl that will output a line break
between each bar. The label control is first used to propel the title of the graph
into the output stream, with a line break following.
Following the "output graph title" code block,
a block of code is repeated three times to display three bars. (This isn't the epitome
of efficiency, but it will be cleaned up later.) The code block starts by putting
a new label control into the lbl variable, and setting it up with some nice
looking colors. The width of the control is then set to define how large the bar
will be, and some text is written onto the bar. Finally, the control is rendered,
thus generating the HTML for the bar. A line break then follows so two bars don't
end up on the same horizontal line.
If you compile this code into a new Web control
library, then add it to your toolbox and drop it onto a Web form in a Web application,
you'll see that it displays the nice little bar graph shown in Figure 2.

Figure 2: A reasonably attractive bar graph
can spruce up any Web site, and the code doesn't have to be complex.
The code in Figure 1 is a basic
example of a composite control. Composite controls delegate work to multiple underlying
Web controls. This is a practical way to create rich controls quickly, and requires
no knowledge of HTML. However, any expert will tell you that composite controls
tend to be rather bloated and inefficient compared to the alternatives. In this
example, instantiating all those label controls comes with a performance penalty
that could be significant in a high-volume Web site. If only there were a reasonably
simple way to output the same HTML without having to create all those controls!
Well, as it turns out, there is a better way.
(advertisement)
Weight Loss Techniques
HTMLTextWriter is a valuable tool that any control
developer should have in his or her arsenal. This is the most direct and efficient
way to output HTML while still having a few basic niceties, such as support for
down-level browsers. Figure 3 shows code that outputs HTML that
is virtually identical to the example in Figure 1, but uses HTMLTextWriter instead
of Web controls to keep things lubricated.
Imports
System.ComponentModel
Imports
System.Web.UI
<DefaultProperty("Text"),
_
ToolboxData("<{0}:BarGraphB
" _
+ "runat=server></{0}:BarGraphB>")>
_
Public
Class BarGraphB
Inherits Web.UI.Control
Protected
Overrides Sub Render _
(ByVal output
As HtmlTextWriter)
'Output Graph Title
output.AddStyleAttribute("width", "80%")
output.AddAttribute("align", "center")
output.RenderBeginTag("div")
output.Write("Projected Output:")
output.RenderEndTag()
'Output first bar
output.AddStyleAttribute("color", "yellow")
output.AddStyleAttribute("background-color",
"gray")
output.AddStyleAttribute("width", "10%")
output.AddAttribute("title", "10%")
output.RenderBeginTag("div")
output.Write("2004")
output.RenderEndTag()
'Output second bar
output.AddStyleAttribute("color", "aqua")
output.AddStyleAttribute("background-color",
"black")
output.AddStyleAttribute("width", "40%")
output.AddAttribute("title", "40%")
output.RenderBeginTag("div")
output.Write("2005")
output.RenderEndTag()
'Output third bar
output.AddStyleAttribute("color", "black")
output.AddStyleAttribute("background-color",
"blue")
output.AddStyleAttribute("width", "80%")
output.AddAttribute("title", "80%")
output.RenderBeginTag("div")
output.Write("2006")
output.RenderEndTag()
End
Sub
End
Class
Figure 3: Using HTMLTextWriter takes some getting
used to, but the efficiency makes it worth the learning curve.
Notice that the example code in Figure 3 isn't
any longer than the code in Figure 1 , nor is it very complex -
as long as you're comfortable with HTML. This code starts the same way, overriding
the Render method provided by the base Control class from which this
control inherits. Then things start to deviate. This is where HTMLTextWriter (which
is passed as a parameter to the Render
method) starts to get used to its fullest extent.
As before, the title for the bar graph is output
first. Instead of instantiating a label control, however, this time the HTML will
be output directly in the form of a <div> tag. Some basic attributes are set
for this tag, such as a centered alignment. Then the title text is output and rendered
to the output stream.
The next three code blocks, which output the three
bars in the graph, are nearly identical to each other. (Again, this isn't the most
efficient way to handle this. And yes, the data is still hard coded. Hang in there;
this will all be resolved.) Each bar is a <div> tag with foreground and background
colors set. The width of each <div> tag determines the size of the bar. The
title attribute provides a tooltip for each bar. The text in the bar is rendered
after the opening <div> tag, and before the closing <div> tag. The resulting
HTML is output to the browser:
<div title="10%" style="color:yellow;background-color:gray;width:10%;">2004</div>
Again, this results in a bar graph that looks
like that shown in Figure 2.
Now that it's been established that heavy use
of HTMLTextWriter tends to lead to better performance than composite controls, this
will be the focus of the remainder of this article. This is not to say that composite
controls are always bad; they have their merits and will be covered in more detail
in upcoming articles.
(advertisement)
Keeping It Real
It's time to get to work. The code in Figure 3 is fine for a prototype, but it needs
a serious overhaul to become a valuable member of your toolbox. First of all, the
data obviously can't be hard-coded in the control. It needs to accept data from
external sources. The new version of the control will provide a
Bar class that allows you to configure properties for each bar, such as
color, text, value, and tooltip. This will allow a bar in the BarGraph control
to be configured with a simple line of code such as this:
BG1.Bars.Add(New BarGraph.Bar("2004", "red", 10,
"10K"))
The Bar class listed in
Figure 4 is nothing remarkable. It's basically just a container that
holds the properties you'll need for the display of each bar. The control will also
contain a collection class named Bars to hold all the
Bar objects for the current instance of the control. This class is standard
collection code. In ASP.NET version 2 you'll be able to use Generics, which will
eliminate the need for most of this boilerplate code.
Public Class cBar
Public
Sub New()
End
Sub
Public
Sub New(ByVal
Text As String,
_
ByVal BarColor
As String, _
ByVal Value
As Double, _
Optional ByVal tooltip As
String = "")
_text = Text
_barColor = BarColor
_Value = Value
_toolTip = tooltip
End
Sub
Dim _text
As String = String.Empty
Public
Property Text() As
String
Get
Return
_text
End Get
Set(ByVal
Value As String)
_text = Value
End
Set
End
Property
Dim _barColor
As String = "gray"
Public
Property BarColor() As
String
Get
Return
_barColor
End
Get
Set(ByVal
Value As String)
_barColor = Value
End
Set
End
Property
Dim _Value
As Double = 0
Public
Property Value() As
Double
Get
Return
_Value
End
Get
Set(ByVal
Val As Double)
_Value = Val
End
Set
End
Property
Dim _toolTip
As String = String.Empty
Public
Property ToolTip() As
String
Get
Return
_toolTip
End
Get
Set(ByVal
Value As String)
_toolTip = Value
End
Set
End
Property
End Class
Figure 4: The overloaded constructor of the
Bar class allows a Bar object to be instantiated and filled with data
in a single line of code.
The BarGraph
control will itself have a number of public properties: Text,
BorderWidth, CellPadding, and CellSpacing. These properties
will allow adjustments to the overall look of the
BarGraph. The code for all the properties looks pretty much the same. The
Text property will contain the title displayed above the graph:
<Bindable(True),
Category("Appearance")> _
Property Text()
As String
Get
Return _text
End
Get
Set(ByVal
Value As String)
_text = Value
End
Set
End Property
Especially interesting are the
attributes at the top of the code that permit data binding and specify the placement
of this property within the Appearance section of the properties window at design
time.
This new version of the control will be in more direct
command of the layout of the bar graph by using an HTML table to precisely position
each element of the control. One example is that the text for the bar cannot be
inside the bar itself, because this will interfere with the size of small bars,
causing them to elongate to accommodate the width of the text. The solution is to
move the associated text into a separate table cell next to the bar. The
Render event of the control should resemble Figure 5.
Protected Overrides
Sub Render (ByVal
output _
As HtmlTextWriter
)
'output the table that wraps around
the bar graph
If MyBase
.Visible Then
output.WriteBeginTag("table")
output.WriteAttribute("width",
"90%")
output.WriteAttribute("align",
"center")
output.WriteAttribute("border",
_border)
output.WriteAttribute("cellPadding",
_cellPadding)
output.WriteAttribute("cellSpacing",
_cellSpacing)
output.Write(HtmlTextWriter.TagRightChar)
output.WriteFullBeginTag("tr")
output.WriteBeginTag("td")
output.WriteAttribute("colspan",
"2")
output.WriteAttribute("align",
"center")
output.Write(HtmlTextWriter.TagRightChar)
output.Write(_text)
output.WriteEndTag("td")
output.WriteEndTag("tr")
output.WriteLine()
'For cosmetic HTML source display
OutputBars(output) 'each
bar is on 1 table row
output.WriteEndTag("table")
End
If
End
Sub
Figure 5. Elements of the bar graph are now placed within an HTML table for
more precise positioning.
The first thing the code in
Figure 5 does is check to make sure the
Visible property of the BarGraph
control is true. Even though this property hasn't been explicitly defined in the
code of the BarGraph control, it still
exists implicitly in the base Control class from which the BarGraph
class inherits, and can therefore be used.
The bulk of the remaining code in Figure 5 should look
somewhat familiar to you at this point. It outputs the HTML necessary to create
a nice-looking two-column HTML table. The BarGraph properties are used where
appropriate to make sure the title of the graph is displayed at the top, and that
border and cell attributes are set correctly as specified by the consumer of the
control. The individual bars are output from the
OutputBars subroutine defined in Figure 6.
Private
Sub OutputBars(ByVal output
As HtmlTextWriter)
Dim bar
As cBar
For
Each bar In Bars()
'output the text column
output.WriteFullBeginTag("tr")
output.WriteBeginTag("td")
output.WriteAttribute("width", "1%")
output.Write(HtmlTextWriter.TagRightChar)
output.Write(bar.Text & " ")
output.WriteEndTag("td")
'output the bar column
output.WriteFullBeginTag("td")
output.AddStyleAttribute("background-color",
_
bar.BarColor)
output.AddStyleAttribute("width", _
bar.Value / LargestBarValue() *
100 & "%")
output.AddAttribute("title", bar.ToolTip)
output.RenderBeginTag("div")
output.RenderEndTag()
output.WriteEndTag("td")
output.WriteEndTag("tr")
output.WriteLine() 'cosmetic
only
Next
End Sub
Figure 6: Each row of
the HTML table consists of a bar column and an associated text column that describes
the bar.
This subroutine loops through
each bar in the Bars collection, outputting one HTML table row for each.
The first column is filled with a description of the bar that's placed in the second
column. The first column should take up no more room than necessary, so its width
is set to 1%. Of course, HTML dictates that this column will grow larger than 1%
if necessary for display, or it may use word wrapping where applicable to help keep
it small. The line:
output.WriteFullBeginTag("tr")
will output this full begin tag:
<tr>. The WriteBeginTag line that follows it leaves off the ending
bracket of the tag (i.e. <td) so the width attribute may be added to it before
the ending bracket (>) will be displayed on the right side of it. Then the bar
text is output with a hard space after it to make it look nicer. Finally, the ending
<td> tag is output before the bar column is rendered.
The bar column is rendered similarly
to the text column. It begins with a full <td> tag. Inside this table cell
a <div> is created to represent the bar. Some attributes are added to it to
specify the color and tooltip, and the required size is calculated and set as the
width for the <div>. The end tag for the <div> is then output, followed
by the end tags for the table cell and table row. The final WriteLine statement
is there only to make the generated HTML source code look nicer.
The control is now quite functional.
If you drop this compiled control onto a Web form, you can configure it from your
code behind with a few lines of code, like this:
With BarGraphC1
.Text = "Projected revenue from software sales:"
.BorderWidth = 1
.Bars.Add(New BarGraph.cBar("2004",
"gray", 10, "10K"))
.Bars.Add(New BarGraph.cBar("2005",
"black", 25, "25K"))
.Bars.Add(New BarGraph.cBar("2006",
"blue", 50, "50K"))
.Bars.Add(New BarGraph.cBar("2007",
"red", 75, "75K"))
End With
This will display a control
that looks like Figure 7 . A number of
BarGraph controls can be dropped onto a page, and they can all be configured
differently, resulting in a page that might look something like that shown in
Figure 8.

Figure 7: (Above) An HTML
table is used to precisely place each element of the bar graph.

Figure 8: (Above) The
final version of the BarGraph control
is flexible enough to allow information to be attractively displayed in a variety
of ways.
(advertisement)
What about Design Time?
Although the BarGraph control
is looking pretty nice at run time, there isn't really any visible display to speak
of at design time. The main reason is that the control doesn't have any data to
display until run time. This situation can be remedied by supplying some dummy data
for the control at design time by associating the control with a designer class.
The designer in Figure 9 is up to the task.
Friend Class
ControlDesigner
Inherits System.Web.UI.Design.ControlDesigner
'Creates random output for display at
design time
Public
Overrides Function GetDesignTimeHtml()
As String
'Create a bar graph control
Dim BarGraphC1
As New BarGraphC()
Dim
prefix="ST1" ?>Rand
As Byte = 1
'Fill the bar graph with
random data
With BarGraphC1
.Text = "Graph Title"
.Bars.Add(New
BarGraph.cBar())
'displays
a random value between 1 and 12.
Rand
= CByte(Int((12 * Rnd()) + 1))
'displays
the first of 3 bars
.Bars.Add(New
BarGraph.cBar("Row1", _
"red",
Rand
, Rand.ToString))
'same routine
(different color) for bar 2
Rand
= CByte(Int((12 * Rnd()) + 1))
.Bars.Add(New
BarGraph.cBar("Row2", _
"black",
Rand
, Rand.ToString))
'same routine
(different color) for bar 3
Rand
= CByte(Int((12 * Rnd()) + 1))
.Bars.Add(New BarGraph.cBar("Row3", _
"blue",
Rand
, Rand.ToString))
.Bars.Add(New
BarGraph.cBar()) 'blank bar
End
With
'Return the HTML that
is output by the control
Dim sw As IO.StringWriter = New
IO.StringWriter()
Dim tw As HtmlTextWriter = New
HtmlTextWriter(sw)
BarGraphC1.RenderControl(tw)
Return sw.ToString()
End
Function
End Class
Figure 9: This designer class
instantiates a BarGraph control at design time, fills it with random data,
and outputs the resulting HTML to Visual Studio.NET for a pleasant design-time experience.
For this to work, a reference
needs to be added to system.design.dll. The following attribute also needs to be
added to your BarGraph class to ensure the control is linked to the designer:
<Designer(GetType(BarGraph.ControlDesigner))>
You should now have a pretty good
idea of how to create custom controls from scratch, without the overhead involved
with using existing Web controls. With some practice you can master such techniques
and create entire control libraries filled with reusable gold. Good luck!
The sample code in this
article is available for
download.
This article was originally published in
ASP.NET Pro Magazine.