Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

VCMBox, an MVC - FuseBox Hybrid Framework

0.00/5 (No votes)
8 Jul 2004 1  
An MVC meets FuseBox Hybrid pattern for .NET, implmented in C# with the the Northwind database.

Introduction

Within the context of my current project, I was required to examine, template, and rework (if required - and yes, it was required) a design pattern by which all the programming team would be able to work too, and one that would work hand in hand with our GUI design. The pattern that has evolved is derived out of the well documented and widely discussed MVC framework, and to a lesser degree the FuseBox methodology which came out of the Cold Fusion community. One caveat at this point: I am not going to discuss background or theory on MVC or FuseBox too much here, it is assumed the reader has already acquired the background, but just in case, here are some links that some people may find useful before continuing:

How to implement this framework is discussed and highlighted with code examples in this article, however, I strongly suggest downloading the code itself and the Ndoc documentation of the code to examine this further. There is simply too much code in the Tutorials solution to discuss in its entirety here.

The Problem & Deriving a pattern from MVC

As I stated above, part of the problem included the need to design the application in a very component driven way, that allowed the team of developers to work individually (following the pattern) and then deliver their components for integration. Without spending too much time on the issue of integration, let's just say that each developer will be assigned a specific high level functional part of the application which means, in short, that they take charge of building the viewports, controller(s), model and data access layers for their assigned functional areas. Every few weeks and in some cases days, all the developers submit their components for integration, and a complete build takes place.

One thing that struck me from the onset was some potential redundancy in the Model, typically models would consist of class objects that model the business or 'domain' being represented in the solution, however due to the efficiencies and benefits of data binding and the use of Data Modeling Objects such as the DataSet and DataTable, I decided to remove any usage of User Defined Classes in the Model to represent the 'domain' data. The idea of the "Model" was therefore adapted, a strongly typed DataSet supplanted any User Defined Classes formerly required for modeling the domain data. Had I stuck with the idea of User Defined Classes to represent domain data, there would have remained a necessity to use objects such as the DataSet, DataTable and the DataReader to act as temporary containers for retrieved data. The Model still needed to support a set of operations (Methods), therefore a class that exposed the required behaviors and schema was what this "Derived" pattern was going to require. The Model's evolution finished with it taking the shape of a User Defined Class that wrapped a strongly typed DataSet, a Data Access Layer to assist with the Data Retrieval and exposed the operational behaviors and computations typically found in an MVC implementation. Exhibit 1.0 demonstrates the publicly visible Domain object, the 'dal' (Data Access Layer) and a single behavior GetSomeDataById().

Exhibit 1.0

What this diagram depicts is the basic relationship between each of the main units of work that make up the pattern. The ViewPort is a class derived from UserControl and represents one of many screens or viewports in the application. The Controller accepts messages or requests for services which are then relayed to the Model. The Model class encapsulates both a strongly typed DataSet (modeling the domain data) and an instance of a Data Access Layer (DAL) which serves as a container for objects we require from the SqlClient Managed - Provider namespace. All of the DAL could easily have been incorporated in the Model, however decoupling them in this manner made visually managing a fair number of SqlClient objects easier (in VS.NET), and also potentially offered re-use on the data access layer without any dependency on the Model.

Exhibit 2.0

Is MVC 'set in stone'?

Keep telling yourself this pattern is NOT strictly MVC or FuseBox for that matter, it has been rejigged and cajoled into something that better suits this one particular project and removed a lot of redundancy or verbosity that simply would have been overkill in a .NET WinForms Desktop Application of its type. There have over the years been a great number of implementations of the MVC framework, and with the advent of new programming languages has come a great number of derivatives. As you see from the above diagram (exhibit 2), the controller would usually be accepting event input from the user and then issuing instructions to the model and viewports to take the appropriate actions with regards to data. Our derived pattern has removed the two way messaging that exists between the View and Controller in MVC, and has also done away with the Controller taking any direct responsibility for messaging from the 'Keyboard' or 'Mouse', largely because the View is constantly being notified of those events through the inherent use of the observer pattern in Windows Forms, User Controls and the common constituent controls found in the System.Windows.Forms namespace. The difference between MVC and the "Derived" pattern is well depicted in the difference between exhibits 2.0 and 3.0.

Exhibit 3.0

