Designing Data Access Classes

for RuBoard

After your base class has been created, you can move on to creating the actual data access classes that make up your data services tier . Once again, the purpose of the data access classes is to isolate the data access logic from the other tiers and provide a robust interface for manipulating the data in your application.

As you design and implement the classes, however, you'll run across several design issues that you must address. This section is designed to give you a feel for those issues and then show how you can abstract the .NET Data Provider used and implement a purely class-based approach in your data services tier, as mentioned on Day 15, "Using ADO.NET in a Multi-Tier Application."

Design Issues

There are several design issues that you'll want to address as you design your classes. These apply not only to data access classes, but also to other classes that you design for reuse.

Note

For additional information on class design issues in .NET, see "Design Guidelines for Class Library Developers" in the online documentation. Microsoft released a tool on www.gotdotnet.com called Microsoft FxCop. This utility analyzes assemblies and provides information as to how well it conforms to the Microsoft design guidelines.


Data-Centric Versus Application-Centric

As you approach data access classes, one of your first decision points is whether to design your data access classes to be data-centric or application-centric. In other words, should the methods of the classes simply reflect how the data is stored in the data store, or should they reflect how the data is used in the application? Using the former approach results in a very granular approach because you create a class for each table in the ComputeBooks database and well-defined methods for each class, such as Get , Add , Delete , and Update . The latter approach typically results in fewer classes defined along functional boundaries, such as a WebData class used in ComputeBook's public Web site. This class might contain methods such as GetTitles that support the arguments typically used for displaying title information on the site. Of course, depending on the complexity you expect, you might also factor the methods into multiple application-specific classes along data-centric lines, such as Titles , Stores , and Orders .

Generally, I recommend using the application-centric approach because it offers the biggest bang for the buck. Using this approach, the classes are immediately able to be used productively and result in fewer lines of code in the presentation and business services tiers. Further, the ways in which the data needs to be accessed in multiple applications usually converge, and so using this approach often results in reusability as well. By contrast, using a data-centric approach results in you having to write more code in both the data services and other tiers. Remember that one of the goals of the data services tier is abstraction, so simply exposing each table as a class basically exposes the data model itself. However, it should be cautioned that using an application-centric approach doesn't mean that your data access classes perform business logic. Always keep in mind that the purpose of the data access classes is to select, insert, update, and delete data, not to apply business rules or processes.

Tip

I would also make the same recommendation at the stored procedure layer. Generally, you want to create application-specific stored procedures that access data as it needs to be manipulated in the application. This entails using WHERE , JOIN , and ORDER BY clauses liberally and returning only the columns required for the application-specific functionality. For this reason, if multiple applications are going to access the same database, you should consider using an additional application-specific prefix in your naming conventions for stored procedures.


Naming Conventions and Constructors

After you've decided on an approach, you need to apply solid coding practices to the design. Two of the primary issues you'll need to consider are using naming conventions and constructors.

Naming Conventions

You've probably noticed that the capitalization conventions for classes, variables , data types, arguments, and so on are slightly different than those you might have used before. In fact, the .NET Framework classes use three different styles of capitalization:

  • Pascal case

  • Camel case

  • Uppercase

graphics/newterm.gif

Pascal case is the most prevalent and refers to identifiers with no spaces and where each individual word is capitalized. In .NET, you should use Pascal case to identify classes, enumerated types, events, public fields, interfaces, methods, namespaces, and properties. For example, the declaration of the ComputeBooksDABase class in Listing 17.1 uses Pascal case.

graphics/newterm.gif

The second most prevalent convention is the use of camel case. Camel case refers to identifiers in which the first word in the identifier is left as lowercase whereas subsequent words are capitalized. In the .NET Framework, camel case is used for parameters and protected fields, so, for example, the constructor of the ComputeBookDAException class was defined as follows :

 Public Sub New(ByVal message as String, ByVal originalException as Exception) 

Note

Notice that the parameters use camel casing, whereas the names of the method and the types use Pascal casing .


