Monday, February 18, 2008

VISITOR Design Pattern

Copyright © 2008, Steven E. Houchin

I ran into an architectural problem after I developed a generic library class, SQLDbConn, that connects to a SQL database given a connection string. It implements an interface IDbConn that can also be used to implement a similar class for MS Access or other databases. An instance of SQLDbConnis created by the application via a Factory function:

static public IDbConn MakeDatabaseConnFactory
(DatabaseConnType eType)

After a databse connection is made through this "Connection" object, the nuts and bolts of accessing the database and retrieving rowsets is done using an application-specific "Model" object, defined by an IDbModel interface. For a SQL connection, that would be an instance of the SQLDbModel class.

Here's the design problem I encountered: the application shouldn't have any hard-coded knowledge of what kind of database it is using. It only has a reference to its IDbConnobject after calling the Factory function above. So, it needs to instantiate its Model object without knowing it will actually be a SQLDbModel object. I don't want to put the logic in SQLDbConnto create it, because that Connection class is generic, so shouldn't know about the application-specific Model class. I also don't want to simply create another Factory function - though that would work - because I have already created a database-type aware object - the Connection object - that knows what kind of database is in use. So, how do I get the generic Connection object (SQLDbConn) to create the specific Model object for me, and still remain application neutral?

The solution I came up with uses the VISITOR design pattern. I added the VISITOR pattern's accept method to the IDbConn interface:

bool AcceptVisitor(IDbConnVisitor visitor);

This allows my application to generically extend the connection object's functionality after its instantiation. The IDbConnVisitor interface referenced above specifies a visitmethod for each type of supported database, but does not dictate what that method does (they could order sandwiches from the deli):

public interface IDbConnVisitor
{
   bool VisitMsAccess(IDbConn dbConn);
   bool VisitSqlOle(IDbConn dbConn);
   bool VisitSqlDa(IDbConn dbConn);
}

Since each instance of a Connection object (like SQLDbConn) knows what kind of database it is, it can call its appropriate visit function, without knowing what that function actually does. For the SQLDbConnclass, its acceptfunction looks like this:

public bool AcceptVisitor(IDbConnVisitor visitor)
{
   try
   {
      return visitor.VisitSqlDa(this);
   }
   catch (System.Exception excp)
   {
      _errorText = excp.Message;
   }
   return false;
}

An MS Access Connection object's AcceptVisitor would similarly call VisitMsAccess.

My application then defines an implementation of IDbConnVisitor via a new class, such as CreateDbModelVisitor, whose job is to provide functions that will instantiate the appropriate Model object for any of the supported database types. Inside, it has a property of type IDbModel that the visitor methods instantiate:

public class CreateDbModelVisitor : IDbConnVisitor
{
   ...
   protected IDbModel _model = null;
   ...
   public bool VisitMsAccess(IDbConn dbConn) { ... }
   public bool VisitSqlOle(IDbConn dbConn) { ... }
   public bool VisitSqlDa(IDbConn dbConn) { ... }
   public IDbModel Model { get { return _model; } }

}

For the SQL database, the visitmethod implementation within CreateDbModelVisitoris as follows:

public bool VisitSqlDa(IDbConn dbConn)
{
   _errorText = "";
   try
   {
      if (dbConn.IsConnected)
      {
         _model = new SqlDbModel(dbConn);
         return true;
      }
      else
      {
         _errorText = "Database has not been connected."
      }
   }
   catch (System.Exception excp)
   {
      _errorText = excp.Message;
   }
   return false;
}

Putting it all together, the application would do the following:

IDbConn dbConn = MakeDatabaseConnFactory(config.DbType);
IDbConnVisitor dbVisitor = new CreateDbModelVisitor();
if (dbConn.AcceptVisitor(dbVisitor))
{
   IDbModel model = dbVisitor.Model;
   ...
   // make Model calls here to access database
}

Note that the only thing the application knows of its database type is a value that it picks up from a config file.

By using a VISITOR design pattern, the database-specific Connection object (IDbConn), which is supposed to be application neutral, can execute an application-specific function (via AcceptVisitor) for that particular type of database, without knowing the details of what that function does. It just calls the accept method, and that's that.

Wednesday, February 13, 2008

Fun with DataGrids

Copyright © 2008, Steven E. Houchin

Recently, I was implementing a DataGrid for an ASP.NET application. ASP.NET is not usually my forte, so I was once again in learning mode.

The first obstacle I stumbled onto was that a lot more columns were displayed when the page came up than what I intended. My DataSource was an ArrayList of objects, which represented information culled from the database. In the page's Page_Load function, I had code that created each column explicitly, using ButtonColumn and BoundColumn objects. The extraneous unwanted columns that displayed were duplicates of the DataSource object's properties.

I discovered the problem was with the AutoGenerateColumns property of the DataGrid. Its default value was True, which causes a DataGrid object to automatically create columns (duh!) identical to the DataSource object's properties ... just what I was seeing. Setting this to False prior to assigning the DataSource value fixed that.

The next obstacle was that my ItemCommand handler wouldn't fire when I clicked on a ButtonColumn item in the list. Worse, my grid completely disappeared afterward. Searching around on the web revealed many others who also had this problem, but nobody seemed to have a good answer. There were some postings about EnableViewState related to this, but they didn't help.

Eventually, I discovered that when the Click event caused a PostBack to the page, the DataGrid reverted back to its initial empty state. My Page_Load code populated the DataGrid only when the call was not a PostBack. Armed with this insight, I changed Page_Load so a PostBack call only decides where to get the data: a database call for non-PostBack versus session variable on PostBack. The code now creates and binds the DataGrid columns each time through Page_Load, since my columns are created on-the-fly. The fixed code follows:



ArrayList allUsers = null;
if (IsPostBack)
{
   // restore array of all users
   allUsers = controller.GenericPageData as ArrayList;
}
else
{
      // get an array of all users as UserProfile objects
      controller.Model_GetAllUserInfo(ref allUsers);

      // save the user data in the controller
      controller.GenericPageData = allUsers as object;
}

if (null != allUsers)
{
   // set the array of users as the data source
   dgUsers.AutoGenerateColumns = false;
   dgUsers.DataSource = allUsers;

   // configure the columns
   ButtonColumn colName = new ButtonColumn();
   colName.DataTextField = "FullName";
   colName.HeaderStyle.BorderStyle = BorderStyle.Outset;
   colName.HeaderStyle.BackColor = Color.Ivory;
   colName.HeaderText = "Full Name";
   colName.CommandName = "View";

   BoundColumn colLogin = new BoundColumn();
   colLogin.DataField = "LoginName";
   colLogin.HeaderText = "Login Name";
   colLogin.HeaderStyle.BorderStyle = BorderStyle.Outset;
   colLogin.HeaderStyle.BackColor = Color.Ivory;

   BoundColumn colId = new BoundColumn();
   colId.DataField = "KeyId";
   colId.HeaderText = "Record Id";
   colId.HeaderStyle.BorderStyle = BorderStyle.Outset;
   colId.HeaderStyle.BackColor = Color.Ivory;

   // add the columns to the datagrid
   dgUsers.Columns.Add(colId);
   dgUsers.Columns.Add(colName);
   dgUsers.Columns.Add(colLogin);

   dgUsers.EnableViewState = false;
   dgUsers.DataKeyField = colId.DataField;
   dgUsers.DataBind();

   // set up the click and edit event callbacks
   dgUsers.ItemCommand +=
      new DataGridCommandEventHandler(this.UsersGrid_OnClick);
   return;
}