Click here to Skip to main content
12,250,257 members (45,931 online)
Click here to Skip to main content
Add your own
alternative version

Stats

49.1K views
348 downloads
50 bookmarked
Posted

Using GridView with Business Objects using Adaptors

, 24 Apr 2007
Rate this:
Please Sign up or sign in to vote.
An example of how to use a ASP.Net GridView control with an ObjectDataSource and a Business Layer

Introduction

This article describes how to use a GridView control and an ObjectDataSource with a business layer. This solution uses domain model adaptors, which are a natural lightweight medium for digesting arbitrarily complex business data for use in a display. This article will cover basic display, updating, sorting and deleting of data in the GridView.

Background

The GridView control is a very rich web control available in the .NET 2.0 platform. Its purpose is to display information in a tabular format allowing optional in-place updates and deletions. In addition, there are literally dozens of properties that can give the developer fine control over the style and formatting of the final product. These properties will not be covered here as there are many good sources.

In order to use the ObjectDataSource, it must be fed from a stateless and IEnumerable collection of Serializable objects. If these restrictions are not appropriate to implement directly on your domain model, then one can use adaptors. The cost is a little extra planning and event handling to keep the domain objects up to date with the adaptors.

The Adaptors

The purpose of the adaptors is to contain a serializable subset of the domain data to be displayed in the GridView. The adaptors in this solution satisfy three interfaces.

IAdaptable is an interface that must be implemented by your domain objects. Its purpose is to help map individual adaptors back to the domain objects that they came from. In this solution, it contains just a single identity field of type long.

public interface IAdaptable
{
    long ObjectID { get; } 
}

IAdaptor<T> where T : IAdaptable is the interface that must be implemented by the adaptors. It is a generic interface taking a single type parameter T, which is the domain type being adapted. The methods it defines are void FillFromObject(T obj) which fills the adaptor with domain data, void FillFromExternalComponent(params object[] updates) which fills the adaptor with external data, e.g. from an updated GridView control, and void FillObject(ref T obj) which updates an object with data from the adaptor. It also contains its own set of properties for mapping adaptors back to domain objects, which in this case is just a single long identity field.

public interface IAdaptor<T> where T : IAdaptable
{
    long ObjectID { get; } 
    void FillFromObject(T obj);
    void UpdateObject(ref T obj);
    void FillFromExternalComponent(params object[] updates);
}

Finally, IAdaptorCollection<U,T> is the interface to be implemented by a collection of adaptors. This interface is used to define the façade to bind to the ObjectDataSource data methods. The type parameter T is the domain type being adapted as above, and the type U is its adaptor U: IAdaptor<T>. Depending on your needs there are many possible choices of methods to included here, and this example follows the spirit of the List interface fairly closely.

public interface IAdaptorCollection<T,U> 
where T : IAdaptor<U> 
where U : IAdaptable
{
    ICollection GetObjects(List<T> collection);
    void AppendObject(List<T> collection, T obj);
    void AppendObjects(List<T> collection, IEnumerable<T> obj);
    void InsertObject(List<T> collection, int pos, T obj);
    void InsertObjects(List<T> collection, int pos, 
        IEnumerable<T> obj);
    void UpdateObject(List<T> collection, int pos, 
        params object[] updates);
    void DeleteObject(List<T> collection, int pos);
    T GetObjectByPosition(List<T> collection, int pos);
    int GetPositionByID(List<T> collection, long id);
} 

The IAdaptorCollection implementation itself must be stateless, but it can bind method call parameters to Session state. It must be stateless because the ObjectDataSource we will bind it to is like all of the other controls on the web page: it gets created and destroyed on every postback. In the example, the adaptors are stored in a generic List in Session state and all of the IAdaptorCollection methods have parameters bound to this List.

The implementation of the adaptor classes is relatively straightforward and is provided in the example code. Other restrictions on the IAdaptorCollection implementation include

  • A default, no arugument constructor.
  • None of the methods used by the ObjectDataSource can be static.
  • The adaptors should be returned by a single method call, and these should expose their data through public properties.

The Example Business Layer

The example Business Layer here is a simple Purchase Order system consisting of a Customer, Purchase Order, and Purchase Items. The GridView control in this example will be used to display the purchase items in a tabular format with columns including Edit/Delete buttons, Item Number, Item Description, Quantity, and Cost. Only the quantity field will be updatable. More details about the example business layer can be found in comments in the example code. ASP.NET uses reflection to bind adaptor properties to parameters in the ObjectDataSource data methods. For the example, the adaptor classes are

