in

ASP.NET Weblogs

brady gaster

yadnb

March 2004 - Posts

  • Awesome Mind-mapping or Visio-like Component

    Thanks to Denny, I've received an awesome pointer to Northwoods, makers of an awesome WinForms (and WebForms) product called GoDiagram that makes developing Visio-like interfaces a SNAP! This is about the most awesome control series I've seen. What's even better is that the samples contain a “Web Walking” program that you can aim at your URL's to find all the other places you're linking to. How awesome! A VERY cool product indeed, worth checking out.
  • excellent at winforms? give me some help!

    Think Visio. You put two boxes (let's say WinForms Panels, 'cuz that's what I'm using) onto a workspace (yet another Panel). You click one of them and you begin to draw a line. You move your mouse over to another panel and click it. “Click!” There is now a line connecting those two panels. And when you move the panels around on the workspace, the line stays connected to both Panels and gets longer/short depending on how you move the Panels around on the workspace.

    Anyone know how to pull this off? Got links? Got samples?

  • StarBrite 86's iPod-lookalike-for-PPC!

    Did anyone get a copy of this!?!?!? But, I just sat down to download it!
  • no help from the ui?

    Jeff actually has a really good point on turning off IntelliSense for a day. I tried this during some classes I've taught - namely the .Net Framework class, which discusses things like assembly linkage, compiler switches, and that sort of fun stuff. The interesting thing was how the students all ended up relatively amused with their apparent lack of syntax. Even those who had gotten to learn C# tended to be challenged at times. I think its a good challenge, Jeff.

    Now if I didn't have all of these deadlines that IntelliSense makes so much easier to reach.

  • MT : Problem #1: Requirements and Processes Change - Adapt!

    The second problem plagues me. I know it plagues us all. How many times have you heard yourself think - or even say out loud - how was this missed? Why am I retrofitting my code now for this? How in the heck did someone forget this?

    This problem is so resident in so many places, projects, and situations. My current project, for instance, had requirement changes days - hours - before deployment. Why? Changes in process, restructuing, that sort of thing. Business changes. The processes which drive it evolve, just like the real world around us. The applications we write should adapt as the world does.

    This is the workflow part of the essay. During this section, I'll expose you to the workflow-specific areas within MT, and point out how this idea can limit rework later on. I'll make use of some typical OO concept to demonstrate this stuff, and remind you - the more work you put up front, the less you do in the end.

    So let's get started - the first part of the workflow portion of MT is the IProcessStep interface. Here's the code for it:

    using System;
    
    namespace MetaTechture.Workflow
    {
     /// <summary>
     /// Represents the fundamental building block of the entire workflow model. 
     /// The IProcessStep interface is the basic component of a workflow process, as
     /// it represents a single step in any process. This interface depicts 
     /// the basic structure of any step in a larger process that an 
     /// application must execute. 
     /// </summary>
     public interface IProcessStep
     {
      /// <summary>
      /// Any process step must contain independent functionality 
      /// that it shall execute. Within this method, the functionality 
      /// should reside. 
      /// </summary>
      /// <remarks>
      /// The point of this class is simple. As an object moves through 
      /// any process, the object's state may alter or be altered as 
      /// a result of the actions within the step. Any class which 
      /// executes multiple steps should not only account for the step
      /// itself, but the change to the object's state as the 
      /// step occurs. To persist state, it is good practice for the 
      /// object to return itself from the execution. In essence, 
      /// the next step can adapt to the new state, and the process 
      /// can continue.
      /// </remarks>
      /// <returns></returns>
      void Execute(ProcessEngine engineContext);
    
      /// <summary>
      /// Within any process, a step can have adverse affects on the overall
      /// execution of the process. As each step occurs, there is the 
      /// potential for error, potential for the process to complete, 
      /// and so on. As each step occurs, this property should be
      /// set so that the engine knows whether to continue in the 
      /// process or to terminate it's execution after this step. 
      /// </summary>
      bool HaltsProcess { get; }
     }
    }
    

    This interface's sole purpose is to mark a class as a “step” in a longer “process.” I'll make use of this idea later in a relatively simplistic process to exemplify the idea. For now, think of it from two points of view.

    Processes are Recipes with Little Ingredients (but room is left for variance)
    Everything has to be done with steps. You get from your house to the car, you take steps, then open the car door, then shut it, and on and on. You make a PB&J, you have to do things in your own certain order (some of us like the PB before the J, others vice versa, for instance). With this notion in mind, I'll move forward and point out the next part of the workflow idea - the process engine. To begin with, the process engine contains an internal Hashtable that I'll use in much the same way that the HttpCache is used. As a process executes other objects within a subsystem will be affected in appropriate measures. For this, we'll use the Hashtable collection:

    using System;
    using System.Collections;
    namespace MetaTechture.Workflow
    {
     /// <summary>
     /// Runs the collection of steps for a given process against a particular 
     /// object instance. 
     /// </summary>
     public class ProcessEngine
     {
      Hashtable ctx;

    When the ProcessEngine class is constructed I'll get the Hashtable ready for usage by declaring it as a new object. To boot, I'll add a Facade around the internal collection by adding an AddToApplicationContext method and an indexer.

    /// we won't allow construction, as the static Run method will be enough.
      public ProcessEngine()
      {
       ctx = new Hashtable();
      }
      public int AddToApplicationContext(object key, object value)
      {
       ctx.Add(key,value);
       return ctx.Count;
      }
      public object this[object key]
      {
       get { return ctx[key]; }
       set { ctx[key] = value; }
      }

    This way, Process Step classes can re-use objects that have been stashed in the Process Engine's execution layer (as you'll see later on, this allows the steps interact with one another in much the same way we work with other caching methodologies).

    Processes Should be Able to Stop Themselves
    In the final stages of the ProcessEngine class, I'll add logic that will once again use the configuration capabilities in .Net to load in the steps for a process. This stage of the code creates instances of classes that inherit the IProcessStep interface, and will add these items to the overall process in the order they appear in the configuration file....

    Which raises an interesting point! Suppose, from our earlier example of making a PB&J, we have a requirement stating the order things should happen -

    • Step 1: Get the Bread
    • Step 2: Apply Peanut Butter
    • Step 3: Apply Jelly

    And you write code implementing this functionality - or rather, you hard-code a method executing this functionality. Then, one day, the world turns upside-down and you learn that in these new days of jellyloving sandwich-eaters, people like their jelly applied before the peanut butter. Guess what? Your method(s) now need to be rewritten to accomodate the new functionality. Sure, each method (GetBread(), ApplyPb(), and ApplyJelly(), if you've been a good refactorer up to this point) may be wonderfully mutually exclusive, but since you're executing these according to the original specification, you now need to edit, recompile, and re-deploy your existing code. With the workflow resident in MT (via .Net's configuration wonders), this problem will vanish!

    So back to the process itself! At this point, we've covered everything except the Run() method, which basically uses the configuration classes shown below to load and Execute() each step. Take notice esepcially here of how the method performs a check to see if each step of the process is supposed to Halt the process. In this implementation, you can see the beginnings of a control - should one step cause problems in the process - or better yet, complete it earlier than expected - cancel everything from there on out. This leaves the decision-making logic up to each step in the process. Here's the code:

    /// <summary>
      /// Executes the collection of providers' functions. 
      /// </summary>
      public void Run(string process)
      {
       // get the name of the type of provider we're supposed to use
       string typeName = String.Empty;
       ProcessEngineManagerSettings settings = 
        ProcessEngineManagerSettings.GetSettings(process);
        
       for(int i=0; i<settings.TypesInProcess.Length; i++)
       {
        typeName = settings.TypesInProcess[i];
        // create an instance of that type
        Type t = Type.GetType(typeName);
        // now create an instance of that type
        object provider = null;
        provider = Activator.CreateInstance(t);
        // make sure it's an implementor of the proivider interface
        IProcessStep step = provider as IProcessStep;
        if(step != null)
        {
         // perform it's functionality
         step.Execute(this);
         if(step.HaltsProcess)
          break;
        }
       }
      }
     }
    }
    

    Not too bad - just create an instance of each class listed in the custom configuration section, in the order we find them all. To finish up this discussion, take a look at the configuration classes, which are listed below:

    using System;
    using cs = System.Configuration.ConfigurationSettings;
    using System.Xml;
    namespace MetaTechture.Workflow
    {
     /// <summary>
     /// The configuration reader
     /// </summary>
     public class ProcessEngineManagerConfigurationReader : System.Configuration.IConfigurationSectionHandler
     {
      public object Create(object parent, 
       object context, 
       XmlNode section) 
      {
       XmlNodeList lstActions = section.SelectNodes("//ExecutionProvider");
       return new ProcessEngineManagerSettings(lstActions);
      }
     }
     /// <summary>
     /// The settings class, which contains the type name to be loaded.
     /// </summary>
     public class ProcessEngineManagerSettings
     {
      public readonly string[] TypesInProcess;
      static string activeProcess = String.Empty;
      internal ProcessEngineManagerSettings(XmlNodeList lst)
      {
       string[] types = new string[0];
       for(int i=0; i<lst.Count; i++)
       {
        if(lst[i].Attributes["process"].Value.ToLower() == activeProcess.ToLower())
        {
         string[] tTmp = new string[types.Length+1];
         Array.Copy(types,0,tTmp,0,types.Length);
         tTmp[types.Length] = lst[i].Attributes["type"].Value;
         types = tTmp;
        }
       }
       TypesInProcess = types;
      }
      
      /// <summary>
      /// Build the name of the path to the nodes we'll run. 
      /// </summary>
      /// <returns></returns>
      public static ProcessEngineManagerSettings GetSettings(string process) 
      {
       string section = "ProcessProviders";
       activeProcess = process;
       ProcessEngineManagerSettings b = 
        (ProcessEngineManagerSettings)cs.GetConfig(section);
       return b;
      }
     }
    }
    

    Once again making use of the Provider concept, we've assumed that, within the configuration file, a series of steps have been listed. These steps, when executed in appropriate order, complete a total process' execution. The XML below is a good example of how a process could be listed and executed within an application making use of the MT workflow concept.

    <?xml version="1.0" encoding="utf-8" ?>
    <
    configuration>

    <configSections>
    <section name="ProcessProviders" type="MetaTechture.Workflow.ProcessEngineManagerConfigurationReader, MetaTechture.Workflow" />
    </configSections>

    <!-- process-specific application settings -->
    <ProcessProviders>

    <!-- steps in various processes -->
    <ExecutionProvider type="MetaTechture.Workflow.BreadStep, MetaTechture.ConsoleClients" process="MakeSandwich" />

    <ExecutionProvider type="MetaTechture.Workflow.JellyStep, MetaTechture.ConsoleClients" process="MakeSandwich" />

    <ExecutionProvider type="MetaTechture.Workflow.PeanutButterStep, MetaTechture.ConsoleClients" process="MakeSandwich" />

    </ProcessProviders>

    </configuration>

    At this point, we've listed a few steps in the process section for a small application. This small little app will make a nice demonstration of using the following three classes - each of which representing or objectifying a small part of a larger process.

    using System;
    namespace MetaTechture.Workflow
    {
     /// <summary>
     /// The first step in the process.
     /// </summary>
     public class BreadStep : IProcessStep
     {
      public void Execute(ProcessEngine engine)
      {
       Console.WriteLine("Get the Bread");
      }
      public bool HaltsProcess
      {
       get
       {
        return false;
       }
      }
     }
     /// <summary>
     /// The second step in the process.
     /// </summary>
     public class PeanutButterStep : IProcessStep
     {
      public void Execute(ProcessEngine engine)
      {
       Console.WriteLine("Put PB on the bread");
      }
      public bool HaltsProcess
      {
       get
       {
        return false;
       }
      }
     }
     /// <summary>
     /// The second step in the process.
     /// </summary>
     public class JellyStep : IProcessStep
     {
      public void Execute(ProcessEngine engine)
      {
       Console.WriteLine("Put jelly on the bread");
      }
      public bool HaltsProcess
      {
       get
       {
        return false;
       }
      }
     }
    }
    

    For now, I'll save the more complex idea of working with the context for a later, more comprehensive example. The thing here is to notice - that each step of the process completes one small aspect of the larger, wholer process. In a sense, we've refactored the entire process of making a sandwich into seperate classes, thereby giving structure and shape to activity. In this way, even the process itself has been objectified and therefore, is extensible in and of itself. Should someone decide that we need to change the PB&J order, the re-coding is minimal - just swap two lines in the config file, and the whole application is altered on the fly. A process could be added to or removed in the same method. We'll make use of this heavily in the study at the end of the post series. For now, let's wrap it all up by taking a look at the console application which kicks off the whole MakeSandwich process.

    using System;
    using MetaTechture.Workflow;
    namespace MetaTechture.ConsoleClients
    {
     /// <summary>
     /// Summary description for Class1.
     /// </summary>
     class Class1
     {
      /// <summary>
      /// The main entry point for the application.
      /// </summary>
      [STAThread]
      static void Main(string[] args)
      {
       RunSandwichStepDemo();
       Console.ReadLine();
      }
      static void RunSandwichStepDemo()
      {
       ProcessEngine engine = new ProcessEngine();
       engine.Run("MakeSandwich");
      }
     }
    }
    

    Happy coding! Next up - a plugin model!

  • MT : Problem #3: The Generic DAL: The DalFactory and Provision

    Up to now, I've shown you the beginnings of some pretty simple interface implementation, which will yield a relatively flexible model for database access in application development lifecycles. As some of the comments have intelligently pointed out, there's some room for improvement; some exception handling would be nice, a little more flexibility inside the methods would most likely augment the model too. We'll leave that for later, or for reader input (yes, that's a hint - I'd love to see some enhancement on this stuff).

    Now, we'll create a factory class that will be used to actually hand out the DAL implementations as needed. This factory will consist solely of static classes, containing multiple overloads of the CreateDbLayer() method. The first of these overloads is relatively simple. Calling code simply says toe the factory “give me a DAL for this type of database” and the DAL does the rest of the work for us.

    using System;
    namespace GenericDal
    {
     /// <summary>
     /// Summary description for DalFactory.
     /// </summary>
     public class DalFactory
     {
      private DalFactory()
      {
      }
      /// <summary>
      /// Creates an instance of the type of DAL being requested. The provided type of object
      /// must implement the IDbLayer interface. 
      /// </summary>
      /// <param name="dataLayerType">The DAL Provider to be returned.</param>
      /// <returns></returns>
      public static IDbLayer CreateDbLayer(Type dataLayerType)
      {
       object o = Activator.CreateInstance(dataLayerType,false);
       if(o is IDbLayer)
        return (IDbLayer)o;
       else
        throw new Exception(o.GetType().ToString() 
         + " is not an implementer of IDbLayer and therefore cannot be returned from this method.");
      }

    In this overload, the factory simply activates an instance of the requested DAL and passes it back to the caller. Then, the caller can set the connection string, query the database for DataSet, read resultsets, whatever is needed. We'll add a second overload to this method now, but this time we'll go ahead and set up the connection string property too (may as well kill two birds with one stone, right?).

       /// <summary>
      /// Creates an instance of the type of DAL being requested. The provided type of object
      /// must implement the IDbLayer interface. Also sets the connection string for the provided
      /// instance's Connection property. 
      /// </summary>
      /// <param name="dataLayerType">The DAL Provider to be returned.</param>
      /// <param name="connectionString">The connection string to be opened.</param>
      /// <returns></returns>
      public static IDbLayer CreateDbLayer(Type dataLayerType, 
       string connectionString)
      {
       IDbLayer l = CreateDbLayer(dataLayerType);
       l.ConnectionString = connectionString;
       return l;
      }

    This time, an instance of the requested IDbLayer implementor is created and the instance's ConnectionString property set. In this way, we've prepared the implementor before ever sending it back to the caller. Now, the caller can begin performing database operations right away.

    Taking a Lesson From ODBC
    At this point, we've created a nice framework for accessing data. We've gotten around most of the common problems of connecting and executing commands, abstracted the process of working in a disconnected situation, and made the process of executing database commands relatively simplistic (maybe too simplistic for some!). Now, let's take this whole idea one step further and a little more real-world-friendly

    Pretty often in the enterprise development industry, we need to connect to more than one database at a time, or during the execution of a particular application. It would be nice if our DAL could accomodate this need. To do so, we'll borrow a practice from ODBC - the practice of accessing data sources by a given name, or key. To begin this discussion, take a look at the application config file for a client application I'll be demonstrating the construction of later on.

    <?xml version="1.0" encoding="utf-8" ?>
    <
    configuration
    >
      <configSections
    >
        <section name="DataProviders"
          type="GenericDal.GenericDalConfigurationReader,
            GenericDal"
    />
       </configSections
    >
       <DataProviders
    >
       <DataSource key="pubsSql" type="GenericDal.SqlServerDal" connectionString="Data   Source=(local); User Id=sa; Password=pass4Sql; Initial Catalog=pubs"
    />
       <DataSource key="nwindSql" type="GenericDal.SqlServerDal" connectionString="Data Source=(local); User Id=sa; Password=pass4Sql; Initial Catalog=Northwind"
    />
       <DataSource key="pubsOleDb" type="GenericDal.OleDbDal" connectionString="Provider=SQLOLEDB.1; Data Source=(local); User Id=sa; Password=pass4Sql; Initial Catalog=pubs"
    />
       </DataProviders
    >
    </
    configuration>

    In this example, we've instructed the client application to use three data sources - two running on SQL Server, onr running in some OLEDB space (also SQL Server, perhaps a poor example but the point is clear that we're using multiple connection strategies). Each DataSource element contains an attribute, key, which identifies each DataSource from the others aggregated in the configuration file.

    The next step is the authoring of a few configuration-related classes. The first of these will perform the requirements of actually reading the XML node from the configuration file.

    using System;
    using System.Configuration;
    using System.Xml;
    using cs = System.Configuration.ConfigurationSettings;
    namespace GenericDal
    {
     public class GenericDalConfigurationReader : IConfigurationSectionHandler
     {
      /// <summary>
      /// Creates an instance of the GenericDal settings object 
      /// from the custom configuration settings.
      /// </summary>
      /// <param name="parent"></param>
      /// <param name="context"></param>
      /// <param name="section"></param>
      /// <returns></returns>
      public object Create(object parent, 
       object context, 
       XmlNode section) 
      {
       GenericDalSettings settings = new GenericDalSettings();
       XmlNodeList lstSources = section.SelectNodes("DataSource");
       
       for(int i=0; i<lstSources.Count; i++)
       {
        XmlNode nodDataSource = lstSources[i];
        string key = nodDataSource.Attributes["key"].InnerText;
        string t = nodDataSource.Attributes["type"].InnerText;
        string connectionString = nodDataSource.Attributes["connectionString"].InnerText;
        settings.Add(key,t,connectionString);
       }
       return settings;
      }
     }

    To persist the settings that are read from the configuration file, we'll create a second class, GenericDalSettings, which will collect and store logical pointers to each of these data sources. In this way, any application has the added functionality of asking the GenericDalSettings class for a particular data source, which is then handed back to the calling application.

    // <summary>
     /// The settings class, which contains the type name to be loaded.
     /// </summary>
     public class GenericDalSettings
     {
      /// <summary>
      /// Hidden internal class used by the Settings class 
      /// to differentiate between providers as requested. 
      /// </summary>
      struct DalSetting
      {
       public DalSetting(string key, string connectionString, string iDbProvider)
       {
        Key = key;
        ConnectionString = connectionString;
        IDbProviderType = iDbProvider;
       }
       public string Key;
       public string ConnectionString;
       public string IDbProviderType;
      }
      GenericDalSettings.DalSetting[] sources;
      const string section = "DataProviders";
      /// <summary>
      /// Hide construction logic from external resources. 
      /// </summary>
      internal GenericDalSettings()
      {
       sources = new DalSetting[0];
      }
      /// <summary>
      /// Allows addition of data sources to the internal array.
      /// </summary>
      /// <param name="key">How calling code will identify each IDbLayer
      /// implementation from others</param>
      /// <param name="type">The specific type of IDbLayer implementor
      /// to be used.</param>
      /// <param name="connectionString">Connection string to 
      /// the requested data source.</param>
      internal void Add(string key, string type, string connectionString)
      {
       DalSetting[] tmpSrc = new DalSetting[sources.Length+1];
       Array.Copy(sources,0,tmpSrc,0,sources.Length);
       tmpSrc[sources.Length] = new DalSetting(key,connectionString,type);
       sources = tmpSrc;
      }
      /// <summary>
      /// Returns a particular DataSource by its name. 
      /// </summary>
      public IDbLayer this[string dataSourceName]
      {
       get
       {
        for(int i=0; i<sources.Length; i++)
        {
         if(sources[i].Key.ToLower() == dataSourceName.ToLower())
         {
          return DalFactory.CreateDbLayer(Type.GetType(sources[i].IDbProviderType),
           sources[i].ConnectionString);
         }
        }
        return null;
       }
      }
      /// <summary>
      /// Loads in the data sources and 
      /// prepares the settings class for usage.
      /// </summary>
      /// <returns></returns>
      public static GenericDalSettings GetSettings() 
      {
       GenericDalSettings b = 
        (GenericDalSettings)cs.GetConfig(section);
       return b;
      }
     }
    }
    

    Finally (and maybe obviously), we'll add a third overload to the factory class. This overload is relatively simple - it takes a single string parameter that will be used to search for a particular data source by it key.

    /// <summary>
      /// Creates an instance of an implementor for the IDbLayer
      /// interface by reading from the custom configuration settings.
      /// See the included sample web config file for an example.
      /// </summary>
      /// <returns></returns>
      public static IDbLayer CreateDbLayer(string dataSourceName)
      {
       string selectedProvider = String.Empty;
       string cnStr = String.Empty;
       
       GenericDalSettings settings = 
        GenericDalSettings.GetSettings();
       
       // figure out the provider
       IDbLayer idb = settings[dataSourceName];
       return idb;
      }

    Now that the data layer has been pretty much completely abstracted, applications making use of it have the capability of asking for a particular data source by it's name, then simply calling command routines provided via the IDbLayer's interface requirements. This mimics ODBC in one way - by allowing named requests for data sources. Think of it like this - any application using this data layer has a System-DSN-like methodology for connecting to and executing login within any of the databases required by that application.

    As the application grows and requires the need for additional data sources, one need only place new lines of code within the configuration file to access those new data sources (and write code to use them of course).

    The next step will take this flexibility idea one step further, to the application logic layer. I'll delve into the workflow conceptualization next, and provide an idea or two for allowing redirection and flexibility within application process flow.

  • Misquoted Again

    No, Robert, that's not what I said. Let me rephrase what I did say during our chat this afternoon (or at least what you should have known I meant). What Robert should have quote me as saying was “There are so many screwed up parts of the Framework, but it's SO money that you just have to deal with it.”

    Isn't this true for any framework? We've all got our gripes and wants, but dude, the Framework is incredible. I'd have to say that I'd almost go as far as to quote Don Box when he said “COM is Love.” DB, if COM is Love, then .NET must be a night of unbridled passion with <Person>.

  • Nice C# Color-coder

    I found Code Highlighter at CSharpFriends.com this evening. I can't recomend it highly enough!
  • MT : Problem #3: The Generic DAL: Basic Structures and Implementations

    To continue with the architectural examination of MT and get into the code, I'll examine the least theoretical - the Data Access Layer. Not going to spend a whole lot of time with a long-winded OO discussion or a review of the GOF patterns here. Instead, I'll get started by first showing the code for the basis of the entire DAL - the IDbLayer interface. Some of these methods might look relatively simple (and similar if you've used the SqlHelper class from the Data Access Building Block). The concept - known by GOF nuts as the Facade Pattern - is something used quite heavily within .NET. Namely, all of the DataAdapters do a pretty good job at the Facade pattern by hiding all of the connection, command-building, other random database processes. Just by calling the Fill() method, for instance, a connection is opened and managed, a command built, data retrieved, connection cut, and dataset populated. We'll use this pattern a bit to obstruct the internal workings of some pretty universal units of functionality, as exemplified in the interface code below.

    using System;
    using System.Data;
    
    namespace GenericDal
    {
        public interface IDbLayer
        {
            /// <summary>
            /// The connection string used to connect to a database. 
            /// </summary>
            string ConnectionString { get; set; }
    
            /// <summary>
            /// Executes the provided commandText SQL query, builds a DataSet, and returns 
            /// the DataSet to the calling code. The array of IDbDataParameter objects are added
            /// to an internal IDbCommand object's Parameters collection prior to the execution.
            /// </summary>
            /// <param name="commandText"></param>
            /// <param name="commandType"></param>
            /// <param name="parameters"></param>
            /// <returns></returns>
            DataSet ExecuteDataSet(string commandText,
                CommandType commandType,
                params IDbDataParameter[] parameters);
    
            /// <summary>
            /// Executes the provided commandText SQL query
            /// and returns a DataReader to the calling code. The array of IDbDataParameter objects are added
            /// to an internal IDbCommand object's Parameters collection prior to the execution.
            /// </summary>
            /// <param name="commandText"></param>
            /// <param name="commandType"></param>
            /// <param name="parameters"></param>
            /// <returns></returns>
            IDataReader ExecuteReader(string commandText,
                CommandType commandType,
                params IDbDataParameter[] parameters);
    
            /// <summary>
            /// Executes the provided commandText SQL query
            /// and returns a DataReader to the calling code. The array of IDbDataParameter objects are added
            /// to an internal IDbCommand object's Parameters collection prior to the execution.
            /// </summary>
            /// <param name="commandText"></param>
            /// <param name="commandType"></param>
            /// <param name="parameters"></param>
            /// <returns></returns>
            IDataReader ExecuteReader(IDbTransaction transaction, 
                string commandText,
                CommandType commandType,
                params IDbDataParameter[] parameters);
    
            /// <summary>
            /// Executes a SQL procedure and returns a single value. 
            /// </summary>
            /// <param name="commandText"></param>
            /// <param name="commandType"></param>
            /// <param name="parameters"></param>
            /// <returns></returns>
            object ExecuteScalar(string commandText,
                CommandType commandType,
                params IDbDataParameter[] parameters);
    
            
            /// <summary>
            /// Executes a SQL procedure and returns nothing. 
            /// </summary>
            /// <param name="commandText"></param>
            /// <param name="commandType"></param>
            /// <param name="parameters"></param>
            void ExecuteNonQuery(string commandText,
                CommandType commandType,
                params IDbDataParameter[] parameters);
    
            /// <summary>
            /// Executes a SQL procedure and returns nothing. 
            /// </summary>
            /// <param name="commandText"></param>
            /// <param name="commandType"></param>
            /// <param name="parameters"></param>
            void ExecuteNonQuery(IDbTransaction transaction, 
                string commandText,
                CommandType commandType,
                params IDbDataParameter[] parameters);
    
            /// <summary>
            /// Executes a SQL procedure and returns a single value. 
            /// </summary>
            /// <param name="commandText"></param>
            /// <param name="commandType"></param>
            /// <param name="parameters"></param>
            /// <returns></returns>
            object ExecuteScalar(IDbTransaction transaction, 
                string commandText,
                CommandType commandType,
                params IDbDataParameter[] parameters);
    
            /// <summary>
            /// Returns a list of parameters for a given stored procedure. 
            /// </summary>
            /// <param name="commandText"></param>
            /// <returns></returns>
            IDataParameterCollection GetCommandParameters(string commandText);
        }
    }
    

    Not too difficult to surmise what we're doing in this section of the code. Nothing too complex to grasp, really, from a conceptual frame of reference. We've accomodated most of the widely-used access methods of getting to databases - disconnected scenarios (via the DataSet-related methods) and connected (via the DataReader-specific methods). In addition, there's a few overloads to accomodate the existence of IDbTransaction objects to make sure implementors provide some sort of support for database transactions that span multiple method calls.

    Now, we'll examine an implementor of this class. To make the discussion a little simple, let's take a look at the SqlServerDal class, which obviously deals with the SqlClient-specific implementations of these methods.

    using System;
    using System.Data;
    using System.Data.SqlClient;
    
    namespace GenericDal
    {
    	public class SqlServerDal : IDbLayer
    	{
    		private string cnStr = String.Empty;
    
    		public SqlServerDal()
    		{
    		}
    
    		public string ConnectionString
    		{
    			get { return cnStr; }
    			set { cnStr = value; }
    		}
    
    		/// <summary>
    		/// Adds parameters to a command object. 
    		/// </summary>
    		/// <param name="cmd"></param>
    		/// <param name="parameters"></param>
    		void PrepareParameters(IDbCommand cmd, params IDbDataParameter[] parameters)
    		{
    			if(parameters != null)
    			{
    				// add the parameters to the SelectCommand.
    				foreach(IDbDataParameter p in parameters)
    				{
    					cmd.Parameters.Add((SqlParameter)p);
    				}
    			}
    		}
    
    		/// <summary>
    		/// Executes the provided commandText SQL query, builds a DataSet, and returns 
    		/// the DataSet to the calling code. The array of SqlParameter objects are added
    		/// to an internal SqlCommand object's Parameters collection prior to the execution.
    		/// </summary>
    		/// <param name="command"></param>
    		/// <param name="commandText"></param>
    		/// <param name="commandType"></param>
    		/// <param name="parameters"></param>
    		/// <returns></returns>
    		public DataSet ExecuteDataSet(string commandText, 
    			System.Data.CommandType commandType, 
    			params IDbDataParameter[] parameters)
    		{
    			SqlConnection cn = new SqlConnection(this.ConnectionString); 
    
    			// instantiate a new dataadapter to fill up the dataset
    			SqlDataAdapter daTmp = new SqlDataAdapter(commandText,cn);
    			daTmp.SelectCommand.CommandType = commandType;
    
    			PrepareParameters(daTmp.SelectCommand, parameters);
    
    			// return the dataset
    			DataSet dsTmp = new DataSet();
    			daTmp.Fill(dsTmp);
    			return dsTmp;
    		}
    		
    		/// <summary>
    		/// Executes the provided commandText SQL query
    		/// and returns a DataReader to the calling code. The array of IDbDataParameter objects are added
    		/// to an internal IDbCommand object's Parameters collection prior to the execution.
    		/// </summary>
    		/// <param name="command"></param>
    		/// <param name="commandText"></param>
    		/// <param name="commandType"></param>
    		/// <param name="parameters"></param>
    		/// <returns></returns>
    		public IDataReader ExecuteReader(string commandText, 
    			System.Data.CommandType commandType, 
    			params IDbDataParameter[] parameters)
    		{
    			SqlConnection cn = new SqlConnection(this.ConnectionString); 
    			SqlCommand cmd = new SqlCommand(commandText,cn);
    			cmd.CommandType = commandType;
    			cmd.Connection = cn;
    
    			PrepareParameters(cmd, parameters);
    		
    			cn.Open();
    			SqlDataReader rdr = cmd.ExecuteReader(CommandBehavior.CloseConnection);
    			return rdr;
    		}
    
    		/// <summary>
    		/// Executes the provided commandText SQL query
    		/// and returns a DataReader to the calling code. The array of IDbDataParameter objects are added
    		/// to an internal IDbCommand object's Parameters collection prior to the execution.
    		/// </summary>
    		/// <param name="commandText"></param>
    		/// <param name="commandType"></param>
    		/// <param name="parameters"></param>
    		/// <returns></returns>
    		public IDataReader ExecuteReader(IDbTransaction transaction, string commandText,
    			CommandType commandType,
    			params IDbDataParameter[] parameters)
    		{
    			SqlConnection cn = new SqlConnection(this.ConnectionString); 
    			SqlCommand cmd = new SqlCommand(commandText,cn,(SqlTransaction)transaction);
    			cmd.CommandType = commandType;
    			cmd.Connection = cn;
    			cmd.Transaction = (SqlTransaction)transaction;
    
    			PrepareParameters(cmd, parameters);
    		
    			cn.Open();
    			SqlDataReader rdr = cmd.ExecuteReader(CommandBehavior.CloseConnection);
    			return rdr;
    		}
    
    		/// <summary>
    		/// Executes a SQL procedure and returns a single value. 
    		/// </summary>
    		/// <param name="commandText"></param>
    		/// <param name="commandType"></param>
    		/// <param name="parameters"></param>
    		/// <returns></returns>
    		public object ExecuteScalar(string commandText,
    			CommandType commandType,
    			params IDbDataParameter[] parameters)
    		{
    			SqlConnection cn = new SqlConnection(this.ConnectionString); 
    			SqlCommand cmd = new SqlCommand(commandText,cn);
    			cmd.CommandType = commandType;
    			cmd.Connection = cn;
    
    			PrepareParameters(cmd, parameters);
    		
    			cn.Open();
    			object o = cmd.ExecuteScalar();
    			cn.Close();
    			return o;
    		}
    		
    		/// <summary>
    		/// Executes a SQL procedure and returns a single value. 
    		/// </summary>
    		/// <param name="commandText"></param>
    		/// <param name="commandType"></param>
    		/// <param name="parameters"></param>
    		/// <returns></returns>
    		public object ExecuteScalar(IDbTransaction transaction, 
    			string commandText, 
    			System.Data.CommandType commandType, 
    			params IDbDataParameter[] parameters)
    		{
    			SqlConnection cn = new SqlConnection(this.ConnectionString); 
    			SqlCommand cmd = new SqlCommand(commandText,cn,(SqlTransaction)transaction);
    			cmd.CommandType = commandType;
    			cmd.Connection = cn;
    			cmd.Transaction = (SqlTransaction)transaction;
    
    			PrepareParameters(cmd, parameters);
    		
    			cn.Open();
    			object o = cmd.ExecuteScalar();
    			cn.Close();
    			return o;
    		}
    
    		public void ExecuteNonQuery(string commandText,
    			CommandType commandType,
    			params IDbDataParameter[] parameters)
    		{
    			SqlConnection cn