The Baker’s Dozen: 13 Productivity Tips for Building a .NET Distributed Application

his installment of “The Baker’s Dozen” presents a Windows Forms database application that demonstrates some of the primary attributes of a distributed architecture. These attributes include authentication and connectivity, data management, business objects, user-interface modules, and reporting. The featured application is a job-costing and invoicing application for a Masonry company, and is available for download. The application contains many functions that are required in most business applications. This article steps through the construction of these key pieces and provides classes and methodologies that you can apply to your next application.

Beginning with the End in Mind
When I started working in .NET, I longed for a small but meaningful demo application that addressed the challenges and requirements that I faced as an applications developer. After I became productive in .NET (thanks too many authors and on-line community leaders), I made a promise to build a Web-based, smart-client business application to serve as a knowledge base for others facing the same learning curve I experienced.

The Common Ground Framework provides capabilities to use multiple connections/databases without changing the software or swapping configuration files.

Along the way, I also decided to build a set of reusable classes that represented the common functionality across the .NET systems I’ve built. In the spirit of my company name and philosophy, I’m calling this set of classes the Common Ground Framework for .NET. Thus, I’ve combined my own knowledge, techniques, and experiences with questions I’ve seen posted on technical forums to drive the content of this demo?and hopefully increase your productivity.

The demo application is based on the job-costing and invoicing needs of a typical contractor company. Many of the features I discuss apply to almost any application type. Thus some of the features may seem very familiar to you. In addition, I’ve also added some features specifically for demonstrative purposes.

The application utilizes concepts from previous Baker’s Dozen articles (DataGrid, Crystal Reports, and Transact-SQL). Each tip focuses on a particular requirement in a distributed database application, and offers either reusable classes and/or methodologies for handling the situation. I will cover the following topics:

  • An overview of the primary pieces of the architecture, and the major tasks that each piece will perform
  • An authentication form class that prompts for user ID and password, database (test DB, production DB) and connection type (Web Service over HTTP, Remoting via TCP)
  • Developing server-side Web service and remoting server interfaces so that client pieces can access the back-end
  • Baker’s Dozen Spotlight: developing client-side code to access the back-end through the Web services/remoting service interfaces referenced above:
  • A data access layer for executing stored procedures against a requested database
  • A one-way synchronization approach for managing lookup tables, to minimize network traffic
  • A basic MDI Parent form class with data-driven menus and a status bar that displays system information
  • A set of subclassed Winform controls to extend base functionality
  • A generic interface to bind ADO.NET data with these controls
  • A generic form class with base functions to handle standard data maintenance tasks
  • Examples of server-side and client-side business objects to validate data and enforce rules
  • Baker’s Dozen Potpourri: several advanced uses of ADO.NET functionality
  • An implementation of basic audit trail functionality to track database changes, and to allow users to view the changes

With each tip, I’ll discuss the areas of the architecture (Web service, client-side, and so on) that are affected.

You can download all the code listings for this article. So once again borrowing from the theme of Van Amsterdam’s bakery, I bring you thirteen productivity tips, in a new “Baker’s Dozen.”

?
Figure 1: The Masonry Solution project.

Tip 1: Planning the Architecture
The demo application contains six components:

  • Client piece
  • Web service proxy/remoting interfaces
  • Web service/remoting server
  • Business objects
  • Data access layer
  • Back-end database (SQL Server)

Table 1 lists the namespaces for the CG Framework classes. Table 2 lists the namespaces for the invoicing application that uses the framework. The download project contains a single solution (see Figure 1) with 19 projects.

Table 1: Common Ground Framework namespaces.

Namespace

Classes

Description

CGS.Business

cgsBaseBizObject

Base business objects

CGS.CrystalReports

cgsCrystalViewer

cgsCrystalManager

Tools for generating Crystal Reports

CGS.DataAccess

cgsDataAccess

Base Data Access Class

CGS.Globals

cgsGlobals

System-wide static properties

CGS.RemoteAccess

cgsClientRemoteAccess

Returns generic communication object that can be cast to a Web service or remoting interface

CGS.Utilities