[Serializable]
    public class PurchaseItemAdaptor : Adaptor<IPurchaseItem>
    {
        private long itemNumber;
        public long ItemNumber { get { return itemNumber; } set { itemNumber = 
            value; } }

        private string description;
        public string Description { get { return description; } set { 
            description = value; } }

        private int quantity;
        public int Quantity { get { return quantity; } set { 
            quantity = value; } }

        private decimal cost;
        public decimal Cost { get { return cost; } set { cost = value; } }

        public override void FillFromExternalComponent(params object[] updates)
        {
            quantity = (int)updates[0];
        }

        public override void FillFromObject(IPurchaseItem obj)
        {
            base.FillFromObject(obj);  
            itemNumber = obj.InventoryNumber;
            description = obj.ItemDescription;
            quantity = obj.Quantity;
            cost = obj.TotalCost();
        }

        public override void UpdateObject(ref IPurchaseItem obj)
        {
            obj.Quantity = quantity;
        }
    }

and

public class PurchaseItemAdaptorCollection : 
    AdaptorCollection<PurchaseItemAdaptor, IPurchaseItem>
{
    public void UpdatePurchaseItem(List<PurchaseItemAdaptor> collection, 
        int pos, int quantity)
    {
        UpdateObject(collection, pos, quantity);
    }
} 

In the PurchaseItemAdaptorCollection update method the first parameter is the generic list of adaptors from Session state, the second parameter pos is the row number of the updated adaptor, and the third parameter is the update value originating from the GridView control.

This code should live in the same library as the domain business objects or some other library. I prefer not to put the adaptors right in App_Code because if there is a ever compilation error in the adaptors, then the adaptor types will become unavailable to the web site build, and subsequent builds will fail for that reason alone even after you fix the original error.

The GridViewand ObjectDataSource

When creating the GridView control using Visual Studio, one specifies a DataSource. Choose the ObjectDataSource, and configure it to point to the PurchaseItemAdaptorCollection with the GetObjects method as the Select method. The next screen should give you a choice to bind parameters in the method call. Choose the collection parameter of the GetObjects() method, choose a Session field and pick a name. In the example, I am using "GridViewOrderItems". The code behind will be responsible for putting a List of PurchaseItemAdaptors into this Session field.

Screenshot - GridViewBusinessLayer1.gif

Having done this once using the GUI to get the syntax right, I prefer to do the rest of the work directly in XML. Here is the XML for the rest of the ObjectDataSource. The row numbers of affected rows will also be kept in a Session field.

<asp:ObjectDataSource ID="OrderItemDataSource" runat="server" 
    DeleteMethod="DeleteObject"
    SelectMethod="GetObjects" 
        TypeName="ExampleBusinessLayer.PurchaseItemAdaptorCollection"
    UpdateMethod="UpdatePurchaseItem">
    <UpdateParameters>
        <asp:SessionParameter Name="collection" 
            SessionField="GridViewOrderItems" Type="Object" />
        <asp:SessionParameter Name="pos" 
            SessionField="GVOrderItems_RowUpdating" Type="Int32" />
        <asp:Parameter Name="quantity" Type="Int32" />
    </UpdateParameters>

The solution uses events emitted by the GridView control to synchronize the data in the GridView with the data in the Business Layer on updates and deletes.

<asp:GridView ID="OrderItems" runat="server"  AllowPaging="True" 
    OnRowDeleting="HandleRowDeleting" OnRowDeleted="HandleRowDeleted" 
    OnRowUpdating="HandleRowUpdating" OnRowUpdated="HandleRowUpdated" 
    AutoGenerateColumns="False" DataSourceID="OrderItemDataSource" 
        PageSize="2" >
    <Columns>
        <asp:CommandField ShowDeleteButton="True" ShowEditButton="True"/>
        <asp:BoundField HeaderText="Item No." ReadOnly="True" 
            DataField="ItemNumber" />
        <asp:BoundField HeaderText="Item Description" ReadOnly="True" 
            DataField="Description"/>
        <asp:BoundField HeaderText="Quantity" DataField="Quantity" />
        <asp:BoundField HeaderText="Cost" ReadOnly="True" 
            DataField="Cost" />
    </Columns>

The basic pattern for updating is to set a row position parameter before the update, and synchronize the domain objects after the update. The reason for this pattern is that the row number is only available in the event arguments before the update, but the adaptors have not been affected until after the action. The code is shown below. (Note that "GridViewUpdateEventArgs" and "GridViewUpdatedEventArgs" are different types.)

protected void HandleRowUpdating(object sender, GridViewUpdateEventArgs args)
{
    // Fires before the update: set the affected row
    int globalRowIndex = (OrderItems.PageIndex * OrderItems.PageSize) + 
        args.RowIndex;
    Session["GVOrderItems_RowUpdating"] = globalRowIndex;
}

