A Tale of a .NET Component (Part II) ByPeter Aitken 2007-02-22
Article Rating: / 0
Rate This Article:
Add This Article To:
In the second part of this series, Peter Aitken continues describing what exactly goes on as he develops a .NET component. Be sure to read this to get some first-hand insight from a professional .NET developer!
Most programming articles focus on one specific aspect of technology. As useful as that approach is, it unavoidably leaves out some aspects of the development process. By looking at every detail of a development project in detail, as this series does, you may gain insights into the process that are not available otherwise.
The component under development is intended to display a basic XY chart in a Windows forms application. The previous article covered how I decided to use a .NET Framework UserControl as the basis for this component and detailed the design specifications—what precisely the control needed to do. I also took you through the coding for the display of the chart's axes and tick marks.
ADVERTISEMENT
Data Representation
The decision as to how the data will be represented is an important one, and a bad decision at this stage can have serious consequences down the road. It's necessary to consider the entire life-cycle of the data, from the time it is assigned to the component by whatever program the component is running in, to the time it appears on the screen as part of an XY chart.
Starting at the beginning, by definition an XY chart plots points each of which consists of an X value and a Y value. Related points are grouped together in a trace or, as it is called in Excel, a data series. Each trace is plotted as a line connecting the trace's individual data points. Some XY charts have the option of displaying symbols at each data point, either with or without the connecting line, but as was covered in the first article, this component needs to plot lines only.
The first choice was easy—how to represent each point. The Framework provides the PointF structure that is specifically designed to hold the X and Y values for a point. Each is stored as a Single value, which will be fine—the extra accuracy of type Double is not needed.
Another advantage has to do with the eventual rendering of each trace. The GraphicsPath class represents a series of connected lines, and can be drawn on the screen with a single line of code. If the points of the line are represented by an array of PointF structures, defining the GraphicsPath is also a one-line operation.
We can see that the component needs to store multiple traces of data, with each trace containing 2 or more points (as PointF structures). My first thought was to use a 2-dimensional array. Given Visual Basic's very flexible arrays, this approach seemed to have promise. One dimension for the traces, and one for the points within each trace. Such an array would be declared something like this:
Dim DataArray(NumberOfTraces - 1, NumberOfPoints - 1) As PointF
Why subtract 1? remember, Visual Basic arrays are 0-based, and to get a dimension with N elements you declare it as N-1 to create elements 0 through N-1).
I had actually started implementing this method of data storage when I realized that it posed a serious problem. There was no guarantee that each trace would contain the same number of points, but the second dimension of the array would have to accommodate the largest trace. This would require some ad hoc method for keeping track of the number of points in each trace, a complication I wanted to avoid. The array approach would also require the use of dynamic arrays and lots of calls to the ReDim statement as data were added to the array, a factor that might compromise performance.
After a little chin-scratching, I hit upon the idea of using collections, one of the .NET Framework's most powerful tools. The XY chart component would have a collection of traces, and within each trace the data would be held in an array. Because each trace's data would have its own array, rather than being part of a larger array that holds all traces, the different trace length problem would disappear.
The Trace Class
Following the principles of object-oriented programming, I implemented a Trace class. Encapsulating a trace in a class offered several advantages, including the ability to define properties for trace-related data such as a trace name, a trace color, and a "visible" property that would determine if the trace was drawn on the screen. To add this new class to your project, follow these steps:
Select Add Class from the Visual Studio Project menu. The Add New Item dialog box will open with the Class template already selected and the default name Class1.vb entered in the Name field.
Change the name to XYTrace.
Click the Add button.
Visual Studio will add XYTrace.vb to the Solution Explorer and open it for editing. At this point only the first and last lines will be present:
Public Class XYTrace
End Class
I initially used the class name Trace but discovered that the .NET Framework already has a class of that name. With the use of namespaces it is possible to have multiple classes of the same name, but for the sake of simplicity I decided to change my class name to XYTrace.
The first step is to add the declarations of the four variables that will hold the property values: color, name, visible, and the array of points. Add the code shown in Listing 1 to the XYTrace class.
Private pTraceColor As System.Drawing.Color
Private pTraceName As String
Private pTraceVisible As Boolean
Private pXYValues() As System.Drawing.PointF
Please note two things about these declarations. First, each variable is declared using the Private modifier. This males these variables inaccessible outside of the class. They will be accessed only by leans of the corresponding property procedures, something I'll get to in a minute. Second, each variable name is the name of the property preceded by a "p". This is my own convention to distinguish class variables that hold properties from other class variables. It also makes it clear which property variable is associated with each property It's not required, of course, but I believe that this is one of this little things, such as making constant names all uppercase, that make your source code a lot easier to read.
Next, you need to add the property procedures that provide the outside world—that is, the program in which the control is embedded—to access these properties. A property procedure is called when the containing program references the property. There are two parts to a read/write property: a Get part that is called when the program reads the property value, and a Set part that is called when the program sets the property value. Here's the property procedure for the TraceName property:
Property TraceName() As String
Get
Return pTraceName
End Get
Set(ByVal value As String)
pTraceName = value
End Set
End Property
When Get is called, all it does is return the value of the corresponding property variable. Likewise, when Set is called, it is passed the new value and assigns it to the property variable. Property procedures can be more complex, as well as being read-only or write only, and we'll see some examples later. The name of the property procedure, TraceName in this case, is the property name that will be visible when the control is used.
Visual Studio provides a shortcut for entering property procedures. Type the keyword Property followed by the name and the type, for example type:
Property TraceName as string
When you press Enter, Visual Studio fleshes out the procedure like this:
Property TraceName() As String
Get
End Get
Set(ByVal value As String)
End Set
End Property
All you need to do is fill in the remaining code. You can go ahead and add these four property procedures, shown in Listing 2, to the class. Place them below the declarations of the variables that you entered earlier.
Property TraceValues() As PointF()
Get
Return pXYValues
End Get
Set(ByVal value As PointF())
pXYValues = value
End Set
End Property
Property TraceVisible() As Boolean
Get
Return pTraceVisible
End Get
Set(ByVal value As Boolean)
pTraceVisible = value
End Set
End Property
Property TraceName() As String
Get
Return pTraceName
End Get
Set(ByVal value As String)
pTraceName = value
End Set
End Property
Property TraceColor() As System.Drawing.Color
Get
Return pTraceColor
End Get
Set(ByVal value As System.Drawing.Color)
pTraceColor = value
End Set
End Property
There's more to add to the XYTrace class, but that will come later. The next step is to implement traces in the XYChart class.
Implementing the Traces Collection
I have already mentioned that I would use a collection to hold the traces that the XY chart is plotting. The .NET Framework's Collection class is a very useful general-purpose container. It's like an ever-expandable bag, with no limit to the type or number of things that can be put in it. There was one problem—collections are untyped, which means that the collection itself puts no restrictions on the type of objects you can place in it. If you wanted a strongly typed collection, you had to write a wrapper for the Collection class that enforced the type restriction.
What do I mean by typing? Most Visual Basic variables are typed, meaning that they are assigned a specific data type when they are declared. For example,
Dim Name As String
creates a variable with the type String. The variable cannot hold a number, an object reference, or a date, but only a string. This strong typing is an important aspect of writing robust programs, because a common cause of bugs and errors is putting the wrong kind of data in the wrong place. Ideally, I did not want the chart's traces implemented as an untyped collection.
Fortunately there was a solution at hand. The .NET Framework does support strongly typed collections but the syntax is completely unintuitive. It is as follows:
Dim CollectionName As New List(Of TypeName)
For our present purposes, you should add the following code to the XYChart. source code, in the same section with the other properties:
Public Traces As New List(Of XYTrace)
The use of the Public modifier makes this property variable available outside the class without the use of a property procedure. I felt that doing without a property procedure in this case would simplify programming and should not lead to any problems.
So, how will this work in practice? You'll see this in more detail in a later article, but there's no harm in peeking ahead. Suppose the programmer has placed an instance of the XYChart control in her program, and it is named XYChart1. Assume also that the data to be plotted have already been placed in a PointF array named Data. Then, the code will create a new instance of the XYTrace class:
Dim t As New XYTrace
Next, set the XYTrace properties:
t.TraceValues = Data
t.TraceName = "Widget Sales"
t.TraceColor = Color.Red
Then, add the trace to the XYChart control's Traces collection:
XYChart1.Traces.Add(t)
Once all of the traces have been added, they are ready to plot. We'll get to the actual plotting code in a later article.
What's Next?
The structure of the XY chart component is starting to take shape. We have code to draw the axes and tick marks, and we have figured out how the data to be plotted will be represented. We have also done a little looking ahead to ensure that these early design steps will not complicate the programming that lies down the road.
The next article will continue with the development of the XY chart control including display of a legend and axis labels. I will also show you how to test the control without having to create a program to embed it in.