cgsUtilities

Miscellaneous utilities

CGS.Winforms.Controls

cgsControls

Subclassed controls

CGS.Winforms.Containers

cgsContainerCriteria

cgsContainerResults

cgsContainerDataEntry

Base containers for data entry, result sets, and selection entry criteria

CGS.Winforms.Forms

cgsFrmDataMaintenance

cgsFrmLogin

cgsFrmAuditTrail

cgsFrmMDIParent

Base forms for data maintenance, login, audit trail, and MDI parent

Table 2: Masonry Invoicing application namespaces.

Namespace

Masonry.Client.Winforms

Masonry.Data.Access

Masonry.DataSets

Masonry.Interfaces

Masonry.Local.BusinessObjects

Masonry.RemotingServer

Masonry.Reports

Masonry.Server.BusinessObjects

Masonry.Webservice

Masonry.Winform.Containers

The client piece contains a rich UI for retrieving and updating data, and reporting capabilities using Crystal Reports. The client communicates with the back end using either Web service references or remoting interfaces. Interfaces provide tremendous value in distributed computing by guaranteeing that back-end classes will contain specific properties and methods. The client piece contains a set of classes and interfaces to simplify back-end access.

The back-end uses either Web services or a remoting server to respond to client requests. While many corporations use Web services for distributed computing, some prefer the increased performance and security of TCP remoting. The demo application supports both. The Web services/remoting server will work with business objects and then data access classes to save/retrieve data from SQL Server, and return result sets to the client. The data access class contains several reusable functions to simply common data tasks.

The application will make extensive use of datasets. Tasks such as data binding, data views, XML integration, reporting, and many other data maintenance tasks are significantly easier with datasets. However, because of the overhead associated with datasets, the application will create an XML string representation when passing dataset contents across physical boundaries/domains.

Author’s Note: The code listings in this article are written in C#. In recognition that roughly half the .NET developers use VB.NET, the downloadable project on www.commongroundsolutions.net contains a separate zip file containing VB.NET code for the listings.


Tip 2: Building a Login Form
You must present users with a sign-on screen to prompt for a user ID and password. You must also allow the user to specify which database to use?a company production database, a test database, and so on. In addition, the sign-on screen must allow the user to select how they’ll connect?the user may be running from a hotel room or other external location where they’ll connect through Web services, or perhaps from inside corporate headquarters where the client piece will access the middle-tier and back-end via remoting.

?
Figure 2: A login form class that prompts for user ID, password, and connection.

Figure 2 displays a sign-on screen from the CGS.Winforms.FrmLogin class. The screen is a generic form that prompts for a user ID and password. The form binds these controls to the static properties found in CGS.Globals. Static properties allow a developer to set properties to specific values and read them later in the application.

The login form also shows a list of available connections by reading the entries in the local XML file CONNECT.XML (see Listing 1). Note that the pulldown contains various options for connecting, either from outside or inside headquarters, and which database to use. The connection class in Tip 4 explains how the application uses the specific contents of this XML file to make the appropriate connection.

The Masonry application creates an inherited form from the CG base login form (Masonry.Client.Winforms, as FrmMasonrytLogin). The base form contains several virtual methods that a developer can override?the most critical is the Boolean function ValidateUserID, which returns a true or false based on the implemented authentication rule. Again, Tip 4 will cover this in detail.

Tip 3: Using Interfaces to Define the Back-End Components for Web Services and Remoting
Suppose you’ve already defined a back-end user business object to perform basic validation of a user ID/password, and a back-end user data object to validate against a back-end database. Now you must construct a Web service and a remoting service for remote clients to execute these functions. Finally, you must also define an interface for the client piece to access either service.

The client piece communicates with back-end functionality by means of interfaces. Interfaces guarantee that when a connection is made, classes will contain specific properties and methods.

The first step is to identify all function points where the client piece will interact (interface) with the back-end. In the case of the sign-on screen, the ValidateUserID function in the login form must call a back-end function with the same name. It will pass the User ID, password, and database key corresponding to the selected database. The back-end will return an XML string with the user’s full name and application status.

