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

12 comments:

Anonymous said...

Hi, Steve! It is very elegant solution.
And, what you think about following changes.
Thanks!

ValZ


protected void Page_Init(object sender, EventArgs e)
{
table = DynamicDataRouteHandler.GetRequestMetaTable(Context);
var defaultValues = Page.GetFilterValuesFromSession(table, (Dictionary)table.GetColumnValuesFromRoute(Context));
GridView1.SetMetaTable(table, defaultValues);
GridDataSource.EntityTypeFilter = table.EntityType.Name;
}


public static Dictionary GetFilterValuesFromSession(this Page page, MetaTable table, Dictionary defaultValues)
{
var queryString = new StringBuilder();
var objectName = table.DataContextPropertyName;
Dictionary filterValues;
if (page.Session[objectName] != null)
{
filterValues = (Dictionary)page.Session[objectName];
foreach (string key in defaultValues.Keys)
{
if (filterValues[key] == null)
filterValues.Add(key, defaultValues[key]);
else
filterValues[key] = defaultValues[key];
}
return filterValues;
}
else
return defaultValues;
}

Steve said...

Hi ValZ, you solution is correct I have not considered integrating redirects from other ListPages with filter values. I will add to the sample well done.

Steve.

Steve said...

Hi ValZ, I've updated appropriatly.

Steve

Anonymous said...

Thanks Steve, your solution helped me... once again!
LB

beren said...

Great job Steve! These filters help me a lot.

Steve said...

I'm working on a big update and an oss project hope to be oput early 2012 :)

Steve

Anonymous said...

Hi

I like this implementation but I'm having one problem with it. If a user have selected some filters, goes into edit and then click on the back button in the browser, the previosly selected filters are not selected. However if I update with F5 the previously selected filters gets selected. Is there any awy to make the filters being selected when the user press on the browsers back button?

Steve said...

yes there is a version of that on my blog here AJAX History in a Dynamic Data Website have fun.

Steve

Anonymous said...

So I will have to combine them if I would like the browser back button support plus the ability to save the selected filters during the whole session?

Steve said...

Yes I would take the principals of one and apply them to the other.

Steve

Mackan said...

Hi

I'm trying to implement a multi select filter on a foreign key column. I have managed to accomplish this but but I don't know how to save and load the selected values. In your example you just save one value for each filter, is it possible to to the same with more values for the same column? Do you know how this could be accomplished? Is it possible to give more than one value to the defaultvalue parameter of the SetMetaTable method?

Steve said...

I have done this e-mail me and I will send you my MultiForeignKey Filter

Steve