graphics/newterm.gif

Uppercase is reserved only for identifiers that consist of two or fewer letters . As a result, you'll typically see uppercase used for namespaces such as System.IO and System.Web.UI . Keep in mind as well that different languages handle capitalization differently, so you don't want to create code that requires case sensitivity. In other words, if you are writing a class in C# (which is case sensitive), you shouldn't create both ReadXml and readXml methods because clients using VB .NET (which is case sensitive) wouldn't be able to differentiate between the two.

What Happened to Hungarian Notation?

VB .NET developers will note that in VB and VBScript, parameters and variables often had a two- or three-letter prefix that denoted the data type. Some also had a prefix that denoted the scope. For example, str was used for strings and int was used for integers, whereas m was used for modules level and l was used for local. This Hungarian notation was informative but resulted in long variable and parameter names. For the most part, Hungarian notation is no longer used in .NET. Instead, the identifier, especially if it is visible externally, should be in camel case and simply describe the meaning of the variable or parameter. The only prefix that you'll typically see is an underscore to denote that a field or method is private to a class.

Namespaces should also use Pascal case, as in the ComputeBooks.Data namespace we discussed earlier.

You should use consistent naming rules when naming your classes and interfaces. In the earlier example of the ComputeBookDAException class, the class name is a noun or noun phrase, uses Pascal casing, and is not prefixed with a "C" as you sometimes see (particularly in previous versions of VB). In addition, class names shouldn't use underscores and should avoid abbreviations where possible. In this case, the class name also contains a piece of its base class name to identify that ComputeBookDAException is a type of Exception . This can be useful as a general rule in order to assist developers using your class in getting an immediate feel for how your class might behave. However, it isn't a hard and fast rule.

Interfaces follow many of the same rules, such as the prohibition on underscores and abbreviations and the use of Pascal casing. However, interfaces should be prefixed with an "I" and can be named using an adjective phrase that describes the functionality exposed by the interface (instead of using a descriptive noun or noun phrase). For example, the .NET Framework contains the ICloneable interface, which is implemented by a whole host of classes, such as String , Stack , Array , SortedList , and Delegate among others. Here, "cloneable" is an adjective that describes the functionality that the class gains by implementing the interface. Class and interface names should also only differ by the "I" prefix when the class is the standard implementation of the interface. For example, the ComputeBooksDABase class might provide the standard implementation of the IComputeBooksDABase interface.

Constructors
graphics/newterm.gif

A concept that might be new to some developers developing data access classes is the use of constructors that can be parameterized and overloaded to allow parameters to be passed into the instance of a class during its initialization. There are several guidelines that you should be aware of, however, when defining constructors on your classes.

First, make sure that you limit the amount of work done in the constructor. Typically, a constructor should only capture the parameters passed to it and initialize private data within the instance. By performing lots of other work, you'll cause the class to appear sluggish and perhaps do extra work for nothing because the user of the class might never call another method on it. Rather, you should defer the work until the user of the class calls the specific feature. Additionally, the parameters you pass into a constructor should be viewed as shortcuts for setting properties on the class after the class has been created using an empty constructor. Thus, the following two C# code snippets should be semantically equivalent:

 Stores s = new Stores(); s.Switch = mySwitch; s.ConnectString = "server=ssosa;database=Quilogy;trusted_connection=yes"; Stores s = new Stores(   "server=ssosa;database=Quilogy;trusted_connection=yes", mySwitch); 

Note

Note as well that the order the properties are set in the first snippet and the order they are exposed in the constructor in the second snippet shouldn't matter. In other words, all properties in a class should be able to be set in any order.


Second, in cases in which the constructor is overloaded to include different signatures, you should use a consistent ordering and naming pattern for the parameters. The typical approach is to provide an increasing number of parameters in order to allow the user of the class to provide more detailed information if required. In the previous example, the Stores class might provide the following three constructors:

 Public Class Stores   Public Sub New()     ' implementation   End Sub   Public Sub New(ByVal connect As String)     ' implementation   End Sub   Public Sub New(ByVal connect As String, ByVal trace As Switch)     ' implementation   End Sub End Class 