It is evident from exhibit 3.0 that our derived pattern largely adheres to the basic Tenets of MVC.

  • MVC Tenet: The Model should know nothing of the Views that may be observing it. The derived pattern dictates that communications made from the view requesting service to add or change domain data in the model are channeled via the controller, and any notification of such change to the domain is handled by way of Event notification propagated back to any view that subscribes to those Events within the Model, furthermore the View is free to request the state of the domain (Model Data) at any given time.
  • MVC Tenet: The Viewport knows the nature of the Model it observes.
  • MVC Tenet: The Viewport knows the controller. (Our derived pattern only uses one method in the controller - the ControlHub(), a static method which accepts a request message).
  • MVC Tenet: The Controller knows both the Model and Viewports. This is where our derivation deviates from classic MVC. The derived pattern's Controller does not know the Viewports at all, enhancing further the idea of decoupled Controllers demonstrating an ability to be easily swapped in and out.

Exhibit 4.0 (Sequence Diagram of 'The Derived Pattern')

So how does all this look as code?

OK, I can hear some saying let's see what this looks like implemented with a code example. So, without further ado, here we go. What is included in the code listings to follow and the source code available here to download is a working example of the 'Derived Pattern' which makes use of the ubiquitous Northwind Database (SQL Server 2000 variety - MSDE will suffice), therefore we will be using the SqlClient Managed provider.

What the download project demonstrates is the usage of the pattern as per its complete design as applied to a limited solution for the Northwind Traders Domain. I have also included complete documentation for the Solution, produced using Ndoc, and this article is embedded within it as the 'Overview' page. The code that makes up this pattern consists of five distinct assemblies, each one fulfilling a part of the solution. The five assemblies are as follows, and each will be discussed in turn:

  1. The Harness assembly.
  2. The ViewPorts assembly.
  3. The Controller assembly.
  4. The Model assembly.
  5. The Data Access Layer assembly.

The Harness assembly can be thought of as another ViewPort assembly, essentially it contains and has rendered within it the ViewPorts found in the ViewPorts assembly itself. In the context of this tutorial, the Harness assembly consists of an MDI parent and child form, the latter of which will contain our ViewPorts from the ViewPorts assembly.

We will now follow through some of the code to present one sample of how to implement this pattern, and I will do so following the workflow as depicted in the sequence diagram included as exhibit 4.0.

The Harness

It should be noted that it is not a requirement for all harness Forms to follow this design.

Exhibit 5.0

The main points of interest in the Harness assembly lie within the nature of the MDI Child Form. The MDI Child Form in this implementation contains an instance of the Model, the Controller and all the ViewPorts that can be displayed in the Workspace panel. The constructor for the MDI Child Form initializes the Model, Controller and ViewPorts, and as result each MDI Form and its Viewports deal with the same Model and Controller which in turn enables our application to contain (n) number of MDI children each with a different data context.

Listing 1.0

public frmNwindMdiChild() 
{ 
    InitializeComponent(); 
    //create a new instance of the model 

    NwindModel = new Model(); 
    //let the controller use the same model by 

    //passing through a reference to the model used 

    //by each instance of the MDI child form Viewport... 

    NwindController = new Controller(NwindModel); 
    //a handler in the MDI child is subscribed to 

    //the Models TreeLoaded Event 

    NwindModel.TreeLoaded += new DataFetchHandler(NwindModel_TreeLoaded); 
    //subscribe to the treeview afterselect event 

    this.tvwNwindEmployees.AfterSelect += 
        new TreeViewEventHandler(tvwNwindEmployees_AfterSelect); 
    //create the new viewports to work in the workspace 

    eHeaderPort = new NorthWindHeader(); 
    eDatailsPort = new EmployeeDetails(NwindController, NwindModel); 
    eOrderDetailsPort = new OrderDetails(NwindController, NwindModel); 
}

As you can see from the above, the Model's TreeLoaded event has been subscribed to with the handler method NwindModel_TreeLoaded, and the event will be raised when our application requests a new MDI Child be created as a consequence of its Load method.

private void frmNwindMdiChild_Load(object sender, System.EventArgs e) 
{ 
    NwindController.ControlHub (
        Mvc.DerivedPattern.NorthWind.Tutorial.Controllers.
        Controller.NorthWindControlMessages.LoadTreeData); 
}

And as a result, the Controller will request a service from the Model which will raise the event, that will in turn notify the observing MDI Child that the data for the TreeView has been loaded. Hence our handler as defined above will run.

