Tuesday, 17 August 2010

A Boolean Date Time Field Template for Dynamic Data 4

Here’s the idea, you want to mark a status of a row with a checkbox and you also need the date time setting one the row to record when the status changed. You normally end up with two fields in your table for this. I decided that in many cases I would only need the date time i.e. if the date time is set then the status has changed is null then not, e.g. to set Order Status to complete set the Complete column to DateTime.Now.

Another reason you may want to use a checkbox to enter a date is to set the date without letting the user enter a custom date.

This turned out to be a little more complicated than I first though and I needed to consult David Ebbo at Microsoft to get the answer. First we’ll deal with the issue I came across that at first was a road block. This is the issue of setting the field to null when the user unchecks the check box.

<%@ Control
    Language="C#" 
    CodeBehind="DateTimeBoolean_Edit.ascx.cs" 
    Inherits="CustomFieldTemplates.DateTimeBoolean_EditField" %>

<asp:CheckBox runat="server" ID="CheckBox1" />
<asp:HiddenField ID="HiddenField1" runat="server" />

Listing 1 – DateTimeBoolean_Edit.ascx

So here in Listing 2 we see the first ExtractValues attempt

protected override void ExtractValues(IOrderedDictionary dictionary)
{
    if (!CheckBox1.Checked)
    {
        // only set to null if unchecked
        dictionary[Column.Name] = String.Empty;
    }
    else if (String.IsNullOrEmpty(HiddenField1.Value))
    {
        // only set a value if there is no current value
        dictionary[Column.Name] = DateTime.Now;
    }
}

Listing 2 – ExtractValues method v1

As you can see if the CheckBox1 is unchecked I set the return value to String.Empty this you would think would do the trick but it doesn't what happens is the value is retained. What confused the matter for me was is if I have a new row or a row with a null value and check it, it sets the value Surprise

It turns out that the reason is that the ExtractValues method is called twice the first time when the field template is initialised and the second time when the updated value is returned. But if look at my logic it returns no value the first time the method is called, but a value is required for the system to know that it is to be set to null the second time it is called Big Grin

So here the full code behind of the Edit template, you will see now that the hidden field is set in the OnDataBound event to make sure we return the correct value the first time ExtractValues is called.

public partial class DateTimeBoolean_EditField : System.Web.DynamicData.FieldTemplateUserControl
{
    protected override void OnDataBinding(EventArgs e)
    {
        base.OnDataBinding(e);

        // keep current value
        HiddenField1.Value = FieldValueString;

        if (FieldValue != null)
            CheckBox1.Checked = true;
        else
            CheckBox1.Checked = false;
    }

    protected override void ExtractValues(IOrderedDictionary dictionary)
    {
        if (!CheckBox1.Checked)
        {
            // only set to null if unchecked
            dictionary[Column.Name] = String.Empty;
        }
        else if (String.IsNullOrEmpty(HiddenField1.Value))
        {
            // only set a value if there is no current value
            dictionary[Column.Name] = DateTime.Now;
        }
        //else
        //    dictionary[Column.Name] = ConvertEditedValue(HiddenField1.Value);
    }

    public override Control DataControl
    {
        get { return CheckBox1; }
    }
}

Listing 3 – DateTimeBoolean_Edit.ascx.cs

And for completeness the read-only version of the field template.

<%@ Control
    Language="C#" 
    CodeBehind="DateTimeBoolean.ascx.cs" 
    Inherits="CustomFieldTemplates.DateTimeBooleanField" %>

<asp:CheckBox ID="CheckBox1" runat="server" Enabled="false" />

Listing 4 – DateTimeBoolean.ascx

public partial class DateTimeBooleanField : System.Web.DynamicData.FieldTemplateUserControl
{
    protected override void OnDataBinding(EventArgs e)
    {
        base.OnDataBinding(e);
        CheckBox1.Checked = !String.IsNullOrEmpty(FieldValueString);
        if (FieldValue != null)
            CheckBox1.ToolTip = FieldValueString;
    }

    public override Control DataControl
    {
        get { return CheckBox1; }
    }
}

Listing 5 – DateTimeBoolean.ascx.cs

You may want to show a date time value instead of the checkbox if so you can just use a copy of the default read-only Date Time field template.

And remember to add a UIHint to set the column to use the new field template

[UIHint("DateTimeBoolean")]
public Nullable<DateTime> ShippedDate { get; set; }

As always happy coding

Saturday, 24 July 2010

Conditional Row Highlighting in Dynamic Data

There are occasions when you want to highlight a row in the GridView (I usually want this based on a Boolean field) so here’s what you do.

First of all we need some way of telling the column to do this an I usually use an attribute see Listing 1 it have two properties one for the value when we want the CSS class to be applied, and the other the CSS class to apply.

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class RowHighlightingAttribute : Attribute
{
    /// <summary>
    /// Initializes a new instance of the <see cref="RowHighlightingAttribute"/> class.
    /// </summary>
    /// <param name="valueWhenTrue">The value when true.</param>
    /// <param name="cssClass">The CSS class.</param>
    public RowHighlightingAttribute(String valueWhenTrue, String cssClass)
    {
        ValueWhenTrue = valueWhenTrue;
        CssClass = cssClass;
    }

    /// <summary>
    /// Gets or sets the value when true.
    /// </summary>
    /// <value>The value when true.</value>
    public String ValueWhenTrue { get; set; }

    /// <summary>
    /// Gets or sets the CSS class.
    /// </summary>
    /// <value>The CSS class.</value>
    public String CssClass { get; set; }
}

Listing 1 – RowHighlightingAttribute

Next we need a way of applying the CSS class based on the condition, see Listing 2.

/// <summary>
/// Highlights the row.
/// </summary>
/// <param name="fieldTemplate">The field template.</param>
public static void HighlightRow(this FieldTemplateUserControl fieldTemplate)
{
    // get the attribute
    var rowHighlighting = fieldTemplate.MetadataAttributes.GetAttribute<RowHighlightingAttribute>();
    // make sure the attribute is not null
    if (rowHighlighting != null)
    {
        // get the GridViewRow, note this will not
        // be present in a DetailsView.
        var parentRow = fieldTemplate.GetContainerControl<GridViewRow>();
        if (parentRow != null 
            && rowHighlighting.ValueWhenTrue == fieldTemplate.FieldValueString)
        {
            // apply the CSS class appending if a class is already applied.
            if (String.IsNullOrWhiteSpace(parentRow.CssClass))
                parentRow.CssClass += " " + rowHighlighting.CssClass;
            else
                parentRow.CssClass = rowHighlighting.CssClass;
        }
    }
}

Listing 2 – HighlightRow extension method

Now to add the extension method to a field template, we will apply it to the Boolean read-only field template.

protected override void OnDataBinding(EventArgs e)
{
    base.OnDataBinding(e);

    object val = FieldValue;
    if (val != null)
        CheckBox1.Checked = (bool)val;

    // apply highlighting
    this.HighlightRow();
}

Listing 3 – Apply highlighting.

For the sample I’ve also added it to the Text.ascx.cs field template.

Adding some attributes

Metadata applied

Figure 1 - Metadata applied

You could also us this technique on other values, but this will do for this sample.

Row Highlighting applied

Figure 2 – Row Highlighting applied.

So you can see with a little bit of work you can add conditional row level highlighting to Dynamic Data.

Download

Using Entity Framework from a different DLL in Dynamic Data

I just thought I should document what we learned in this thread here Entity Framework: Entity Model in a separated DLL first of all it is easy to do in Dynamic Data 4 and needs a fix on each page for Dynamic Data 1 (this version that shipped with Visual Studio 2008 SP1 and .Net 3.5 SP1)

Note: see also Rick Andersons blog post here Explicit connection string for EF and L2S

Adding Model from DLL or Class Library

Sample Project

Figure 1 – sample project with Model in Class Library

To use the Northwind Model in our project we need to get a new instance of the Northwind context, to do this we could add the code to instantiate it in the Glabal.asax.cs file. But for me the object of having it in an external class library/DLL is to keep it reusable, so I had the idea of building the connection string in the external class library rather than locally. We add Listing 1 to the class library, as a partial class of the NorthwindEntities class, pass-in the server name (we could pass-in more but the server name will do here).

public partial class NorthwindEntities
{
    /// <summary>
    /// Gets a new NorthwindEntities object.
    /// </summary>
    public static ObjectContext GetContext(String server, String database)
    {
        EntityConnectionStringBuilder entityBuilder = new EntityConnectionStringBuilder();
        entityBuilder.Provider = "System.Data.SqlClient";
        entityBuilder.ProviderConnectionString =
            String.Format("server={0};database={1};Integrated Security=SSPI;MultipleActiveResultSets=True",
            server,
            database);
        entityBuilder.Metadata = @"res://*";
        var objContext = new NorthwindContext.NorthwindEntities(entityBuilder.ToString());

        return objContext;
    }
}

Listing 1 – GetContext helper method

So now all we need to add this to our Dynamic Data app is use this in the RegisterContext method in the Global.asax.cs

DefaultModel.RegisterContext(() => NorthwindContext.NorthwindEntities.GetContext("aragorn","Northwind"),
            new ContextConfiguration()
            {
                ScaffoldAllTables = true
            });

Listing 2 – Using the GetContext helper

Fixing it up to work in Original version of Dynamic Data

This work beautifully in Dynamic Data 4, but not in the previous release Sadwe need the fix mentioned in Rick Andersons post

GridDataSource.ContextCreating += delegate(object ceSender,
    EntityDataSourceContextCreatingEventArgs ceArgs)
    {
        ceArgs.Context = (ObjectContext)table.CreateContext();
    };

Listing 3 – Fix for Dynamic Data 1

Added to the List page it looks like this:

protected void Page_Init(object sender, EventArgs e)
{
    DynamicDataManager1.RegisterControl(GridView1, true /*setSelectionFromUrl*/);
    GridDataSource.ContextCreating += delegate(object ceSender,
        EntityDataSourceContextCreatingEventArgs ceArgs)
        {
            ceArgs.Context = (ObjectContext)table.CreateContext();
        };
}

Listing 4 – Added to List page template

You have to add this to each page template and add it twice on the ListDetails page template.

Tuesday, 20 July 2010

Using Entity Framework Code First (CTP4) in Dynamic Data

Following the sample EF CTP4 Walkthrough: Productivity Improvements you end up with a sample DbContext and of course I immediately thought of using this in Dynamic Data using the “ASP.NET Dynamic Data Entities Web Application” template (Figure 1),

Select ASP.NET Dynamic Data Entities Web Application

Figure 1 - Select ASP.NET Dynamic Data Entities Web Application

but I got the following error:

Error when using DbContext with DD4 EF Template.

Figure 2 – Error when using DbContext with DD4 EF Template.

This my my default code Listing 1

DefaultModel.RegisterContext(typeof(Models.ProductCatalog),
    new ContextConfiguration()
    {
        ScaffoldAllTables = true
    });

Listing 1 – my default DbContext registration

So I fired off an e-mail to Microsoft and was told “the guideline will be to use DD with DomainService, which will have full support of DbContext in the future” although that wont work out of the box at the moment so, I gave upBlushing

Then Diego Vega posted on twitter a link to Rowan Millers blog EF CTP4 Tips & Tricks: WCF Data Service on DbContext this turned out to be very useful, he talks about getting the underlying ObjectContext see Listing 2.

public class ProductCatalog : DbContext
{
    public ProductCatalog(String connectionString)
    {
        // set default connection string
        this.Database.Connection.ConnectionString = connectionString;
    }

    public ObjectContext UnderlyingContext
    {
        get { return this.ObjectContext; }
    }

    public DbSet<Category> Categories { get; set; }
    public DbSet<Product> Products { get; set; }
}

public class Category
{
    public string CategoryId { get; set; }
    public string Name { get; set; }

    public virtual ICollection<Product> Products { get; set; }
}

public class Product
{
    public int ProductId { get; set; }
    public string Name { get; set; }
    public string CategoryId { get; set; }

    public virtual Category Category { get; set; }
}

Listing 2 – my ProductCatalog

In Listing 2 I use this feature to get the underlying ObjectContext (Also note my setting the connection string in the constructor). So now we have a cool way of getting Entity Framework Code First CTP4 working with Dynamic Data 4. This makes me very happyHappy Wizzard

So you will see that all we need to do to get this working with Dynamic Data now is to replace the

typeof(Models.ProductCatalog)

with

() => new Models.ProductCatalog(connectionString).UnderlyingContext

in the RegisterContext method to get at the underlying ObjectContext, which now leads us to Listing 3 .

public static void RegisterRoutes(RouteCollection routes)
{
    var connectionString = "Data Source=aragorn;Initial Catalog=ProductCatalog;Integrated Security=True";
    DefaultModel.RegisterContext(() => new Models.ProductCatalog(connectionString).UnderlyingContext,
        new ContextConfiguration()
        {
            ScaffoldAllTables = true
        });

    routes.Add(new DynamicDataRoute("{table}/{action}.aspx")
    {
        Constraints = new RouteValueDictionary(new { action = "List|Details|Edit|Insert" }),
        Model = DefaultModel
    });
}

Listing 3 – Global.asax RegisterRoutes method.

Now we can Edit, Update and Insert using Entity Framework Code First feature in CTP4.

Download

Nice: One of my favourite things about this is deployment of the DB is that if the DB does not exist on the server specified in the connection string the Entity Framework CTP4 will create it for you (or if you did not specify a connection string it will try to create it in the local SQL Express instance.
!Important: CTP5 make some minor API changes see Dabid Ebbo’s article here Using Dynamic Data with EF Code First and NuGet

Saturday, 17 July 2010

Autocomplete Filter from the old Futures project working in Dynamic Data 4

I was sorry that the Autocomplete filter from the old Futures project was not added to Dynamic Data 4 CryingSo I thought I would do it, so here it is. I also thought I would make it work with the Entity Framework as well.

First of all if you don’t have a copy of the old futures project then go get it from here Dynamic Data Futures VS2008 SP1 RTM this has many useful samples that are worth adapting to DD4.

Copying The Necessary Files

First we need to copy some filed from the old Futures project to the new project in Visual Studio 2010, see Figure 1 for the files we need and Figure 2 for the where we need to copy them

Autocomplete files LinqExpressionHelper.cs

 Figure 1 - Required files from old Futures project

Files Added to Custom Filter project

Figure 2 - Files Added to Custom Filters project

The Files will now need updating to work with this project.

Change namespace in each file

For this I usually just do a global replace in entire project

Ctrl+H  “Find and Replace”

Figure 3 - Ctrl+H  “Find and Replace”

Remove redundant using

In the Autocomplete.ascx.cs file remove the redundant using statement for Microsoft.Web.DynamicData.

Fix up the AutocompleteFilter.asmx.cs service.

First we need to add some references:

Download and add a reference to Ajax Control Toolkit,

Next we need is to add the using for generic collections;

using System.Collections.Generic;

Then we need to change this;

 return queryable.Cast<object>().Select(row => CreateAutoCompleteItem(table, row)).ToArray();

for this;

var values = new List<String>();
foreach (var row in queryable)
{
    values.Add( CreateAutoCompleteItem(table, row));
}

return values.ToArray(); 

This also makes it compatible with the Entity Framework

Fix up the Autocomplete filter it’s self.

The very first thing is the filter needs to inherit from “System.Web.DynamicData.QueryableFilterUserControl”

public partial class Autocomplete_Filter : System.Web.DynamicData.QueryableFilterUserControl

Add the following Usings:

using System.Web.UI;
using System.Collections;

Then we need to add some properties;

private new MetaForeignKeyColumn Column
{
    get { return (MetaForeignKeyColumn)base.Column; }
}

public override Control FilterControl
{
    get { return AutocompleteTextBox; }
}

Next replace ALL  occurrences of InitialValue with DefaultValue

In the CleantButton_Click event add OnFilterChanged() call

public void ClearButton_Click(object sender, EventArgs e)
{
    // this would probably be better handled using client JavaScirpt
    AutocompleteValue.Value = String.Empty;
    AutocompleteTextBox.Text = String.Empty;
    OnFilterChanged();
}

Remove the SelectedValue property and add the following event handler and method.

protected void AutocompleteValue_ValueChanged(object sender, EventArgs e)
{
    OnFilterChanged();
}

public override IQueryable GetQueryable(IQueryable source)
{
    string selectedValue = String.IsNullOrEmpty(AutocompleteValue.Value) ? null : AutocompleteValue.Value
    if (String.IsNullOrEmpty(selectedValue))
        return source;

    IDictionary dict = new Hashtable();
    Column.ExtractForeignKey(dict, selectedValue);
    foreach (DictionaryEntry entry in dict)
    {
        string key = (string)entry.Key;
        if (DefaultValues != null)
            DefaultValues[key] = entry.Value;

        source = ApplyEqualityFilter(source, Column.GetFilterExpression(key), entry.Value);
    }
    return source;
}

Listing 1 – New Methods for Autocomplete filter.

Next we need to hook-up the HiddenField’s OnValueChanged event handler to point to AutocompleteValue_ValueChanged method we just added, so it looks like this.

<asp:HiddenField
    runat="server" 
    ID="AutocompleteValue" 
    OnValueChanged="AutocompleteValue_ValueChanged"/>

Add a link to the Autocomplete css

Open site.master and drag the AutocompleteStyle.css to the Head section

<link href="AutocompleteStyle.css" rel="stylesheet" type="text/css" />

Now we need to add some styling to the Autocomplete.ascx filter, I’m just adding

CssClass="DDControl"

to the TextBox and Button controls.

Add some Metadata

In Listing 1 we have added a FilterUIHint setting the Autocomplete filter as the the filter for the Orders Customer property.

[MetadataTypeAttribute(typeof(Order.OrderMetadata))]
public partial class Order
{
    internal sealed class OrderMetadata
    {
        [FilterUIHint("Autocomplete")]
        public Customer Customer { get; set; }
    }
}

Listing 3 -  sample metadata

You should now have a working sample run it up and go to the Orders table list page:

Autocomplete filter in action

Figure 4 - Autocomplete filter in action

Downloads

Happy Coding Happy Wizzard

Thursday, 15 July 2010

EF CTP4 Released!

Entity Framework Code First CTP4 is now available see following links:

And a quick note from my testing:

public class ProductCatalog : DbContext
{
    public ProductCatalog()
    {
        this.Database.Connection.ConnectionString =
           "Data Source=.;Initial Catalog=ProductCatalog;Integrated Security=True";
    }

    public DbSet<Category> Categories { get; set; }
    public DbSet<Product> Products { get; set; }
}

Above is from the first sample you create in VS2010 with EF Code First, and a quick way to create your DB on a different server than on localhost\SQLEXPRESS thought this is super cool.

Happy Wizard

Sunday, 13 June 2010

Securing Dynamic Data 4 (Replay)

This is an updated version of the series Securing Dynamic Data Preview 4 from July 2009 here I playnto streamline the class libraries for the RTM version of Dynamic Data 4  and Visual Studio 2010.

This version is mostly the same as in Part 1 except I’ve done a great deal of refactoring and so I will list everything again here. The main difference is that there are now no user controls to replace the Delete buttons. Also I have changed the permissions system to be restrictive by default at Table level i.e. you must have a permission set on every table for the table to be seen, but a Column level you deny columns you don’t want to be seen.

Permissions Enums

The TableActions (renamed from TableDeny) enum Listing 1 has had a CombinedActions class Listing 2 added that combine sets of TableActions into logical security groups (i.e. ReadOnly equates to combining TablesActions Details and List to give an more descriptive was of assigning rights to a security Role).

/// <summary>
/// Table permissions enum, allows different
/// levels of permission to be set for each 
/// table on a per role bassis.
/// </summary>
[Flags]
public enum TableActions
{
    /// <summary>
    /// Default no permissions
    /// </summary>
    None = 0x00,
    /// <summary>
    /// Details page
    /// </summary>
    Details = 0x01,
    /// <summary>
    /// List page
    /// </summary>
    List = 0x02,
    /// <summary>
    /// Edit page
    /// </summary>
    Edit = 0x04,
    /// <summary>
    /// Insert page
    /// </summary>
    Insert = 0x08,
    /// <summary>
    /// Delete operations
    /// </summary>
    Delete = 0x10,
}

Listing 1 – TableActions

/// <summary>
/// Combines Table permissions enums
/// into logical security groups
/// i.e. ReadOnly combines TableActions
/// Details and List
/// </summary>
public static class CombinedActions
{
    /// <summary>
    /// Read Only access 
    /// TableActions.Details or 
    /// TableActions.List
    /// </summary>
    public const TableActions ReadOnly = 
        TableActions.Details | 
        TableActions.List;
    /// <summary>
    /// Read and Write access 
    /// TableActions.Details or 
    /// TableActions.List or
    /// TableActions.Edit
    /// </summary>
    public const TableActions ReadWrite = 
        TableActions.Details | 
        TableActions.List | 
        TableActions.Edit;
    /// <summary>
    /// Read Insert access 
    /// TableActions.Details or 
    /// TableActions.List or 
    /// TableActions.Insert
    /// 
    /// </summary>
    public const TableActions ReadInsert = 
        TableActions.Details | 
        TableActions.List | 
        TableActions.Insert;
    /// <summary>
    /// Read Insert and Delete access 
    /// TableActions.Details or 
    /// TableActions.List or 
    /// TableActions.Insert or 
    /// TableActions.Delete)
    /// </summary>
    public const TableActions ReadInsertDelete = 
        TableActions.Details | 
        TableActions.List | 
        TableActions.Insert | 
        TableActions.Delete;
    /// <summary>
    /// Read and Write access 
    /// TableActions.Details or 
    /// TableActions.List or 
    /// TableActions.Edit or 
    /// TableActions.Insert)
    /// </summary>
    public const TableActions ReadWriteInsert = 
        TableActions.Details | 
        TableActions.List | 
        TableActions.Edit | 
        TableActions.Insert;
    /// <summary>
    /// Full access 
    /// TableActions.Delete or
    /// TableActions.Details or 
    /// TableActions.Edit or 
    /// TableActions.Insert or 
    /// TableActions.List)
    /// </summary>
    public const TableActions Full = 
        TableActions.Delete | 
        TableActions.Details | 
        TableActions.Edit | 
        TableActions.Insert | 
        TableActions.List;
}

Listing 2 – CombinedActions

ColumnActions Listing 3  are used to deny either Write or Read access.

/// <summary>
/// Actions a Column can 
/// have assigned to itself.
/// </summary>
[Flags]
public enum ColumnActions
{
    /// <summary>
    /// Action on a column/property
    /// </summary>
    DenyRead = 1,
    /// <summary>
    /// Action on a column/property
    /// </summary>
    DenyWrite = 2,
}

Listing 3 – ColumnActions

Secure Dynamic Data Route Handler

The SecureDynamicDataRouteHandler has changed very little since the original article all I have added is the catch all tp.Permission == CombinedActions.Full in the if statement to streamline the code.

/// <summary>
/// The SecureDynamicDataRouteHandler enables the 
/// user to access a table based on the following:
/// the Roles and TableDeny values assigned to 
/// the SecureTableAttribute.
/// </summary>
public class SecureDynamicDataRouteHandler : DynamicDataRouteHandler
{
    /// <summary>
    /// Creates the handler.
    /// </summary>
    /// <param name="route">The route.</param>
    /// <param name="table">The table.</param>
    /// <param name="action">The action.</param>
    /// <returns>An IHttpHandler</returns>
    public override IHttpHandler CreateHandler(
        DynamicDataRoute route,
        MetaTable table,
        string action)
    {
        var httpContext = HttpContext.Current;
        if (httpContext != null && httpContext.User != null)
        {
            var usersRoles = Roles.GetRolesForUser(httpContext.User.Identity.Name);
            var tablePermissions = table.Attributes.OfType<SecureTableAttribute>();

            // if no permission exist then full access is granted
            if (tablePermissions.Count() == 0)
                return null;

            foreach (var tp in tablePermissions)
            {
                if (tp.HasAnyRole(usersRoles))
                {
                    // if no action is allowed return no route
                    var tpAction = tp.Permission.ToString().Split(new char[] { ',', ' ' }, 
                        StringSplitOptions.RemoveEmptyEntries);

                    if (tp.Permission == CombinedActions.Full || tpAction.Contains(action))
                        return base.CreateHandler(route, table, action);
                }
            }
        }
        return null;
    }
}

Listing 4 – Secure Dynamic Data Route Handler

This then covers all Edit, Insert and Details actions but not Delete.

Delete Actions

In the previous article we had a User Control that handled securing the Delete action, here we have a SecureLinkButton. All we do is override the Render method and test to see if the button is disabled via the users security roles.

/// <summary>
/// Secures the link button when used for delete actions
/// </summary>
public class SecureLinkButton : LinkButton
{
    private const String DISABLED_NAMES = "SecureLinkButtonDeleteCommandNames";
    private String[] delete = new String[] { "delete" };

    /// <summary>
    /// Raises the <see cref="E:System.Web.UI.Control.Init"/> event.
    /// </summary>
    /// <param name="e">
    /// An <see cref="T:System.EventArgs"/> 
    /// object that contains the event data.
    /// </param>
    protected override void OnInit(EventArgs e)
    {
        if (ConfigurationManager.AppSettings.AllKeys.Contains(DISABLED_NAMES))
            delete = ConfigurationManager.AppSettings[DISABLED_NAMES]
                .ToLower()
                .Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);

        base.OnInit(e);
    }

    /// <summary>
    /// Renders the control to the specified HTML writer.
    /// </summary>
    /// <param name="writer">
    /// The <see cref="T:System.Web.UI.HtmlTextWriter"/> 
    /// object that receives the control content.
    /// </param>
    protected override void Render(HtmlTextWriter writer)
    {
        if (!IsDisabled())
            base.Render(writer);
        else
            writer.Write(String.Format("<a>{0}</a>", Text));
    }

    /// <summary>
    /// Determines whether this instance is disabled.
    /// </summary>
    /// <returns>
    /// 	<c>true</c> if this instance is 
    /// 	disabled; otherwise, <c>false</c>.
    /// </returns>
    private Boolean IsDisabled()
    {
        if (!delete.Contains(CommandName.ToLower()))
            return false;

        // get restrictions for the current
        // users access to this table
        var table = DynamicDataRouteHandler.GetRequestMetaTable(Context);
        var usersRoles = Roles.GetRolesForUser();
        var tableRestrictions = table.Attributes.OfType<SecureTableAttribute>();

        // restrictive permissions
        if (tableRestrictions.Count() == 0)
            return true;

        foreach (var tp in tableRestrictions)
        {
            // the LinkButton is considered disabled if delete is denied.
            var action = CommandName.ToEnum<TableActions>();
            if (tp.HasAnyRole(usersRoles) && (tp.Actions & action) == action)
                return false;
        }
        return true;
    }
}

Listing 5 – Secure Link Button

In more detail the IsDisabled method check to see if the LinkButtons CommandName is the same as the the “SecureLinkButtonDeleteCommandNames” application setting set in the web.config, note the default is “delete”. And then if the user does not have Delete permission then the button is disabled.

So how do we use this SecureLinkButton we add a tagMapping in the web.config file see Listing 6.

<configuration>
    <system.web>
        <pages>
            <controls>
                <!-- custom tag assignments -->
                <add tagPrefix="asp" namespace="NotAClue.Web.DynamicData" 
                    assembly="NotAClue.Web.DynamicData" />
            </controls>
            <!-- custom tag mappings -->
            <tagMapping>
                <add tagType="System.Web.UI.WebControls.LinkButton"
                    mappedTagType="NotAClue.Web.DynamicData.SecureLinkButton" />
            </tagMapping>
        </pages>
    </system.web>
</configuration>

Listing 6 – Tag Mapping in web.config

This means that our SecureLinkButton will replace the LinkButton throughout the site, however if you do not like this you can just rename each delete <asp:LinkButton to <asp:SecureLinkButton and then you will get the same effect and not add the tagMapping section to the web.config.

The Secure Meta Model

Here the two main parts are the SecureMetaTable and the three MetaColumn types (SecureMetaColumn, SecureMetaForeignKeyColumn and SecureMetaChildrenColumn)

SecureMetaTable

In the SecureMetaTable we override the GetScaffoldColumns method and filter the column list to where columns do not have a DenyRead action applied for any of the current users security roles.

SecureMetaColumn, SecureMetaForeignKeyColumn and SecureMetaChildrenColumn

With these types we do have to repeat ourselves a little as we override the IsReadOnly property to check to see if the column has a DenyWrite action applied for one of the users roles.
Note: Thanks to the ASP.NET team for listening and making this property virtual.

There is one issue I found and that is the default FieldTemplateFactory caches the DynamicControl model (ReadOnly, Edit and Insert) I did toy with adding the relevant code the default EntityTemplates see Listing 7, but decided again it.

protected void DynamicControl_Init(object sender, EventArgs e)
{
    DynamicControl dynamicControl = (DynamicControl)sender;
    dynamicControl.DataField = currentColumn.Name;

    // test for read-only column
    if (currentColumn.IsReadOnly)
        dynamicControl.Mode = DataBoundControlMode.ReadOnly;
}


Listing 7 – adding control mode code to the default EntityTemplates

Instead I decided to use a custom FieldTemplateFactory see Listing 8

public class SecureFieldTemplateFactory : FieldTemplateFactory
{
    public override IFieldTemplate CreateFieldTemplate(MetaColumn column,
        DataBoundControlMode mode, 
        string uiHint)
    {
        // code to fix caching issue
        if (column.IsReadOnly)
            mode = DataBoundControlMode.ReadOnly;

        return base.CreateFieldTemplate(column, mode, uiHint);
    }
}

Listing 8 – Secure Field Template Factory

The code here is simple we just check to see if the column is read-only (remembering that the SecureMetaColumns are already checking this for us) then set the Mode to DataBoundControlMode.ReadOnly. This nicely keeps our code DRY.

Secure Table and Column Attributes

These are essentially unchanged from the previous series of articles with just a little refactoring to make the code more readable.

!Important: For code see sample at end of article.

Putting It Together

Nearly all the work to get Secure Dynamic Data working is done simply in the Global.asax file.

Note: There are some changes you need to make to add Login etc but that is standard ASP.Net and specific to Dynamic Data.

Adding Security to Dynamic Data

Figure 1 – Adding Security to Dynamic Data

Also you need the tag mapping from Listing 6, there are some more bits we need to do but they are standard ASP.Net Security, so let’s get that done next.

!Important: To use this as we currently are you will need SQL Server 200x Express installed otherwise you will need to add a specific connection string and use Creating the Application Services Database for SQL Server to make your ASPNETDB database for membership and roles.
<authentication mode="Forms">
    <forms loginUrl="~/Login.aspx" protection="All" defaultUrl="~/Default.aspx" path="/"/>
</authentication>
<authorization>
    <deny users="?"/>
</authorization>
<membership>
    <providers>
        <remove name="AspNetSqlMembershipProvider"/>
        <add name="AspNetSqlMembershipProvider"
            type="System.Web.Security.SqlMembershipProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
            connectionStringName="LocalSqlServer"
            enablePasswordRetrieval="false"
            enablePasswordReset="true"
            requiresQuestionAndAnswer="true"
            applicationName="/"
            requiresUniqueEmail="false"
            passwordFormat="Hashed"
            maxInvalidPasswordAttempts="5"
            minRequiredPasswordLength="7"
            minRequiredNonalphanumericCharacters="0"
            passwordAttemptWindow="10"
            passwordStrengthRegularExpression=""/>
    </providers>
</membership>
<roleManager enabled="true" />

Listing 9 – Adding standard ASP.Net security to web.config

<location path="Site.css">
    <system.web>
        <authorization>
            <allow users="*"/>
        </authorization>
    </system.web>
</location>

Listing 10 – Allowing access to style sheet.

With SQL Server 200x Express edition installed you will get the ASPNETDB created automatically.

Note: I generally do this to create the ASPNETDB then move it to where I want it and setup a specific connection string. Also you can use the ASP.Net Configuration utility to create users and roles.
ASP.Net Configuration Utility
Figure 1 - ASP.Net Configuration Utility

Downloads

I think that is about it, so here is the download it contains three projects the Class Library and two sample projects one Entity Framework and one Linq to SQL. Have fun.

Sunday, 6 June 2010

Part 3 – A Cascading Hierarchical Field Template & Filter for Dynamic Data

This would have been the final part of the series of articles, however I realised that we will need a final article that covers the more complex action of the CascadingHierarchicalFilter, this where at each level of selection we filter the list.

Cascading Hierarchical Filter

Figure 1 – Cascading Hierarchical Filter.

e.g. in Figure 1 the CascadingHierarchicalFilter (the basic version) no selection occurs until we select a value from the final list control, however in the complex CascadingHierarchicalFilter we will filter at each level and so we will hold that over until we have completed the basic version of the CascadingHierarchicalFilter.

And so to the basic version, first we will need a starting place so copy the ForeignKey filter and rename it to CascadingHierarchical and the class name to CascadingHierarchicalFilter. Next we remove the content of the aspx page so it looks like Listing 1.

<%@ Control 
    Language="C#" 
    CodeBehind="CascadeHierarchical.ascx.cs" 
    Inherits="CascadeHierarchicalFieldTemplate.CascadeHierarchicalFilter" %>

Listing 1 – CascadingHierarchical.aspx page

Then we start on the code behind by adding our member variables, adjusting the two properties and replacing the default Page_Init with ours, which is very similar to the Edit templates Page_Init see Listing 2.

#region member variables
private const string NullValueString = "[null]";
// hold the current data context to access the model
private object context;
// hold the list of filters
private SortedList<int, HierachicalListControl> filters = new SortedList<int, HierachicalListControl>();
// hold the attribute
private CascadeHierarchicalAttribute cascadeHierarchicalAttribute;
#endregion

private new MetaForeignKeyColumn Column
{
    get { return (MetaForeignKeyColumn)base.Column; }
}

public override Control FilterControl
{
    get
    {
        if (filters[0] != null && filters[0].ListControl != null)
            return filters[0].ListControl;
        else
            return null;
    }
}

protected void Page_Init(object sender, EventArgs e)
{
    //if (!Column.IsRequired)
    // check we have a cascade hierarchical attribute if not throw error
    cascadeHierarchicalAttribute = Column.GetAttribute<CascadeHierarchicalAttribute>();
    if (cascadeHierarchicalAttribute == null)
        throw new InvalidOperationException("Was expecting a CascadeFilterAttribute.");

    // check we have correct column type if not throw error
    if (!(Column is MetaForeignKeyColumn))
        throw new InvalidOperationException(String.Format("Column {0} must be a foreign key column navigation property", Column.Name));

    // get current context
    context = Column.Table.CreateContext();

    // get hierarchical cascade columns
    var parentColumns = new SortedList<int, String>();
    for (int i = 0; i < cascadeHierarchicalAttribute.Parameters.Length; i++)
        parentColumns.Add(i, cascadeHierarchicalAttribute.Parameters[i]);

    // add extra column to represent this column itself
    parentColumns.Add(cascadeHierarchicalAttribute.Parameters.Length, String.Empty);

    //get current column into a local variable
    MetaForeignKeyColumn currentColumn = Column;

    // setup list of filter definitions
    for (int i = 0; i < parentColumns.Count; i++)
    {
        // get parent column name
        var parentColumnName = parentColumns[i];

        // create dropdown list
        var ddl = new DropDownList()
        {
            ID = String.Format("ListControl{0}", i),
            Enabled = false,
            AutoPostBack = true
        };

        // create filter
        var filter = new HierachicalListControl(ddl) { Column = currentColumn };

        // check for last parent filter
        if (!String.IsNullOrEmpty(parentColumnName))
        {
            // set parent column from parent table
            filter.ParentColumn = (MetaForeignKeyColumn)currentColumn.ParentTable.GetColumn(parentColumnName);

            // set current column to parent column
            currentColumn = filter.ParentColumn;
        }
        else
        {
            // this is the last parent and has
            // no parent itself so set to null
            filter.ParentColumn = null;
            currentColumn = null;
        }
        // add filter to list of filters
        filters.Add(i, filter);
    }

    // add dropdown list to page in correct order 2, 1, 0
    // last parent, parent<N>, child
    for (int i = parentColumns.Count - 1; i >= 0; i--)
    {
        // setup dropdown list
        filters[i].ListControl.Items.Clear();
        filters[i].ListControl.Items.Add(new ListItem("------", String.Empty));

        // add parent list controls event handler
        if (i > 0)
            filters[i].ListControl.SelectedIndexChanged += ListControls_SelectedIndexChanged;
        else
            filters[i].ListControl.SelectedIndexChanged += ListControl0_SelectedIndexChanged;

        // add control to place holder
        this.Controls.Add(filters[i].ListControl);
    }

    if (DefaultValue != null)
        PopulateAllListControls(DefaultValue);
    else
    {
        // fill last parent filter
        var lastParentIndex = filters.Count - 1;
        var parentTable = filters[lastParentIndex].Column.ParentTable;
        var parentQuery = parentTable.GetQuery(context);

        // set next descendant list control
        PopulateListControl(lastParentIndex, parentQuery);
    }
}

Listing 2 – Member variables and Page_Init

Now we add the following Listings 3 & 4 these are virtually the same as from the Edit template and so I am thinking in the next revision they may all be removed to the class library.

/// <summary>
/// Sets the default values.
/// </summary>
/// <param name="fieldValue">The value.</param>
private void PopulateAllListControls(object fieldValue)
{
    var displayStrings = new SortedList<int, String>();

    #region Get list of propert values
    // get property values
    var propertyValues = new SortedList<int, Object>();
    propertyValues.Add(0, fieldValue);
    for (int i = 0; i < filters.Count - 1; i++)
    {
        var parentName = filters[i].ParentColumn.Name;
        object pv = propertyValues[i].GetPropertyValue(parentName);
        propertyValues.Add(i + 1, pv);
    }
    #endregion

    // stating at the first filter and work way up to the last filter
    for (int i = 0; i < filters.Count; i++)
    {
        var parentTable = filters[i].Column.ParentTable;
        var parentQuery = parentTable.GetQuery(context);
        IQueryable listItemsQuery;
        if (i == cascadeHierarchicalAttribute.Parameters.Length)
        {
            listItemsQuery = parentQuery.GetQueryOrdered(parentTable);
        }
        else
        {
            var pcol = filters[i + 1].Column;
            var selectedValue = filters[i].ParentColumn.GetForeignKeyString(propertyValues[i]);
            listItemsQuery = parentQuery.GetQueryFilteredFkColumn(pcol, selectedValue);
        }

        // set next descendant list control
        PopulateListControl(i, listItemsQuery);

        // set initial values
        var selectedValueString = filters[i].Column.Table.GetPrimaryKeyString(propertyValues[i]);
        ListItem item = filters[i].ListControl.Items.FindByValue(selectedValueString);
        if (item != null)
            filters[i].ListControl.SelectedValue = selectedValueString;
    }
}

Listing 3 – PopulateAllListControls

/// <summary>
/// Handles the SelectedIndexChanged event of the parentListControls control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">
/// The <see cref="System.EventArgs"/> instance containing the event data.
/// </param>
/// <summary>
/// Setups the parent list control.
/// </summary>
/// <param name="table">The table.</param>
/// <param name="filterIndex">The parent id.</param>
/// <param name="items">The items.</param>
public void PopulateListControl(int filterIndex, IQueryable items)
{
    // clear the list controls list property
    filters[filterIndex].ListControl.Items.Clear();
    // enable list control
    filters[filterIndex].ListControl.Enabled = true;

    // add unselected value showing the column name
    // [Styles]
    filters[filterIndex].ListControl.Items.Add(
        new ListItem(String.Format("[{0}]",
            filters[filterIndex].Column.DisplayName), NullValueString));

    foreach (var row in items)
    {
        // populate each item with the display string and key value
        filters[filterIndex].ListControl.Items.Add(
            new ListItem(filters[filterIndex].Column.ParentTable.GetDisplayString(row),
            filters[filterIndex].Column.ParentTable.GetPrimaryKeyString(row)));
    }
}

/// <summary>
/// Resets all descendant list controls.
/// </summary>
/// <param name="startFrom">The start from.</param>
private void ResetAllDescendantListControls(int startFrom)
{
    for (int i = startFrom - 1; i >= 0; i--)
    {
        filters[i].ListControl.Items.Clear();
        filters[i].ListControl.Items.Add(new ListItem("----", String.Empty));
        filters[i].ListControl.Enabled = false;
    }
}

Listing 4 – PopulateListControl and ResetAllDescendantListControls

Listing 5 is again virtually the same as the Edit templates but I kept it separate as it is an event handler and may make more sense to keep it in the template.

/// <summary>
/// Handles the SelectedIndexChanged event for each List control, 
/// and populates the next list control in the hierarchy.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">
/// The <see cref="System.EventArgs"/> instance containing the event data.
/// </param>
void ListControls_SelectedIndexChanged(object sender, EventArgs e)
{
    // get list control
    var listControl = (ListControl)sender;

    // get the sending list controls id as an int
    var id = ((Control)sender).ID;

    // use regular expression to find list control index
    var regEx = new Regex(@"\d+");
    var parentIndex = int.Parse(regEx.Match(id).Value);

    if (listControl.SelectedValue != NullValueString)
    {
        if (parentIndex > 0)
        {
            // set child index
            var childIndex = parentIndex - 1;

            // get parent table
            var parentTable = filters[childIndex].Column.ParentTable;

            // get query from table
            var query = parentTable.GetQuery(context);

            // get items for list control
            var itemQuery = query.GetQueryFilteredFkColumn(
                filters[parentIndex].Column,
                listControl.SelectedValue);

            // populate list control
            PopulateListControl(childIndex, itemQuery);

            // reset all descendant list controls
            ResetAllDescendantListControls(childIndex);
        }
    }
    else
    {
        // reset all descendant list controls
        ResetAllDescendantListControls(parentIndex);
    }
}

Listing 5 – ListControls_SelectedIndexChanged

Next we replace the DropDownList1_SelectedIndexChanged with ListControl0_SelectedIndexChanged Listing 6 really only a name change to make the code read clearly.

protected void ListControl0_SelectedIndexChanged(object sender, EventArgs e)
{
    OnFilterChanged();
}

Listing 6 – ListControl0_SelectedIndexChanged

Finally we need to change the GetQueryable method to replace DropDownList1 with filters[0].ListControl.

Download

Happy coding

Note: With a few changes around the last two listings you could adapt this to either the old Dynamic Data Futures VS2008 SP1 RTM filters on CodePlex or Josh Heyes’s Dynamic Data Filtering.

Monday, 31 May 2010

Part 2 – A Cascading Hierarchical Field Template & Filter for Dynamic Data

In this second part we are going to complete the field templates by adding the CascadeHierarchical_Edit field template (see Figure 1) and in the next adding a CascadeHierarchical filter.

Starting with a copy of the ForeignKey_Edit field template as our base we will end up with a field template that looks like Figure 1 allowing you to have a hierarchy ‘n’ levels deep.

Cascade Hierarchical Field Template

Figure 1 – Cascade Hierarchical Field Template in action.

Note: It turns out the the Product field on the Order is only available in insert not edit so I have moved away from Northwind and have created a very construed Vehicles sample DB, I have included as a script in the download at the end of this article.

The first we must make the copy of the ForeignKey_Edit field template, and then make sure we rename it to CascadeHierarchical_Edit, also we must change the class name all it’s files; this sample being a Web Application project there are three files:

  • CascadeHierarchical_Edit.ascx
  • CascadeHierarchical_Edit.ascx.cs
  • CascadeHierarchical_Edit.ascx.designer.cs

Each must be modified as follows (I generally follow the naming convention use in the other standard field templates i.e. Text_Edit’s class name is Text_EditField), so we will set our field templates class name to CascadeHierarchical_EditField.

Note: if you change CascadeHierarchical_Edit.ascx first then you will not need to change the CascadeHierarchical_Edit.ascx.designer.cs will happen automatically. (I don’t know why the CascadeHierarchical_Edit.ascx.cs is not also changed automatically when the ascx file’s Inherits property is changed).

In the .cs file change the class name to CascadeHierarchical_EditField and in the CascadeHierarchical_Edit.ascx change the Inherits property to  be namespace.CascadeHierarchical_EditField, namespace is usually the name of the project, in any case if will already be filled in in the template unless you are copying from one project to another.

Now we have a basis to begin, lets first start with the Page_Init and some explanation.

<%@ Control 
    Language="C#" 
    CodeBehind="CascadeHierarchical_Edit.ascx.cs" 
    Inherits="CascadeHierarchicalFieldTemplate.CascadeHierarchical_EditField" %>

<asp:RequiredFieldValidator 
    runat="server" 
    ID="RequiredFieldValidator1" 
    CssClass="DDControl DDValidator" 
    Display="Static" 
    Enabled="false"/>
<asp:DynamicValidator 
    runat="server" 
    ID="DynamicValidator1" 
    CssClass="DDControl DDValidator" 
    Display="Static"/>

Listing 1 – the aspx page.

Here in Listing 1 you can see all we have are the required and dynamic validators, this is all we need at the dropdown lists will all be created dynamically.

#region member variables
// hold the current data context to access the model
private object context;
// hold the list of filters
private SortedList<int, HierachicalListControl> filters = new SortedList<int, HierachicalListControl>();
// hold the attribute
private CascadeHierarchicalAttribute cascadeHierarchicalAttribute;
#endregion

protected void Page_Init(object sender, EventArgs e)
{
    // check we have a cascade hierarchical attribute if not throw error
    cascadeHierarchicalAttribute = Column.GetAttribute<CascadeHierarchicalAttribute>();
    if (cascadeHierarchicalAttribute == null)
        throw new InvalidOperationException("Was expecting a CascadeFilterAttribute.");

    // check we have correct column type if not throw error
    if (!(Column is MetaForeignKeyColumn))
        throw new InvalidOperationException(String.Format("Column {0} must be a foreign key column navigation property", Column.Name));

    // get current context
    context = Table.CreateContext();

    // get hierarchical cascade columns
    var parentColumns = new SortedList<int, String>();
    for (int i = 0; i < cascadeHierarchicalAttribute.Parameters.Length; i++)
        parentColumns.Add(i, cascadeHierarchicalAttribute.Parameters[i]);

    // add extra column to represent this column itself
    parentColumns.Add(cascadeHierarchicalAttribute.Parameters.Length, "");

    //get current column into a local variable
    MetaForeignKeyColumn column = ForeignKeyColumn;

    // setup list of filter definitions
    for (int i = 0; i < parentColumns.Count; i++)
    {
        // get parent column name
        var parentColumnName = parentColumns[i];

        // create dropdown list
        var ddl = new DropDownList()
        {
            ID = String.Format("ListControl{0}", i),
            Enabled = false,
            AutoPostBack = true
        };
        // create filter
        var filter = new HierachicalListControl(ddl) { Column = column };

        // check for last parent filter
        if (!String.IsNullOrEmpty(parentColumnName))
        {
            // set parent column from parent table
            filter.ParentColumn = (MetaForeignKeyColumn)column.ParentTable.GetColumn(parentColumnName);

            // set current column to parent column
            column = filter.ParentColumn;
        }
        else
        {
            // this is the last parent and has
            // no parent itself so set to null
            filter.ParentColumn = null;
            column = null;
        }
        // add filter to list of filters
        filters.Add(i, filter);
    }

    // add dropdown list to page in correct order 2, 1, 0
    // last parent, parent<N>, child
    for (int i = parentColumns.Count - 1; i >= 0; i--)
    {
        // setup dropdown list
        filters[i].ListControl.Items.Clear();
        filters[i].ListControl.Items.Add(new ListItem("------", ""));

        // add parent list controls event handler
        if (i > 0)
            filters[i].ListControl.SelectedIndexChanged += ListControls_SelectedIndexChanged;

        // add control to place holder
        this.Controls.Add(filters[i].ListControl);
    }

    if (Mode == DataBoundControlMode.Insert)
    {
        // fill last parent filter
        var lastParentIndex = filters.Count - 1;
        var parentTable = filters[lastParentIndex].Column.ParentTable;
        var parentQuery = parentTable.GetQuery(context);

        // set next descendant list control
        PopulateListControl(lastParentIndex, parentQuery);
    }
}

Listing 2 -  Page_Init

The reason for using Page_Init (see Listing 2) is that we are going to create the dropdown lists dynamically, these need to be instantiated in the OnInit event to be fully involved in post back.

The first thing we need to do in the Page_Init is to make sure we have the correct Column Type (MetaForeignKeyColumn) and that the column has a CascadeHierarchicalAttribute assigned, if either of these in not present then we throw an error.

There are three for loops in the Page_Init, the first for loop we are building a list of parent columns to help with the next for loop which builds the filters list (by having two loops we make it easy to put the list controls onto the page in the most logical order, last parent to the left and child to the right).

Note: We also set each dropdown list’s SelectedIndexChanged event to post back to the same ListControls_SelectedIndexChanged handler.

This list of  filters is used throughout the rest of the field template to build each list control and made up of a dropdown list the current column and it’s parent column (see Listing 3).

/// <summary>
/// Class to contains information about cascading dropdown lists
/// </summary>
protected internal class HierachicalListControl
{
    /// <summary>
    /// Returns a <see cref="System.String"/> that represents this instance.
    /// </summary>
    /// <returns>
    /// A <see cref="System.String"/> that represents this instance.
    /// </returns>
    public override string ToString()
    {
        var parentColumn = ParentColumn != null ? ParentColumn.Name : "null";
        return String.Format("{0}.{1}", Column.Name, parentColumn);
        //return this.Column.Name;
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="HierachicalListControl"/> class.
    /// </summary>
    /// <param name="column">This column.</param>
    /// <param name="parentColumn">This column's parent column.</param>
    public HierachicalListControl(ListControl listControl)
    {
        ListControl = listControl;
    }

    /// <summary>
    /// Gets or sets the filter column.
    /// </summary>
    /// <value>The column.</value>
    public MetaForeignKeyColumn Column { get; set; }

    /// <summary>
    /// Gets or sets the filter column's parent column.
    /// </summary>
    /// <value>The parent column.</value>
    public MetaForeignKeyColumn ParentColumn { get; set; }

    /// <summary>
    /// Gets or sets the list control.
    /// </summary>
    /// <value>The list control.</value>
    public ListControl ListControl { get; set; }
}

Listing 3 – HierachicalListControl used in list of filters .

Note: I have overridden the ToString method to make the filters list more readable in debug mode, in Column.ParentColumn format, this is not required for the sample to function correctly.

The final for loop is use to initialise each dropdown list with it’s default item, hook-up the SelectionIndexChanged event and then add it to the page.

The final segment of code in Page_Init is the if statement for when we are in insert mode as the OnDataBound event will not fire, so we populate the last filter the (highest in the hierarchy).

protected void Page_Load(object sender, EventArgs e)
{
    if (filters[0] != null &&
        filters[0].ListControl != null)
    {
        RequiredFieldValidator1.ControlToValidate = "ListControl0";
        DynamicValidator1.ControlToValidate = "ListControl0";
        SetUpValidator(RequiredFieldValidator1);
        SetUpValidator(DynamicValidator1);
    }
}

Listing 4 – Page_Load event handler

The Page_Load event just makes sure that we have some filter[0] and then sets up the validators. The next event handler is the OnDataBinding handler here we make sure we are in Edit mode and that the current value is not null and then we call the main method for setting up the filters SetupListControls.

protected override void OnDataBinding(EventArgs e)
{
    base.OnDataBinding(e);

    // Set initial value
    if (Mode == DataBoundControlMode.Edit && FieldValue != null)
        PopulateAllListControls(FieldValue);
}

/// <summary>
/// Sets the default values.
/// </summary>
/// <param name="fieldValue">The value.</param>
private void PopulateAllListControls(object fieldValue)
{
    var displayStrings = new SortedList<int, String>();

    #region Get list of propert values
    // get property values
    var propertyValues = new SortedList<int, Object>();
    propertyValues.Add(0, fieldValue);
    for (int i = 0; i < filters.Count - 1; i++)
    {
        var parentName = filters[i].ParentColumn.Name;
        object pv = propertyValues[i].GetPropertyValue(parentName);
        propertyValues.Add(i + 1, pv);
    }
    #endregion

    // stating at the first filter and work way up to the last filter
    for (int i = 0; i < filters.Count; i++)
    {
        var parentTable = filters[i].Column.ParentTable;
        var parentQuery = parentTable.GetQuery(context);
        IQueryable listItemsQuery;
        if (i == cascadeHierarchicalAttribute.Parameters.Length)
        {
            listItemsQuery = parentQuery.GetQueryOrdered(parentTable);
        }
        else
        {
            var pcol = filters[i + 1].Column;
            var selectedValue = filters[i].ParentColumn.GetForeignKeyString(propertyValues[i]);
            listItemsQuery = parentQuery.GetQueryFilteredFkColumn(pcol, selectedValue);
        }

        // set next descendant list control
        PopulateListControl(i, listItemsQuery);

        // set initial values
        var selectedValueString = filters[i].Column.Table.GetPrimaryKeyString(propertyValues[i]);
        ListItem item = filters[i].ListControl.Items.FindByValue(selectedValueString);
        if (item != null)
            filters[i].ListControl.SelectedValue = selectedValueString;
    }
}

Listing 5 – OnDataBinding and PopulateAllListControls

The first thing SetupListControls does is get a list of actual values for each filter in the cascade of filters using fieldValue as a starting point, working back from the passed in fieldValue, it calls the method GetPropertyValue which uses reflection to get the value of the next entity using the current value and the parent column’s name. These values are in turn used to fill each filter with appropriately filtered values. The last parent getting an unfiltered list of items as it has no parent itself.

/// <summary>
/// Gets the property value.
/// </summary>
/// <param name="sourceObject">The source object.</param>
/// <param name="propertyName">Name of the property.</param>
/// <returns>The named properties value.</returns>
public static Object GetPropertyValue(this Object sourceObject, 
    string propertyName)
{
    if (sourceObject != null)
        return sourceObject.GetType()
            .GetProperty(propertyName)
            .GetValue(sourceObject, null);
    else
        return null;
}

Listing 6 – GetPropertyValue

The two extension methods used to get the list of items are called GetQueryOrdered Listing 7 or GetQueryFilteredFkColumn Listing 8.

/// <summary>
/// Gets the query ordered.
/// </summary>
/// <param name="sourceQuery">The source query.</param>
/// <param name="table">The table.</param>
/// <returns></returns>
public static IQueryable GetQueryOrdered(this IQueryable sourceQuery, MetaTable table)
{
    // get display column attribute
    var displayColumnAttribute = table.GetAttribute<DisplayColumnAttribute>();

    // check to see if sort is assigned
    if (displayColumnAttribute == null || displayColumnAttribute.SortColumn == null)
        return sourceQuery;

    // {row.OrderBy(row => row.Name)}
    var orderByCall = GetOrderByCallExpression(
        sourceQuery, 
        table, 
        displayColumnAttribute.SortColumn, false);

    // create and return query
    return sourceQuery.Provider.CreateQuery(orderByCall);
}

Listing 7 – GetQueryOrdered

/// <summary>
/// Gets a list of entities from the source IQueryable 
/// filtered by the MetaForeignKeyColumn's selected value
/// </summary>
/// <param name="sourceQuery">The query to filter</param>
/// <param name="fkColumn">The column to filter the query on</param>
/// <param name="fkSelectedValue">The value to filter the query by</param>
/// <returns>
/// An IQueryable of the based on the source query 
/// filtered but the FK column and value passed in.
/// </returns>
public static IQueryable GetQueryFilteredFkColumn(this IQueryable sourceQuery, 
    MetaForeignKeyColumn fkColumn, String fkSelectedValue)
{
    // if no filter value return the query
    if (String.IsNullOrEmpty(fkSelectedValue))
        return sourceQuery;

    // order query
    sourceQuery = GetQueryOrdered(sourceQuery, fkColumn.Table);

    // {RequiredPlots}
    var parameterExpression = Expression.Parameter(sourceQuery.ElementType, fkColumn.Table.Name);

    // {(RequiredPlots.Builders.Id = 1)}
    var body = BuildWhereClause(fkColumn, parameterExpression, fkSelectedValue);

    // {RequiredPlots => (RequiredPlots.Builders.Id = 1)}
    var whereLambda = Expression.Lambda(body, parameterExpression);

    // {Developers.Where(RequiredPlots => (RequiredPlots.Builders.Id = 1))}
    var whereCall = Expression.Call(typeof(Queryable),
        "Where",
        new Type[] { sourceQuery.ElementType },
        sourceQuery.Expression,
        Expression.Quote(whereLambda));

    // create and return query
    return sourceQuery.Provider.CreateQuery(whereCall);
}

Listing 8 – GetQueryFilteredFkColumn

Listings 7 & 8 are form the IQueriableExtensionMethods.cs class file in the NotAClue.Web.DynamicData class library, and are a part of my dynamic Linq Expression extension methods that originally came from the ASP.NET July 2007 Futures Source Code sample on ASP.Net on CodePlex.com, I continue to expand and modify them as I need to; so I wont go into detail here as there better posts on Linq Expression than any I could write.

Note: There is a useful sample in the C# samples see Dynamic LINQ (Part 1: Using the LINQ Dynamic Query Library on Scott Guthrie's blog, “Dynamic Linq” allows you to easily build Linq queries on the fly.
/// <summary>
/// Handles the SelectedIndexChanged event of the parentListControls control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
/// <summary>
/// Setups the parent list control.
/// </summary>
/// <param name="table">The table.</param>
/// <param name="filterIndex">The parent id.</param>
/// <param name="items">The items.</param>
public void PopulateListControl(int filterIndex, IQueryable items)
{
    // clear the list controls list property
    filters[filterIndex].ListControl.Items.Clear();
    // enable list control
    filters[filterIndex].ListControl.Enabled = true;

    // add unselected value showing the column name
    // [Styles]
    filters[filterIndex].ListControl.Items.Add(
        new ListItem(String.Format("[{0}]", 
            filters[filterIndex].Column.DisplayName), ""));

    foreach (var row in items)
    {
        // populate each item with the display string and key value
        filters[filterIndex].ListControl.Items.Add(
            new ListItem(filters[filterIndex].Column.ParentTable.GetDisplayString(row),
            filters[filterIndex].Column.ParentTable.GetPrimaryKeyString(row)));
    }
}

Listing 9 – PopulateListControl

Once we have the query filtered and ordered we then set the list control’s items up by calling PopulateListControl (Listing 9), then finally we set the initial value in of the list control.

/// <summary>
/// Handles the SelectedIndexChanged event for each List control, 
/// and populates the next list control in the hierarchy.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">
/// The <see cref="System.EventArgs"/> instance containing the event data.
/// </param>
void ListControls_SelectedIndexChanged(object sender, EventArgs e)
{
    // get list control
    var listControl = (ListControl)sender;

    // get the sending list controls id as an int
    var id = ((Control)sender).ID;

    // use regular expression to find list control index
    var regEx = new Regex(@"\d+");
    var parentIndex = int.Parse(regEx.Match(id).Value);

    if (!String.IsNullOrEmpty(listControl.SelectedValue))
    {
        if (parentIndex > 0)
        {
            // set child index
            var childIndex = parentIndex - 1;

            // get parent table
            var parentTable = filters[childIndex].Column.ParentTable;

            // get query from table
            var query = parentTable.GetQuery(context);

            // get items for list control
            var itemQuery = query.GetQueryFilteredFkColumn(
                filters[parentIndex].Column,
                listControl.SelectedValue);

            // populate list control
            PopulateListControl(childIndex, itemQuery);

            // reset all descendant list controls
            ResetAllDescendantListControls(childIndex);
        }
    }
    else
    {
        // reset all descendant list controls
        ResetAllDescendantListControls(parentIndex);
    }
}

Listing 10 – parentListControls_SelectedIndexChanged

The next thing we have to deal with is SelectedIndexChanged event on each dynamically created filter, we do this in the ListControls_SelectedIndexChanged handler (Listing 10) we first get the posting list control and then extract the filter index from it’s name. Remember all the filters are named ListControl{N} where N is the index into the filters list for the filter.

Note: We use a regular expression “\d+” to match one or more digits in the list control’s ID

Then we check to see if the list control has a SelectedValue if not we reset it and all it’s children to the default of "----" and Enabled to false. If it has a SelectedValue then we get a query on the child column filtered by the current SelectedValue and populate the next list control in line.

We then call SetupParentListControl to populate the control and finally ResetAllDescendantListControls to set any descendant controls to their default value of "----" and disabled.

The last two methods as two of the standard field template methods just slightly change to avoid exceptions seen here in Listings 11.

protected override void ExtractValues(IOrderedDictionary dictionary)
{
    // If it's an empty string, change it to null
    string value;
    if (filters[0] != null && filters[0].ListControl != null)
        value = filters[0].ListControl.SelectedValue;
    else
        value = String.Empty;

    if (String.IsNullOrEmpty(value))
        value = null;

    ExtractForeignKey(dictionary, value);
}

public override Control DataControl
{
    get
    {
        if (filters[0] != null && filters[0].ListControl != null)
            return filters[0].ListControl;
        else
            return null;
    }
}

Listing 11 – ExtractValues method and DataControl property.

These two methods merely check to see if there are nay controls before processing.

Download

The sample is a Visual Studio 2010 and .Net 4.0 sample but the code and field template should work with Visual Studio 2008 SP1 and .Net 3.5 SP1 DD Web Application.

Note: The script for creating the Vehicles database is in the zip file.