Of all the controls in the Asp.Net WebForms family I think I like the ObjectDataSource the most. It’s a shiny beacon of hope in a framework otherwise bogged down by the perils of ViewState, heavy and un-testable infrastructure classes, and much to be desired in terms of abstractions. I know everyone else is all about the MVC angle bracket soup these days but for those (stuck or by choice) in WebForms land, ODS is fantastic. What it allows you to do, in short, is to make often awkward data-binding your bitch. Let’s see how.
The essence of all data source controls is allowing you, the developer, to short-circuit traditional architectural techniques like layering and get to having working CRUD in a very just-let-me-get-this-done-and-go-home-what-do-you-mean-unit-testing fashion. SqlDataSource goes direct to a database, XmlDataSource goes to xml, and so on. ObjectDataSource is the only one that lets you break out of the 2-tier forms-over-data model that Microsoft likes to push. But what about LinqDataSource? Well, ODS works by binding to methods on a specified class in your code. Which, since it’s your code, allows you much more freedom than any other the others, including the LinqDataSource. Want to provide a specific instance of the binding class to be consumed by the ODS at runtime by your DI container of choice? Want to invoke custom business logic or validation during the CRUD binding-calls? Want to use constructor injection and make your binding class actually, you know, testable? Want to actually put code in a place other than an web form’s code-behind? Want to leverage server-side paging in Linq to Sql to only fetch and display the data you’re concerned with showing? Want to pipe in some custom data from the http cache? All doable with ODS, the others not so much.
For a class to serve as a binding source for an ObjectDataSource, it needs:
- One or more methods to return stuff to bind against. Business objects, a DataTable, whatever. These methods can optionally have
- Two integer parameters for paging (defaulting to ‘maximumRows’ and ‘startRowIndex’) indicating a subset of data to be returned
- A string parameter for sorting, provided in the form of ColumnName DIRECTION such as ProductName ASC or UnitPrice DESC
- A public constructor. Unless you subscribe to the ObjectDataSource’s ObjectCreating event in order to provide a custom instance before the binding calls are executed.
- A public method that returns the total number of rows not including the fetched-page size. This is only necessary if your ODS has EnablePaging set to true.
- Some attributes on the class or methods that do nothing but tell Visual Studio HEY! SHOW ME AS BINDABLE. Technically these are optional.
Which, in the case of Northwind might look like:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Northwind.Model;
namespace Northwind.DataBinding
{
[DataObject(true)]
public class ProductQuerySource : IDisposable
{
private NorthwindDataContext _dataContext;
private bool _disposeContextWhenDone = false;
private int _count = 0;
public ProductQuerySource(NorthwindDataContext dataContext)
{
if (dataContext == null)
throw new ArgumentNullException("dataContext");
_dataContext = dataContext;
}
public ProductQuerySource()
: this(new NorthwindDataContext())
{
_disposeContextWhenDone = true;
}
[DataObjectMethod(DataObjectMethodType.Select)]
public IList FetchAll(int startAt, int pageSize)
{
_count = _dataContext.Products.Count();
var query = _dataContext.Products.Skip(startAt).Take(pageSize).ToList();
return query;
}
[DataObjectMethod(DataObjectMethodType.Select)]
public IList FetchAll(int startAt, int pageSize, string sortExpression)
{
_count = _dataContext.Products.Count();
IQueryable products = _dataContext.Products;
if (!string.IsNullOrEmpty(sortExpression))
products = _dataContext.Products.OrderBy(sortExpression);
return products.Skip(startAt).Take(pageSize).ToList();
}
public int GetCount()
{
return _count;
}
public void Dispose()
{
if (_disposeContextWhenDone)
_dataContext.Dispose();
}
}
}
With our ObjectDataSource markup resembling
<asp:ObjectDataSource ID="odsNorthwindProducts" runat="server"
OldValuesParameterFormatString="original_{0}"
SelectMethod="FetchAll" TypeName="Northwind.DataBinding.ProductQuerySource"
EnablePaging="true"
MaximumRowsParameterName="pageSize"
StartRowIndexParameterName="startAt"
SelectCountMethod="GetCount" SortParameterName="sortExpression">
</asp:ObjectDataSource>
Link that sucker to a GridView with AllowPaging and AllowSorting both set to “true” and you’ll get a grid displaying Products in which only the products currently being rendered are even returned from the database. Click on a column header to sort? Current paged fetched and sorted in database. Fantastic. Note that standard parameter-binding applies here as well, Fetch() could easily have a categoryid parameter which might limit the results to all Products in that category. Since its all just methods on a class, our binding class can be tested using whatever the your testing strategy of choice happens to be.
Oh, but what about that pesky sorting? We can’t sort a linq query by a string, can we? Well, yeah, normally that would be the case, but not to fear, because but by taking a bit of code based on this post by Pradeep Mishra (which has alot of typos in the code there, beware) we’ll be able to dynamically use those string sort expressions to invoke Linq’s OrderBy and OrderByDescending methods. Which, in the case of Linq to Sql, will result in the respective ORDER BY clauses being added to the query once it is executed against the database.
using System;
using System.Linq.Expressions;
namespace System.Linq
{
public static class LinqExtensions
{
public static IQueryable<TEntity> OrderBy<TEntity>(this IQueryable<TEntity> source, string sortExpression) where TEntity : class
{
if (string.IsNullOrEmpty(sortExpression))
return source; // nothing to sort on
var entityType = typeof(TEntity);
string ascSortMethodName = "OrderBy";
string descSortMethodName = "OrderByDescending";
string[] sortExpressionParts = sortExpression.Split(' ');
string sortProperty = sortExpressionParts[0];
string sortMethod = ascSortMethodName;
if (sortExpressionParts.Length > 1 && sortExpressionParts[1] == "DESC")
sortMethod = descSortMethodName;
var property = entityType.GetProperty(sortProperty);
var parameter = Expression.Parameter(entityType, "p");
var propertyAccess = Expression.MakeMemberAccess(parameter, property);
var orderByExp = Expression.Lambda(propertyAccess, parameter);
MethodCallExpression resultExp = Expression.Call(
typeof(Queryable),
sortMethod,
new Type[] { entityType, property.PropertyType },
source.Expression,
Expression.Quote(orderByExp));
return source.Provider.CreateQuery<TEntity>(resultExp);
}
}
}
So having that in place means we can have SortParameterName=”sortDirection” in our ODS markup as well as the sortDirection string parameter on the binding method. Hello, server-side paging and sorting.
I should note that this sorting and paging isn’t unique to Linq To Sql- it applies to anything queryable by Linq. It’s just that leveraging this approach with Linq To Sql yields for a more elegant sorting and paging solution when working with a database than some other the other switch-based or (god forbid) stored-procedure based approaches.