Inheritance is one of the most fascinating features in the Visual Studio.NET languages.
We have discussed this feature in several articles in the past, mainly to explain the basic concepts. Now we are going to take a look at what inheritance can actually do for you, rather than how it works.
Inheritance in general is a fairly simple concept. In past issues of Component Developer Magazine, we have taken a look at the basic ideas behind inheritance. However, we've only seen a few simple examples of how to use this technology in real-life scenarios. Now we are going to take a closer look at how you can use inheritance to speed up development and increase the quality of your business objects as well as your interface components. The examples I'll be using are simple, but they come from real-life applications. However, I removed some additional code to keep the examples as simple as possible.
The basic idea behind the examples is not new to most developers familiar with languages such as Visual Basic. We are going to consider a typical 3-tiered application that uses business objects to talk to a SQL Server database and use the information in a separated interface layer. What's new is the way we are going to construct the individual components. All the objects we will use in our examples will be subclassed from other classes. This enables us to generate individual objects very quickly without having to worry about quality, since the classes we subclass from have already been debugged.
We are not going to build the backend for this example; instead, we will use the Northwind database that ships with SQL Server as an example database. The first step in talking to that database is building the required business objects.
Building an Abstract Business Object
There are two entities we should use in this particular example: Territories and Regions. Both entities exist in the form of tables in the SQL Server Northwind demo database (see Figure 1). For our example, we will create a simple business object that enables us to query data, modify it, verify the modifications, and save the data. If we were to build this type of business object in Visual Basic 6, we would need to write two entirely separate business objects that would perform very similar tasks. The main difference between the two business objects is the field names and the validation rules. Everything else is identical. However, we would end up duplicating the behavior in both (and potentially subsequent) objects. This appears to be a very frustrating process, especially if we find out later that there is a glitch in our logic and we have to plow through all of them to fix that issue.
Using Visual Studio.NET, we're going to take a very different approach: we will create an abstract class that encapsulates the basic business object behavior. We will not use that class directly?that's why we refer to it as an "abstract" class. However, we will subsequently derive (subclass) individual business objects from that class, and configure them to perform useful tasks.
Thanks to the language independent inheritance model used by .NET and the CLR, it doesn't really matter which language we use to implement the business object. Listing 1 shows the C# version, while Listing 2 is the equivalent in Visual Basic.NET.
The code is fairly straightforward. Let's take a look at the GetData() method first, since we need to use this method to retrieve data before we can do anything with it. This method creates a DataSet as well as a SqlConnection and a SqlDataAdapter object to talk to SQL Server and query data into the DataSet. Note that the connection uses hard-coded information to find the SQL Server database. This is done only to keep the example simple. The real-life version of this object uses an external mechanism (such as a registry setting or other application options) to configure the connection string. Also, the error-handling code has been removed from this example (again, to keep it as simple as possible).
The most exciting part of this method is the two lines that query the data and fill the DataSet. We're using a fairly simple SELECT command to query the data (you can easily imagine a more powerful version, if desired). The interesting part of the SELECT statement is provided by two fields that are members of the class: sFields and sTableName. sFields defaults to "*", which means that all fields will be queried. The field for the table name is empty, which would apparently lead to the creation of an invalid SELECT statement. But keep in mind that we don't indend to use this abstract class directly. Instead, we'll create subclasses and provide the missing information there. For now, we'll just assume that the table name will be "Region", "Territories" or any other table name. Therefore, the resulting select statement would be similar to the following:
SELECT * FROM Region
The GetData() method constructs the select statement it will use to fill the DataSet. As you may know, DataSets can contain more than one table. When we fill the DataSet using a particular SQL command (such as the one above), we need to specify the name of the resulting table. We use the sTableName field to do so, to make this whole process very generic. You can see the sTableName being sent to the data adapter Fill() method in this code from the C# version:
oAd.Fill(oDS,this.sTableName);
Note that the C# version of this code defines the GetData() method as virtual (as all the other methods) and the VB.NET version declares it as "overridable." This allows us to override this method dynamically in subclassed business objects to accommodate custom behavior (as we will see below).
Our basic business object also has some functionality to verify data. The form has an ArrayList field named cReqFields. By default, this array list is empty, but it is designed to hold a list of names of fields that cannot be blank. The Verify method uses this list to iterate over all the rows and fields in the DataSet to check whether any of the required fields have been left blank. If a blank field is found, the method returns false.
At this point, this method is rather simple and doesn't provide a lot of information about why the verification failed. However, we could easily provide more information, either through properties or a more sophisticated return value (or perhaps an out-parameter). Also, we could add similar functionality to check for unique field values, if required.
So, now we have a basic business object, which would work in theory. But we can't really use it, because it is missing some information. As mentioned above, we will provide that information in subclasses. So let's go ahead and create a subclassed business object to talk to the "Region" entity. Here's the C# version:
public class Regions : BusinessObject
{
public Regions()
{ this.sTableName = "Region"; }
}
And, the Visual Basic.NET version:
Public Class Regions
Inherits BusinessObject
Public Sub New()
Me.sTableName = "Region"
End Sub
End Class
As you can see, these classes are trivial. All they do is set the sTableName field to define the entity. We do so in the class' constructor. The class will automatically inherit the behavior from its parent class (BusinessObject) and will, therefore, be totally functional.
There is nothing to keep us from building business objects by the hundreds, all inheriting from BusinessObject. Here's the object we are going to use for the Territories (C#):
public class Territories: BusinessObject
{
public Territories ()
{
this.cReqFields.Add("TerritoryID");
this.cReqFields.Add("RegionID");
this.sTableName = "Territories";
this.sFields =
"TerritoryID, TerritoryDescription";
}
}
The code is very similar to the previous example, with the exception that we are limiting the fields that are returned. Also, we are specifying two fields that can't be blank in case someone tries to update the DataSet through this business object. I'm sure you can figure out the Visual Basic.NET code on your own.
At this point, you're probably thinking one of two thoughts: 1) "Well, big deal! But in real-life scenarios, things are not that simple. It's unlikely that two business objects are this similar." Or 2) "All we are doing is setting a property. I could have built the same kind of object in Visual Basic 6."
Good points! Let's address both thoughts with one scenario:
Consider a business object that retrieves order information. Orders are typically stored in two separate tables: Orders and Order Details (line items). ADO.NET enables us to embed both tables in a single DataSet, but our basic business object doesn't feature that kind of functionality. However, we can easily create a subclass that incorporates that behavior. Listing 3 and Listing 4 show the appropriate classes in C# and VB.NET.
The basic idea is simple: First, we use the business object as intended. We are doing so by setting the table name to "Orders." We are also overriding the GetData() method to perform custom behavior. However, before we add our code, we need to execute the code defined in the parent class through the following line of code:
DataSet oDS = base.GetData();
And in Visual Basic.NET:
Dim oDS As DataSet = MyBase.GetData()
This executes the code we wrote for the BusinessObject class and returns the DataSet generated in that code. This gives us the order records. Then, we query the order details and fill them into the DataSet we just generated. Finally, we return the DataSet that now contains two tables.
This is only one example of how we can take care of complex variations of our standard business object. I'm sure you can think of many other examples, and you will find that you can implement them all in a similar fashion.
You may notice that the code we created isn't terribly efficient. We recreate the connection to the database, for instance. We had to do so because we didn't design our abstract class very well. It would have been much better to attach the connection object to a field of the class, rather than a local variable within the method. If it was a field, it would be available to us in the subclass, and we wouldn't have to recreate an expensive connection (expensive in the performance sense). Another option would have been to add a special method that receives the connection object as a parameter. In the BusinessObject class, that method would have been empty, but we could have overridden it in the subclasses.
As you can see, inheritance isn't free. You want to give a lot of thought to your object hierarchies beforehand. We will tackle "Designing for Inheritance" in a future article.
Creating ASP.NET Interface Components
At this point, we have several different business objects we can use to access data. It is now time to utilize these business objects to display data in an interface layer. One common scenario is the need for drop-down lists in a Web page. An example would be drop-down lists that enable users to select regions and territories.
Let's think about how we would do this in regular ASP: We would have to create the drop-down lists in every page where we want them to appear and code both lists individually. If we were good ASP developers, we would probably use a server-side-include, so we didn't have to constantly start from scratch. But overall, it would be very cumbersome. There would be very little design-time support. Also, we couldn't set properties on those included code snippets and unless we needed the drop-down list the same exact way we originally defined it, we'd have to recode it anyway.
In ASP.NET, we can leverage the power of inheritance. We can build an abstract drop-down list that provides the basic behavior and appearance; then we can create subclasses for the individual lists. Listing 5 and Listing 6 show the basic drop-down list class in C# and VB.NET.
As you can see, our drop-down list is subclassed from a standard drop-down list class that ships with ASP.NET. This ensures that our object looks and behaves like any other drop-down list (during run-time as well as during design-time).
In addition to the inherited standard behavior, we add two methods: ConfigureObject() and FillDropDown(). The first method doesn't have any code. We simply created it to be better prepared for inheritance than we were with our business object class. We can override the method in subclasses to configure the drop-down list and add custom behavior before data gets queried and put into the list by the FillDropDown() method. Both methods are called from their class constructor.
Note that the FillDropDown() method relies on a business object being instantiated. Our basic class never instantiates that business object. We take care of that in subclasses (which is why we have the ConfigureObject() method in the first place).
The FillDropDown() method simply retrieves a DataSet from the business object and binds it to the dropdown list. We also have to tell the object which fields to display (drop-down lists can display only one column and use a second field for the internal value). Drop-down lists come with all of the required functionality built in, so all we have to do is set a few properties in subclasses (inheritance works over multiple levels of subclasses, of course).
All that's left to do now is create subclasses, set a few properties, and we are all set. Listing 7 shows a C# version of a specialized drop-down list that displays Regions. We override the ConfigureObject() method to instantiate the Region business object. We also specify that we want to display the "RegionDescription" field and use the "RegionID" field as the internal value (primary key). Also, since DataSets can contain multiple tables, we have to specify the table we want to use ("Region"), even though there is only one table.
Listing 8 shows the Territories drop-down list, this time written in Visual Basic.NET.
At this point, we are basically done. However, in order to make this class as easy to use as possible, we want to make sure we can add it to the Visual Studio.NET toolbox. This is ensured by the ToolboxData attribute, which specifies the tag that's to be added to the ASP.NET page when the class (control) is dropped.
To add the list to your toolbox, simply right-click on the toolbox, and select "Customize Toolbox." This launches the dialog shown in Figure 2, which you can use to browse to your file containing the drop-down list class. Once the class has been added to your toolbox, it can be used like any other ASP.NET control (Figure 3). Figure 4 shows the drop down lists in action.
Creating Windows Forms Interface Components
Just as we created drop-down lists for Web Forms (ASP.NET), we can also create similar components for Windows Forms environments. Listing 9 shows the C# version of such as class.
Note that the code is almost identical to the ASP.NET code, with the exception of some inconsistencies in property names and the fact that in Windows Forms the term "combo box" is used instead of "drop down list." The only real difference is that the Windows control doesn't allow us to specify a table name so that the control can automatically pick the correct table out of the DataSet. For this reason, we can simply add similar functionality ourselves.
Just like in the ASP.NET version, we need to subclass the abstract combo box class to create a functional control. Here's some code (C#) that does so:
public class RegionComboBox : aDataComboBox
{
protected override void ConfigureObject()
{
this.oBiz = new Regions();
this.ValueMember = "RegionID";
this.DisplayMember = "RegionDescription";
this.DataMember = "Region";
}
}
Figure 5 shows the new combo boxes in action.
Conclusion
Inheritance is a very powerful technique that you don't want to miss in your everyday development efforts. There is a learning curve attached to inheritance, but the productivity curve is much steeper than the learning curve and it is therefore worthwhile to familiarize yourself with inheritance beyond the basic principles. In the next issue of Component Developer Magazine, we will take a close look at how we can design our classes for inheritance to maximize code reuse.
Markus Egger