private void NwindModel_TreeLoaded() 
{ 
    //The Model is informing the Viewport that 

    //data has possibly been found 

    //And if there are any employees then create 

    //the root node and let the tree begin to enumerate. 

    if(NwindModel.Employee.tblLoadAllEmployees.Rows.Count > 0) 
    { 
        //This will trigger a recursive building of the TreeView Nodes 

        EmployeesNode root = new EmployeesNode(this.tvwNwindEmployees); 
    } 
}

How to Show a ViewPort

The TreeView on our MDI Child should contain three possible (strongly typed) tree nodes, all of which have been subclassed from TreeNode, hence our handler (see Listing 1.0 above) will evaluate which of these TreeNode types has been selected in order to determine which ViewPort to display.

Listing 2.0

private void tvwNwindEmployees_AfterSelect(object sender, 
    TreeViewEventArgs e) 
{ 
    switch(e.Node.GetType().FullName) 
    { 
        case "Mvc.DerivedPattern.NorthWind.Tutorial." +
                "Harness.frmNwindMdiChild+EmployeesNode" : 
            this.pnlEmployeeWorkspace.Controls.Clear(); 
            this.pnlEmployeeWorkspace.Controls.Add(eHeaderPort); 
            eHeaderPort.Show(); 
            break; 
        case "Mvc.DerivedPattern.NorthWind.Tutorial." +
                "Harness.frmNwindMdiChild+EmployeeNode" : 
            this.pnlEmployeeWorkspace.Controls.Clear(); 
            this.pnlEmployeeWorkspace.Controls.Add(eDatailsPort); 
            eDatailsPort.Show(((EmployeeNode)e.Node).Id); 
            break; 
        case "Mvc.DerivedPattern.NorthWind.Tutorial." +
                "Harness.frmNwindMdiChild+OrderNode" : 
            this.pnlEmployeeWorkspace.Controls.Clear(); 
            this.pnlEmployeeWorkspace.Controls.Add(eOrderDetailsPort); 
            eOrderDetailsPort.Show(((OrderNode)e.Node).Id); 
            break; 
        default : //shouldnt happen so do nothing 

            break; 
    } 
}

Once the correct node type has been evaluated, the Show() method of the appropriate ViewPort is called. Note that the Show method is an overridden version of that inherited from UserControl from which each ViewPort derives. We will discuss the Show method in more detail shortly.

A ViewPort

With the exception of the Forms that make up the Harness, ViewPorts consist of subclasses of the UserControl class.

Exhibit 6.0

The ViewPort depicted above comprises a single GroupBox and a collection of Label controls, TextBox controls, Button control and a customized subclass of the DateTimePicker, all of which will be data bound to a DataTable in the Model. Each ViewPort, including the above, is required to implement the interface IViewPort which is defined as below:

Listing 3.0

interface IViewPort 
{ 
    void PopulateViewPort(); 
    void Show(int id); 
    void SetUpBindings(); 
}

The Show() method is a new implementation of one already inherent to the UserControl class, and deviates only in its expected argument of type int. The implementation of the Show() method is as below:

public void Show(int id) 
{ 
    LoadEmployeeDetails(id); 
    base.Show(); 
}

A Controller calling Worker method is called in the body of the Show method's implementation.

private void LoadEmployeeDetails(int id) 
{ 
    //set the parameter expected in the dal 

    NwindModel.EmployeeDal.sqlCmdSelectEmployeeDetails.
    Parameters["@EmployeeID"].Value = id; 
    //Call the Controller with a request message 

    this.NwindController.ControlHub(
    Mvc.DerivedPattern.NorthWind.Tutorial.Controllers.
    Controller.NorthWindControlMessages.LoadEmployeeDetails); 
}

The Show() method calls a private worker method (LoadEmployeeDetails) that assigns the NorthWind employees ID to a Parameter, the worker method then calls the Controller with a message to retrieve the domain data for the employee with a matching ID. Finally, the base class Show method is called in order to render the ViewPort.

PopluateViewPort() contains the required code to populate a Viewport by binding to data in the Model, and it must adhere to the delegate signature as defined in the model by:

public delegate void DataFetchHandler();

All the Model's events in this implementation are of the type DataFetchHandler.

PopulateViewPort is subscribed as the handler for events that are raised in the Model when data has been requested and found, subsequently the constituent controls of the ViewPort are bound to one of the DataTables in the Model. A typical implementation of PopulateViewPort is as follows:

