Scott Hanselman

The Weekly Source Code 47 - ASP.NET 3.5 Dynamic Data: FilterRepeaters and Dynamic Linq Query Generation

January 13, 2010 Comment on this post [5] Posted in ASP.NET | ASP.NET Ajax | ASP.NET Dynamic Data | ASP.NET MVC | Source Code
Sponsored By

First, let me start this post by thanking Tatham Oddie. He helped my buddy John Batdorf and I debug our issue remotely from Australia. He's patient, kind, opinionated and Tatham's got a darn fine blog that you should subscribe to now. I also found great inspiration from Stephen Naughton's excellent blog. He's continually pushing ASP.NET and Dynamic Data to do fun things and I was able to use 95% of his auto-complete code as I found it. And finally Marcin Dobosz's blog is where I started, taking his Dynamic Data sample Filter Repeaters and ending up at the Dynamic Data Futures samples.

Technical Disclaimer: This is me just messing about with the .NET 3.5 SP1 Dynamic Data samples as this non-profit wanted .NET 3.5. The Filters I'm messing with here are from the VS2008 SP1 Dynamic Data Futures from May of 2009 but are included out of the box and are much cooler in .NET 4 in Visual Studio 2010 Beta 2. Some method names and base classes have changed, but the idea is effectively the same. If you're using .NET 4 you'll notice you'll get the ~/DynamicData/Filters folder when you do File | New Project and a new QueryableFilterRepeater rather than an AdvancedFilterRepeater, and like that. In my 3.5 project I included the DynamicDataExtensions Project in my solution, while in .NET 4 all this functionality is in System.Web.DynamicData itself. I'll update this project and blog a sample when .NET 4 is closer to release.

Ok, the story is that John and I are doing some volunteer work and updating a local non-profit's website on the side in our copious spare time. They have simple needs, on the front end they want a nice simple autocomplete textbox and maybe a filter drop down or two. This'll send the user to a results page with a non-editable paged grid and a dynamically generated image for printing. On the backend, they want a few more dropdowns for admins and then a nice editable grid for searching, paging, etc.

