Wednesday 8 December 2010

Filter History for Dynamic Data 4

I did a AJAX History in a Dynamic Data Website article back in February 2009 to make filter respect the back button in the browser, this however was not enough for my customers they wanted the filter to be remembered even when they came back to the page without using the back button.

So I decided to store the filter info in Session state for this sample sample (but you could also use a database I have where the client wanted the filters to be remembered across sessions and by user).

I’m adding this extension to the previous sample from Five Cool Filters for Dynamic Data 4 (which now has a couple more filters added) to give history across all the filters.

Extension Methods

So to the code there are two extension methods for this sample AddFilterValueToSession which you can guess adds a filters current value to session and GetFilterValuesFromSession which retrieves an Dictionary of values for the specified table.

/// <summary>
/// Adds the filters current value to the session.
/// </summary>
/// <param name="page">The page.</param>
/// <param name="column">The column.</param>
/// <param name="value">The value.</param>
public static void AddFilterValueToSession(this Page page, MetaColumn column, Object value)
{
    Dictionary<String, Object> filterValues;
    var objectName = column.Table.DataContextPropertyName;

    // get correct column name 
    var columnName = column.Name;
    if (column is MetaForeignKeyColumn)
        columnName = ((MetaForeignKeyColumn)column).ForeignKeyNames.ToCommaSeparatedString();

    // check to see if we already have a session object
    if (page.Session[objectName] != null)
        filterValues = (Dictionary<String, Object>)page.Session[objectName];
    else
        filterValues = new Dictionary<String, Object>();

    // add new filter value to session object
    if (filterValues.Keys.Contains(columnName))
        filterValues[columnName] = value;
    else
        filterValues.Add(columnName, value);

    // add back to session
    if (page.Session[objectName] != null)
        page.Session[objectName] = filterValues;
    else
        page.Session.Add(objectName, filterValues);
}

Listing 1 – AddFilterValueToSession

If you follow the code and comments you will see that first we try to get the session object for this table from session and if it is not there we simply create one. Then we add/update the value for this column and finally save back to session.

Note: that Listing 1 uses an extension method ToCommaSeperatedString() this just simplifies the code for us by removing reusable code.
Updated: As ValZ posted in the comments I have updated the GetFilterValuesFromSession extension method to make sure any links from other List pages are honoured, for instance if you go to the Suppliers page and click on View Products link but already have a filter value that will now be honoured. I’ve also used IDictionary so we don’t have to cast the default values that are passed in.
/// <summary>
/// Gets the filter values from session.
/// </summary>
/// <param name="page">The page.</param>
/// <param name="table">The table.</param>
/// <param name="defaultValues">The default values.</param>
/// <returns>An IDictionary of filter values from the session.</returns>
public static IDictionary<String, Object> GetFilterValuesFromSession(this Page page, 
    MetaTable table, 
    IDictionary<String, Object> defaultValues)
{
    var queryString = new StringBuilder();
    var objectName = table.DataContextPropertyName;
    if (page.Session[objectName] != null)
    {
        var sessionFilterValues = new Dictionary<String, Object>((Dictionary<String, Object>)page.Session[objectName]);
        foreach (string key in defaultValues.Keys)
        {
            if (!sessionFilterValues.Keys.Contains(key) || sessionFilterValues[key] == null)
                sessionFilterValues.Add(key, defaultValues[key]);
            else
                sessionFilterValues[key] = defaultValues[key];
        }
        var t = (Dictionary<String, Object>)page.Session[objectName];
        return sessionFilterValues;
    }
    else
        return defaultValues;
}

Listing 2 – GetFilterValuesFromSession

In Listing 2 we simply get the Dictionary object back from session and then return it.

Applying to the Filters

Listing 3 is a for the ForeignKey filter, here call the AddFilterValueToSession extension method passing in the Column and value from the DropdownList.

protected void DropDownList1_SelectedIndexChanged(object sender, EventArgs e)
{
    OnFilterChanged();
    Page.AddFilterValueToSession(Column, DropDownList1.SelectedValue);
}

Listing 3 – ForeignKey filter

Every filter in that sample has this applied and there are several different examples in there.

Extracting the Filter Values

Now all we need is to get the values and pass them back to the filters each time we arrive at a List page. In Listing 4 I have updated the Page_Init method,  I first attempt to get the filter values from session if none are found I fall back to default List page behaviour.

protected void Page_Init(object sender, EventArgs e)
{
    table = DynamicDataRouteHandler.GetRequestMetaTable(Context);

    var defaultValues = Page.GetFilterValuesFromSession(table, table.GetColumnValuesFromRoute(Context));
    GridView1.SetMetaTable(table, defaultValues);

    GridDataSource.EntityTypeFilter = table.EntityType.Name;
}

Listing 4 – Updating the List page