void IViewPort.PopulateViewPort()
{ 
    foreach(Control ctl in this.grpEmployeeDetails.Controls) 
    { 
        ctl.DataBindings.Clear(); 
    } 
    boundRow = (DataRowView)
        this.BindingContext[NwindModel.Employee.tblSelectEmployee].Current; 
    ((IViewPort)this).SetUpBindings(); 
}

PopulateViewPort firstly clears the bindings of each constituent control contained within the GroupBox's Controls collection, gets the current row of data from the Model through the BindingContext of the ViewPort, and then calls SetUpBindings().

The SetUpBindings method simply binds the controls on the ViewPort to the data in the Model.

void IViewPort.SetUpBindings() 
{ 
    this.txtId.DataBindings.Add("Text", boundRow, "EmployeeID"); 
    this.txtTitle.DataBindings.Add("Text", boundRow, "TitleOfCourtesy"); 
    this.txtFirstName.DataBindings.Add("Text", boundRow, "FirstName"); 
    this.txtLastName.DataBindings.Add("Text", boundRow, "LastName"); 
    this.txtPosition.DataBindings.Add("Text", boundRow, "Title"); 
    this.dtpBirthDate.DataBindings.Add("Value", boundRow, "BirthDate"); 
    this.dtpHireDate.DataBindings.Add("Value", boundRow, "HireDate"); 
    this.txtAddress.DataBindings.Add("Text", boundRow, "Address"); 
    this.txtCity.DataBindings.Add("Text", boundRow, "City"); 
    this.txtRegion.DataBindings.Add("Text", boundRow, "Region"); 
    this.txtPostCode.DataBindings.Add("Text", boundRow, "PostalCode"); 
    this.txtCountry.DataBindings.Add("Text", boundRow, "Country"); 
    this.txtExt.DataBindings.Add("Text", boundRow, "Extension"); 
    this.txtNotes.DataBindings.Add("Text", boundRow, "Notes"); 
}

The Controller

The derived pattern's Controller is the single point of communication for ViewPorts with regards to making requests for data retrieval and change. Due to the one way nature of the relationship between ViewPorts and Controller, the message types are also defined within the Controller in the shape of an enumerated type, and the only method or behavior is that of the ControlHub which evaluates the message and instructs the Model to perform the requested service. This particular slant on the controller differs somewhat from those implementations where the controller responds directly to an event, it resembles more closely the Cold Fusion or ASP implementation of a FuseBox which evaluates FuseActions. For more on FuseBox, see this PPT presentation or fusebox.org.

Listing 4.0 (The Control Messages Enumeration & ControlHub)

public enum NorthWindControlMessages : int 
{ 
    LoadTreeData = 0, 
    LoadEmployeeDetails = 1, 
    LoadOrderDetails = 2, 
    UpdateEmployeeDetails = 3, 
    UpdateOrder = 4 
}

The ControlHub body switches on the message and then makes behavior (method) requests on the Model.

public void ControlHub(NorthWindControlMessages msg) 
{ 
    switch(msg) 
    { 
        case NorthWindControlMessages.LoadTreeData : 
            this.EmployeeModel.LoadTreeData(); 
            break; 
        case NorthWindControlMessages.LoadEmployeeDetails : 
            this.EmployeeModel.LoadEmployeeDetails(); 
            break; 
        case NorthWindControlMessages.LoadOrderDetails : 
            this.EmployeeModel.LoadOrderDetails(); 
            break; 
        case NorthWindControlMessages.UpdateEmployeeDetails : 
            this.EmployeeModel.UpdateEmployeeDetails(); 
            break; 
        case NorthWindControlMessages.UpdateOrder : 
            this.EmployeeModel.UpdateOrder(); 
            break; 
    } 
}

The Model & Data Access Layer

As stated earlier, the Model class acts as a wrapper to a strongly typed DataSet representing the Domain Model, encapsulates the Data Access Layer that provides the mechanics of working with the data, and finally consists of behaviors that make use of both of these.

Exhibit 7.0

For those of you accustomed to using Visual Studio .NET, exhibit 7.0 demonstrates the use of a strongly typed DataSet to represent the Domain Model, in this particular case, it consists of five DataTables. If you scan back over the code above in listing 3.0 and look specifically at the implementation of the PopulateViewPort method, you will notice that the ViewPort is directly interacting with the Model in order to retrieve a DataRowView object with which it will DataBind. The Model behaviors however act upon the DataTables in the Domain Model by either filling them with data or changing the database if their contents have changed, as per requests made via the ViewPorts. In keeping with the flow of the code presented thus far, we will examine some behavioral action made inside the Model.