protected void HandleRowUpdated(object sender, GridViewUpdatedEventArgs args)
{
    // Fires after the update: get the affected row
    int globalRowIndex = (int)Session["GVOrderItems_RowUpdating"];
    // Find the corresponding domain object
    PurchaseItemAdaptorCollection helper = new PurchaseItemAdaptorCollection();
    List<PurchaseItemAdaptor> orderItems =
        (List<PurchaseItemAdaptor>)Session["GridViewOrderItems"];
    PurchaseItemAdaptor adaptor = helper.GetObjectByPosition(orderItems, 
        globalRowIndex);
    long domainID = adaptor.ObjectID;
    // Update the domain object
    IPurchaseOrder po = (IPurchaseOrder)Session["PurchaseOrder"];
    IPurchaseItem item = po.FindByID(domainID);
    adaptor.UpdateObject(ref item);
    // (Optional) Refresh the adaptor to catch any quantities 
    // that changed because of business rules
    adaptor.FillFromObject(item);
    // (Optional) Resort 
    // HandleSort(sender, args);
    // Clean up the Session state
    Session.Remove("GVOrderItems_RowUpdating");
}

Note that the adaptor refreshes the domain object in case business rules force a change, which are properly encapsulated in the Business objects. In our example, the total cost is a function of Quantity, and this is evaluated in the domain object. If this code were absent, the total cost would not update with changes in quantity until a subsequent postback. This can be omitted if business rules never cause such a change. Similarly, the search results may need to be resorted after the update.

The code for row deletion is similar, except that the work is done completely on the first GridView event. After the delete event, the affected adaptor is gone already, so the mapping to domain objects is lost.

Sorting

It should be noted that sorting is not directly supported by ObjectDataSource as is the case for SQLDataSource. Instead you can bind sorting methods to the GridView or to other controls to handle sorting. To indicate sorting, I usually prefer to use a drop down list of column choices plus a check button to indicate forward/reverse sorting. It is also common to use links in the column headers to accomplish the same thing, but I've always found a drop down list to be easier for users see right away which column is currently being sorted.

The sorting method used in this solution uses a HandleSort method bound to the OnSelectedIndexChanged event of a column chooser drop down list and to the OnCheckChanged event of a reverse sort check box. After sorting, a call to rebind the GridView is required. A sorting algorithm is given in the example code based on the popular QuickSort algorithm using delegate templates to compare values. (This was inspired by a similar implementation in Sestoft and Hansen; see the References below.)

public delegate int SortCompare&lT>(T p1, T p2);

Each choice in the column list gets its own comparison function, and the reverse sorting is handled building delegates on the fly. With the example for "Item Description" shown,

protected static int Compare1(PurchaseItemAdaptor p1, PurchaseItemAdaptor p2)
{
    return string.Compare(p1.Description, p2.Description);
}

SortCompare<PurchaseItemAdaptor>[] comparers = new 
    SortCompare<PurchaseItemAdaptor>[] 
{ Compare0, Compare1, Compare2, Compare3 };

protected void HandleSort(object sender, EventArgs e)
{
    int sign = cbReverse.Checked?-1:1;
    SortCompare<PurchaseItemAdaptor> sc = delegate(
        PurchaseItemAdaptor p1, PurchaseItemAdaptor p2)
    {
        return (sign * comparers[ddlSortingChooser.SelectedIndex](p1, p2));
    };
    PurchaseItemAdaptor[] items = ((
      List<PurchaseItemAdaptor>)Session["GridViewOrderItems"]).ToArray();
    QuickSort<PurchaseItemAdaptor>.Sort(items, sc);
    Session["GridViewOrderItems"] = new List<PurchaseItemAdaptor>(items);
    OrderItems.DataBind();
}

In any case, it is important to make sure that the adaptors are themselves sorted in Session state, otherwise subsequent GridView operations using the row number likely aren't going to hit the correct data.

Using the code

To run the code in Visual Studio, unzip the accompanying file and open the GridViewExample.sln file. The GridView is located in a user control called OrderItems.ascx. Open the GridViewExample.aspx page and viola!

Screenshot - GridViewBusinessLayer2.gif

Depending on how you've coded the update handler, the rows may be automatically resorted on update. In the provided example, this was turned off because resorting could cause the row to jump to another page.

Points of Interest