Both the client and server pieces will use this interface (see Listing 2). Although the client piece will not have design-time access to the actual back-end ValidateUserID function, the interface guarantees that classes implementing the interface will contain certain methods. This also allows the developer to develop for specific interfaces.

The second step is to define and develop the business object for ValidateUserID (see Listing 3), which resides in Masonry.Server.BusinessObjects. Note four key items in Listing 3.

  • The user business object implements the IUserObject interface.
  • The user business object inherits from the CG base business object.
  • The base business object in Listing 4 inherits from MarshalByRefObject, from the System namespace. Classes that support remoting must inherit from MarshalByRefObject in order to provide access across application domain boundaries.
  • Finally, the business object references a data access layer that Tip 5 will cover.

After building the business object and making it “remoting-friendly,” the third step is to define both a Web service and remoting service to manage client Web/remoting requests.

You can easily create a Web service in VS.NET. Assuming you have IIS running, you can create a new project as an ASP.NET Web service, and then add as many different Web services as needed. The demo project utilizes one Web service per business object. Each Web service is named after the corresponding business object, with a ‘w‘ prefix (for example, wUserObject, wInvoice, and so on). In many instances, the Web service function is a basic wrapper for the client piece to access the desired business object. Listing 5 shows the Web service for the user object. Note that the Web service implements the IUserObject interface.

The client piece will access the Web service by creating a proxy for this service, and specifying the actual URL. I’ll cover this in more detail in Tip 4.

You have to work more to create a remoting server because VS.NET does not offer a project template for remoting. Listing 6 shows a basic remoting server.

Setting up a remoting server requires a few steps. Because the client piece will access the remoting server through a TCP port, you must open and register a TCP channel for that port. The example has port number 8228 hard-coded?you may wish to read that entry out of a configuration file on the server. Finally, you need to register the business object to which the client piece will remote.

The application uses a simple Windows Form application to launch the remoting server. In a real-world situation, you’ll want to define this as a Windows service that automatically launches when the server boots up.

Tip 4: Baker’s Dozen Spotlight: Building the Client Piece to Connect via Web Services or Remoting
The client piece must communicate with business objects through either a .NET Web service or a remoting service. I’ll show you how to construct a set of classes to build and utilize these services, where the server address is not known until run time. For remoting, you’ll construct a basic but functional solution for the client to invoke and execute code on a server machine.

Now that you’ve constructed the back-end, you need to add components to the client piece to connect to the server. Let’s start with the Web service.

First, in the project where you intend to call the Web service, you must add a reference to Masonry.Interfaces. Remember that this contains the public interface IUserObject: while the client piece doesn’t have the code for the user object, it knows what functions exist through the interface. This will become very important over the next several steps.

Next, you must add a Web reference to the Web service you created in Tip 3. You can right-click and select Add Web Reference…, and select the Web service you created earlier. Note that .NET adds the Web service with a default name of localhost: you’ll need to change the name to wUserObject. (Remember that all Web services are named after their corresponding business object, plus a “w” prefix.)

Normally you only need to add the Web service reference. However, because you declared earlier that the Web service implements the IUserObject interface, you must implement that interface in the client Web reference. Why? Because .NET “drops” the interface implementation when the Web reference proxy is created; however, you can add it back in by selecting “show all files” in Solution Explorer, and navigating all the way down the Web reference until you see the file Reference.cs. You’ll see the declaration of public class wUserObject that derives from the SoapHttpClientProtocol class, but does not implement IUserObject. Simply add a reference to the Masonry.Interfaces namespace, and add the reference to IUserObject at the end of the class declaration.

Author’s Note: Important! If you modify the Web service project, you must update the Web reference. Doing so will overwrite the manual change you made to Reference.cs, so you’ll need to add it back in. (Some developers may choose to bypass the .NET step of adding a Web reference, and instead opt to generate the Web service proxy with their own custom tool.)