Listing 5.0 (Model Behavior)

public void LoadEmployeeDetails() 
{ 
    Employee.tblSelectEmployee.Clear(); 
    Employee.tblSelectEmployee.AcceptChanges(); 
    employeeDal.sqlConNwind.Open(); 

    //set up the Select Command 

    employeeDal.sqlDadMultiUse.SelectCommand = 
    employeeDal.sqlCmdSelectEmployeeDetails; 

    //set up the Update Command 

    employeeDal.sqlDadMultiUse.UpdateCommand = 
    employeeDal.sqlCmdUpdateEmployee; 
    //Update Commands parameters 

    employeeDal.sqlCmdUpdateEmployee.Parameters.Clear(); 

    employeeDal.sqlCmdUpdateEmployee.Parameters.Add(
    new System.Data.SqlClient.SqlParameter("@EmployeeID", 
        SqlDbType.Int, 4, ParameterDirection.Input, 
        false, new byte(), new byte(), 
        Employee.tblSelectEmployee.Columns["EmployeeID"].
    ColumnName, DataRowVersion.Current, null)); 

    employeeDal.sqlCmdUpdateEmployee.Parameters.Add(
    new System.Data.SqlClient.SqlParameter("@FirstName", 
        SqlDbType.NVarChar, 10, ParameterDirection.Input, false , 
    new byte(), new byte(), Employee.tblSelectEmployee.
    Columns["FirstName"].ColumnName, 
    DataRowVersion.Current, null)); 

    employeeDal.sqlCmdUpdateEmployee.Parameters.Add(
    new System.Data.SqlClient.SqlParameter("@LastName", 
        SqlDbType.NVarChar, 20, ParameterDirection.Input, 
    false, new byte(), new byte(), 
        Employee.tblSelectEmployee.Columns["LastName"].
    ColumnName, DataRowVersion.Current, null)); 

    employeeDal.sqlCmdUpdateEmployee.Parameters.Add(
    new System.Data.SqlClient.SqlParameter("@TitleOfCourtesy", 
        SqlDbType.NVarChar, 25, ParameterDirection.Input, 
    true, new byte(), new byte(), 
        Employee.tblSelectEmployee.Columns["TitleOfCourtesy"].
    ColumnName, DataRowVersion.Current, null)); 

    employeeDal.sqlCmdUpdateEmployee.Parameters.Add(
    new System.Data.SqlClient.SqlParameter("@Title", 
        SqlDbType.NVarChar, 30, ParameterDirection.Input, 
    true, new byte(), new byte(), 
    Employee.tblSelectEmployee.Columns["Title"].ColumnName, 
    DataRowVersion.Current, null)); 

    employeeDal.sqlCmdUpdateEmployee.Parameters.Add(
    new System.Data.SqlClient.SqlParameter("@BirthDate", 
        SqlDbType.DateTime, 16, ParameterDirection.Input, 
    true, new byte(), new byte(), 
        Employee.tblSelectEmployee.Columns["BirthDate"].
    ColumnName, DataRowVersion.Current, null)); 

    employeeDal.sqlCmdUpdateEmployee.Parameters.Add(
    new System.Data.SqlClient.SqlParameter("@HireDate", 
        SqlDbType.DateTime, 16, ParameterDirection.Input, true, 
    new byte(), new byte(), Employee.tblSelectEmployee.
    Columns["HireDate"].ColumnName, DataRowVersion.Current, null)); 

    employeeDal.sqlCmdUpdateEmployee.Parameters.Add(
    new System.Data.SqlClient.SqlParameter("@Address", 
        SqlDbType.NVarChar, 60, ParameterDirection.Input, 
    true, new byte(), new byte(), Employee.tblSelectEmployee.
    Columns["Address"].ColumnName, DataRowVersion.Current, null)); 

    employeeDal.sqlCmdUpdateEmployee.Parameters.Add(
    new System.Data.SqlClient.SqlParameter("@City", 
        SqlDbType.NVarChar, 15, ParameterDirection.Input, 
    true, new byte(), new byte(), Employee.tblSelectEmployee.
    Columns["City"].ColumnName, DataRowVersion.Current, null)); 

    employeeDal.sqlCmdUpdateEmployee.Parameters.Add(
    new System.Data.SqlClient.SqlParameter("@Region", 
        SqlDbType.NVarChar, 15, ParameterDirection.Input, 
    true, new byte(), new byte(), Employee.tblSelectEmployee.
    Columns["Region"].ColumnName, DataRowVersion.Current, null)); 

    employeeDal.sqlCmdUpdateEmployee.Parameters.Add(
    new System.Data.SqlClient.SqlParameter("@PostalCode", 
        SqlDbType.NVarChar, 10, ParameterDirection.Input, 
    true, new byte(), new byte(), Employee.tblSelectEmployee.
    Columns["PostalCode"].ColumnName, DataRowVersion.Current, null)); 

    employeeDal.sqlCmdUpdateEmployee.Parameters.Add(
    new System.Data.SqlClient.SqlParameter("@Country", 
        SqlDbType.NVarChar, 15, ParameterDirection.Input, 
    true, new byte(), new byte(), Employee.tblSelectEmployee.
    Columns["Country"].ColumnName, DataRowVersion.Current, null)); 

    employeeDal.sqlCmdUpdateEmployee.Parameters.Add(
    new System.Data.SqlClient.SqlParameter("@HomePhone", 
        SqlDbType.NVarChar, 24, ParameterDirection.Input, 
    true, new byte(), new byte(), Employee.tblSelectEmployee.
    Columns["HomePhone"].ColumnName, DataRowVersion.Current, null)); 

    employeeDal.sqlCmdUpdateEmployee.Parameters.Add(
    new System.Data.SqlClient.SqlParameter("@Extension", 
        SqlDbType.NVarChar, 4, ParameterDirection.Input, 
    true, new byte(), new byte(), Employee.tblSelectEmployee.
    Columns["Extension"].ColumnName, DataRowVersion.Current, null)); 

    employeeDal.sqlCmdUpdateEmployee.Parameters.Add(
    new System.Data.SqlClient.SqlParameter("@Notes", 
        SqlDbType.NText, 1073741823, ParameterDirection.Input, 
    true, new byte(), new byte(), Employee.tblSelectEmployee.
    Columns["Notes"].ColumnName, DataRowVersion.Current, null)); 

    employeeDal.sqlDadMultiUse.Fill(Employee, 
    Employee.tblSelectEmployee.TableName); 
    CloseDalConnection(); 
    //The Model now may have a new employee selected in the viewport 

    //so raise the event for Viewport that consumes it. 

    if(EmployeeDetailSelected != null) 
    { 
        EmployeeDetailSelected(); 
    } 
}