It's worthwhile to take a breather at this point and review some of the design decisions made in this example.

  • Storing the adaptors in Session state is the most interesting design decision in the example. By storing business data in Session state, the methods handling the adaptors have to be scoped so that they can access Session state or have the Session state passed in through a method parameter by the ObjectDataSource. For me, this was the biggest conceptual gotcha because it is just more natural to want to have a stateful adaptor collection class in Session state feeding the ObjectDataSource rather than a stateless collection class taking a collection as a method parameter. It ain't gonna happen in most circumstances because the ObjectDataSource gets destroyed and rebuilt on every postback along with all of the other controls.
  • The adaptors encapsulate the information about how to map updates from a GridView to specific adaptor properties, and keeps it relatively encapsulated within two closely related classes. The GridViewUpdatedEventArgs also contain information about changing data. In principle, you can use that information to accomplish the update. But the logic is already encapsulated in the adaptor class, and so the adaptor should be used to actually do the update.
  • During updates, the ObjectDataSource doesn't use a row number, but will use the specified UpdateParameters to select a method to update the adaptors. If not specified, the ObjectDataSource will bind listed parameters to named properties of the adaptors using the parameter name, but it will not actually bind them to the GridView unless the corresponding parameters in GridView are updatable and visible. There may be a way around this in general, but in this solution that would mean updatable identity fields, which would be a bad thing. In order to find the correct adaptor to update, one needs the row number in the GridView which is only available in the GridView.
  • Another approach could be to recreate the adaptors and rebind the GridView on each postback. This is an ironclad method to guarantee that the GridView is synced with the data in the business layer, but it could incur a lot of unnecessary rebinds. For events that do not originate on the GridView itself, I think this is the proper route. For actions like update and delete that originate in the GridView, it is possible to handle the events arising from user actions directly and fix up the domain data directly and avoid a rebind.
  • It is possible to sort using the GridView with ObjectDataSource by setting the SortParameterName property on the ObjectDataSource. This will cause the ObjectDataSource to append a parameter of the given name to the parameter list when it calls the specified select method, GetObjects(). For example, if one chooses to sort on the Cost column keeping everything else the same, one sets SortParameterName="Cost" on the ObjectDataSource, and the call will have the signature GetObject(List<PurchaseItemAdaptor> collection, object Cost). Nothing actually gets passed in this extra parameter (as verified in the Visual Studio debugger) but it appears to solely function as a way for reflection to select the correct method at runtime to get the adaptors.

Conclusion

When I first contemplated using GridView, I already had a Business Layer complete with domain objects and database methods, and so I tried to bolt it onto the existing infrastructure. While I was enticed by all of the features that GridView, I failed utterly to incorporate it into my code on the first try! After several hours of playing around with GridView and encountering every possible gotcha, I ended up spending several more hours writing my own tabular display user control from scratch that did paging and sorting and had a few of the other essential features. The above implementation is very much the result of many other subsequent trials and errors.

This solution uses lightweight adaptors to feed the ObjectDataSource instead of domain objects directly. It may be appropriate for some business layers to directly adopt the Serialization and Statelessness requirements needed to hook up to an ObjectDataSource. The nice thing about using domain model adaptors is that they can naturally be reused in many places in your architecture. For example, I use adaptors to feed a GridView, to exchange information between my domain model and external databases and remote services.

In the end, I've come to think that GridView is a great choice for Business Layers. The above solution requires a fair amount of planning, but it actually is not a lot of code, and what code there is is mostly boilerplate. Plus, the code overall has a good organization when using the GridView; better than I achieved without it.

References

  • C# Precisely, 2nd Ed., by Peter Sestoft and Henrik I. Hansen, MIT Press (2004)
  • Pro ASP.NET 2.0 in C# 2005, by Matthew MacDonald and Mario Szpuszta, APress (2005)
  • More information at my blog, The Solarium

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

Share

About the Author

ggraham412
Web Developer
United States United States
No Biography provided

You may also be interested in...

Comments and Discussions

 
GeneralMy vote of 5 Pin
manoj kumar choubey9-Feb-12 22:20
membermanoj kumar choubey9-Feb-12 22:20 
GeneralVery Cool Pin
amagondes26-May-08 1:38
memberamagondes26-May-08 1:38 
GeneralVery nice! Pin
Nan Sheng14-Oct-07 15:07
memberNan Sheng14-Oct-07 15:07 
GeneralBusiness objects are not data base objects Pin
blorq1-May-07 11:36
memberblorq1-May-07 11:36 
GeneralRe: Business objects are not data base objects Pin
devstuff2-May-07 0:32
memberdevstuff2-May-07 0:32 
GeneralRe: Business objects are not data base objects Pin
ggraham4122-May-07 12:18
memberggraham4122-May-07 12:18 

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.

| Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.160426.1 | Last Updated 24 Apr 2007
Article Copyright 2007 by ggraham412
Everything else Copyright © CodeProject, 1999-2016
Layout: fixed | fluid