Finally, you can make the connection to the outside world to validate the user ID and password. Because the user may connect via Web services or remoting, you want to abstract out that behavior to a function that returns a generic connection object that you can use for either purpose. Listing 7 demonstrates the heart of the CG Framework: a class that returns a generic connection object. It contains the mechanics of most connection attempts that occur in the client piece. This is an example of the factory design pattern, which creates an object that subsequent classes will instantiate.

Listing 8 provides a full example that uses this connection class. Listing 7 and Listing 8 represent the most critical pieces of code in the client piece.

  • Declare an object (oUserObject) of the interface being used for the particular module (IUserObject).
  • Create an instance of the ClientRemoteAccess class (see Listing 8) as oRemoteAccess. This class creates an object reference to either the Web service or the remoting server object, depending on which connection the user chose at startup.
  • Set the appropriate connection properties for either the remoting interface and/or the Web service reference.
  • Call the function GetAccessObject in ClientRemoteAccess. The function will create a new instance of a connection object, using the URL or TCP address that’s associated with the connection that the user selected at login.
  • Cast the return object to the interface. You can now use the return object to directly communicate with the back-end business objects.

Again, you can use these four steps for most connections to the server: this represents the mechanics of communicating with the back-end, and demonstrates the value of interfaces.

Tip 5: Building a Data Access Layer
The application will use stored procedures to retrieve, add, and update data. You must develop a function to build a connection associated with the requested database, and build necessary functions to execute stored procedures. This ties together Tip 2, Tip 3, and Tip 4 to show a complete “wire-to-wire, and back” process.

The CG Framework provides a base data access class (CGS.DataAccess.cgsDataAccess) for many common database tasks. These tasks include building connection strings, executing stored procedures with parameters, managing transactions, and supporting multiple databases. As stated in Tip 1, developers can build their data access layer by inheriting from cgsDataAccess.

As a basic introduction, the ValidateUserID function in the Masonry.DataAccess.UserDA class (see Listing 9) provides an example of using methods that the base data access class exposes. The primary base method you’ll use in ValidateUserID is SPRetrieveData. Let’s take a few minutes and walk through the parameters for this method.

The first parameter (required) is the database key. The base data access class utilizes the database key (sent by the client piece) to build the appropriate connection string. It does this by reading an XML file (Database.xml, see Listing 10) on the server that contains the connection information for each possible database key. As stated earlier, this functionality allows a company to add database connections without the need for a software update.

The second parameter is the name of the stored procedure, and it is also a required parameter. The third and fourth parameters are optional. The third parameter is an ArrayList of the parameters to be passed to the stored procedure. The fourth parameter is the timeout factor. The developer should only pass the collection of parameters if necessary, and they only need to pass a timeout factor if the specified stored procedure is intensive and requires more time than the default command timeout provides.

While not included in the listing example, SPRetrieveData contains an additional parameter for typed datasets, so that result sets can be specifically named. By default, result sets are named “table,” “Table1,” “Table2,” etc. Developers who have constructed typed datasets and wish to use specific names for each table in the corresponding result set will find the default names for the result sets frustrating. Fortunately, the CG Framework deals with this by permitting the developer to pass a reference to the typed dataset as a parameter. SPRetrieveData will utilize the TableMappings function to name each result set based on the corresponding table name in the typed dataset.

Author’s Note: Developers must keep stored procedure result sets in sync with the corresponding tables in the typed dataset!

Tip 6: Handling Lookup Tables that Change Periodically or Infrequently
The application uses several lookup tables (list of items, different business codes, and so on) that change once a month, or maybe a few times a year. You would waste bandwidth by constantly querying these tables to populate pick lists; however, you cannot go to the other extreme and hard-code these values in the software. I’ll show you how to implement a process that will retrieve these tables at initial start-up, save them locally as XML tables, load them back into datasets at subsequent start-up, and compare datetime stamps on the server for any new insertions/modifications.