We each thought this would be a fun opportunity to see if could put it together in a night and learn more about ASP.NET Web Forms and Dynamic Data at the same time. It's all very meta, so we decided we'd be meta also. (Remember, that means effectively "solution non-specific." We'd like to be able to use whatever things we create for ASP.NET Dynamic Data in other projects.)

Meta Meta

When creating a site with ASP.NET Dynamic Data, the pages you create are templates - they are kinds of pages - rather than instances of pages themselves. So, I create a List.aspx, and it's not specific to (in my case) Brick objects. It's for listing any kind of object I want to in my solution.

Here's a simplified example of my list.aspx in my Dynamic Data solution. The things we care about are that it has a:

  • LinqDataSource
  • GridView
  • AdvancedFilterRepeater - Again, this was the name it had in the 3.5 Dynamic Data Futures. It's an extension of the standard Filter Repeater that comes with ASP.NET. It has a DelegatingFilter in its ItemTemplate.

And that's it.

<ContentTemplate>
<asp:ValidationSummary ID="ValidationSummary1" runat="server" EnableClientScript="true"
HeaderText="List of validation errors" />
<asp:DynamicValidator runat="server" ID="GridViewValidator" ControlToValidate="GridView1" Display="None" />

<asp:AdvancedFilterRepeater id="AdvancedFilterRepeater" runat="server">
<HeaderTemplate>
<table>
</HeaderTemplate>
<ItemTemplate>
<tr>
<td valign="top"><%# Eval("DisplayName") %>:</td>
<td><asp:DelegatingFilter runat="server" ID="DynamicFilter" OnSelectionChanged="OnFilterSelectedIndexChanged" />
</tr>
</ItemTemplate>
<FooterTemplate>
</table>
</FooterTemplate>
</asp:AdvancedFilterRepeater>

<asp:GridView ID="GridView1" runat="server" DataSourceID="GridDataSource"
AllowPaging="True" AllowSorting="True" CssClass="gridview">
<Columns>
<asp:TemplateField>
<ItemTemplate>
<asp:HyperLink ID="DetailsHyperLink" runat="server"
NavigateUrl='<%# table.GetActionPath(PageAction.Details, GetDataItem()) %>'
Text="Details" />
</ItemTemplate>
</asp:TemplateField>
</Columns>

<PagerStyle CssClass="footer"/>
<PagerTemplate>
<asp:GridViewPager runat="server" />
</PagerTemplate>
<EmptyDataTemplate>
There are currently no items in this table.
</EmptyDataTemplate>
</asp:GridView>

<asp:LinqDataSource ID="GridDataSource" runat="server" EnableDelete="false" EnableInsert="false" EnableUpdate="false">
<WhereParameters>
<asp:DynamicControlParameter ControlID="AdvancedFilterRepeater" />
</WhereParameters>
</asp:LinqDataSource>

<br />
</ContentTemplate>

The AdvancedFilter repeater is what's cool and it's what Marcin blogged about and that ended up in the samples. Stephen Naughton has since extended the FilterRepeater even more with his wonderful "Dynamic Data Futures" series.

The FilterRepeater is a special Repeater control that binds to a collection of columns (properties) in an table (list<object>) and make dynamically make controls that will allow filtering.

For example, here's my Brick object model (actually the "buddy metadata class" of my Brick object. It's a mirror of what's in the database:

[MetadataType(typeof(Brick_MD))]
public partial class Brick
{
public class Brick_MD
{
[Filter(FilterControl = "AutoComplete")]
public object Name { get; set; }

[Filter(FilterControl = "Integer", AuthenticatedOnly = true)]
public object Square { get; set; }

[Filter(FilterControl = "Integer")]
public object Year { get; set; }

public object Side { get; set; }

public object Row { get; set; }

public object BrickNum { get; set; }

public object Order { get; set; }
}

}

imageNotice the Filter attributes. I've extended it with a property called AuthenticatedOnly for properties that only authenticated users (non-anonymous) can see filters for. I've also said that one property/column needs an AutoComplete style filter (without being specific about the implementation) and two others need Integer style (again, implementation non-specific metadata. Could be combo, slider, whatever.)

ASP.NET Dynamic Data uses a special folder by convention called DynamicData with sub-folder like FieldTemplates for specific kinds of fields (Integer, DateTime, Person, etc) and PageTemplate for specific pages (List, Edit, Details, etc.)

The FilterRepeater sample extends this concept with a ~/DynamicData/Filters folder. This is set in the GetFilterControl method inside the FilterFactor if you want to see where the actual mention of the 'Filters' directory exists in the code. The point is that there's a string on my meta-model that says "AutoComplete" and that maps by convention to the ~/DynamicData/Filters/AutoComplete.ascx control, just as "Integer" maps to Integer.ascx, etc.

This is all Integer.ascx is, for example:

<%@ Control Language="C#" AutoEventWireup="true" CodeFile="Integer.ascx.cs" Inherits="AdvancedFilterRepeaterSite.Integer_Filter" %>

<asp:DropDownList ID="DropDownList1" runat="server" AutoPostBack="true" EnableViewState="false">
<asp:ListItem Text="All" Value="" />
</asp:DropDownList>

It's just a dropdown list. When the FilterRepeater is checking out the columns/properties on the Brick_MD object, it's saying, "oh, I to put a Integer.ascx here" and it'll feed it the data it needs. All these controls like the Integer one derive from FilterUserControlBase and just have to override the properties or methods for the behavior they want to change.

All this is pretty straight forward, and you can do really cool stuff with it.

Dynamically Generating Generically Specific LINQ Queries

However, things got hairy when we noticed that the DropDownList being created didn't have the values sorted ahead of time. This made weird dropdowns with the integers in whatever order they were in the database already.

You would think you'd want some code like this behind your Integer Filter Control:

protected void Page_Init(object sender, EventArgs e) {
var items = Column.Table.GetQuery();

//magic psuedo code here
var result = items.Select(row => row.Property).Distinct.OrderBy(colvalue => colvalue)

foreach (var item in result) {
if (item != null) DropDownList1.Items.Add(item.ToString());
}
}

Notice the "magic" part. That's the LINQ query we wish we could call, but remember that I don't want to make a query against a specific table and a specific column. If I did, this wouldn't be the Integer Filter Control, it'd be the Year Filter Control or the Month Filter Control. It'd be specific, not generic.

We need to dynamically create a LINQ query. But just like you don't want to just make dynamic SQL by concatenating strings, the same applies for LINQ. Instead we will use the System.Linq.Expressions.Expression base class to creating a tree that represents our LINQ query. Another metaphor/example would be using XmlDocuments and the DOM to make XML rather than making it with a bunch of strings.

We want effectively, this query, made specific. Each time this Integer Filter Control is run it'll be specific to the column (and even the type, arguably, could be something other than integer) we're currently making a control for.

Items.Select(row => row.Property).Distinct.OrderBy(colvalue => colvalue)

We need the building blocks for this query, starting with whatever "row" is.

// row
var entityParam = Expression.Parameter(Column.Table.EntityType, "row");

Because we are a FilterUserControlBase, we can ask Dynamic Data for the typeof(row) which is in Column.Table.EntityType. We'll be using the types and info that DynamicData provides to create this query.

Next we get the Property of row by passing in (building on) the entityParam we just made. Now "columnLambda" is of type LambdaExpression.

// row => row.Property
var columnLambda = Expression.Lambda(Expression.Property(entityParam, Column.EntityTypeProperty), entityParam);

Next the Select. Here we are dynamically generating a LINQ function call. This is LINQ's idea of Reflection, effectively. The Type[] are the params. Remember what the Select call looks like: Select<TSource, TResult>(IEnumerable<TSource>, Func<TSource, TResult>) and we're giving it just those things it wants.

// Items.Select(row => row.Property)
var selectCall = Expression.Call(typeof(Queryable), "Select", new Type[] { items.ElementType, columnLambda.Body.Type }, items.Expression, columnLambda);

Now we add just Distinct to the previous call. Look at the comment. See how we are building on the previous selectCall expression?

// Items.Select(row => row.Property).Distinct
var distinctCall = Expression.Call(typeof(Queryable), "Distinct", new Type[] { Column.EntityTypeProperty.PropertyType }, selectCall);

Here's what Tatham saved our bacon. I got turned around and he broke up my crazy one-liner into clearer code. Eilon Lipton helped me get this straight as well.

In the first expression, the "sortValue" string doesn't matter. It just has to be something so we can make a lambda that is x=>x.

In the second, we call the standard LINQ OrderBy and give it the (x=>x), or in this example the year=>year, so really,
Items.Select(brick => brick.Property).Distinct.OrderBy(year => year)

// colvalue => colvalue
var sortParam = Expression.Parameter(Column.EntityTypeProperty.PropertyType, "sortValue");
var columnResultLambda = Expression.Lambda(sortParam, sortParam);
// Items.Select(row => row.Property).Distinct.OrderBy(colvalue => colvalue)
var ordercall = Expression.Call(typeof(Queryable), "OrderBy",
new Type[] { Column.EntityTypeProperty.PropertyType, columnResultLambda.Body.Type },
distinctCall, columnResultLambda);

Then we actually CALL the query and fill the dropdown list so here's the whole thing:

protected void Page_Init(object sender, EventArgs e) {
var items = Column.Table.GetQuery();
var entityParam = Expression.Parameter(Column.Table.EntityType, "row");

// row => row.Property
var columnLambda = Expression.Lambda(Expression.Property(entityParam, Column.EntityTypeProperty), entityParam);

// Items.Select(row => row.Property)
var selectCall = Expression.Call(typeof(Queryable), "Select", new Type[] { items.ElementType, columnLambda.Body.Type }, items.Expression, columnLambda);

// Items.Select(row => row.Property).Distinct
var distinctCall = Expression.Call(typeof(Queryable), "Distinct", new Type[] { Column.EntityTypeProperty.PropertyType }, selectCall);


// colvalue => colvalue
var sortParam = Expression.Parameter(Column.EntityTypeProperty.PropertyType, "sortValue");
var columnResultLambda = Expression.Lambda(sortParam, sortParam);

// Items.Select(row => row.Property).Distinct.OrderBy(colvalue => colvalue)
var ordercall = Expression.Call(typeof(Queryable), "OrderBy",
new Type[] { Column.EntityTypeProperty.PropertyType, columnResultLambda.Body.Type },
distinctCall, columnResultLambda);

var result = items.Provider.CreateQuery(ordercall);

foreach (var item in result) {
if (item != null) DropDownList1.Items.Add(item.ToString());
}
}

So what?

Conclusion

image Now I can add [Filter(FilterControl = "Integer")] to any meta-model in any of my ASP.NET Dynamic Data sites and automatically get a nice simple set of Filters, driven completely by attribute with their implementation both separate from my logic and completely generic to my tables.

For a complete working sample, go check out the VS2008 SP1 Dynamic Data Futures from May of 2009. I modified (barely) the Integer.acsx.cs to add the OrderBy which was flummoxing me. I needed to break it all down to figure out those few lines.

I also modified the FilterAttribute to add the AuthenticatedOnly attribute like [Filter(FilterControl = "Integer", AuthenticatedOnly = true)] :

using System;

namespace Microsoft.Web.DynamicData.Extensions {
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple=false)]
public sealed class FilterAttribute : Attribute {

public FilterAttribute() {
Order = Int32.MaxValue;
Enabled = true;
}

public string FilterControl { get; set; }

// Lower values take precedence before greater values
public int Order { get; set; }

public bool Enabled { get; set; }

public bool AuthenticatedOnly { get; set; }
}
}

