VS2005: Using RAD Tools for N-Tier Development
Visual Studio .NET 2005: Using RAD Tools for N-Tier Development
Andy Beaulieu, MCT, MCSD.NET
09/06/2005


DOWNLOAD THE SAMPLE SOLUTION


Summary
Visual Studio .NET 2005 provides great advances in its RAD tools and designers, making it possible to create functional applications without writing much of any code. However, many of the new tools and designers are targeted towards creating 2-Tier applications where presentation, business, and data access logic are bundled into a single tier. This approach is bad for many reasons including scalability and reusability of business logic components. This article describes how the new RAD tools in Visual Studio 2005 can be used to develop N-Tier applications. It provides step-by-step instructions with screen shots and explores the powerful code generation that VS.NET 2005 provides.

Contents
Drag-and-Drop Applications?
What's Wrong with This Picture?
A Simple N-Tier Architecture
The Data Access Layer
The Business Logic Layer
The Presentation Layer - Web UI
Adding Validation to the Web UI
The Presentation Layer - Windows UI
Adding Validation to the Windows UI
Summary

Drag-and-Drop Applications?
It is quite simple in VS.NET 2005 to create an application that allows editing of data in a tabular view. Just follow these steps:

(1) Start VS.NET 2005 and select File/New/Web Site... and enter "http://localhost/2TierDemo" in the Location field.

(2) Switch Default.aspx to Design view and expand the Server Explorer.

(3) Right-click the Data Connections node in Server Explorer and select "Add Connection..." Enter the connection info below to connect to the Northwind database:

 

(4) Expand the new Data Connection you added to Server Explorer until you reach the Tables Node, and then drag/drop the Customers table onto the Default.aspx form. VS.NET will create a new GridView control for you.

(5) At this point, you can run the web application and view the Customers data in a table.

(6) Let's make the table editable and dress it up a bit. Locate the Smart Tag for the GridView object - to do so, float the mouse over the GridView in Design View and look in the upper-right corner of the GridView object. You will see a little arrow with a box around it pointing to the right. Click this Smart Tag arrow and you will see a properties window like that below. Check all of the checkbox options to allow paging, sorting, editing, deleting, and selection. Also, select the Auto Format option and select the Colorful format.


(7) Run the application.

What's Wrong with this Picture?
Being able to whip together an editable grid in 30 seconds without writing a single line of code is quite impressive. But let's look at what the designer created for us. Open the code-behind file Default.aspx.vb and you will notice it is quite empty! This is because a lot of code generated by the VS.NET 2005 designers is declarative, and needs no source code to execute. Now take a look at Default.aspx in "Source View." The two main components we will see are a GridView control (used to bind to a datasource and display a tabular UI), and a SqlDataSource component (a database agnostic component used to connect to any relational database, and perform retreival and update functions).

        <asp:SqlDataSource ID="SqlDataSource1" runat="server" ConnectionString="<%$ ConnectionStrings:NorthwindConnectionString1 %>"
            DeleteCommand="DELETE FROM [Customers] WHERE [CustomerID] = @original_CustomerID"
            InsertCommand="INSERT INTO [Customers] ([CustomerID], [CompanyName], [ContactName], [ContactTitle], [Address], [City],...) 
                           VALUES (@CustomerID, @CompanyName, @ContactName, @ContactTitle, @Address, @City, @Region, @PostalCode, ...)"
            ProviderName="<%$ ConnectionStrings:NorthwindConnectionString1.ProviderName %>"
            SelectCommand="SELECT [CustomerID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], ..."
            UpdateCommand="UPDATE [Customers] SET [CompanyName] = @CompanyName, [ContactName] = @ContactName, [ContactTitle] = @ContactTitle, ...D">
            <InsertParameters>
                <asp:Parameter Name="CustomerID" Type="String" />
                <asp:Parameter Name="CompanyName" Type="String" />
                <asp:Parameter Name="ContactName" Type="String" />
                <asp:Parameter Name="ContactTitle" Type="String" />
                <asp:Parameter Name="Address" Type="String" />
                <asp:Parameter Name="City" Type="String" />
                <asp:Parameter Name="Region" Type="String" />
                <asp:Parameter Name="PostalCode" Type="String" />
                <asp:Parameter Name="Country" Type="String" />
                <asp:Parameter Name="Phone" Type="String" />
                <asp:Parameter Name="Fax" Type="String" />
            </InsertParameters>
            <UpdateParameters>
                <asp:Parameter Name="CompanyName" Type="String" />
                <asp:Parameter Name="ContactName" Type="String" />
                <asp:Parameter Name="ContactTitle" Type="String" />
                <asp:Parameter Name="Address" Type="String" />
                <asp:Parameter Name="City" Type="String" />
                <asp:Parameter Name="Region" Type="String" />
                <asp:Parameter Name="PostalCode" Type="String" />
                <asp:Parameter Name="Country" Type="String" />
                <asp:Parameter Name="Phone" Type="String" />
                <asp:Parameter Name="Fax" Type="String" />
                <asp:Parameter Name="original_CustomerID" Type="String" />
            </UpdateParameters>
            <DeleteParameters>
                <asp:Parameter Name="original_CustomerID" Type="String" />
            </DeleteParameters>
        </asp:SqlDataSource>
    