The demo application uses a pick list of possible Material Types (Slate, Fuel, Stone, Rock, etc.) for each construction job. The company might add a new type each month. To avoid retrieving the entire list every time, the system can perform a check at various times to see if any new types have been added by using a DateLastUpdated column. For instance:

  • When the user loads the application for the first time, run a stored procedure to return the material type table to a client-side dataset.
  • When the user closes the software, save the client-side dataset to an XML file on the user’s hard disk.
  • When the user re-loads the software, read the XML table back into memory, and sort on DateLastUpdated (descending order) to obtain the most recent chronological date.
  • Call a stored procedure to return all rows in the Material Type table with a more recent DateLastUpdated value than the most recent DateLastUpdated on the client end.
  • Use the ADO.NET Merge function to combine the new incoming rows from Step 4 into the existing in-memory table. Since some of incoming rows from Step 4 could be updates and some could be insertions, you must define primary keys for the Merge function to work correctly.

Listing 11 contains a stored procedure for steps 1 and 4. The procedure contains a parameter for DateLastUpdated. Note the parameter is initialized to NULL, and the query utilizes the T-SQL COALESCE function. This allows you to execute the procedure with no parameters and return all rows, and also execute the procedure with a date and return only those rows that have been added/modified more recently than the parameter you supply. The calls to execute the stored procedure would be as follows.

   DsMaterials = new Masonry.DataSets.DsMaterials();   DsMaterials = oMaterialObject.GetData();

You can do steps 2 and 3 on the client side.

   // Step 2   DsMaterials.WriteXml("Materials.xml",      XmlWriteMode.WriteSchema);      // Step 3   DsMaterials.ReadXml("Materials.xml",      XmlReadMode.ReadSchema);      // Create view, sort on Date desc, look at top row   DataView Dv = new DataView(DsMaterials.Tables[0]);   Dv.Sort = "DateLastUpdate DESC";   DateTime dMaxLastUpdate =      Convert.ToDateTime(Dv[0]["DateLastUpdate"]);

After you retrieve the most recent last update in step 3, you can call the same function you called in Step 1, this time with the parameter. You can call it once at the beginning of the application, or anytime you want to check for updates to the Materials file.

   DsNewData = new Masonry.DataSets.DsMaterials();   DsNewData = oMaterialObject.GetData(dMaxLastUpdate);

If DsNewData contains any data, the last step is to merge the new data into the existing data.

   DsMaterials.Merge(DsNewData);

Tip 7: Building a Statusbar Control on the Main Screen
You want to display specific system information in a status bar at the bottom of the screen.

The CG Framework contains a base MDI parent class. A developer can inherit from a base form (CGS.Winforms.Forms.cgsFrmMDIParent) to create a main MDI parent screen.

The cgsFrmMDIParent form contains a StatusBar control with three pre-designed panels to display the current user, connection, and system info (see Figure 3). The form contains a virtual method called SetStatusBar (see Listing 12) that a developer can accept as the default, or override.

?
Figure 3: Status bar at the bottom of the main screen.

Tip 8: Extending Windows Forms Controls
You want to subclass the main Windows Forms controls to extend behavior and establish a default appearance.

One of the first recommendations when developing with a new tool is to subclass the UI controls. Many organizations and clients have specific requirements about user interfaces, such as data entry behavior and colors. By abstracting the desired functionality and appearance into a set of subclasses, developers can deliver a solution with a consistent look and feel.

Unlike other development environments, a developer cannot subclass the Windows Forms controls visually?you can only subclass them through code. While some initially view this as disconcerting, the reality is that one spends very little development time subclassing. Additionally, the process is a good exercise in working with the language.

The CG Framework contains a set of starter classes in the CGS.Winforms.Controls namespace, as cgsControls. Let’s take a look at a subclassed version of the label control.

   public class cgsLabel: System.Windows.Forms.Label   {      override public Font Font      {         get { return base.Font; }         set { ; }      }         public cgsLabel()      {         base.Font = new  Font("Verdana",8);      }   }

This is a very simple example that merely sets the font to Verdana. However, it differs from the most commonly seen method for setting the default font.

   // Don't use this example   public cgsLabel()   {      this.Font = new  Font("Verdana",8);   }

This approach is problematic?not so much in concept as in practice in VS.NET. In the latter example, when you drop an instance of this label on a form, and you set other design-time properties of the label (such as Caption, Location, and so on), the Windows Forms designer generates code in the InitializeComponent method. This generated code includes the value for the font property.

