ControlFreak
LANGUAGES: VB.NET | HTML
ASP.NET VERSIONS: 1.0 | 1.1
Bar Graphs to Go
Creating a Custom Graph Control from Scratch
By Steve C. Orr
In the inaugural ControlFreak column 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
"<{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(" ' 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. 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. 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 +
"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 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 "color:yellow;background-color:gray;width:10%;">2004 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. 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: 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: The bar
column is rendered similarly to the text column. It begins with a full
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. 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 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: 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. Steve C.
Orr is an MCSD and
a Microsoft MVP in ASP.NET. He's an independent consultant who develops
effective software solutions for many leading companies in the Seattle area.
When he's not busy designing software systems or writing about it, he can often
be found loitering at local user groups and habitually lurking in the ASP.NET
newsgroup. Find out more about him at http://Steve.Orr.net
or e-mail him at mailto:[email protected].
")
Figure 2: A reasonably attractive bar graph
can spruce up any Web site, and the code doesn't have to be complex. . The WriteBeginTag line that follows it leaves off the ending bracket of
the tag (i.e. ) 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 tag is output before the bar column is rendered.
tag. Inside this table cell a
Figure 7: An HTML table is used to precisely
place each element of the bar graph.
Figure 8: The final version of the BarGraph control is flexible enough to
allow information to be attractively displayed in a variety of ways.