As you can see, the designer has created a 2-Tier application. The User Interface and Data Access logic all live within our ASP.NET page.  If we were to enhance our page to add validation or other logic, we would be piling Business Logic into this same tier as well. This is less than optimal for reusability, maintainability and scalabilty of the components.

A Simple N-Tier Architecture
In the simplest of N-Tier models, we have a Presentation (User Interface) Layer, a Business Logic Layer, and a Data Access Layer. Below is a quick summary of how these different layers interact, and what .NET components we'll use to create them. Keep in mind that one of the goals of this architecture is to be able to use the RAD designers that VS2005 provides us (there are of course other architetures worth exploring using templated code generation and other methods).
Tier Description What we'll create
Presentation Layer Providers the User Interface to the user ASP.NET pages and WindowsForms application
Business Logic Layer Provides validation and workflow. TableAdapter Extension Class (inherited from TableAdapter in Data Access Layer)
Data Access Layer Provides a "programmer friendly" representation of the Database entities TableAdapter Objects
Database Acts as a persistent store for application data. SQL Server (Northwind Sample Database)

The Data Access Layer
The purpose of the Data Access Layer (DAL) is to create a "programmer friendly" library for retrieving and updating data. By programmer friendly, I mean that the developer is freed from the mundane and error prone tasks of creating ADO.NET objects, and can instead concentrate on the more important Business Logic and Presentation layers. In several of my past projects, I made use of code generation to create a DAL based on the schema of the database. The idea is, there are always basic operations that you need to perform on almost every table. So instead of coding these by hand, why not have a code generator do it for you? Today there are several popular code generation tools including CodeSmith that do such code generation.

The.NET Framework 2.0 introduces a new component, called the TableAdapter, which is ideal for creating DAL Components. You can think of the TableAdapter component as a generated DAL component. It exists within the context of a Typed DataSet, and connects to the database to fill and update the Typed DataSet.

Let's see how we can use the designers in VS.NET 2005 to quickly generate a DAL component for the Products entity in the Northwind database.

(1) Create a new, blank solution file by selecting File/New/Project, then selecting Other Project Types/Visual Studio Solutions/Blank Solution. Name the solution "NTierDemo"

(2) Add a new Class Library project called DataAccessLayer by right-clicking the solution and select Add/New Project/Class Library.

(3) Delete the default generated Class1.vb, we won't be needing it.

(4) Add a new DataSet item called "DSProducts.xsd" to your project by right-clicking the project and selecting Add/New Item/DataSet.

(5) Right now we have a blank DataSet item in our project, to which we will add the Products schema and TableAdapter to. With the DSProducts.xsd open in the Data Designer, select Data/Add/TableAdapter from the main VS.NET menu. This launches the TableAdapter Query Configuration Wizard

(6) On the first page of the Wizard, you can select an existing Data Connection or choose an existing one. In this case, we'll take the existing NorthwindConnectionString.


(7) The next step of the Wizard lets you select the source of the TableAdapter's command objects. In this case, we will select "Use SQL statements." (Note that stored procedures, in general, outperform embedded SQL statements since they have an optimized plan already determined and stored).


(8) In the next step of the wizard, we specify the SQL statement to use as the source of data. You can type in SQL in the provided textbox, but let's click the "Query Builder..." button at the bottom of the screen. Then, select the Products table from the list of available tables.


(9) The Query Builder is a RAD tool to create SQL queries. It allows you to graphically specify tables to join, columns to select, and sort order, among other parameters. Let's select the "All Columns" checkbox in the Products table as shown below, then select OK.


(10) Back on the main page of the Wizard, click the Advanced button. This will show the Advanced Options Dialog. Uncheck the "Use Optimistic Concurrency" checkbox. If this checkbox were left checked, the SQL statements generated by the wizard would include criteria to enforce optimistic concurrency checks. But there are better ways of handling this, for example through using a timestamp (rowversion) column. In fact, at the time of this writing, the latest beta of VS.NET will automatically take advantage of a timestamp column for optimistic concurrency checks!


(11) Click the Finish button to complete the wizard.

(12) At this point, our Data Access Component has several useful methods available: 
  Fill and GetData - retrieve all Product records into a DataTable
  Insert - inserts a new Product record into the database
  Update - updates an existing Product record
  Delete - deletes a Product record

But it is easy to extend our Data Access Component with new methods. Let's add a GetById method which will return a single Productbased on Id. Once again, right-click the DataSet in the Designer and select Add/Query. This time, for the select query, we want to select a single Product row based on ProductId.


(19) When choosing "Method to Generate" for the GetById methods, select both options for filling a DataTable and returning a DataTable.


The Business Logic Layer
The purpose of the Business Logic Layer (BLL) is to provide validation and workflow for the application. By separating the BLL into a separate tier, we can reuse a single set of components from different clients (for example an ASP.NET client and a WindowsForms client).

Before we begin creating our Business Logic Layer, let's take a closer look at the ObjectDataSource control, as we will be using this component to data bind our UI elements to our BLL components. Most other Data Source controls in .NET 2.0 are designed for use in a 2-tier architecture, but the ObjectDataSource control is specifically designed to bind UI elements such as the GridView control directly to a middle tier Business Logic Component.

However, in order to use the ObjectDataSource control, our BLL component methods must adhere to certain design patterns. The "read" methods used to retrieve records from the database can look like any of the following:

' a method returning a generic DataSet
Public Function GetProducts() As DataSet

' a method returning a strongly typed DataSet
Public Function GetProducts() As ProductDataSet

' a method returning a list implementing IEnumerable, a Collection, or an Array
Public Function GetProducts() As ProductCollection


... and the Update, Insert, and Delete methods must accept each field as individual parameters, or accept a single object which contains all record properties as class properties. For example...

' an Update method accepting each field as a separate parameter
 Public Sub Update(ByVal ProductId As Integer, ByVal ProductName As String, _
    ByVal CategoryId As Integer, ByVal Description As String)

' an Update method accepting a single object
' (the object must expose individual fields as properties)
Public Sub Update(ByVal oProductsBLL As CProductsBLL)


As it turns out, the TableAdapter class we created in the previous exercise can be used as a source for the ObjectDataSource component. It exposes a "Get" method that returns a Products typed DataSet, and also Insert/Update/Delete statements that accept fields as parameters. Because of this, we can simply inherit from the TableAdapterClass to create our Business Logic Layer component, and add our validation logic to this extension layer.

Another little trick we will use is that of a Custom Exception Class to store validation errors. By using this method, we will be able to take advantage of events in our Presentation Layer later on to easily raise and handle validation errors.

So let's first take care of the business of creating our Custom Exception Class:

(1) Right-click the solution in Solution Explorer and select Add/Add New Project/Class Project. Name this project "Utilities"

(2) Add a new class to the Utilities project called CExceptionDetail. This will store the details of a single validation error:
Public Class CExceptionDetail
    Private m_iRowNum As Integer
    Private m_sColumnName As String
    Private m_sValidationError As String

    Public Sub New()
        m_iRowNum = 0
        m_sColumnName = ""
        m_sValidationError = ""
    End Sub

    Public Sub New(ByVal iRowNum As Integer, ByVal sColumnName As String, ByVal sValidationError As String)
        m_iRowNum = iRowNum
        m_sColumnName = sColumnName
        m_sValidationError = sValidationError
    End Sub

    Public Property RowNum() As Integer
        Get
            Return m_iRowNum
        End Get
        Set(ByVal value As Integer)
            m_iRowNum = value
        End Set
    End Property

    Public Property ColumnName() As String
        Get
            Return m_sColumnName
        End Get
        Set(ByVal value As String)
            m_sColumnName = value
        End Set
    End Property

    Public Property ValidationError() As String
        Get
            Return m_sValidationError
        End Get
        Set(ByVal value As String)
            m_sValidationError = value
        End Set
    End Property
End Class

    
(3) Add a new class to the Utilities project called CValidationException with code as follows. It is a fairly simple class that takes advantage of Generics (another new feature in .NET 2.0) to store a collection of CexceptionDetail.

Public Class CValidationException
    Inherits ApplicationException

    Public ErrorsCollection As New List(Of CExceptionDetail)

     Public Sub AddException(ByVal iRowNum As Integer, ByVal sColumnName As String, ByVal sValidationError As String)
        ErrorsCollection.Add(New CExceptionDetail(iRowNum, sColumnName, sValidationError))
    End Sub

    Public Overrides Function ToString() As String
        Dim sRetVal As String = "The following validation errors occurred:" & vbCrLf
        Dim oExceptionDetail As CExceptionDetail
        For Each oExceptionDetail In ErrorsCollection
            sRetVal &= "(Row " & oExceptionDetail.RowNum & ") " & oExceptionDetail.ColumnName & ": " & _
                       oExceptionDetail.ValidationError & vbCrLf
        Next
        Return sRetVal
    End Function

End Class

    
With our Custom Exception Class created, let's go back to the business of creating our BLL Component:

(1) Right-click the solution in Solution Explorer and select Add/Add New Project/Class Project. Name this project "BusinessLogicLayer" You can delete the default "Class1.vb" file from the project.

(2) Add a new reference to the DataAccessLayer and Utilities projects by right-clicking the project and selecting "Add Reference"

(3) Add a new class called "ProductsTableAdapterEx" which will inherit from the ProductsTableAdapter class and include validation logic. The key here is the CheckIsValid method, which validates the input parameters and fills an Errors Collection with any validation errors encountered. If any validation errors are found, then a custom validation exception is raised (we'll see how to handle this later in the User Interface layer) :

Imports DataAccessLayer.DSProductsTableAdapters
Imports Utilities

Public Class ProductsTableAdapterEx
    Inherits ProductsTableAdapter

    Public Overrides Function GetDataById(ByVal ProductId As Integer) As DataAccessLayer.DSProducts.ProductsDataTable
        Return MyBase.GetDataById(ProductId)
    End Function

    Public Overrides Function Insert(ByVal ProductName As String, ByVal SupplierID As System.Nullable(Of Integer), _
        ByVal CategoryID As System.Nullable(Of Integer), ByVal QuantityPerUnit As String, _
        ByVal UnitPrice As System.Nullable(Of Decimal), ByVal UnitsInStock As System.Nullable(Of Short), _
        ByVal UnitsOnOrder As System.Nullable(Of Short), ByVal ReorderLevel As System.Nullable(Of Short), _
        ByVal Discontinued As Boolean) As Integer
        Dim oValidationErrors As New CValidationException
        CheckIsValid(oValidationErrors, 0, ProductName, SupplierID, CategoryID, QuantityPerUnit, UnitPrice, _
                     UnitsInStock, UnitsOnOrder, ReorderLevel, Discontinued)
        If oValidationErrors.ErrorsCollection.Count > 0 Then Throw oValidationErrors
        Return MyBase.Insert(ProductName, SupplierID, CategoryID, QuantityPerUnit, UnitPrice, UnitsInStock, _
                             UnitsOnOrder, ReorderLevel, Discontinued)
    End Function

    Public Overrides Function Update(ByVal ProductName As String, ByVal SupplierID As System.Nullable(Of Integer), _
        ByVal CategoryID As System.Nullable(Of Integer), ByVal QuantityPerUnit As String, ByVal UnitPrice As System.Nullable(Of Decimal), _
        ByVal UnitsInStock As System.Nullable(Of Short), ByVal UnitsOnOrder As System.Nullable(Of Short), _
        ByVal ReorderLevel As System.Nullable(Of Short), ByVal Discontinued As Boolean, ByVal Original_ProductID As Integer) As Integer
        Dim oValidationErrors As New CValidationException
        CheckIsValid(oValidationErrors, 0, ProductName, SupplierID, CategoryID, QuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnOrder, _
                     ReorderLevel, Discontinued)
        If oValidationErrors.ErrorsCollection.Count > 0 Then Throw oValidationErrors
        Return MyBase.Update(ProductName, SupplierID, CategoryID, QuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel, _
                             Discontinued, Original_ProductID)
    End Function

    Public Overrides Function Update(ByVal dataSet As DataAccessLayer.DSProducts) As Integer
        Update(dataSet.Products)
    End Function

    Public Overrides Function Update(ByVal dataRow As System.Data.DataRow) As Integer
        Update(dataRow("ProductID"), dataRow("ProductName"), dataRow("SupplierID"), dataRow("CategoryID"), _
               dataRow("QuantityPerUnit"), dataRow("UnitPrice"), dataRow("UnitsInStock"), dataRow("UnitsOnOrder"), 
               dataRow("ReorderLevel"), dataRow("Discontinued"))
    End Function

    Public Overrides Function Update(ByVal dataTable As DataAccessLayer.DSProducts.ProductsDataTable) As Integer
        Dim oValidationErrors As New CValidationException
        Dim drProducts As DataAccessLayer.DSProducts.ProductsRow
        Dim iRowNum As Integer
        For Each drProducts In dataTable
            If drProducts.RowState = DataRowState.Added Or drProducts.RowState = DataRowState.Modified Then
                CheckIsValid(oValidationErrors, iRowNum, drProducts.ProductName, drProducts.SupplierID, drProducts.CategoryID, _
                             drProducts.QuantityPerUnit, drProducts.UnitPrice, drProducts.UnitsInStock, drProducts.UnitsOnOrder, _
                             drProducts.ReorderLevel, drProducts.Discontinued)
            End If
            iRowNum += 1
        Next

        If oValidationErrors.ErrorsCollection.Count > 0 Then Throw oValidationErrors

        Return MyBase.Update(dataTable)
    End Function

    Public Sub CheckIsValid(ByRef oValidationException As CValidationException, ByVal iRowNum As Integer, ByVal ProductName As String, _
                            ByVal SupplierID As Integer, ByVal CategoryID As Integer, ByVal QuantityPerUnit As String, _
                            ByVal UnitPrice As Decimal, ByVal UnitsInStock As Short, ByVal UnitsOnOrder As Short, _
                            ByVal ReorderLevel As Short, ByVal Discontinued As Boolean)
        If ProductName Is Nothing OrElse ProductName.Trim.Length = 0 Then
            oValidationException.AddException(iRowNum, "ProductName", "ProductName is a required field.")
        End If
    End Sub

End Class


The Presentation Layer - Web UI
The purpose of the presentation layer is to present a user interface to the user for input and validation. One great advantage you gain from tiering out an application is that you can create different clients (for example ASP.NET and WindowsForms), but still reuse the business logic and data access layers.

For this walk-thru, we will create an ASP.NET web client much like we did in the initial "2-tier" demo at the beginning of the article. But in this case, we will use our Customer BLL class as a data source, using the new ObjectDataSource component.

(1) Right-click the NTierDemo solution and select Add/New Web Site... In the Location textbox, enter http://localhost/NTierDemoClient


(2) Add a reference to the BusinessLogicLayer and Utilities projects by right-clicking the NTierDemoClient project and selecting Add Reference.

(3) Go to Design View for Default.aspx and drag-drop a GridView component from the toolbox onto the form. Click the Smart Tag for the GridView component and select <New data source...> from the Choose Data Source dropdown.


(4) Select the Object Data Source Type.


(5) On the next screen of the wizard, select the ProductsTableAdapterEx class from the BusinessLogicLayer project.


(6) There are four tabs on the "Define Data Methods" step of the wizard which can be used to define what methods should be called for each read and update operation. The wizard will automatically select the correct methods on our Business Logic Component..


(7) At this point, it is interesting to open the code-behind file, Default.aspx.vb, and note the generated ObjectDataSource control:
       <asp:ObjectDataSource ID="ObjectDataSource1" runat="server" DeleteMethod="Delete"
            InsertMethod="Insert" SelectMethod="GetData" TypeName="BusinessLogicLayer.ProductsTableAdapterEx"
            UpdateMethod="Update">
            <DeleteParameters>
                <asp:Parameter Name="Original_ProductID" Type="Int32" />
            </DeleteParameters>
            <UpdateParameters>
                <asp:Parameter Name="ProductName" Type="String" />
                <asp:Parameter Name="SupplierID" Type="Int32" />
                <asp:Parameter Name="CategoryID" Type="Int32" />
                <asp:Parameter Name="QuantityPerUnit" Type="String" />
                <asp:Parameter Name="UnitPrice" Type="Decimal" />
                <asp:Parameter Name="UnitsInStock" Type="Int16" />
                <asp:Parameter Name="UnitsOnOrder" Type="Int16" />
                <asp:Parameter Name="ReorderLevel" Type="Int16" />
                <asp:Parameter Name="Discontinued" Type="Boolean" />
                <asp:Parameter Name="Original_ProductID" Type="Int32" />
            </UpdateParameters>
            <InsertParameters>
                <asp:Parameter Name="ProductName" Type="String" />
                <asp:Parameter Name="SupplierID" Type="Int32" />
                <asp:Parameter Name="CategoryID" Type="Int32" />
                <asp:Parameter Name="QuantityPerUnit" Type="String" />
                <asp:Parameter Name="UnitPrice" Type="Decimal" />
                <asp:Parameter Name="UnitsInStock" Type="Int16" />
                <asp:Parameter Name="UnitsOnOrder" Type="Int16" />
                <asp:Parameter Name="ReorderLevel" Type="Int16" />
                <asp:Parameter Name="Discontinued" Type="Boolean" />
            </InsertParameters>
        </asp:ObjectDataSource>

The TypeName property is the class name of our BLL Component. This is the class that the ObjectDataSource control will create on Select and Update of the records. The SelectMethod, InsertMethod, UpdateMethod and DeleteMethod properties are the methods that will be called on the BLL component to Read, Insert, Update, and Delete. The DataObjectTypeName property is the class that will be used as a parameter to the update methods.

(8) Lastly, on the GridView smart tag, check all of the "Enable" checkboxes for Paging, Sorting, Editing, Deleting, and Selection.


Go ahead and run the solution, and try editing and updating a row.

Adding Validation to the Web UI 
One of the goals of an N-Tier architecture is to always place validation in the middle-tier. It is very tempting to break this rule because of the vast array of Validator controls provided by ASP.NET, and the fact that you can save round-trips to the web server by taking advantage of client-side validation. But it's important to appreciate the consitency and reusaility you gain by coding your validation into the Business Logic Layer instead of the Presentation Layer.

We'll use CustomValidator controls to signal validation errors to our users. To add a CustomValidator control to a column in the GridView, we will create a TemplateColumn.

(1) First, delete the existing ProductName bound column by clicking on the "ProductName" column header (you will see the column highlight) and then choosing "Remove Column" from the GridView's Smart Tag.

(2) Next we will add our TemplateField. From the GridView's SmartTag, select "Add New Column" and choose "TemplateField." Enter "Prodcut Name" for the Header Text.

(3) To edit the new TemplateField, right-click anywhere on the GridView and select "Edit Template" and then "Column[10] - Product Name"

(4) Drag a Label control into the ItemTemplate section of the designer and name it lblProductName. Click the SmartTag for lblProductName and select "Edit DataBindings." Bind the Text property of the Label control to the ProductName field as shown below.


(5) Drag a TextBox control and a CustomValidator control into the EditItemTemplate section, and name them txtProductName and vldProductName, respectively.  Using the SmartTag of the TextBox control, set the databinding for the Text property to field ProductName as you did in the previous step. Then set the Text property of the CustomValidator to an asterisk (*). The Template layout should now look similar to below.  To exit the template designer, click the SmartTag and select "End Template Editing."


(6) Finally, add a ValidationSummary control to the top of the Web Form.

Currently, our BLL component will raise our Custom Exception when it encounters a validation error. This is done through the CheckIsValid function. All we need to do is catch the exception and show the errors to the user. The GridView control provides a RowUpdated event which allows us to trap any exceptions that are raised during an Update. But before we handle the RowUpdated event, let's make a handy utility function that we can use to automatically flag the CustomValidator controls on our web page.

As long as we maintain a consistent name for our CustomValidator controls, we can cycle through the controls on a web form and set the IsValid and ErrorMessage properties with the following utility function:
    Public Shared Sub ApplyValidationToWebControl(ByRef oControl As System.Web.UI.Control, ByRef lstExceptions As List(Of CExceptionDetail))
        Dim oExceptionDetail As CExceptionDetail

        For Each oExceptionDetail In lstExceptions
            Dim oFindValidationControl As CustomValidator = CType(oControl.FindControl("vld" & oExceptionDetail.ColumnName), CustomValidator)
            If Not oFindValidationControl Is Nothing Then
                oFindValidationControl.IsValid = False
                oFindValidationControl.ErrorMessage = oExceptionDetail.ValidationError
            End If
        Next

    End Sub    
    
Using the above utility function, we can easily apply the validation errors to the user interface by handling the RowUpdated event as follows:
    Protected Sub GridView1_RowUpdated(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.GridViewUpdatedEventArgs) _
Handles GridView1.RowUpdated If Not e.Exception Is Nothing Then ' if an exception was caught, then we can get details from the InnerException. If TypeOf (e.Exception.InnerException) Is CValidationException Then Dim oExceptionDetails As List(Of CExceptionDetail) oExceptionDetails = CType(e.Exception.InnerException, CValidationException).ErrorsCollection ' notify the control not to raise the error further e.ExceptionHandled = True ' keep the grid in edit mode e.KeepInEditMode = True ' apply the validation to any matching CustomValidators CValidationUtil.ApplyValidationToWebControl(GridView1.Rows(GridView1.EditIndex), oExceptionDetails) End If End If End Sub
The Presentation Layer - Windows UI
One of the advantages to using an N-Tier architecture is that you can reuse the same Business Logic Layer components for multiple clients. In this section we will create a WindowsForms client that uses the same BLL component that we created above.

(1) Add a new project called NTierDemoWinClient by right-clicking the solution and selecting "Add/New Project" and then "Windows Application."

(2) Add a reference to the BusinessLogicLayer, DataAccessLayer, and Utilities projects by right-clicking the project and selecting "Add Reference"

(3) Add a new Data Source by selecting Data/Add New Data Source. This starts the Data Source Configuration Wizard. In the first step of the Wizard, select a data source type of  "Object"


(4) Select the DSProducts DataSet and select Next and then Finish.


(5) Modify the display mode of the Data Source to "Details" by expanding the Data Source window and right-clicking on the Products DataTable.


(6) Drag-and-drop the Products DataTable from the Data Sources window onto Form1. Note that a BindingSource and BindingNavigator are automatically added for you.


(7) On the ProductsBindingNavigator Toolstrip, select the "Save" icon named bindingNavigatorSaveItem and set its Enabled property to True (note that if you were binding a SQLDataSource, this step would not be necesary, but we need to do this with binding to an object).

(8) We can now add code to fill and update the Data using our BLL component:
    Private m_taProducts As New ProductsTableAdapterEx

    Private Sub frmProductsDataBind_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
        ProductsBindingSource.DataSource = m_taProducts.GetData
    End Sub

    Private Sub bindingNavigatorSaveItem_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _
        Handles bindingNavigatorSaveItem.Click
        ' this is an update
        ProductsBindingSource.EndEdit()
        m_taProducts.Update(ProductsBindingSource.DataSource)
    End Sub    
    
Adding Validation to the Windows UI
When we implemented validation above in our Web UI, we created a utility function to loop through controls on a web form and automatically set the Validator controls to an invalid state. We can create a similar method for a WindowsForms control using the SetColumnError method. Using SetColumnError in conjunction with the ErrorProvider control will make it very easy to flag validation errors to the user.
    Public Shared Sub ApplyValidationToDataTable(ByRef dtData As DataTable, _
        ByRef lstExceptions As System.Collections.Generic.List(Of CExceptionDetail))
        Dim oExceptionDetail As CExceptionDetail

        For Each oExceptionDetail In lstExceptions
            dtData.Rows(oExceptionDetail.RowNum).SetColumnError(oExceptionDetail.ColumnName, oExceptionDetail.ValidationError)
        Next

    End Sub
    
By using SetColumnError, we will see how the ErrorProvider Control will automatically show the user Validation Errors as they are caught.

(1) Add an ErrorProvider control from the Toolbox onto your form.

(2) Set the ErrorProvider DataSource property to "ProductsBindingSource"

(3) Modify the Save event so that it catches our Custom Validation Exception and uses the utility function to display the errors:
    Private Sub bindingNavigatorSaveItem_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _
        Handles bindingNavigatorSaveItem.Click
        ' this is an update
        ProductsBindingSource.EndEdit()
        Try
            m_taProducts.Update(ProductsBindingSource.DataSource)
        Catch ex As CValidationException
            MessageBox.Show("Error occurred:" & ex.ToString())
            CValidationUtil.ApplyValidationToDataTable(ProductsBindingSource.DataSource, ex.ErrorsCollection)
        End Try
    End Sub
    
Summary
VS2005 adds many new wizards and designers that speed the development of data applications. But if you use these tools in their default manner, you may tie yourself into a 2-Tier architecture. In this article we have examined various methods we can use to leverage the new designers and wizards to quickly create an N-Tier architecture.