Now suppose that your company decides to use Tahoma or a different point size. Changing the font property above will NOT change the font on all forms where the label subclass was used, because the forms use the generated code the first time any other property of the label was modified. The only workaround is to highlight all the label controls, bring up the property sheet, go to the Font control, right-click, and choose “Reset.” This is (in this author’s opinion) a sub-standard way of addressing the situation. Hopefully future versions of Visual Studio .NET will address this behavior.

For now, you can address this by setting the base font in the constructor and blocking the Set statement. This prevents the Windows Forms designer from generating code, and ensures that the application will always derive the font from the subclass. (This will have the opposite effect?because the Set statement is blocked off, a developer cannot override the font of a label that’s used on the control. If you need this capability you should modify the behavior to support this).

Tip 9: A Generic Solution for Data Binding
cgsControls also offers capabilities to simplify data binding. Some developers have difficulty implementing data binding across different controls. Data binding for a TextBox differs from a ComboBox, and for a CheckBox. Another challenge is that data binding sometimes requires a measurable amount of code.

The CG Framework simplifies this through a binding interface.

   public interface IBoundControl   {      void BindControl();   }

Once again, here is where interfaces become valuable. As I discussed in prior tips, an interface is an agreement?that any class/control that implements the interface will contain a specific set of functions. In the case above, all the subclassed controls in the cgsControls class will implement the public interface IboundControl.

   public class cgsComboBox :    System.Windows.Forms.ComboBox , IBoundControl      public class cgsTextBox :    System.Windows.Forms.TextBox , IBoundControl      public class cgsDateTime :    System.Windows.Forms.DateTimePicker, IBoundControl      public class cgsCheckBox :    System.Windows.Forms.CheckBox, IBoundControl

The next step is to add some properties to each control that implement the IBoundControl interface. These properties will define the table and column to which each control will data bind. Most controls (CheckBox, TextBox, DateTimePicker) require only two properties, one for the data table and one for the data column.

   private DataTable _DtSource;   public DataTable DtSource   {      get {return _DtSource ;}      set {_DtSource  = value; }   }   private DataColumn _DcSource;   public DataColumn DcSource   {      get {return _DcSource ;}      set {_DcSource  = value; }   }

The ComboBox, however, requires additional properties to display values from a foreign table:

  • An additional data table property if the ComboBox displays descriptions from a foreign table (DtForeignTable, which becomes the ComboBox’s data source)
  • A data column property for the column being displayed (DcDisplayMember, as DisplayMember)
  • A data column property for the column from the foreign table that maps to the actual data column source (DcValueMember, as ValueMember).

In many cases, DcValueMember and DcSource will be the same (e.g. a classcode in a Child table may also be called classcode in the parent description table), but that may not always be the case. The database schema may use ‘PK’ and ‘FK’ conventions, thus the support for two properties.

Next, controls that implement the IBoundControl interface must contain a function called BindControl. Here is one of the great beauties of interfaces: the BindControl function for each control can be different, based on the type of control. This is critical because each control is bound differently. The TextBox and DateTimePicker controls are bound as follows.

   public void BindControl()   {      if(this._DcSource != null &&         this._DtSource != null)      {         this.DataBindings.Clear();         this.DataBindings.Add("Text",         this._DtSource,         this._DcSource);      }   }

The BindControl function for the checkbox differs because the binding applies to the checked property, not the text property. The checked property returns a Boolean that can be bound to a corresponding Boolean data type. The function for the ComboBox control also differs because the function must set the additional properties for the foreign table, and the display/value member.

In addition, the TextBox class in cgsControls also contains code to display currency/decimal data in the correct format.