Now if we switch back to the ForeignKey filter and the Page_Init method Listing 5 you will see that I have not had to change anything here, because we are adding the default values in the List page they will arrive in each filter via the DefaultValue property.

protected void Page_Init(object sender, EventArgs e)
{
    if (!Page.IsPostBack)
    {
        if (!Column.IsRequired)
        {
            DropDownList1.Items.Add(new ListItem("[Not Set]", NullValueString));
        }
        PopulateListControl(DropDownList1);

        // Set the initial value if there is one
        string initialValue = DefaultValue;
        if (!String.IsNullOrEmpty(initialValue))
        {
            DropDownList1.SelectedValue = initialValue;
            // optional add auto filters to history
            Page.AddFilterValueToSession(Column, initialValue);
        }
    }
}

Listing 5 – updating the filter

Updated: I’ve also added a optional Page.AddFilterValueToSession(Column, initialValue); to each filter so that filter values passed in in query string can be added to history, NOTE: I’ve only added this to the ForeignKey filter so it’s easy to remove if not required.

I’ll show one complex example of setting up the values in a filter, we will look at me DateRange filter for this. First look at Listing 6 here I am concatenating both values together separated by ‘|’.

protected void btnRangeButton_Click(object sender, EventArgs e)
{
    var button = (Button)sender;
    if (button.ID == btnClear.ID)
    {
        txbDateFrom.Text = String.Empty;
        txbDateTo.Text = String.Empty;
    }
    OnFilterChanged();
    Page.AddFilterValueToSession(Column, String.Format("{0}|{1}", txbDateFrom.Text, txbDateTo.Text));
}

Listing 6 – DateRange button click event

Then if you look at the DateRange filters Page_Init method all need to do is check I have a value, reveres the concatenation and put the values in the correct textboxes.

protected void Page_Init(object sender, EventArgs e)
{
    // set correct date time format
    txbDateFrom_CalendarExtender.Format = DATE_FORMAT;
    txbDateTo_CalendarExtender.Format = DATE_FORMAT;

    if (!Column.ColumnType.Equals(typeof(DateTime)))
        throw new InvalidOperationException(String.Format("A date range filter was loaded for column '{0}' but the column has an incompatible type '{1}'.",
            Column.Name, Column.ColumnType));

    if (DefaultValue != null)
    {
        var values = DefaultValue.Split(new char[] { '|' });
        if (values.Length == 2)
        {
            txbDateFrom.Text = values[0];
            txbDateTo.Text = values[1];
        }
    }
}

Listing 7 – DateRange Page_Init method

Updated: Add clear filters button to clear all filters for a table.

Clear Filters

Last we add a button to clear the filters

<div class="DD">
    <asp:ValidationSummary ID="ValidationSummary1" runat="server" EnableClientScript="true"
        HeaderText="List of validation errors" CssClass="DDValidator" />
    <asp:DynamicValidator runat="server" ID="GridViewValidator" ControlToValidate="GridView1"
        Display="None" CssClass="DDValidator" />
    <asp:QueryableFilterRepeater runat="server" ID="FilterRepeater">
        <ItemTemplate>
            <asp:Label runat="server" Text='<%# Eval("DisplayName") %>' OnPreRender="Label_PreRender" />
            <asp:DynamicFilter runat="server" ID="DynamicFilter" OnFilterChanged="DynamicFilter_FilterChanged" />
            <br />
        </ItemTemplate>
    </asp:QueryableFilterRepeater>
    <asp:Button 
        ID="ClearFiltersButton" 
        runat="server" 
        Text="Clear Filters" 
        CssClass="DDControl"
        OnClick="ClearFiltersButton_Click" />
    <br />
</div>

Listing 8 – Clear filters button

and in the post back of the button

protected void ClearFiltersButton_Click(object sender, EventArgs e)
{
    Page.ClearTableFilters(table);
    Response.Redirect(table.ListActionPath);
}

Listing 9 – Clears filters button method

and lastly we need the method the clears the filters for us,

public static void ClearTableFilters(this Page page, MetaTable table)
{
    var objectName = table.DataContextPropertyName;

    if (page.Session[objectName] != null)
        page.Session[objectName] = null;
}

Listing 10 – Clear filters extension method

Lastly we will need to hide the button when there are no filters.

protected override void OnPreRenderComplete(EventArgs e)
{
    RouteValueDictionary routeValues = new RouteValueDictionary(GridView1.GetDefaultValues());
    InsertHyperLink.NavigateUrl = table.GetActionPath(PageAction.Insert, routeValues);
    if (FilterRepeater.Controls.Count == 0)
        ClearFiltersButton.Visible = false;
    base.OnPreRenderComplete(e);
}

Listing 11 – hiding the clear filters button

So then all done then.

Download

Friday 26 November 2010

BLACK FRIDAY/CYBER MONDAY DEAL:

Note  I don’t normally post adds for products but this is really easy to create icons I’m always creating favicons for my sites and it make the task real easy.

sale

All Axialis products are available at 50 percent Off. Starting Black Friday, this offer ends Nov 30. This is for 5 days, and 5 days only. Half price but full service! You get the same Axialis advantages: professionally-designed products, LIFETIME LICENCE! (never pay for future updates), access to thousands of ready-to-use image object packs, and more...

So don't miss this opportunity! ACT NOW and get 50 percent off your total order!

IconWorkshop 6.53 Released:

IconWorkshop

This minor release includes various bug fixes and enhancements:

  • Compatible with Visual Studio 2010: The installer detects and add the IconWorkshop add-in in Visual Studio 2010.
  • Ability to create icons for iPhone 4: This new version includes new image presets to create those icons.
  • New Object Packs: Two new object packs are provided to create iPhone icons (Applications and Tab Bar Icons).
  • Various other enhancements and bug fixes.

Registered users can freely upgrade to this latest version using the built-in update feature.

Sunday 21 November 2010

Five Cool Filters for Dynamic Data 4

Starting with the Range Autocomplete, Range & MultiForeignKey Filters from the old Dynamic Data Preview 4 Refresh, I have also added five of my filters:

  1. DateRange
  2. StartsWith
  3. EndsWith
  4. Contains
  5. Where

I won’t go into much detail here as Oleg Sych has done this great series on DD4 filters, that pretty much explains it all for you.

I am using the Ajax Control Toolkit for the date picker in the DateRange filter but other than that it’s not much different form the Range filter from the Dynamic Data Preview 4 Refresh sample.

!Important: One thing I would like to point out is that both Entity Framework and Linq to SQL have a small but important issue. This issue is the the “Contains” Expression used in the above filter of the same name is actually expressed as the “Like ‘%{0}%’” and so you DO NOT get full text search using “Contains”.

“Contains” when it was introduced in SQL Server 7 was given a big build up and to be omitted in EF and L2S is a big let down.

Some examples:

Other Filters

Figure 1 – DateRange, StartsWith, EndsWith, Contains, Where and AutoComplete filters

Range and Multi Select Filters

Figure 2 – Range and Multi Select Filters

I just thought that these filters would be useful for your tool boxes.

!Important: I can’t take credit for all the Expression helpers most come from the above mentioned Preview 4 that is no longer on the ASP.Net Codeplex site.
Download

Note I’ve included my five filters and the three filters from the Preview.

As always happy coding.

Sunday 14 November 2010

Making the Text in the DynamicData List Pages GridView Wrap.

Why won’t the text in my Dynamic Data List page GridView wrap? This is answered here and in this post on the ASP.NET Dynamic Data Forum here Re: DynamicField within GridView truncating data where Yuipcheng asks;

“I commented out the code to truncate the FieldValueString, but the rendered text doesn't wrap since the td tag has (style="white-space: nowrap;")”

I answered here but I thought I would document it here also.

Before After
Before After

So you may be asking Dynamic Data 1 didn’t do this, so why does Dynamic Data 4 do it?

The answer it has to do with the validation “*”  showing up next to the field in Edit mode and appears to have just gotten into the GridView as by default we don't do inline editing.

The solution from David Fowler is to create an class and inherit from System.Web.DynamicData.DefaultAutoFieldGenerator and then override the CreateField method, so here it is:

/// <summary>
/// Add the option to the Default Auto Field Generator 
/// to not add the (Style="white-space: nowrap;") to each field.
/// </summary>
public class AdvancedAutoFieldGenerator : DefaultAutoFieldGenerator
{
    private Boolean _noWrap;

    /// <summary>
    /// Initializes a new instance of the 
    /// <see cref="AdvancedAutoFieldGenerator"/> class.
    /// </summary>
    /// <param name="table">The table.</param>
    /// <param name="noWrap">if set to <c>true</c> 
    /// the (Style="white-space: nowrap;") is added 
    /// to each field.</param>
    public AdvancedAutoFieldGenerator(MetaTable table, Boolean noWrap)
        : base(table)
    {
        _noWrap = noWrap;
    }

    protected override DynamicField CreateField(
        MetaColumn column, 
        ContainerType containerType, 
        DataBoundControlMode mode)
    {
        DynamicField field = base.CreateField(column, containerType, mode)

        // override the wrap behavior
        // with the noWrap parameter
        field.ItemStyle.Wrap = !_noWrap;
        return field;
    }
}

Listing 1 – Advanced Field Generator

and we apply it in the Page_Load event of the List page (or you own custom page)

// apply custom auto field generator
GridView1.ColumnsGenerator = new AdvancedAutoFieldGenerator(table, false);

This will then remove the unwanted style,

Note: If you still want this on some pages I have not hard coded it, so you can change it in code where you want to.

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.