...and the AdvancedFilterRepeater to act on that new property:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.DynamicData;
using System.Web.UI.WebControls;
using System.Web;

namespace Microsoft.Web.DynamicData.Extensions {
public class AdvancedFilterRepeater : FilterRepeater {

protected override IEnumerable<MetaColumn> GetFilteredColumns() {
// sort the filters by their filter order as specified in FilterAttribute.
return Table.Columns.Where(c => IsFilterableColumn(c)).OrderBy(column => column, new FilterOrderComparer());
}

protected bool IsFilterableColumn(MetaColumn column) {
if (column.IsCustomProperty) return false;

var filterAttribute = column.Attributes.OfType<FilterAttribute>().FirstOrDefault();
if (filterAttribute != null)
{
if (filterAttribute.AuthenticatedOnly == true && (HttpContext.Current.User == null || HttpContext.Current.User.Identity.IsAuthenticated == false)) return false;

return filterAttribute.Enabled;
}

if (column is MetaForeignKeyColumn) return true;

if (column.ColumnType == typeof(bool)) return true;

return false;
}

private class FilterOrderComparer : IComparer<MetaColumn> {
public int Compare(MetaColumn x, MetaColumn y) {
return GetWeight(x) - GetWeight(y);
}

private int GetWeight(MetaColumn column) {
var filterAttribute = column.Attributes.OfType<FilterAttribute>().FirstOrDefault();
return filterAttribute != null ? filterAttribute.Order : Int32.MaxValue;
}
}

}
}

Disclaimer: I did this in .NET 3.5 SP1. I'm still learning this deeper stuff myself, and I'd say that while this meta-LINQ stuff is powerful, it's too complex to generate dynamic LINQ. Remember also that this post is on the outer outer edge of what you'd want to do. It's not representative of a typical ASP.NET Dynamic Data experience, or a typical LINQ experience. However, it is nice to know that the whole thing is incredibly extensible just that if I want to go this deep, I can. I'll go talk to that team and see what they've got planned for the future! If we've missed something, leave it in the comments or email me and I'll update this post. I'll find out both what has been improved for ASP.NET Dynamic Data 4 and for C# 4.

Related Links

Good ASP.NET WebForms Resources and Blogs

Webforms Q&A

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Hosting By
Hosted in an Azure App Service
January 13, 2010 12:43
Tatham Oddie's blog looks good, but where's the RSS to subscribe to?
January 13, 2010 13:29

Tatham Oddie's blog rss:
http://blog.tatham.oddie.com.au/feed/
January 13, 2010 17:28
You should change the series title to "The Monthly Source Code" ;)
January 13, 2010 19:10
Since manual expression building is pretty painful, it makes for a great use of extension methods. I did something similar with making an OrderBy(string) method to support dynamic ordering in the good old ObjectDataSource (shameless plug of my neglected blog)
January 13, 2010 19:13
Shameless plug fail. http://bit.ly/15IJDJ

Comments are closed.

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.