December 2008 - Posts

Dynamic ListView LayoutTemplate

There are times when you want to let the user change layout dynamically. You can use css to do this but lets look at what the ListView control offers. To get started with the ListView you need a LayoutTemplate and ItemTemplate.

<asp:ListView runat="server" ID="listView">       

    <LayoutTemplate>

        <asp:PlaceHolder runat="server" ID="itemPlaceholder"></asp:PlaceHolder>

    </LayoutTemplate>

    <ItemTemplate>

        <%# Eval("CategoryName") %>

    </ItemTemplate>

</asp:ListView>

The ListView replaces the control with ID="itemPlaceholder" with the zero or more instances of the Selected/Alternating/ItemTemplate.

There is a method on TemplateControl (LoadTemplate) which allows users to dynamically load a user control as a ITemplate. Lets use this to load our LayoutTemplate from a user control.

User Control:


<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="FlowLayout.ascx.cs" Inherits="ListViewLayouts.FlowLayout" %>

 

<div id="flow">

    <asp:PlaceHolder runat="server" ID="itemPlaceholder"></asp:PlaceHolder>

</div>

Code Behind:

protected void Page_Init(object sender, EventArgs e) {

    listView.LayoutTemplate = LoadTemplate("~/ListViewLayouts/FlowLayout.ascx");

}

When we run the page we get the following exception:

An item placeholder must be specified on ListView 'listView'. Specify an item placeholder by setting a control's ID property to "itemPlaceholder". The item placeholder control must also specify runat="server".

The problem here is that user controls are naming containers and when the ListView internally tries to find the control with ID="itemPlaceholder" it fails. So what is the id of the itemPlaceholder? We'll we could start guessing that it might be something like ctl001$itemPlaceholder, but that doesn't seem like a good solution. Instead we can create our own template that will allow us to specify the ID of the user control so that the itemPlaceholderID is more predictable.

public class CustomTemplate : ITemplate {

    private string _virtualPath;

    private string _controlID;

 

    public CustomTemplate(string virtualPath, string controlID) {

        _virtualPath = virtualPath;

        _controlID = controlID;

    }

 

    public void InstantiateIn(Control container) {           

        Control control = (Control)BuildManager.CreateInstanceFromVirtualPath(_virtualPath, typeof(Control));

        control.ID = _controlID;

        container.Controls.Add(control);

    }

}


This template gives us the opportunity to specify a controlID for the user control we are going to load. Now we instantiate a new CustomTemplate and specify the control ID as well as path to the user control.

protected void Page_Load(object sender, EventArgs e) {

    listView.LayoutTemplate = new CustomTemplate("~/ListViewLayouts/FlowLayout.ascx", "flowLayout");           

}

Don't forget to set the ItemPlaceHolderID proprety on the ListView.

<asp:ListView runat="server" ID="listView" ItemPlaceholderID="flowLayout$itemPlaceholder">

        <ItemTemplate>

            <%# Eval("CategoryName") %>

        </ItemTemplate>

</asp:ListView>

Now we can load the LayoutTemplate at runtime.

Posted by davidfowl with 7 comment(s)

AutoFieldGenerators

In 3.5 SP1 we added new properties to GridView and DetailsView which allows the page developer to change the way AutoGenerateColumns creates its columns. This feature is well know in Dynamic Data, but it is not tied to this technology. Dynamic Data takes advantage of this by looking at the meta data that users set on properties to generate columns.

You too can roll your own IAutoFieldGenerator. Lets look at the interface:

public interface IAutoFieldGenerator {

    ICollection GenerateFields(Control control);

}

The interface itself is pretty weird but it gets the job done. GenerateFields takes the control that we're generating the fields for, and expects to get some ICollection of stuff back. If we had the chance to redo this interface we'd probaby rewrite it to be like this:

public interface IAutoFieldGenerator {

    IEnumerable<DataControlField> GenerateFields(Control control);

}

Now it's clear what we expect to get back, but thats besides the point. Lets implement our own.

public class ColumnGenerator : IAutoFieldGenerator {

private IEnumerable<string> _columns;

public ColumnGenerator(IEnumerable<string> columns) {

    _columns = columns ?? Enumerable.Empty<string>();

}

 

public ICollection GenerateFields(Control control) {

    return (from column in _columns

            select new BoundField {

                SortExpression = column,

                HeaderText = column,

                DataField = column

            }).ToArray();

    }

}


We're going to pass a set of column names to our generator that just creates bound fields with the column's name. To make use of our new generator we can just set it like this:

GridView

gridView1.ColumnsGenerator = new ColumnGenerator(Columns);

DetailsView

detailsView1.RowsGenerator = new ColumnGenerator(Columns);

You can do alot of cool things with these generators. Some things I can think of off the top of my head:

  • Hide/Show columns dynamically based on metadata (like what we do with dynamic data)
  • Hide/Show columns based on permissions
  • Create a configurable UI that allows users to hide or show columns based on their preferences.

I've written a sample that allows you to hide or show columns based on a selectable UI. You can download it here:

AutoFieldGenerator.zip

Here is a screen shot of it running.

Posted by davidfowl with 9 comment(s)

How <%# Bind %> Works

In my last post I spoke about 2-way databinding and how it can be used to extract values from control properties. How does this all work? Lets take a look at a page with 2-way databinding:

<asp:LinqDataSource ID="productsSource"

    runat="server"

    ContextTypeName="FowlerSamples.NorthwindDataContext"

    EnableDelete="True"

    EnableInsert="True"

    EnableUpdate="True" TableName="Products">

</asp:LinqDataSource>       

<asp:GridView ID="products"

    DataKeyNames="ProductID,CategoryID"

    AutoGenerateColumns="False"

    runat="server" DataSourceID="productsSource">

    <Columns>

        <asp:CommandField ShowEditButton="True" />               

        <asp:BoundField DataField="ProductName" />

        <asp:TemplateField>

            <EditItemTemplate>

                <asp:LinqDataSource

                    ID="categoriesSource"

                    runat="server"

                    ContextTypeName="FowlerSamples.NorthwindDataContext"

                    TableName="Categories" AutoGenerateWhereClause="true">

                </asp:LinqDataSource>

                <asp:DropDownList

                    runat="server"

                    ID="categories"

                    DataSourceID="categoriesSource"

                    DataTextField="CategoryName"

                    DataValueField="CategoryID"

                    SelectedValue='<%# Bind("CategoryID") %>'>                           

                </asp:DropDownList>                       

            </EditItemTemplate>                   

        </asp:TemplateField>

    </Columns>

</asp:GridView>

 In the above example, the GridView has a template field with an EditItemTemplate that has a DropDownList that is 2-way databound. We're going to introduce a small error in the page in order to see what the generated code looks like:

<EditItemTemplate>

     <asp:LinqDataSource

         ID="categoriesSource"

         runat="server"

         ContextTypeName="FowlerSamples.NorthwindDataContext"

         TableName="Categories" AutoGenerateWhereClause="true">

     </asp:LinqDataSource>

     <asp:DropDownList

        runat="server"

        ID="categories"

        DataSourceID="categoriesSource"

        DataTextField="CategoryName"

        DataValueField="CategoryID"

        SelectedValue='<%# Bind("CategoryID") %>'>                           

    </asp:DropDownList>

    <%# Eval(3) %>

</EditItemTemplate>


When we try to run this page we'll get a compile error and the famous ASP.NET YSOD(Yellow Screen of Death):



Click on Show Complete Compilation Source, if your curious about how ASP.NET converts your the markup to code.

When examining the source, we see a rather interesting method:

[System.Diagnostics.DebuggerNonUserCodeAttribute()]

public System.Collections.Specialized.IOrderedDictionary @__ExtractValues__control8(System.Web.UI.Control @__container) {

    System.Collections.Specialized.OrderedDictionary @__table;

    System.Web.UI.WebControls.DropDownList categories;

 

    categories = ((System.Web.UI.WebControls.DropDownList)(@__container.FindControl("categories")));

 

    @__table = new System.Collections.Specialized.OrderedDictionary();

 

    if ((categories != null)) {

        @__table["CategoryID"] = categories.SelectedValue;

    }

 

    return @__table;

}


As you can see in the above method, an OrderedDictionary is created and the SelectedValue property of the DropDownList is pushed into the "CategoryID" field. But how does this get all the way to the data control? Each control has a BuildControl method associated with it, if we examine the BuildControl method for the TemplateField it becomes a bit more clear how things get hooked up.

 

[System.Diagnostics.DebuggerNonUserCodeAttribute()]

private global::System.Web.UI.WebControls.TemplateField @__BuildControl__control7() {

    global::System.Web.UI.WebControls.TemplateField @__ctrl;

 

    @__ctrl = new global::System.Web.UI.WebControls.TemplateField();

 

    @__ctrl.EditItemTemplate = new System.Web.UI.CompiledBindableTemplateBuilder(new System.Web.UI.BuildTemplateMethod(this.@__BuildControl__control8), new System.Web.UI.ExtractTemplateValuesMethod(this.@__ExtractValues__control8));

 

    return @__ctrl;

}

The EditItemTemplate property is of type ITemplate. CompiledBindableTemplate implements both ITemplate and IBindableTempalte.

public interface IBindableTemplate : ITemplate {           

    IOrderedDictionary ExtractValues(Control container);

}

It's slowly coming together. So lets put together what we're learnt so far:

  1. Code Gen creates ExtractValues method that returns the dictionary of values for each ITemplate that has a Bind expression.
  2. The BuildControl method for the ITemplate's container (TemplateField in this case) assigns a new CompiledBindableTemplate to an ITemplate (EditItemTemplate in this case)
  3. CompiledBindableTemplate implements IBindableTemplate, which has a method, ExtractValues which returns the dictionary given a container.

It almost all makes sense now. Each data control uses this mechanism to extract values from template fields with 2-way databinding expressions.
What can you do with your new found knowledge?

<asp:FormView

    runat="server"

    ID="formView"

    DefaultMode="Edit">

    <EditItemTemplate>

        <asp:TextBox ID="textBox" runat="server" Text='<%# Bind("Name") %>'></asp:TextBox>

        <asp:TextBox ID="textBox1" runat="server" Text='<%# Bind("Age") %>'></asp:TextBox>

        <asp:Button runat="server" ID="updateButton" CommandName="Update" Text="Update" />

    </EditItemTemplate>

</asp:FormView>


And the code behind:

 

class Person {

    public string Name { get; set; }

    public int Age { get; set; }

}

 

protected void Page_Load() {

    formView.ItemUpdating += formView_ItemUpdating;

    if (!IsPostBack) {

        formView.DataSource = new Person[] { new Person { Name = "David", Age = 22 } };

        formView.DataBind();

    }

}

 

protected void formView_ItemUpdating(object sender, FormViewUpdateEventArgs e) {

    IBindableTemplate template = formView.EditItemTemplate as IBindableTemplate;

    if (template != null) {

        IOrderedDictionary values = template.ExtractValues(formView);

        Response.Write(values["Name"]);

        Response.Write(values["Age"]);

    }

}


How cool is that? :)
Posted by davidfowl with 3 comment(s)

Getting your data out of the data controls

After you've created a layout with a data control of your choice and eventually bound them to some datasource, you almost always want to get that data back out of them(and why wouldn't you, its your data). One thing you'll learn about developing asp.net webpages is that its like petting a porcupine (yikes). If you go with the flow, you probably won't get stuck, but the moment you try to go against the grain you end up with a hand full of thorns.

Most of the data controls have events ending in ing and ed e.g. RowUpdating, RowUpdated etc. In most of the event handlers of the ing events it is easy to get the values from the event args.

private void OnGridViewRowUpdating(object sender, GridViewUpdateEventArgs e) {

    IDictionary keys = e.Keys;

    IDictionary newValues = e.NewValues;

    IDictionary oldValues = e.OldValues;

}

Forgive me for the variation in my code formatting I'm trying to find the right one.

If we look at this event we can see the GridView nicely packages for us the new values, old values and the keys for the updating row. Unfortunately these dictionaries are only filled out if you are bound to a datasource control :(. So that means when you bind to some raw collection and hook up to the DataSource property then call databind, if you try to handle the delete or update events these dictionaries are going to be empty. Right now you must be asking yourself how can you get those dictionaries filled out just as if you were bound to a Datasource control? The good news is you CAN do it(this is what this blog post is all about right?).

Going Hunting in the Control Tree



This is probably one of the worst things you can do. From the time you write code that depends on the immediate layout of your page then your asking for trouble. I often see people on the forums writing code like this:

((TextBox)GridView1.Rows[e.RowIndex].Cells[2].Controls[1]).Text

Seeing stuff like that makes my spine tingle (and not in a good way). DO NOT write code like this!

FindControl
FindControl is a very powerful method on Control that allows you to search for a nested control, NOT synonymous to DOM function getElementById. I Often see people abusing find control and not understand that it is NOT recursive by default and complain when code like this

((TextBox)GridView1.FindControl("TextBox1")).Text

Throws a null reference exception. My advice is use Use FindControl as a last resort.

2-way DataBinding
2-way Databinding is a cool feature in ASP.NET 2.0 which allows the user to write some special syntax to bind against property values they would like to extract from a control.
There is alot of magic going on behind the scenes (which I will blog about in a separate post), but you do not need to know how it works to use it, e.g

<asp:GridView runat="server" ID="GridView1">

    <Columns>

        <asp:TemplateField>

            <EditItemTemplate>

                <asp:TextBox ID="TextBox1" runat="server" Text='<%# Bind("ProductName") %>'></asp:TextBox>

            </EditItemTemplate>

        </asp:TemplateField>

    </Columns>

</asp:GridView>

When you write the <%# Bind("ProductName") #> expression then the value of the Text property is pushed into a dictionary which the data control can retrieve later. So how does GridView/FormView/DetailsView/ListView get these values? Each data control has a method which is responsible for populating a dictionary of name value pairs from field name to value. Here is a mapping of data control to method used to extract values:

GridView => protected ExtractRowValues
FormView => protected ExtractRowValues
DetailsView = > protected ExtractRowValues
ListView => public ExtractItemValues

As we can see with the exception of ListView these oh so useful methods are protected, that means we can't call them from our code if we are using these built in controls. What can we do to surface these methods:

  • Derive new controls that expose ExtractRowValues through a public method
  • Use private reflection to call the protected method (yikes)
  • Stick all of your interesting fields in DataKeyNames, then use the DataKeys[rowIndex].Values[fieldName] (and watch your ViewState grow :( )
  • Do Nothing :)

To rectify some of this I've written a little helper for the GridView (and the other controls if you demand) that basically duplicates the functionality of ExtractRowValues method.

public static IDictionary GetValues(GridViewRow row) {           

    IOrderedDictionary values = new OrderedDictionary();

    foreach (DataControlFieldCell cell in row.Cells) {

        if (cell.Visible) {

            // Extract values from the cell

            cell.ContainingField.ExtractValuesFromCell(values, cell, row.RowState, true);

        }

    }

 

    return values;

}

The method itself is pretty simple. It iterates over the control collection of the GridViewRow and calls ExtractValuesFromCell on each cell which puts values into the dictionary.

Uses
Most data controls support writing custom commands and handling some Command event(RowCommand for GridView). This method would come in handy if you needed to get the values out of the GridView for some custom command you wanted to execute.

If you can, use 2 way databinding, if your control doesn't have a useful property to bind against and you need to do some more logic to get the right value then use FindControl.

Edit:
I've updated the code to loop over the Cells collection instead of the Controls collection (which is alot cleaner). Also there is no real compelling reason to return a strongly typed IDictionary<string, object>, so I just return the IDictionary instead.

DetailsView:

Reader daveh551 needed some help adapting the code to DetailsView so here it is:

public static IDictionary GetValues(DetailsView detailsView) {
    IOrderedDictionary values = new OrderedDictionary();
    foreach (DetailsViewRow row in detailsView.Rows) {
        // Only look at Data Rows
        if (row.RowType != DataControlRowType.DataRow) {
            continue;
        }
        // Assume the first cell is a header cell
        DataControlFieldCell dataCell = (DataControlFieldCell)row.Cells[0];                
        // If we are showing the header for this row then the data is in the adjacent cell
        if (dataCell.ContainingField.ShowHeader) {
            dataCell = (DataControlFieldCell)row.Cells[1];
        }
 
        dataCell.ContainingField.ExtractValuesFromCell(values, dataCell, row.RowState, true);
    }
    return values;
}

FormView:

public static IDictionary GetValues(FormView formView) {
    IOrderedDictionary fieldValues = new OrderedDictionary();
    ExtractValuesFromBindableControls(fieldValues, formView);
    IBindableTemplate itemTemplate = null;
 
    if (formView.CurrentMode == FormViewMode.ReadOnly && formView.ItemTemplate != null) {
        itemTemplate = formView.ItemTemplate as IBindableTemplate;
    }
    else if (formView.CurrentMode == FormViewMode.Edit && formView.EditItemTemplate != null) {
        itemTemplate = formView.EditItemTemplate as IBindableTemplate;
    }
    else if (formView.CurrentMode == FormViewMode.Insert && formView.InsertItemTemplate != null) {
        itemTemplate = formView.InsertItemTemplate as IBindableTemplate;
    }
    if (itemTemplate != null) {
        foreach (DictionaryEntry entry in itemTemplate.ExtractValues(formView)) {
            fieldValues[entry.Key] = entry.Value;
        }
    }
    return fieldValues;
}
 
private static void ExtractValuesFromBindableControls(IOrderedDictionary values, Control container) {
    IBindableControl control = container as IBindableControl;
    if (control != null) {
        control.ExtractValues(values);
    }
    foreach (Control childControl in container.Controls) {
        ExtractValuesFromBindableControls(values, childControl);
    }
} 

Let me know if you find any bugs.
Hope this helps

UPDATE: I've had some requests to add more data controls and to convert the code to VB etc. So I decided to instead put the code in an assembly and make available for download here.

Posted by davidfowl with 35 comment(s)

Dynamic Sorting with Linq

When trying to implement a Business Logic Layer (i will refer to this as BLL) with linq one thing that is an annoyance is sorting. Lets say you had a BLL that was being used by ObjectDataSource and had a select method that does sorting and paging, then you'd probably be tempted to write something like this:

private IQueryable<Product> SortBy(IQueryable<Product> source, string sortBy) {

    int desc = sortBy.IndexOf("DESC");

    bool isDescending = desc >= 0;

    if (isDescending) {

        sortBy = sortBy.Substring(0, desc).Trim();

    }

    switch (sortBy) {

        case "ProductName":

            if (isDescending) {

                return source.OrderByDescending(p => p.ProductName);

            }

            source = source.OrderBy(p => p.ProductName);

            break;

        case "UnitPrice":

            if (isDescending) {

                return source.OrderByDescending(p => p.UnitPrice);

            }

            source = source.OrderBy(p => p.UnitPrice);

            break;

..... One for every property

    }

    return source;

}

But that can be really tedious and you probably have more than one object. Would you want to duplicate that for Categories, Suppliers, etc? I don't think so. Linq takes
advantage of another C# 3.0 feature called Expression Trees and we can take advantage of these in our code to build a dynamic sort expression for any object.

The code:
So how do we do this? I'm not going to explain how expression trees work in this blog post but there are plenty of resources out there  that you can take a look at if your interested.
The method is an extension method on IQueryable<T> that takes the IQuerable<T> source and the sort parameter which should be the property name that you would like to sort by. In the case of DataSources like ObjectDataSource, the sort parameter will contain "DESC' if the sort direction is descending. Here is the code:

public static class QueryExtensions {

    public static IQueryable<T> SortBy<T>(this IQueryable<T> source, string propertyName) {

        if (source == null) {

            throw new ArgumentNullException("source");

        }

        // DataSource control passes the sort parameter with a direction

        // if the direction is descending          

        int descIndex = propertyName.IndexOf(" DESC");

        if (descIndex >= 0) {

            propertyName = propertyName.Substring(0, descIndex).Trim();

        }

 

        if (String.IsNullOrEmpty(propertyName)) {

            return source;

        }

 

        ParameterExpression parameter = Expression.Parameter(source.ElementType, String.Empty);

        MemberExpression property = Expression.Property(parameter, propertyName);

        LambdaExpression lambda = Expression.Lambda(property, parameter);

 

        string methodName = (descIndex < 0) ? "OrderBy" : "OrderByDescending";

 

        Expression methodCallExpression = Expression.Call(typeof(Queryable), methodName,

                                            new Type[] { source.ElementType, property.Type },

                                            source.Expression, Expression.Quote(lambda));

 

        return source.Provider.CreateQuery<T>(methodCallExpression);

    }

}

And a link to the file.

QueryExtensions.cs

Hope this helps

Posted by davidfowl with 16 comment(s)
Filed under: , ,
More Posts