Notice the use of the Data Access Layer instance employeeDal and the Domain Model instance Employee.

The Data Access Layer

The Data Access Layer class consists of objects found entirely in the SqlClient namespace, namely SqlConnection, SqlDataAdapter and SqlCommand. As I mentioned, the visual assistance provided by way of using a component to act as a container in the DAL is useful, and the loose coupling allows greater reuse.

Exhibit 8.0

If you glance upward a little to Listing 5.0, you will notice that the Model method is making use of both sqlCmdSelectEmployeeDetails and sqlCmdUpdateEmployee from the Data Access Layer object and filling the Domain Model object Employee, specifically the DataTable tblSelectEmployee.

Note

  • This article's purpose is purely to outline the 'Derived Pattern', and does not take into account general programming chores such as exception handling and validation, those who wish to implement the pattern should make such considerations as a design concern for themselves.
  • The working title I have given this framework is VCMbox, however I would greatly appreciate any suggestions that are less 'cheesy' or predictable, so don't be shy.

Finally

Some things to consider when trying to get the example code working are:

  • The solution was created using Visual Studio 2003, and the NorthWind Database in SQL Server 2000 which at a cursory glance appears to be different with regards to schema in previous versions of SQL Server. I have not had the opportunity to check Version 7.0 and below, therefore, if the schema is indeed different, the application may or may not work as expected. I therefore recommend readers install SQL Server 2000 evaluation copy on the machine they plan to evaluate the code base.
  • The example solution does not work with the Access version of NorthWind, the SqlClient managed provider has been provided for only.
  • If any inconsistencies between the code base and the documentation are encountered, please let me know and I will update it.
  • Remember to change the connection string to an appropriate setting. See the Data Access Layer project, and set it in the constructor.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here