So the final question you may be asking is: “OK, so I’ve set binding properties… What calls the BindControl function?” If all data-bound controls reside on a common entry container or form, you could, as one option, place the following code in the common form.

   protected virtual void UpdateBindings()   {   // 1) loop through each control   // 2) if the control implements the interface,   // 3)  call the control's BindControl function    foreach(Control oControl in this.Controls)      if(oControl is cgsControls.IBoundControl)         ((cgsControls.IBoundControl)oControl).         BindControl();   }

Tip 10: Integrating Filter Capabilities with Data Maintenance Functions
The application must allow users to retrieve a list of data according to runtime criteria (such as jobs entered/updated between two dates, jobs for a set of customers, jobs exceeding a dollar threshold). You must develop a user-friendly interface screen for users to make filter selections and present the results in a screen that users may view, navigate through, print to Excel, etc. This tip presents a generic form class and form class manager for data maintenance functions.

?
Figure 4: Example of data maintenance screen.

The CG Framework contains a base data maintenance form named cgsFrmDataMaintenance in the cgs.Winforms namespace. You can use this form to automate many instances where users look up data by criteria on the first screen (see Figure 4), view result sets on the second screen, add/edit/delete data on the third screen, and even view the history of changes to data (see Tip 13 for more on viewing audit trail data). The result set portion integrates the DataGrid that I presented in the November/December 2004 issue of CoDe Magazine. I’ve enhanced the DataGrid so that you can export the contents to Excel. The data maintenance screen utilizes the data binding in the previous tip.

The data maintenance form contains buttons for selection criteria, viewing result sets, and adding/editing data. A developer can design containers (user controls) to drop onto this form: the form manages the display of each container by looking for containers that implement one of three different interfaces (ICriteriaContainer, IResultContainer, and IEntryContainer). To simplify the creation of new containers to meet your data maintenance needs, the CG Framework supplies three base containers in cgs.Winforms.Containers that implement each of these interfaces (cgsContainerCriteria, cgsContainerResult, and cgsContainerEntry).

Figure 4 shows a basic example of the maintenance form. The active criteria panel displays a very simple criteria screen. The developer can design any criteria container, inheriting from cgsContainerEntry. The interface contains a function called GetCriteria, which you can override to read the criteria selections. The developer can then design a results screen that inherits from cgsContainerResult, using the function ShowCriteriaResults. Finally, the developer can design a data entry/update container that inherits from cgsContainerEntry, using the function ShowDetailData. An application could contain several maintenance forms, all using the same general mechanics and all containing the same look and feel.

Many functions in the Common Ground Framework are defined as virtual-allowing a developer to extend or override.

This type of consistency greatly helps users.

The criteria, results, and data buttons automatically look for containers on the form that implement the corresponding interfaces. Because of this, all a developer needs to do is define each of the three specific containers, drop them on an instance of a maintenance form based on cgsFrmDataMaintenance, and run the form. Once again, the power of interfaces!

Again, the base maintenance form contains stub virtual functions for each button: this allows you to implement your own code for retrieving result sets, adding data, and so on. The Word document in the zip download contains a list of all methods you can tap into.

Tip 11: Server-side and Client-side Business Objects
A development team often must decide where to implement business rules. For performance, or simply when no valid reason exists to go back to the server, some business objects may reside in the client piece. Automatic calculations during data entry represent one example.

The namespaces in Table 2 include business objects for both the client and server pieces. During the technical design phase of a project, the development team must decide where to implement business rules. Client-side business objects are a perfectly valid approach, so long as they remain separate from UI classes.

One example is a calculation of an extended amount during data entry. The user may change quantities and prices several times during data entry: a round-trip to the server to recalculate and display the total would be slow and inefficient. Therefore, developers should construct a client-side business object and call it at the appropriate times.

   // Developer may chose to pass the entire datarow,   // or simply the quantity and unit price values   private decimal CalculateTotal(DataRow DrDetail)   {      decimal nTotal = 0;      nTotal = Convert.ToDecimal( DrDetail["Qty"])         * Convert.ToDecimal( DrDetail["UnitPrice"]);      return nTotal;   }

Tip 12: Baker’s Dozen Potpourri – Miscellaneous Uses of ADO.NET
Optimal use of database stored procedures should normally minimize the need for heavy processing in other layers of the application. However, there are instances where a developer needs to munge through several sets of data to produce a result. Here are a few mini-tips that demonstrate the more powerful capabilities in ADO.NET.

If you’ve used ADO.NET you know that it does not have the full capabilities of a SQL language. However, a combination of the more advanced capabilities of ADO.NET, along with a little “elbow grease,” can usually get the job done.

For example, suppose a user is looking at a set of orders, and wants to filter on orders where the Account name contains the word “National.” So long as a relation exists between the orders table and the account table, a developer can reference parent columns in a RowFilter as follows.

   MyDataSet.Relations.Add("RelName",      DtAccounts.Columns["AcctID"],      DtOrders.Columns["AcctID"],false);   System.Text.StringBuilder sbRowFilter = new       System.Text.StringBuilder();   string cAcctName = "National";   sbRowFilter.AppendFormat("Parent(RelName) like       '%{0}%'",cAcctName);   DtOrders.DefaultView.RowFilter = sbRowFilter;

While it’s a simple property, some people (myself included!) get confused by the syntax for creating a primary key.

   // Untyped dataset   DtMaterials.PrimaryKey = new DataColumn[] {       DtMaterials.Columns["PrimaryKey"]};   // Typed dataset   DtMaterials.PrimaryKey = new DataColumn[] {       DtMaterials.PrimaryKey};

A developer may need to create and maintain multiple views of a DataTable?perhaps a different Sort and RowFilter for each view.

   // Multiple DataViews — the developer can create   // two different dataviews on the same table, and   // then bind them to two different datagrids   DataView dv1 = new DataView(DtMaterials);   dv1.Sort = " amount Desc ";   dv1.RowFilter = " AccountType = 1 ";   myDataGrid1.DataSource = dv1;   DataView dv2 = new DataView(DtMaterials);   dv2.Sort = " acctname Desc ";   dv2.RowFilter = " amount > 5000 ";   myDataGrid2.DataSource = dv2;

Tip 13: Implementing Basic Audit Trail Capability into an Application
How many times have you heard someone ask, “Who changed this data, and when?” Audit trail functionality is a vital component in many database management systems. This tip introduces a generic form to show audit trail data to the user.

In my T-SQL article that appeared in the February/March issue of CoDe Magazine, I presented some update trigger code to write changes to an audit trail log. The CG Framework extends this by providing a base form class (FrmAuditTrail) for viewing audit trail changes. Figure 5 shows a basic example of audit trail history for a particular record.

?
Figure 5: Example of audit trail history screen.

The base data maintenance form back in Figure 4 contains an option to view the audit trail form for the current record, by performing the following.

   cgsFrmAuditTrail oAuditTrailForm = new       cgsFrmAuditTrail();   oAuditTrailForm.cTableName =       this.GetPrimaryTable();   oAuditTrailForm.nPrimaryKey =       this.GetCurrentPrimaryKey();   oAuditTrailForm.ShowAuditTrail();

The download project contains a script to generate the entire SQL database. The database includes the necessary tables for the audit trail classes. This includes an audit trail table (AuditTrail), a user table (UserName), a data dictionary to store English-like names for each column (MasonryDataDictionary), and update trigger code to insert rows into AuditTrail.

Closing Thoughts
You can pull the entire project, as well as the script for generating the database, from www.commongroundsolutions.net. The zip file contains a Word document with release notes on all functionality in the CG Framework.

I hope this article provides some tips for developers who are learning .NET. There are many things in the framework that are not covered in this article, such as incorporating Crystal Reports, as well as some of the other capabilities in the base data access class. The release notes (Common Ground Framework.DOC) lists all the capabilities in the framework.

There are also many opportunities to improve the framework, such as encrypting the XML tables, retaining the selected connection at sign-on, and even adding some intelligence to the sign-on process to automatically determine which connection to use, based on the user’s current IP address.

If you have suggestions or ideas you’d like to share, I’d love to hear from you. Feel welcome to contact me. You can find the entire source code online at www.commongroundsolutions.net.

This is an ongoing project, so there may be a few enhancements from time to time. Check the release notes Word document for details.

Share the Post:
Share on facebook
Share on twitter
Share on linkedin

Overview

Recent Articles: