Click here to Skip to main content
15,879,239 members
Articles / Programming Languages / C#
Article

Custom DataBindable BusinessObjects and The Typed DataSet

Rate me:
Please Sign up or sign in to vote.
4.80/5 (36 votes)
3 Jan 200716 min read 149.3K   2K   147   38
An article about Custom Businessobjects in conjunction with the DataSet to Retrieve Data

Introduction

There has been, and still is a lot of discussion going around of what kind of technique a developer in general should use to represent the data from the DataLayer through the BusinessLayer to the Presentation Layer. In common, there are 2 main "ideas" floating around, those who swear the "one and only object Paradigm" (thus them who use an O/R mapping tool to map the database fields to custom object properties), and those who are using the intermediate of the DataSet (typed) to present the Data in the Presentation Layer.

Some years ago, as being a .NET Architect for a while now, i had to make this difficult decision. As, at that time binding custom business object in the .NET framework wasn't as trivial as it is now, I decided to create BusinessObjects holding a "typed dataset" as data datacontainer and implementing some base "Business Classes" to forsee a general way of validating the data in the 'rows' of the DataContainer.

For all, there's one constant, in modern development, we should implement separate layers/tiers in our development (and for ones : a layer is a logical separation of our code, a tier is a physical separation of the code, which means that a logical layered application will run on a single machine, some logic (like remoting or WebService) enhancments should be added to make the layers run on separate tiers (machines)).

Allright, so I decided to choose the DataSet "camp" of the story, and ... neverthless some people think that DataSets (typed) are lazy and dumb containers creating a lot of overhead, I felt (and still feel ...) comfortable with this dumb implementation in my Business Layer ! No O/R mapping to forsee, data in the DataBase is brought in a Transparent way to the Presentation Layer and relational data is a piece of cake !  Allright ... if you change the "DataBase", you have to do some "plumbing" to make the program compile again (as with pure customobjects you won't get errors, but if you forget to add the new properties to you're bizzclasses (for those who discard themselves from code generators ...) you will not be noticed from having new property data ...

The 2 worlds of "DataCentric" (DataSets) and "Object Centric" (Custom Objects) will probably never reconcile, but as binding custom businessobjects in .NET 2.0 and automated data-access (SQl-Server only !) code generation become more mature, I'll put the spot on reconciling those 2 in the next Article.

I will be presenting a simple logically layered framework implementing a "pure businessobjects" layer for DataBinding to "the Presentation Layer" and using the new featured data-access capabilities in .NET 2.0 (yep ! which are based on (typed) DataSets !!!) to render the data to our custom business objects. And for sake of simplicity, we will keep stuck with realy simple BusinessObjects (so omitting caching, bizzrules and so forth) ...

The reason why I want to post this Article, is because I already saw some articles or partial code projects explaining some O/R mapping and databinding principles, but it's hard to find a real wel documented and full flavoured example on the net. So, not only implementing the "Get()" part (that's the easy part !) but also explaining the "Save()" part (that's the hard one !), and not only for simple data objects, but for more real world related "parent-child" data. Well ... if you're looking for such a fully flavoured example, start reading this article and I hope you'll enjoy the contents !

This article will be presented as a "step-by-step" tutorial, so you can create the project from scratch or look right into the demo code if you want to ...

Using the code

Step 1 : Creating the Data Access Layer

Alright ! Let's hit the road starting with building the Data Access Layer. First we will create an empty C# .NET "Solution", call it "DCAF" (DataCentricApplicationFrameWork) . Next Add a  C# "Library" type project  to the Solution, called "DCAF.DataLayer". So far you should have next view (see Fig1.).

Image 1

Fig 1. Initial Solution

For the purpose of our demo, we will use the Northwind database and creating a form which shows us all customers and related orders. So, our "DataAccessLayer" will hold a typed dataset called CustomerOrderTS to serve this purpose. So select "Add" , next "New Item" in the Solution Explorer of the current DataLayer Project, next select "DataSet" from the displayed template list and call it CustomerOrderTDS.xsd. Next select the TypedDataSet in the Solution Explorer, and click on the server explorer link in the Left Panel. Now you should see the data connections panel appearing in the left pane (see Fig2.).

Image 2

Fig 2. Server Explorer

No we will add our DataBase DataSource, thus the Customer and Order tables from the NortWhind Database. For doing this, first "right-click" the Data Connections item, and select Add Connection from the rollout. Next select your SQL-Server instance, select the NorthWind database from the DataBase rollout and hit the "OK" button. (see fig3.).

Image 3

Fig 3. Add Connection

Next you should see the NortWind Database in the left panel, now rollout the Treeview for the Tables component, select Customers and Orders tables (see Fig 4.) and drag them to the middle panel empty container for the CustomerOrdersTDS. Tables are automatically added to the surface.

Image 4

Fig 4. Create DataSet (typed)

Going to delve deeper in the created code for the typed dataset is beyond the scop of this article, but note  that tables + relations as known in the NorthWind Database are added, and each table has his own TableAdapter right out of the Box. Each TableAdapter has a by default created access method to load all table data. We will meet the use of those TableAdapters further in the document, when creating the data service classes.

Now that we have created our DataContainers, we have to create our data service classes. Those classes will be used as intermediate between our DataAccessLayer and the BusinessLayer. In fact our data service classes will request the data from the DataBaseServer, put them in a TypedDataSet (for our demo,  the one we created a minute ago) and deliver the DataSet to our Custom BusinessObjects.

Before implementing all this service stuff, we should add one method to our created Typed DataSet, which is a method that will grab our Customer and Order data in one single batch. We will add this code as a method to the Partial Class definition (which is new in 2.0) of our typed dataset. For doing this, select CustomerOrdersTDS and select the "Code" icon of the Solution Explorer . This should result in the code as mentioned in Fig 5.

Image 5

Fig 5. Partial Class Definition

Now we will add the code involved to retrieve the batch query for customers and orders

C#
using DCAF.DataLayer.CustomerOrderTDSTableAdapters;

namespace DCAF.DataLayer {

    partial class CustomerOrderTDS
    {
        public static CustomerOrderTDS GetCustomerOrders()
        {
            CustomersTableAdapter custAdapter = new CustomersTableAdapter();
            OrdersTableAdapter ordAdapter = new OrdersTableAdapter();

            CustomerOrderTDS ds = new CustomerOrderTDS();
            custAdapter.Fill(ds.Customers);
            ordAdapter.Fill(ds.Orders);

            return ds;
        }
    }
}

As you can notice from the code, the static method GetcustomerOrders() returns the data required for our Customer and Order BusinessObject in one single track.

The last step which we have to take when developing our DataLayer, is the construction of our Service class which will be the intermediate between our DataLayer and the BusinessClasses involved.

So let's start by creating a new class, called CustomerOrderService.csThe end result is shown in the code next beneath:

C#
using System;
using System.Collections.Generic;
using System.Text;
using DCAF.DataLayer.CustomerOrderTDSTableAdapters;

namespace DCAF.DataLayer
{
    public class CustomerOrderService
    {
        #region "Storage"

        private CustomersTableAdapter m_customersAdapter = null;
        protected CustomersTableAdapter CustomersAdapter
        {
            get
            {
                if (m_customersAdapter == null)
                {
                    m_customersAdapter = new CustomersTableAdapter();
                }
                return m_customersAdapter;
            }
        }

        private OrdersTableAdapter m_ordersAdapter = null;
        protected OrdersTableAdapter OrdersAdapter
        {
            get
            {
                if (m_ordersAdapter == null)
                {
                    m_ordersAdapter = new OrdersTableAdapter();
                }
                return m_ordersAdapter;
            }
        }
        
        #endregion "Storage"

        #region "Public Interface
        
        public CustomerOrderTDS.CustomersDataTable GetCustomers()
        {
            return CustomersAdapter.GetData();
        }

        public CustomerOrderTDS.OrdersDataTable GetOrders()
        {
            return OrdersAdapter.GetData();
        }

        public CustomerOrderTDS GetCustomerOrders()
        {
            return CustomerOrderTDS.GetCustomerOrders();
        }

        #endregion "Public Interface"
    }
    
}

As you can retrieve from the code, we use the separate TableAdapters for retrieving atomic table data, and use or enhanced partial typed dataset method to retrieve customers and orders in a single track. As you may noticed, for returning the single Customer or Order table data, we just use the default method GetData() on the Adapter, nothing more is involved here !

All right, that's all what is concerned about the DataLayer and DataLayer service classes, let's move to our Business Layer right away !

Step 2 : Creating the Custom Business Layer

First thing to do is to add a new project to our solutions, again, it's type is a ClassLibrary, call it DCAF.BusinessLayer.

Because our Custom BusinessObjects should be knowing where to get their data, we should at this point add a reference to the DCAF.DataLayer Assembly. This can be easily done by selecting References in the project folder, next right-mouse click, choose "Add reference", go to the Projects tab and select the Assembly, just as shown in Fig 6. 

Image 6

Fig 6. Adding Assembly reference to the project

Next, add 2 classes to the project, called CustomerBO (which will hold the Customer definition) and OrderBO (which will hold the Order definition).

C#
using System;
using System.Collections.Generic;
using System.Text;
using DCAF.DataLayer;
using DCAF.DataLayer.CustomerOrderTDSTableAdapters;

namespace DCAF.BusinessLayer
{
    public class CustomerBO
    {
        private string m_CustomerId;
        private string m_CompanyName;
        private List<OrderBO> m_Orders = new List<OrderBO>();

        public string CustomerId
        {
            get { return m_CustomerId; }
            set { m_CustomerId = value; }
        }
        public string CompanyName
        {
            get { return m_CompanyName; }
            set { m_CompanyName = value; }
        }

        public List<OrderBO> Orders
        {
            get { return m_Orders; }
        }

        }
   }

The Customer Business Object class contains 2 properties, one for it's ID, and one for it's Name, and holds a Collection of Orders (List) Also Note the using of the  DCAF.DataLayer references here, we'll come back to this in a minute !

C#
namespace DCAF.BusinessLayer
{
    public class OrderBO
    {
        private int m_OrderId;
        private string m_ProductName;
        private CustomerBO m_Customer;
        private DateTime m_OrderDate;

        public CustomerBO Customer
        {
            get { return m_Customer; }
            set { m_Customer = value; }
        }

        public int OrderId
        {
            get { return m_OrderId; }
            set { m_OrderId = value; }
        }
        public string ProductName
        {
            get { return m_ProductName; }
            set { m_ProductName = value; }
        }

        public DateTime OrderDate
        {
            get { return m_OrderDate; }
            set { m_OrderDate = value; }
        }
    }
}

The Orders table on the other side, contains also some property values and a reference to it's containing customer !

And finally ... we should implement our O/R mapping function to Fill our Custom BusinessObjects with Data ! For this purpose, i've implemented next method (explanation follows after the code segment !). Method should be added to the CustomerBO class !

C#
public static List<CustomerBO> GetCustomerOrders()
{
        //--- Create the DataService
        CustomerOrderService dataService = new CustomerOrderService();

        CustomerOrderTDS dataContainer;

        //--- Create the DataContainer for Customers and Orders retrieval
        dataContainer = new CustomerOrderTDS();

        //--- Get the Data from the DataService into the DataContainer
        dataContainer.Merge(dataService.GetCustomerOrders());

        //--- Create a CustomerObject List to hold the Customers
        List<CustomerBO> custList = new List<CustomerBO>();

        //--- Loop through the CustomerData from our DataContainer
        foreach (CustomerOrderTDS.CustomersRow custRow in
                                  dataContainer.Customers.Rows)
        {
            //--- Create a Customer Object Instance
            CustomerBO customer = new CustomerBO();

            //--- Map the Relational data to Object properties
            //for Customer
            ORM.RelationalToObject(customer, custRow);

            //--- Select the Related Orders of the Customer
            CustomerOrderTDS.OrdersRow[] orderRows =
                     (CustomerOrderTDS.OrdersRow[])custRow
                     .GetChildRows("FK_Orders_Customers");

            //--- Loop through the related OrderData for the
            // Current Customer
            int numOrder = 0;
            foreach (CustomerOrderTDS.OrdersRow orderRow in
                                      orderRows)
            {
                numOrder++;

                //--- Create an Order Object Instance
                OrderBO order = new OrderBO();

                //--- Map the Relational data to Object
                //properties for Order
                ORM.RelationalToObject(order, orderRow);

                //--- Add the Customer Reference to the Order
                order.Customer = customer;

                order.ProductName = string.Format("Product
                      {0}-{1}", order.OrderId, numOrder);

                order.Initializing = false;

                //--- Relate the Order to The Current Customer
                customer.Orders.Add(order);
            }

            customer.Initializing = false;

            //--- Add the Customer to the CustomerList
            custList.Add(customer);
          }
        return custList;
    }

So, we create a static method called GetCustomerOrders which returns a List of CustomerObjects (note : you can also use the generic counterpart of List, List<T>). First, we grab our data through the DataService in our Typed DataSet. Next we cycle through the DataContainer Rows for Customer, add Customer Info, get the ChildRows() for Orders, Add OrderInfo and finally adding the Customer object to the List.

To simplify the task of O/R mapping, I've added a class ORM to the Project. This class holds 2 methods, one for mapping the relational data to the object properties (called ORM.RelationalToObject) and another for mapping the properties back to relational data when saving the object, and this in a dynamic manner, so the methods can be re-used for any table/object mapping scenario. The code beneath shows the method for RelationalToObject mapping. The opposite methode will be explained when we describe our save routine.

C#
public static void RelationalToObject( object p_obj, DataRow p_dataRow)
{
     //--- Get Object Properties
     PropertyDescriptorCollection props = TypeDescriptor.GetProperties(p_obj);

     //--- Apply OR-Mapping
                       
     foreach (DataColumn column in p_dataRow.Table.Columns)
     {
         for (int propertyIndex = 0; propertyIndex < props.Count; propertyIndex++)
         {
             PropertyDescriptor prop;
             try
             {
                 prop = props[propertyIndex];
             }
             catch
             {
                 continue;
             }
                    
             //--- Omit IListTypes (childcollections)
             if (prop.PropertyType.GetInterface("IList") == null)
             {
                 if (prop.Name.Trim().ToLower() == column.ColumnName.Trim().ToLower())
                 {
                     prop.SetValue(p_obj, p_dataRow[column]);
                 }
             }
         }
     }

We're done with our BusinessLayer, and ones more, just for this demo, I kept to a very basic implementation of the BusinessObject, without being concerned about BizzRules, Caching, PropertyChanged Notification (will be included when describing the Save() part) and so on, cause those implementations are beyond the scope of this article.

Step 3 : Creating the Presentation Layer

Presenting the Data in the Windows Form

Well, now that we are finished with all the plumbing code for the DataLayer and BusinessLayer we want to vizualize our business data in the Presentation Layer. You will notice that separating the logic in layers, does not only result in more easily maintainable code, but adds also a great number of transparancy to the development cycle, which means that the learning curve for the Presentatien Layer programmer is less time conzuming, because the Presentation Layer programmer does not to be aware of the implementation details of the other layers (business en data), he can just grab the ready made custom object and concentrate on presenting them in the Presentation Layer.

First thing todo is adding a new project, this time we're talking about a Windows Application. So start adding a new project to the Solution and call it CDAF.PresentationLayer and rename the Form1 class to CustomerAdmin.

In this demo, we want to represent our customers and related orders, each in a grid related to each others. So the first thing the Presentation Layer programmer has to do is adding a DataSource to the project. You can achieve this by selecting Data and select next Add New DataSource ... on the menu (see Fig.7).

Image 7

Fig 7. Add a new DataSource to the project

Next choose object as DataSource (see Fig. 8) and "Click" the "Next" button.

Image 8

Fig 8. Add an object DataSource

Next we have to choose the object location for the DataSource, this means we have to add a reference to our BusinessLayer Assembly ! Click "Add Reference" add this point (see Fig. 9).

Image 9

Fig 9. Add a reference to the object location Assembly

Next you have to select the DCAF.BusinessLayer Assembly from the Project tab list (see Fig. 10).

Image 10

Fig 10. Select the Assembly Reference

Next we have to Choose our object to Bind to. Select the CustomerBO at this point (see Fig. 11).

Image 11

Fig 11. Select the Object to DataBind

Finally click "Next", then "Finish". Notice that a DataSources folder has been added to the properties of our Presentation Layer Project (see Fig. 12).

Image 12

Fig 12. Embedded DataSources in the Presentation Layer

Allright ! Now that we have our DataSource added to the Project, we will add object instances for our Customer and Order BusinessObject. So, first select Show DataSources from the Data tab (see Fig. 13).

Image 13

Fig 13. Show DataSources

As you can see (see Fig 14.), the DataSources panel on the left shows our CustomerBO object. At this point, first select CustomerBO grid icon and drag to the form, next do the same for Orders grid Icon.

Image 14

Fig 14. DataSources Panel

Finally, the form should show up as follow (see Fig. 15).

Image 15

Fig 15. Initial Object Bound Customer Form

Isn't this an impressive enhancement of the VS IDE ?

The IDE did not only add our grids, but also a bindingnavigator, customerbindingsource and orderbindingsource (see Fig. 16), just right out of the box ! By default the CustomerGrid.DataSource is bound to the customerBOBindingSource, the OrderGrid.DataSource is bound to the ordersBindingSource and the customerBOBindingNavigator.DataSource is bound to the customerBOBindingSource.

Image 16

Fig 16. Binding Tools

So, what's left for the UI programmer is getting the Data from our CustomerBO and binding the appropriate BindingSources, as you can see from the code below !

C#
private void CustomerAdmin_Load(object sender, EventArgs e)
{
    //--- Get the data through the static method of our CustomerBO
    customerBOBindingSource.DataSource = DCAF.BusinessLayer.CustomerBO
    .GetCustomerOrders();

    //--- DataBind the BindingSources
    ordersBindingSource.DataSource = customerBOBindingSource;
    ordersBindingSource.DataMember = "Orders";

}

Now Hit 'F5' and smile ! You see from the result (see Fig 17.) that our grids are loaded smoothly. Select another customer and you will see that the order grid automatically adapt his bindings to reflect the right data !

Image 17

Fig 17. CustomerAdmin

As you can see from the OrderGrid, we're still left with a minor problem. For the Customer Column, we would like to see the CustomerID instead of the Customer Object type declaration. The reason why the grid displays the Customer Object Type declaration, is due to the nature of the Bound Column. If you open the Grid properties for the OrderGird, and select the Columns (Collection) property, then you will see that the column is bound to the instance of the Customer Object reference in the OrderObject. Now, if we want instead to have some "meaningfull" data in the column, let's say CustomerID, we have to subscribe to the CellFormatting event of the OrderGrid and add the code as mentioned Below.

C#
private void OnCellFormatting(object sender,
                 DataGridViewCellFormattingEventArgs e)
{
    //--- Display CustomerID instead of CustomerObject

    if (ordersDataGridView.Columns[e.ColumnIndex]
            .DataPropertyName == "Customer")
    {
        object currentCustomer = customerBOBindingSource.Current;

        PropertyDescriptorCollection props = TypeDescriptor
                           .GetProperties(currentCustomer);
        PropertyDescriptor propDesc = props.Find("CustomerID", true);

        try
        {
            e.Value = propDesc.GetValue(currentCustomer).ToString();
        }
        catch
        {
            e.Value = "[UNDEFINED]";
        }

    }
}

If you re-run the application after adapting the code, you will see that only the CustomerID column is bound ! (see Fig. 18).

Image 18

Fig 18. CustomerAdmin Enhanced

Update the DataBase with New, Added & Deleted data

So far, we've been adding code to bind the data from the DataLayer to the Data Aware components (DataGridView) of the Presentation Layer. At this point, users can add, delete or modify customer or order related data through the interface of the DataGridView. After adding, deleting or modifying data, the user can choose to update the entered information to the Database. In the remainder section of this article we will describe the necessary steps involved to update the database with then newly, modified or deleted data.

There's quit some code involved to update our CustomObjects to the Datalayer, first, we'll start by adding two list instances as private members of our Customer Administration form. These list instances will keep track of deleted customers and orders.

C#
private List<CustomerBO> m_deletedCustomers = null;
private List<OrderBO> m_deletedOrders = null;

Next, we should handle the AddingNew event of the customerBoBindingSource which handles the Add of a new CustomerObject for the BindingSource. (See Fig. 19).

Image 19

Fig 19. Handling the AddingNew event for the CustomerBindingSource

The code which handles the add of a new customer to our CustomerBindingSource is shown here below.

C#
private void customerBOBindingSource_AddingNew(object sender,
                          AddingNewEventArgs e)
{
    CustomerBO customer = new CustomerBO();
    customer.IsNew = true;
    customer.IsDirty = true;
    customer.CompanyName = "<new Customer>";
    customer.CustomerId = "<new CustomerID>";
    e.NewObject = customer;
    customer = null;
}

The same eventhandler should be activated for our OrderBindingSource. Note (see code below) that the code which is responsible for adding a new order should also take a reference to the Parent collection (Customer). As or orders object has to hold an unique OrderID, we will set this ID to a negative incremental value. Our "Orders" DataSet contains an OrderID column which has been set as an AutoIncremental Column. The  ID set in the OrderObject is set to a negative value to avoid conflicts when inserting a new order row in the DataBase. When inserting a new Row in the OrderTable at ServerSide, the DataBase server will add the new OrderID and assing a new unique OrderID which will be returned in the Update DataSet and should be syncronized with the Object Values at the ClientSide (I'll explain this feature in next § of this document).

C#
private void ordersBindingSource_AddingNew(object sender,AddingNewEventArgs e)
{
    OrderBO order = new OrderBO();
    order.IsNew = true;
    order.IsDirty = true;
    order.OrderDate = DateTime.Today;
    order.ProductName = "<new productname>";
    //--- Set Temporary OrderID - Will be set to negative
    //value to avoid update conflicts
    m_lastOrderID--;
    order.OrderId = m_lastOrderID;

    //--- Get the Current Customer
    CustomerBO currentCustomer = (CustomerBO)customerBOBindingSource.Current;

    if (!(currentCustomer == null))
    {
        order.Customer = currentCustomer;
    }
    else
    {
        throw new NullReferenceException("Customer for Order not found !");
    }

    e.NewObject = order;

    order = null;
}

Handling the modified event is quite more transparent to the Presentation Layer developer. There are a few classes involved to handle the property modified portion of the CustomerBO or OrderBO.  We'll discuss these classes step by step. These helper classes are implemented in the Business Layer Assembly of the Project.

Let's start first with the IEventPublisher interface. This interface holds the blueprint for the Object Property Modification event. Let's take a closer look at this interface class:

C#
public interface IEventPublisher
{
   void RegisterListener<T>(T p_listener) where T : IEventListener;
   void UnregisterListener<T>(T p_listener) where T : IEventListener;
   void NotifyListeners();
   bool IsDirty {get; set;}
   bool Initializing { get; set;}
}

The IEventPublisher Interface holds method interfaces for registering or unregistering listener objects, notify the listeners when an object property has changed their values, and 2 property settings, first the IsDirty property which puts the attached object in a modified state, and the Initializing property which is set to true while loading the object data (so preventing to launch the modified events while loading the data in the objects).

Next we take at a look at the IEventListener interface. This interface holds a reference to the Publisher object (in our case the customer or order object) and descibes the method signature of the method that is involved to handle the "modified" event for the object properties.

C#
public interface IEventListener
{
    void OnNotification(IEventPublisher p_publisher);
}

The Publisher class is the base class for each custom object business class. Is BusinessObject derives directly from the Publisher Class. The Publisher class holds the methods to "Register" or "Unregister" Listener Objects. Listener Objects are those objects which will be Notified of changes in the Properties of the BusinessObject Class. A typical listener could be a Windows Form. This base class also contains some base properties which sets the BusinessObjects to some initial state like "IsNew", "IsDirty" (modified) of "IsInitializing" state.

C#
   public abstract class Publisher : IEventPublisher
{
    private delegate void m_eventHandler(IEventPublisher p_publisher);
    private event m_eventHandler m_event;

    #region "IEventPublisher Implementation"

    public void RegisterListener<T>(T p_listener) where T : IEventListener
    {
        m_event += new m_eventHandler(p_listener.OnNotification);
    }

    public void UnregisterListener<T>(T p_listener) where T : IEventListener
    {
        m_event -= new m_eventHandler(p_listener.OnNotification);
    }

    public void NotifyListeners()
    {
        if (m_event != null)
            m_event(this);
    }

    protected bool m_isDirty = false;
    public bool IsDirty
    {
        get { return m_isDirty; }
        set { m_isDirty = value; }
    }

    protected bool m_initializing = true;
    public bool Initializing
    {
        get { return m_initializing; }
        set { m_initializing = value; }

    }

    protected bool m_isNew = false;
    public bool IsNew
    {
        get { return m_isNew; }
        set { m_isNew = true; }
    }

    #endregion "IEventPublisher Implementation"
}

The ObjectChanged Listener class hold the OnNotification method which is executed when Properties of the BusinessObjectClass (which are registered to the NotifyChanged Event) get changed. 

C#
public class ObjectChangedListener : IEventListener
{
    #region "IEventListener Members"

    public void OnNotification(IEventPublisher p_publisher)
    {
        if (!p_publisher.Initializing)
        {
            p_publisher.IsDirty = true;
        }
    }

    #endregion "IEventListener Members"
}

In this case (see code below) the Notification event is attached to theset property. So when a property value changed the event will be thrown (properties of our CustomerBO).

C#
public string CustomerId
 {
     get { return m_CustomerId; }
     set
     {
         m_CustomerId = value;
         NotifyListeners();
     }
 }
 public string CompanyName
 {
     get { return m_CompanyName; }
     set
     {
         m_CompanyName = value;
         NotifyListeners();
     }
 }

When the user hits the save button, next code will be executed to update our backend database with the added, modified or deleted objects. As a lot of code is involved here, i've implemented the code explanation within the source.

C#
private void customerBOBindingNavigatorSaveItem_Click(object sender,
                                                      EventArgs e)
{

    //--- Validate the Value which loses control first
    this.Validate();

    //--- End the CurrentEdit on the BindingSources
    customerBOBindingSource.EndEdit();
    ordersBindingSource.EndEdit();

    //--- Create Customer and Order List to hold the 
    //Changed objects
    List<CustomerBO> changedCustomers = null;
    List<OrderBO> changedOrders = null;

    //--- Loop through the ObjectStacks to catch changed 
    //property data
    foreach (object obj in customerBOBindingSource)
    {
        CustomerBO customer = (CustomerBO)obj;
        if (customer.IsDirty)
        {
            if (changedCustomers == null)
                changedCustomers = new List<CustomerBO>();
            changedCustomers.Add(customer);
        }
        foreach (OrderBO order in customer.Orders)
        {
            if (order.IsDirty)
            {
                if (changedOrders == null)
                    changedOrders = new List<OrderBO>();
                changedOrders.Add(order);
            }
        }
        customer = null;
    }

    if (changedCustomers != null || changedOrders != null ||
        m_deletedCustomers != null || m_deletedOrders !=
        null)
    {
        bool IsUpdateOK = true;

        try
        {
            //--- Update through Business Object Layer
            int numUpdate = m_customerBO
                .SaveCustomerOrders(changedCustomers, 
                m_deletedCustomers, changedOrders, 
                m_deletedOrders);

            //--- Display Success
            MessageBox.Show(string.Format("{0} rows were 
                successfully updated to the database !", 
                numUpdate.ToString()));
        }
        catch (Exception ex)
        {
            IsUpdateOK = false;

            //--- Show Error
            MessageBox.Show(ex.Message, "Error Occured 
                Update Failed!");
        }
        finally
        {                                    
            //--- Reset object state & Release Resources

            if (IsUpdateOK)
            {
                //--- First Syncronize the OrderID's with 
                //Server Versions

                if (changedOrders != null)
                {
                    m_customerBO
                        .SyncroOrderID(changedOrders);
                }

                if (changedCustomers != null)
                {
                    foreach (CustomerBO customer in 
                        changedCustomers)
                    {
                        customer.IsDirty = false;
                        customer.IsNew = false;
                    }
                    changedCustomers = null;
                }

                if (changedOrders != null)
                {
                    foreach (OrderBO order in changedOrders)
                    {
                        order.IsDirty = false;
                        order.IsNew = false;
                    }
                    changedOrders = null;
                }

                //--- Release Helper Objects
                m_deletedCustomers = null;
                m_deletedOrders = null;

                //--- Refresh the OrderGrid
                ordersDataGridView.Refresh();
            }                    
        }        
    }            
}

While Added and Modified objects can be traced in the contained list, deletes can not. So we have to keep trace of those objects manualy. For this reason we have to implement next event handlers in the code.

At the definition section :

C#
public partial class CustomerAdmin : Form
{
    private CustomerBO m_customerBO;
    private List<CustomerBO> m_deletedCustomers = null;
        private List<OrderBO> m_deletedOrders = null;
        . . .
}

Code which takes care of deleting a Customer or Order object:

C#
private void customerBODataGridView_UserDeletingRow(object sender,
                                          DataGridViewRowCancelEventArgs e)
{
    OnCustomerOrderDelete(sender,e);            
}

private void OnCustomerOrderDelete(object sender, 
                                   DataGridViewRowCancelEventArgs e)
{
    if (m_deletedCustomers == null)
        m_deletedCustomers = new List<CustomerBO>();

    m_deletedCustomers.Add((CustomerBO)e.Row.DataBoundItem);

    if (((CustomerBO)e.Row.DataBoundItem).Orders != null)
    {
        if (m_deletedOrders == null)
            m_deletedOrders = new List<OrderBO>();

        m_deletedOrders.AddRange(((CustomerBO)
            e.Row.DataBoundItem).Orders);
    }            
}

private void ordersDataGridView_UserDeletingRow(
    object sender, DataGridViewRowCancelEventArgs e)
{
    OnSingleOrderDelete(sender,e);
}

private void OnSingleOrderDelete(object sender, 
                                 DataGridViewRowCancelEventArgs e)
{
    if (m_deletedOrders == null)
        m_deletedOrders = new List<OrderBO>();
    m_deletedOrders.Add((OrderBO)e.Row.DataBoundItem);
}

At last, our BusinessLayer Class takes care of handling the update to the DAL. The implementation as shown beneath also handles DbConcurrency issus in an Optimistic way.

C#
public int SaveCustomerOrders(List<CustomerBO> p_addedOrModifiedCustomers, 
                              List<CustomerBO> p_deletedCustomers, 
                              List<OrderBO> p_AddedOrModifiedorders, 
                              List<OrderBO> p_deletedOrders)
{                       
    //--- Create the Service to update the Data
    CustomerOrderService dataService = new 
        CustomerOrderService();
    //--------------------------------------------------------
    //--- Step 1 : Add Deleted Customer Object Information ---
    //--------------------------------------------------------

    if (p_deletedCustomers != null)
    {
        foreach (CustomerBO deletedCustomerObject in 
            p_deletedCustomers)
        {                  
            //--- Create New CustomerRow to Hold Reference to
            // the Deleted CustomerRow
            CustomerOrderTDS.CustomersRow deletedCustomerRow 
                = m_dataContainer.Customers.NewCustomersRow();

            //--- Check if Row Exists in Our DataContainer
            deletedCustomerRow = 
                m_dataContainer.Customers
                .FindByCustomerID(deletedCustomerObject
                .CustomerId);

            //--- Set RowState to Delete in our DataContainer
            if (deletedCustomerRow != null)
            {
                deletedCustomerRow.Delete();
            }                    
        }
    }            
    //--------------------------------------------------------
    //--- Step 2 : Add Deleted Order Object Information    ---
    //--------------------------------------------------------

    if (p_deletedOrders != null)
    {
        foreach (OrderBO deletedOrderObject in 
            p_deletedOrders)
        {
            //--- Create a New OrderRow to Hold Reference to 
            //the Deleted OrderRow
            CustomerOrderTDS.OrdersRow deletedOrderRow = 
                m_dataContainer.Orders.NewOrdersRow();

            //--- Check if Row Exists in Our DataContainer
            deletedOrderRow = 
                m_dataContainer.Orders.FindByOrderID(
                deletedOrderObject.OrderId);

            //--- Set RowState to Delete in our DataContainer
            if (deletedOrderRow != null)
            {
                deletedOrderRow.Delete();
            }
        }
    }
    //-------------------------------------------------------------
    //--- Step 3 : Add New/Modified Customer Object Information ---
    //-------------------------------------------------------------

    if (p_addedOrModifiedCustomers != null)
    {
        foreach (CustomerBO addedOrModifiedCustomerObject in
            p_addedOrModifiedCustomers)
        {                    
            //--- First Check If Current Customer Object is a
            // New or Modified Object in our DataContainer
            if (addedOrModifiedCustomerObject.IsNew)
            {
                //--- Add a New CustomerRow to Our  
                //DataContainer.Customer Table

                CustomerOrderTDS.CustomersRow newCustomerRow 
                    = m_dataContainer.Customers
                    .NewCustomersRow();

                //--- Map object properties to rowcolumns
                ORM.ObjectToRelational(
                    addedOrModifiedCustomerObject, 
                    newCustomerRow);

                //--- Add the New Customer Row to Our 
                //DataContainer
                m_dataContainer.Customers
                    .AddCustomersRow(newCustomerRow);
            }
            else
            {
                //--- Get Modified Row Information
                CustomerOrderTDS.CustomersRow 
                    modifiedCustomerRow = m_dataContainer
                    .Customers.NewCustomersRow();

                //--- Map object properties to rowcolumns
                ORM.ObjectToRelational(
                    addedOrModifiedCustomerObject, 
                    modifiedCustomerRow);

                if (modifiedCustomerRow != null)
                {
                    //--- Search modified Row in 
                    //DataContainer
                    CustomerOrderTDS.CustomersRow 
                        customerRowToModify =  
                        m_dataContainer.Customers
                        .FindByCustomerID(
                        modifiedCustomerRow.CustomerID);

                    //--- Map Changed Data, RowState will be 
                    //set to True for our 
                    //DataContainer.Customer Row !
                    if (customerRowToModify != null)
                    {
                        for (int i = 0; i < m_dataContainer
                            .Customers
                            .Columns
                            .Count; i++)
                        {
                            customerRowToModify[i] = 
                                modifiedCustomerRow[i];
                        }
                    }
                }                        
            }                     
        }
    }

    //------------------------------------------------------------
    //--- Step 4 : Add New/Modified Order Object Information   ---
    //------------------------------------------------------------

    if (p_AddedOrModifiedorders != null)
    {
        foreach (OrderBO addedOrModifiedOrderObject in 
            p_AddedOrModifiedorders)
        {
            //--- First Check if Current Order is a New or 
            //Modified object in our DataContainer
            if (addedOrModifiedOrderObject.IsNew)
            {
                //--- Add a New OrderRow to Our 
                //DataContainer.Order Table
                CustomerOrderTDS.OrdersRow newOrderRow = 
                    m_dataContainer.Orders.NewOrdersRow();

                //--- Map object properties to rowcolumns
                ORM.ObjectToRelational(
                    addedOrModifiedOrderObject, newOrderRow);

                //--- Map Foreign Key for Customers
                newOrderRow.CustomerID = 
                    addedOrModifiedOrderObject
                    .Customer
                    .CustomerId;

                //--- Add the New Order Row to Our 
                //DataContainer
                m_dataContainer.Orders
                    .AddOrdersRow(newOrderRow);
            }
            else
            {
                //--- Get Modified Row Information
                CustomerOrderTDS.OrdersRow 
                    modifiedOrderRow = m_dataContainer
                    .Orders
                    .NewOrdersRow();

                //--- Map object properties to rowcolumns
                ORM.ObjectToRelational(
                    addedOrModifiedOrderObject, 
                    modifiedOrderRow);

                if (modifiedOrderRow != null)
                {
                    //--- Search Modified Row in 
                    //DataContainer
                    CustomerOrderTDS.OrdersRow 
                        orderRowToModify = m_dataContainer
                        .Orders
                        .FindByOrderID(
                        modifiedOrderRow.OrderID);

                    //--- Map Changed Data, RowState will be 
                    //set to True for our 
                    //DataContainer.Order Row !
                    if (orderRowToModify != null)
                    {

                        for (int i = 0; i < m_dataContainer
                            .Orders
                            .Columns
                            .Count; i++)
                        {
                            System.Data.DataColumn column = 
                                orderRowToModify
                                .Table.Columns[i];

                            if (!column.ReadOnly)
                            {
                                orderRowToModify[i] = 
                                    modifiedOrderRow[i];
                            }
                        }

                        //--- Re-Map Foreign Key for 
                        //Customer
                        orderRowToModify.CustomerID = 
                            addedOrModifiedOrderObject
                            .Customer.CustomerId;                              
                    }
                }
            }
        }
    }

    //--- Update through the DataService
    if(m_dataContainer.HasChanges())
    {
        bool updateOK = true;

        try
        {
            return dataService
                .SaveWithTransaction(
                m_dataContainer,false);

        }
        catch (DBConcurrencyException dbconcEx)
        {
            //--- DbConcurrency Occured, ask User either to 
            //Persist his changes or reload from server

            string message = dbconcEx.Message + "\r\n";
            message += "Persist changes to the DataBase 
                [Yes]\r\n" +
                "Reload the changed Data from the 
                Server [No]";
                string caption = "DbConcurrency !";
            MessageBoxButtons buttons = 
                MessageBoxButtons.YesNo;
            DialogResult result;

            // Displays the MessageBox.

            result = MessageBox.Show(message, caption, 
                buttons);

            if (result == DialogResult.Yes)
            {

                //--- Persist the changes to the Database
                try
                {
                    return dataService
                        .SaveWithTransaction(
                        m_dataContainer, true);
                }
                catch (Exception ex)
                {
                    m_dataContainer.RejectChanges();
                    updateOK = false;
                    throw ex;
                }
            }
            else
            {
                try
                {
                    //--- Reload data from server and merge 
                    //with local data
                    if (m_dataContainer.HasErrors)
                    {
                        if (m_dataContainer
                            .Customers.HasErrors)
                        {
                            //------------------------------------------
                            //--- Resolve DbConcurrency For Customers---
                            //------------------------------------------

                            //---Get Serverside RowData
                            CustomerOrderTDS.CustomersRow[] 
                            customerErrorRows 
                                = (CustomerOrderTDS.
                                CustomersRow[])
                                m_dataContainer
                                .Customers.GetErrors();

                            //---Merge with local DataSet
                            foreach (CustomerOrderTDS
                                .CustomersRow 
                                customerErrorRow 
                                in customerErrorRows)
                            {
                                m_dataContainer
                                    .Customers
                                    .Merge(
                                    dataService
                                    .GetCustomerRow(
                                    customerErrorRow
                                    .CustomerID));
                            }

                            //--- Remap changed Data to 
                            //object Data
                            foreach (CustomerBO 
                                customerObject 
                                in p_addedOrModifiedCustomers)
                            {
                                //--- Get the Updated Row 
                                //from our DataContainer
                                CustomerOrderTDS
                                    .CustomersRow customerRow =
                                    (CustomerOrderTDS
                                    .CustomersRow)
                                    m_dataContainer
                                    .Customers
                                    .FindByCustomerID(
                                    customerObject
                                    .CustomerId);

                                //--- Only Update Error 
                                //Objects
                                bool isErrorRow = false;
                                foreach 
                                    (CustomerOrderTDS
                                    .CustomersRow 
                                    customerErrorRow 
                                    in customerErrorRows)
                                {
                                    if (customerErrorRow
                                        .CustomerID == 
                                        customerObject
                                        .CustomerId)
                                    {
                                        isErrorRow = true;
                                        break;
                                    }
                                }

                                if (customerRow != null && 
                                isErrorRow)
                                {
                                    ORM.RelationalToObject(customerObject, <BR>                                                           customerRow);
                                }
                            }
                        }

                        //------------------------------------------
                        //--- Resolve DbConcurrency For Orders   ---
                        //------------------------------------------

                        if (m_dataContainer.Orders.HasErrors)
                        {
                            //--- Get ServerSideData
                            CustomerOrderTDS.OrdersRow[] orderErrorRows
                                = (CustomerOrderTDS.OrdersRow[])
                                        m_dataContainer.Orders.GetErrors();


                            //--- Merge with local DataSet
                            foreach (CustomerOrderTDS.OrdersRow orderErrorRow 
                                                            in orderErrorRows)
                            {
                                m_dataContainer.Orders.Merge(
                                 dataService.GetOrderRow(orderErrorRow.OrderID));

                            }

                            //--- Remap changed Data to object Data
                            foreach (OrderBO orderObject  <BR>                                                  in p_AddedOrModifiedorders)
                            {
                                /*--- Get the Updated Row from our <BR>                                      DataContainer */
                                CustomerOrderTDS.OrdersRow orderRow =
                                    (CustomerOrderTDS.OrdersRow)
                                        m_dataContainer.Orders.FindByOrderID(<BR>                                                         orderObject.OrderId);

                                //--- Only Update Error Objects
                                bool isErrorRow = false;
                                foreach (CustomerOrderTDS.OrdersRow orderErrorRow <BR>                                                            in orderErrorRows)
                                {
                                    if (orderErrorRow.OrderID == <BR>                                                         orderObject.OrderId)
                                    {
                                        isErrorRow = true;
                                        break;
                                    }
                                }

                                if (orderRow != null && isErrorRow)
                                {
                                    ORM.RelationalToObject(orderObject, <BR>                                                           orderRow);
                                }

                            }
                        }                                    
                    }
                }
                catch (Exception ex)
                {
                    m_dataContainer.RejectChanges();
                    updateOK = false;
                    throw ex;
                }                                            
            }                                        
        }
        finally
        {
            if (updateOK)
            {
                //--- Accept the Changes on the DataSet
                m_dataContainer.AcceptChanges();
            }
        }
    }

    return 0;            
}

Voila . . . that's all folks ! Hope you enjoyed reading the article.

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


Written By
Web Developer
Belgium Belgium
Working in the IT-Branch for more then 10 years now. Starting as a programmer in WinDev, moved to Progress and actualy working in .NET since 2003. At the moment i'm employed as a .NET architect and teamleader at BERCO N.V. at Ronse (Belgium). In my spare time, i'm a die hard mountainbiker and together with my son Jarne, we're climbing the hills in the "Flemish Ardens" and the wonderfull "Pays des Collines". I also enjoy "a p'tit Jack" (Jack Daniels Whiskey) or a "Duvel" (beer) for "l'après VTT !".

Comments and Discussions

 
GeneralMy vote of 5 Pin
Manoj Kumar Choubey4-Apr-12 22:36
professionalManoj Kumar Choubey4-Apr-12 22:36 
GeneralAbout Partial Table Adapter classes in the solution file... Pin
Bhushan198030-Jan-10 18:41
Bhushan198030-Jan-10 18:41 
QuestionHow config Custom ConnectionString Pin
fugees9-Mar-08 20:39
fugees9-Mar-08 20:39 
Hi Emmanuel71
Tkanks for your article.I have a question about Custom ConnectionString.
when i want to connet another computer database ,how to modify this connectionString ?
AnswerRe: How config Custom ConnectionString Pin
Emmanuel Nuyttens19-Mar-08 5:22
Emmanuel Nuyttens19-Mar-08 5:22 
GeneralFantastic Article !! But... Pin
jwk20087-Nov-07 21:57
jwk20087-Nov-07 21:57 
QuestionORM without the codegen? Pin
chuma2620-Sep-07 20:13
chuma2620-Sep-07 20:13 
QuestionEnum types Pin
Michael_Brandt14-Aug-07 9:33
Michael_Brandt14-Aug-07 9:33 
GeneralThanks! Great Example Pin
jonpdar2-Aug-07 7:47
jonpdar2-Aug-07 7:47 
Generallinking a excel customer spreadsheet Pin
smmm43437-Jul-07 12:11
smmm43437-Jul-07 12:11 
GeneralTypedDataset adapter Pin
TheEagle23-Jun-07 0:43
TheEagle23-Jun-07 0:43 
GeneralRe: TypedDataset adapter Pin
Emmanuel7130-Jun-07 2:09
Emmanuel7130-Jun-07 2:09 
QuestionQuestion about your code Pin
darel_yng17-Jun-07 12:38
darel_yng17-Jun-07 12:38 
AnswerRe: Question about your code Pin
Emmanuel7117-Jun-07 20:53
Emmanuel7117-Jun-07 20:53 
GeneralRe: Question about your code Pin
darel_yng21-Jun-07 10:27
darel_yng21-Jun-07 10:27 
GeneralCustomers.RegionID Pin
krazykoder15-Jun-07 13:01
krazykoder15-Jun-07 13:01 
GeneralRe: Customers.RegionID Pin
Emmanuel7118-Jun-07 3:05
Emmanuel7118-Jun-07 3:05 
Generalmany to many relation in a dataset Pin
juan pablo carlos11-Jun-07 1:01
juan pablo carlos11-Jun-07 1:01 
GeneralRe: many to many relation in a dataset Pin
Emmanuel7112-Jun-07 1:58
Emmanuel7112-Jun-07 1:58 
GeneralLost explenation in the article Pin
Thommie.h27-May-07 4:27
Thommie.h27-May-07 4:27 
GeneralRe: Lost explenation in the article Pin
Emmanuel7112-Jun-07 2:01
Emmanuel7112-Jun-07 2:01 
QuestionMessageBox on BusinessLayer? Pin
Il.Socio18-Apr-07 6:04
Il.Socio18-Apr-07 6:04 
AnswerRe: MessageBox on BusinessLayer? Pin
dimitri_potjev20-Apr-07 1:24
dimitri_potjev20-Apr-07 1:24 
GeneralGood Article, but Pin
Joey Chömpff16-Jan-07 23:09
Joey Chömpff16-Jan-07 23:09 
GeneralRe: Good Article, but Pin
Emmanuel7117-Jan-07 8:33
Emmanuel7117-Jan-07 8:33 
GeneralGood article, enhancement suggestion Pin
baruchl8-Jan-07 20:50
baruchl8-Jan-07 20:50 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.