graphics/newterm.gif

In this case, the user can pass in more detailed information by using the second constructor and passing in a class derived from System.Diagnostics.Switch to capture trace output. Along these same lines, C# includes an interesting syntax that enables you to easily allow one constructor to call another using what is referred to as an initializer list. This is useful because it allows a default constructor (the one with no arguments), for example, to call one of the more specific constructors using default parameters. If the Stores class were rewritten in C#, the default constructor could call one of the other constructors automatically, as follows:

 public Class Stores {   public Stores() : this("server=ssosa")   {     // implementation   }   public Stores(String connect)   {     // implementation   }   public Stores(String connect, Switch trace)   {     // implementation   } } 

For special purposes, you can also create constructors that aren't public. Creating a protected constructor allows derived classes to pass specific information to the base class that it needs to operate while creating an empty private constructor in a class if you don't want instances of the class to be created. The latter technique is useful if the class exposes only Shared ( static in C#) methods and so shouldn't be able to be instantiated . Keep in mind that if you don't create a constructor for a class, both C# and VB .NET will create a default constructor that accepts no arguments. However, after you create a constructor that accepts arguments, the compiler doesn't create the empty constructor.

Finally, constructors must also be considered when you create derived classes. For example, it's often necessary to pass information obtained in a parameterized constructor into the base class. This is the case, for example, when a class derived from Exception accepts a message in its constructor, as the ComputeBookDAException class does in Listing 17.2. At first glance, this might seem difficult because you would think that a base class must be initialized before its descendant classes. However, both VB .NET and C# include specific syntax that enables you to pass information from a derived constructor to its base class. In VB .NET, this is done by calling the New method of the MyBase object in the first line of the constructor, passing it the arguments it needs, as shown in Listing 17.2. In C#, the constructor declaration uses the base keyword like so:

 public ComputeBookDAException(String message) : base(message) {   // other initialization here } 

In both cases, the base class is initialized using the message parameter passed to the constructor.

Overloaded and Private Methods

Finally, the design of your data access classes should take advantage of the ability to overload methods. This technique, which allows a single method to expose multiple signatures so that developers can choose which signature to call based on their needs, is especially effective in the methods used to query a data store. For example, consider the Books class shown in Listing 17.4.

Listing 17.4 Data access class. This class takes advantage of overloaded and private methods to allow querying on the Titles table.
 Option Strict On Imports System.Data.SqlClient Imports System.Data Namespace ComputeBooks.Data   Public Class Books : Inherits ComputeBooksDABase     Public Sub New(ByVal connect As String)       MyBase.New(connect)     End Sub     Public Sub New()       MyBase.New()     End Sub     Public Overloads Function GetTitles() As DataSet       _checkDisposed()       Return _getTitles(Nothing, Nothing, Nothing, Nothing, Nothing)     End Function     Public Overloads Function GetTitles(ByVal isbn As String) As DataSet       _checkDisposed()       ' Validate       If isbn Is Nothing OrElse isbn.Length = 0 Then         Throw New ArgumentNullException("isbn", "Cannot be null or empty")         Return Nothing       End If       Return _getTitles(Nothing, Nothing, isbn, Nothing, Nothing)     End Function     Public Overloads Function GetTitles(ByVal params() As Object) As DataSet       _checkDisposed()       ' Validate       If params.Length <> 5 Then         Throw New ArgumentException("Must pass in an array of 5 values")         Return Nothing       End If       Dim author, isbn, title As String       Dim pubdate As Date       Dim catId As Guid       Try         author = CType(params(0), String)         title = CType(params(1), String)         isbn = CType(params(2), String)         pubdate = CType(params(4), Date)         catId = CType(params(3), Guid)       Catch e As Exception         Throw New ArgumentException("Arguments are not of the correct type")       End Try       Return _getTitles(author, title, isbn, pubdate, catId)     End Function     Private Function _getTitles(ByVal author As String, ByVal title As String, _      ByVal isbn As String, ByVal lowPubDate As Date, _      ByVal catID As Guid) As DataSet       Dim da As New SqlDataAdapter("usp_GetTitles", MyBase.SqlCon)       Dim titleDs As New DataSet()       da.SelectCommand.CommandType = CommandType.StoredProcedure       Try         If Not isbn Is Nothing AndAlso isbn.Length > 0 Then           da.SelectCommand.Parameters.Add(New SqlParameter("@isbn", _             SqlDbType.NVarChar, 10))           da.SelectCommand.Parameters(0).Value = isbn         Else           If Not title Is Nothing AndAlso title.Length > 0 Then             da.SelectCommand.Parameters.Add(New SqlParameter("@title", _               SqlDbType.NVarChar, 100))             da.SelectCommand.Parameters(0).Value = title           End If           If Not author Is Nothing AndAlso author.Length > 0 Then             da.SelectCommand.Parameters.Add(New SqlParameter("@author", _               SqlDbType.NVarChar, 250))             da.SelectCommand.Parameters(0).Value = author           End If           If lowPubDate.Equals(Nothing) Then             da.SelectCommand.Parameters.Add(New SqlParameter("@pubDate", _               SqlDbType.DateTime))             da.SelectCommand.Parameters(0).Value = lowPubDate           End If           If Not catID.Equals(Guid.Empty) Then             da.SelectCommand.Parameters.Add(New SqlParameter("@catId", _               SqlDbType.UniqueIdentifier))             da.SelectCommand.Parameters(0).Value = catID           End If         End If         da.Fill(titleDs)         Return titleDs       Catch e As SqlException         da = Nothing         titleDs = Nothing         Call MyBase.ThrowComputeBookException("GetTitles Failed", e)       Finally         If Not MyBase.SqlCon Is Nothing Then MyBase.SqlCon.Close()       End Try     End Function       ' Other methods here     Private Sub _checkDisposed()       If MyBase.Disposed Then         Throw New ObjectDisposedException("Books has been disposed")       End If     End Sub   End Class End Namespace 
graphics/analysis.gif

You'll notice in Listing 17.4 that the Books class exists in the ComputeBooks.Data namespace and inherits from ComputeBooksDABase . After exposing both a parameterized and empty constructor, it exposes three signatures for the GetTitles method using the Overloads keyword.

Note

The Overloads keyword isn't required in VB as long as none of the method signatures uses it. However, I recommend using it because it makes explicit your intention . In C#, there is no equivalent keyword.


You'll notice that methods allow the caller to specify no arguments, only the ISBN, or an array of optional parameters. In all three cases, the public method ultimately calls the private _getTitles method that accepts each argument individually. Obviously, implementing the logic of the GetTitles method once in a private method makes it easier to maintain and extend. However, before the private method is called, the public method is responsible for validating the arguments. For example, the signature that accepts the ISBN ensures that a valid value is passed in and, if not, throws the ArgumentNullException . In the case of the method that accepts the array of objects, it checks to ensure that the array contains the correct number of objects and then parses the array into the local variables passed to the private method.

The private _getTitles method is then responsible for creating the SqlDataAdapter object and populating its Parameters collection. Note that the parameters are added to the collection only if the values are valid. In this way, the method can effectively support optional parameters. Note also that the usp_GetTitles stored procedure is where the work of creating the SELECT statement to retrieve the data is performed. In this way, only one stored procedure is needed to query the Titles table in different ways. After the parameters are populated , the DataSet is filled and returned.

Note

The usp_GetTitles stored procedure defaults the parameters to NULL and then builds a SELECT statement based on the parameters that are actually passed. The statement is then executed using the SQL Server sp_executesql system stored procedure.


Using this technique also has the advantage of being maintainable because the signature that exposes the array of objects can obviously be extended to query using different or an extended set of parameters without affecting the public interface exposed to callers .

To call the GetTitles method, a client can then instantiate the Books class and then call either of the three versions of the GetTitles method. This is shown in the following snippet:

 Books b = new Books(_connect); DataSet ds = new DataSet(); ds = b.GetTitles();  // returns all titles ds = b.GetTitles("06720001X");  //returns one ISBN ds = b.GetTitles(new Object[]   {"Fox, Dan", null, null, null, null } );  // returns books authored by me 

Abstracting Providers

As you learned last week, one of the decisions you must make when designing an application is which .NET Data Provider to use because ADO.NET supports both specific and generic providers. Which provider you choose depends on a variety of factors, including the functionality and performance of the provider in addition to the likelihood of your switching data stores in the future. In fact, many independent software vendors (ISVs) developing packaged applications will need to make this decision very early in their use of ADO.NET.

You'll notice that both the ComputeBooksDABase and Books classes assume that they will always access SQL Server because they are hardcoded to use the objects in the System.Data.SqlClient namespace. In the event that ComputeBooks moved to an Oracle solution in the future, these classes would have to be rewritten to use either the OleDb provider or a specific Oracle provider that might be available by then. To make these classes more generic, one solution, of course, would be to simply use the OleDb provider because it can be used to access either SQL Server or Oracle based on the OLE DB provider in use. Although this would result in a single code base, you would lose the performance and functionality gains of a specific provider like SqlClient. A second option is to develop multiple versions of each class and then compile various versions of the application using command line switches. This might appeal to ISVs that need to support multiple data stores but require the performance of specific providers.

However, a more elegant solution is to design your classes with an additional layer of abstraction using the abstract factory design pattern. Using this pattern, both the base class and the data access class can rely on a factory class to create the objects of the appropriate type based on configuration settings. The ProviderFactory class that uses this approach is shown in Listing 17.5.

Listing 17.5 Abstracting providers. This class abstracts the creation of provider-specific objects and uses interfaces to return the objects.
 Namespace ComputeBooks.Data   Public Enum ProviderType     SqlClient = 0     OLEDB = 1   End Enum   Public Class ProviderFactory     Sub New(ByVal provider As ProviderType)       _pType = provider     End Sub     Sub New()     End Sub     Private _pType As ProviderType = ProviderType.SqlClient     Private _pTypeSet As Boolean = False     Public Property Provider() As ProviderType       Get         Return _pType       End Get       Set(ByVal Value As ProviderType)         If _pTypeSet Then           Throw New ReadOnlyException("Provider already set to " & __             pType.ToString)         Else           _pType = Value           _pTypeSet = True         End If       End Set     End Property     Public Function CreateDataAdapter(ByVal commandText As String, _       ByVal connection As IDbConnection) As IDataAdapter       Try         Select Case _pType           Case ProviderType.SqlClient             Return New SqlDataAdapter(commandText, _               CType(connection, SqlConnection))           Case ProviderType.OLEDB             Return New OleDbDataAdapter(commandText, _               CType(connection, OleDbConnection))         End Select       Catch e As Exception         Throw New ComputeBookDAException( _           "Could not create IDbDataAdapter object", e)       End Try     End Function     Public Overloads Function CreateParameter(ByVal paramName As String, _      ByVal paramType As Object) As IDataParameter       Try         Select Case _pType           Case ProviderType.SqlClient             Return New SqlParameter(paramName, paramType)           Case ProviderType.OLEDB             Return New OleDbParameter(paramName, paramType)         End Select       Catch e As Exception         Throw New ComputeBookDAException( _           "Could not create IDataParameter object", e)       End Try     End Function     Public Overloads Function CreateParameter(ByVal paramName As String, _       ByVal paramType As Object, ByVal size As Integer) As IDataParameter       Try         Select Case _pType           Case ProviderType.SqlClient             Return New SqlParameter(paramName, _               CType(paramType, SqlDbType), size)           Case ProviderType.OLEDB             Return New OleDbParameter(paramName, _               CType(paramType, OleDbType), size)         End Select       Catch e As Exception         Throw New ComputeBookDAException( _           "Could not create IDataParameter object", e)       End Try     End Function     Public Function CreateConnection(ByVal connect As String) As IDbConnection       Try         Select Case _pType           Case ProviderType.SqlClient             Return New SqlConnection(connect)           Case ProviderType.OLEDB             Return New OleDbConnection(connect)         End Select       Catch e As Exception         Throw New ComputeBookDAException( _           "Could not create IDbConnection object", e)       End Try     End Function     Public Function CreateCommand(ByVal cmdText As String, _      ByVal connection As IDbConnection) As IDbCommand       Try         Select Case _pType           Case ProviderType.SqlClient             Return New SqlCommand(cmdText, CType(connection, SqlConnection))           Case ProviderType.OLEDB             Return New OleDbCommand(cmdText, _               CType(connection, OleDbConnection))         End Select       Catch e As Exception        Throw New ComputeBookDAException("Could not create IDbCommand object", e)       End Try     End Function   End Class End Namespace 
graphics/analysis.gif

The ProviderFactory class simply exposes a property called Provider , which can be passed in the constructor and which specifies the provider that is to be used using the ProviderType enumerated type (which, in this case, supports the SqlClient and OleDb providers). The class then includes a series of Create methods that create the appropriate data adapter, parameter, connection, and command objects for the provider using a simple Select Case statement. Note that the CreateParameter method is overloaded so that the size can also be specified.

Note

A more generic technique to implement a provider factory could easily be implemented by using a configuration file that enumerated the supported providers. Then the Activator class in the System namespace could be used to dynamically create instances of the appropriate classes. We'll look at this technique tomorrow.


In all cases, the factory methods accept parameters and return values using the interfaces that both providers support and that we discussed on Day 8, "Understanding .NET Data Providers."

After the ProviderFactory is in place, classes such as Books can create an instance of the class in their constructors, passing in the appropriate provider, typically found in a configuration file, like so:

 Public Sub New(ByVal connect As String, ByVal provider As ProviderType)   MyBase.New()   _pf = New ProviderFactory(provider)   MyBase.Con = _pf.CreateConnection(connect) End Sub 

In this case, the variable _pf refers to a private data member that references an instance of the ProviderFactory class. Note that the constructor of the class has been augmented to support passing in the provider as well as the connection string. In addition, the base class has also been modified so that the protected Con property (of type IDbConnection ) can be set using the CreateConnection method.

Note

If your base class needs to rely on the ProviderFactory as well, you might instead simply declare a private variable of ProviderFactory in the base class and expose it as a protected property.


Methods within the data access classes such as _getTitles can then be rewritten to use the instance of the ProviderFactory class as shown in Listing 17.6.

Listing 17.6 Using the ProviderFactory class. This method uses the ProviderFactory to create the appropriate objects based on the provider.
 Private Function _getTitles(ByVal author As String, ByVal title As String, _  ByVal isbn As String, ByVal lowPubDate As Date, _  ByVal catID As Guid) As DataSet   Dim da As IDbDataAdapter   Dim titleDs As New DataSet()   da = _pf.CreateDataAdapter("usp_GetTitles", MyBase.Con)   da.SelectCommand.CommandType = CommandType.StoredProcedure   Try     If Not isbn Is Nothing AndAlso isbn.Length > 0 Then       da.SelectCommand.Parameters.Add(_pf.CreateParameter("@isbn", _         DbType.String, 10))       da.SelectCommand.Parameters(0).Value = isbn     Else       If Not title Is Nothing AndAlso title.Length > 0 Then         da.SelectCommand.Parameters.Add(_pf.CreateParameter("@title", _           DbType.String, 100))         da.SelectCommand.Parameters(0).Value = title       End If       If Not author Is Nothing AndAlso author.Length > 0 Then         da.SelectCommand.Parameters.Add(_pf.CreateParameter("@author", _           DbType.String, 250))         da.SelectCommand.Parameters(0).Value = author       End If       If lowPubDate.Equals(Nothing) Then         da.SelectCommand.Parameters.Add(_pf.CreateParameter("@pubDate", _           DbType.DateTime))         da.SelectCommand.Parameters(0).Value = lowPubDate       End If       If Not catID.Equals(Guid.Empty) Then         da.SelectCommand.Parameters.Add(_pf.CreateParameter("@catId", _           DbType.GUID))         da.SelectCommand.Parameters(0).Value = catID       End If     End If     da.Fill(titleDs)     Return titleDs   Catch e As Exception     da = Nothing     titleDs = Nothing     Call MyBase.ThrowComputeBookException("GetTitles Failed", e)   Finally     If Not MyBase.Con Is Nothing Then MyBase.Con.Close()   End Try End Function 

The point to note in Listing 17.6 is that the method doesn't use any of the SqlClient- specific objects (or enumerations), so it can be executed using either provider.

Returning Custom Objects

The final issue to consider when designing data access classes is how the data will be returned. As you learned on Day 15, your choices include exposing DataSet objects, data readers, or custom objects. Although the first two options are simplified by the fact that ADO.NET supports them natively, the third option is attractive because it provides a pure object-based interface to the data for clients rather than through the abstraction of DataSet or data reader. As you'll see, however, this approach also results in you having to write more code to populate the classes and manipulate their data.

As an example, consider the case in which ComputeBooks wants to expose its customer data using custom classes. To begin, you would write a Customer class that exposed the properties of the customer. In addition, you could factor properties that have their own properties such as Address into separate classes as shown in Listing 17.7.

Listing 17.7 Data as an object. This listing shows the Customer and Address classes used to represent a customer.
 <Serializable()> _ Public Class Customer   Private _custId As Guid   Private _fName As String   Private _lName As String   Private _emailAddress As String   <XmlElement()> _   Public Address As Address   Public Sub New(ByVal values() As Object)     'Array of values     If values.Length = 8 Then       Me.CustomerID = values(0)       Me.FName = values(1)       Me.LName = values(2)       Me.EmailAddress = values(7)       Me.Address = New Address()       With Me.Address         .Street = values(3)         .City = values(4)         .StateProv = values(5)         .PostalCode = values(6)       End With     Else       Throw New ComputeBookDAException( _        "The array passed into the Customer was invalid")     End If     End Sub   Public Sub New()   End Sub   <XmlAttributeAttribute("ID")> _   Public Property CustomerID() As System.Guid     Get       Return _custId     End Get     Set(ByVal Value As System.Guid)       _custId = Value     End Set   End Property   <XmlElement()> _   Public Property FName() As String     Get       Return _fName     End Get     Set(ByVal Value As String)       _fName = Value     End Set   End Property   <XmlElement()> _   Public Property LName() As String     Get       Return _lName     End Get     Set(ByVal Value As String)       _lName = Value     End Set   End Property   <XmlElement()> _   Public Property EmailAddress() As String     Get       Return _emailAddress     End Get     Set(ByVal Value As String)       _emailAddress = Value     End Set   End Property  End Class <Serializable()> _ Public Class Address   Private _address As String   Private _city As String   Private _stateProv As String   Private _postalCode As String   Private _addType As AddressType   Public Enum AddressType     Home = 0     Business = 1   End Enum   <XmlAttributeAttribute()> _   Public Property Type() As AddressType     Get       Return _addType     End Get     Set(ByVal Value As AddressType)       _addType = Value     End Set   End Property   <XmlElement()> _   Public Property Street() As String     Get       Return _address     End Get     Set(ByVal Value As String)       _address = Value     End Set   End Property   <XmlElement()> _   Public Property City() As String     Get       Return _city     End Get     Set(ByVal Value As String)       _city = Value     End Set   End Property   <XmlElement()> _   Public Property StateProv() As String     Get       Return _stateProv     End Get     Set(ByVal Value As String)       _stateProv = Value     End Set   End Property   <XmlElement()> _   Public Property PostalCode() As String     Get       Return _postalCode     End Get     Set(ByVal Value As String)       _postalCode = Value     End Set   End Property End Class 
graphics/analysis.gif

Although much of Listing 17.7 is straightforward, the interesting aspect is that the class is marked with the System.Xml.Serialization.SerializableAttribute . This attribute ensures that the common language runtime can serialize an instance of this class. This is necessary if the object is passed between application domains or serialized to XML using the XmlSerializer class, for example, if you wanted to save the object to disk. In addition, each property is marked with an attribute that specifies how the property is to be represented when serialized (in this case, either an element, attribute, or array, although you could also specify other XML types or ignore the property altogether).

Tip

If you already have an XSD document that describes your data, you can alternatively use the XSD.exe command-line utility to auto-generate these skeleton classes for you.


To represent multiple customers, you could create a strongly typed collection class to hold them as shown in Listing 17.8.

Listing 17.8 Creating a collection. This class holds a collection of Customer objects and is derived from CollectionBase .
 Public Class CustomersCollection : Inherits CollectionBase   ' Stores a collection of customers   Public Sub Add(ByVal c As Customer)     Me.InnerList.Add(c)   End Sub   Default Public ReadOnly Property Item( _    ByVal i As Integer) As Customer     Get       Return Me.InnerList(i)     End Get   End Property End Class 

In this case, the CustomersCollection class inherits from CollectionBase and simply holds an ArrayList that contains the Customer objects. Because this class supports the IList and IEnumerable interfaces, it can be bound to controls in both Windows and Web Forms user interfaces, as you learned yesterday . This class can then be used to return a collection of customers in a data access class like so:

 Public Class Customers : Inherits ComputeBooksDABase   <XmlArray()> _   Public Function GetCustomers() As CustomersCollection     ' implementation   End Function   ' Other members End Class 

After this infrastructure has been coded, you have to write some code that loads the persisted state of the object to an instance of your class. For example, your data access class could expose a GetCustomer method like that shown in Listing 17.9 to create an instance of the Customer object and return it to the client.

Listing 17.9 Populating an object. Behind the scenes, you can use an ADO.NET data reader to populate your objects.
 Public Function GetCustomer( _   ByVal emailAddress As String) As Customer   ' Go get a customer   Dim cm As IDbCommand   Dim parm As IDataParameter   Dim dr As IDataReader   Dim cust As Customer   Dim values(7) As Object   cm = pf.CreateCommand("usp_GetCustomers", _     MyBase.Con)   cm.CommandType = CommandType.StoredProcedure   parm = pf.CreateParameter("@emailAddress", DbType.String)   parm.Value = emailAddress   cm.Parameters.Add(parm)   Try     MyBase.Con.Open()     dr = cm.ExecuteReader(CommandBehavior.CloseConnection)     dr.Read()     dr.GetValues(values)     _cleanValues(values)     cust = New Customer(values)  ' Create a new Customer    Catch Ex As Exception      MyBase.ThrowComputeBookException("Could not get customer " & _        emailAddress, Ex)     Return Nothing   Finally    dr.Close()   End Try   Return cust End Function 
graphics/analysis.gif

In Listing 17.9, the ProviderFactory from Listing 17.5 is used to create the command object to read the customer information from a stored procedure. The GetValues method then reads the entire row into an array and passes it to a private method that performs cleanup, such as trimming the string values and converting the null columns ( DBNull.Value ) to Nothing . The array is then passed to the constructor of the Customer object shown in Listing 17.7 and the new customer is returned from the method. Of course, you could also extend this code to retrieve more than one customer and populate the CustomerCollection . You would also need to provide your own implementation to save changes to the customer back to the data store.

for RuBoard


Sams Teach Yourself Ado. Net in 21 Days
Sams Teach Yourself ADO.NET in 21 Days
ISBN: 0672323869
EAN: 2147483647
Year: 2002
Pages: 158
Authors: Dan Fox

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net