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.
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>
<SelectParameters>
<asp:SessionParameter Name="collection"
SessionField="GridViewOrderItems" Type="Object" />
</SelectParameters>
<DeleteParameters>
<asp:SessionParameter Name="collection"
SessionField="GridViewOrderItems" Type="Object" />
<asp:SessionParameter Name="pos"
SessionField="GVOrderItems_RowDeleting" Type="Int32" />
</DeleteParameters>
</asp:ObjectDataSource>
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>
</asp:GridView>
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)
{
int globalRowIndex = (OrderItems.PageIndex * OrderItems.PageSize) +
args.RowIndex;
Session["GVOrderItems_RowUpdating"] = globalRowIndex;
}
protected void HandleRowUpdated(object sender, GridViewUpdatedEventArgs args)
{
int globalRowIndex = (int)Session["GVOrderItems_RowUpdating"];
PurchaseItemAdaptorCollection helper = new PurchaseItemAdaptorCollection();
List<PurchaseItemAdaptor> orderItems =
(List<PurchaseItemAdaptor>)Session["GridViewOrderItems"];
PurchaseItemAdaptor adaptor = helper.GetObjectByPosition(orderItems,
globalRowIndex);
long domainID = adaptor.ObjectID;
IPurchaseOrder po = (IPurchaseOrder)Session["PurchaseOrder"];
IPurchaseItem item = po.FindByID(domainID);
adaptor.UpdateObject(ref item);
adaptor.FillFromObject(item);
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!
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
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.