Nov 29 2011

Advanced Data Filtering Techniques in the Mvc Controls Toolkit

Category: MVC | Entity Framework | Asp.netFrancesco @ 04:04

Mvc Controls Toolkit Datagrid Updated Tutorial

Data Filtering, in the New Mvc 3 Version of the Mvc Controls Toolkit 

The Mvc Controls Toolkit has the ability to bind a ViewModel property to a LinQ expression that defines a Data Filtering criterion. As already pointed out in a previous post this capability enhances modularity by allowing the developer to change the filtering options without modifying the ViewModel or the controller. In fact the whole filtering logic can be encapsulated in a single module that communicates with the remainder of the system just through that LinQ expression.

In this post I show how this feature may improve also the user experience by enabling the web site user to define his filtering criteria at run-.time. Better options are available in the commercial version of the Mvc Control Toolkit

The DataFilterClauseFor HtmlHelper extension method defines a single clause of a filering LinQ expression, where a clause is a simple constraint (Less Than, Equal, StartSWith, and so on) on a column. The Web Site user can enable or disable each clause,  can choose the operator(Less Than, Equal, StartSWith, and so on) to apply and can supply the required parameters with customizable UI. All enabled clauses defined by several DataFilterClausesFor helpers are put together with a logical and, thus defining a complex filtering criterion.

The DataFilterClauseFor extension method has the following parameters:

this HtmlHelper<VM> htmlHelper,
            Expression<Func<VM, Expression<Func<T, bool>>>> filter,
            Expression<Func<T, F>> field,
            string filterName="",
            FilterCondition initialCondition = FilterCondition.Equal

Where:

  • filter is the lambda expression defining the property of the ViewModel that receives the final LinQ expression.
  • field is the lambda expression defining the data item column to be used in the filter clause. The data item type T determines the IQueryable<T> or IEnumerable<T> that filter can be applied to. Please note, T is not necesarilly a DTO type! Infact, if the application is well Layered any IQueryable coming from the DataLayer should have been converted into an IQueryable based on a Business Layer or Presentation Layer class.before reaching the controller. We can convert an IQueryable<S> OriginalQ into an IQueryable<T> ModifiedIQ  with a simple LinQ statement like this one:  from x in OriginalIQ select new T {Property1=x.Property1,……}.
  • filterName, is a name that is needed to identify univocally a clause in case there are several clauses involving the same column.
  • initilalCondition is the filtering opearion that is initially chosen for the clause. If needed this value can be changed by the user through customizable UI.

The DataFilterClauseFor extension method returns an HtmlHelper object we can use to render the UI that allows the user to select both the operation of the filter clause, and its parameter.

For instance if we would like to apply a filtering on the Name property of the ToDoView class of my previous post example Mvc Controls Toolkit Datagrid Updated Tutorial, we can write:

<div >
            @{var hName = Html.DataFilterClauseFor(
                  m => m.ToDoFilter, f => f.Name,
                  "byNameFilter",
                  MVCControlsToolkit.Linq.FilterCondition.StartsWith);}
            @hName.CheckBoxFor(m => m.Selected, new { @class = "byNameFilter_checkbox" })
             &nbsp; Filter by name
            <span  class="byNameFilter">
                @hName.FilterClauseSelect(
                    hName.ViewData.Model.Condition,
                    (Expression<Func<ToDoView, string>>)(m=>m.Name))
                @hName.TypedTextBoxFor(m => m.Search, watermarkCss: "watermark")
            </span>  
            @hName.ViewsOnOff("byNameFilter", false)
        </div>

First we call the DataFilterClauseFor extension method to get the HtmlHelper to render our UI. Then  we render a checkbox that enables/disables the filter clause by changing the value of the Selected property.

The Condition property of the hName HtmlHelper model contains the operation selected by the user. Initially it is set to StartsWith since we passed MVCControlsToolkit.Linq.FilterCondition.StartsWith as last parameter of the DataFilterClauseFor extension method. We render this property with the FilterClauseSelect extension method whose oiutput is a select that lists all filtering operations. The names of all operations can be localized and customized with the help of resources files as detailed in the documantation. The FilterClauseSelect extension method has also an optional parameter to specify which filtering operations to list in the select as shown in the example below:

@hName.FilterClauseSelect(
                    hName.ViewData.Model.Condition,
             (Expression<Func<ToDoView, string>>)(m => m.Name),
             conditions: FilterCondition.StartsWith | FilterCondition.Equal)

Finally, the Search property contains the clause filter parameter. We render this property with a TypedTextBox.

The ViewOnOff extension method makes the content of the span with Css class “ByNameFilter” to appears just when the CheckBox is checked. The initial state of the content (visible or not) is defined by the second parameter of the ViewOnOff extension method, whose value is false in our case . When the content is not visible it is completely detached from the DOM to avoid that client side validation logics might prevent the submit of the form (invisible fields are validated, while fileds detached from the DOM are not).

Further clauses may be added in a similar way. For instance, if we would like to add two more optional clauses do define a range of dates we can write:

<div>
            @{var hFromDate = Html.DataFilterClauseFor(m => m.ToDoFilter, f => f.DueDate, "fromDateFilter");}
            @hFromDate.CheckBoxFor(m => m.Selected, new { @class = "fromDateFilter_checkbox" })
             &nbsp; From:
            <span class="fromDateFilter">
                @{hFromDate.ViewData.Model.Condition = MVCControlsToolkit.Linq.FilterCondition.GreaterThanOrEqual;}
                @hFromDate.HiddenFor(m => m.Condition)
                @hFromDate.DateTimeFor(m => m.Search, DateTime.Today.AddMonths(-6), true)
                    .DateCalendar(inLine: false, calendarOptions: new CalendarOptions
                       {
                           ChangeYear = true,
                           ChangeMonth = true,
                       })
               
            </span>
            @hFromDate.ViewsOnOff("fromDateFilter", false)
        </div>
        <div>
            @{var hToDate = Html.DataFilterClauseFor(m => m.ToDoFilter, f => f.DueDate, "toDateFilter");}
            @hToDate.CheckBoxFor(m => m.Selected, new { @class = "toDateFilter_checkbox" })
             &nbsp; To:
            <span class="toDateFilter">
                @{hToDate.ViewData.Model.Condition = MVCControlsToolkit.Linq.FilterCondition.LessThanOrEqual;}
                @hToDate.HiddenFor(m => m.Condition)
                @hToDate.DateTimeFor(m => m.Search, DateTime.Today.AddMonths(6), true)
                    .DateCalendar(inLine: false, calendarOptions: new CalendarOptions
                       {
                           ChangeYear = true,
                           ChangeMonth = true,
                       })
            </span>
             @hToDate.ViewsOnOff("toDateFilter", false)
        </div>

Since in this case the user cannot choose the filtering operations on the DueDate field we store it the pre-defined values  MVCControlsToolkit.Linq.FilterCondition.GreaterThanOrEqual and MVCControlsToolkit.Linq.FilterCondition.LessThanOrEqual are stored into hidden fields.

It is worth pointing out that all filtering information come from the client, therefore a malicious user might try a denial of service attack by sending a manipulated filtering request involving  columns that are too difficult to filter (columns with no indexes defined on them, for instance). In order to defend ourselves from such an attack the Transformation Handler that receives the filtering data automatically discards columns that are not decorated with the CanSortAttribute that is defined in theMVCControlsToolkit.DataAnnotations namespace.

If we would like to allow just some filtering operation on a column, we can specify them in the CanSortAttribute as detailed in the documentation. We can use the CanSortAttribute.AllowedForProperty static method to get all filtering operations allowed on a property as shown in the examples below:

@hName.FilterClauseSelect(
                    hName.ViewData.Model.Condition,
             (Expression<Func<ToDoView, string>>)(m => m.Name),
             conditions: CanSortAttribute.AllowedForProperty(typeof(ToDoView), "Name"))

 

@hName.FilterClauseSelect(
     hName.ViewData.Model.Condition,
     (Expression<Func<ToDoView, string>>)(m => m.Name),
     conditions: CanSortAttribute.AllowedForProperty(
        FilterCondition.Equal |
        FilterCondition.StartsWith |
        FilterCondition.GreaterThanOrEqual |  
        FilterCondition.LessThanOrEqual,                             
        typeof(ToDoView), "Name"))

Summing up, with the DataFilterClauseFor extension method we can define filtering criteria as the logical and of simple clauses allowing the web site user to choose the operation to perform in each clause.

When the filtering criterion cannot be expressed as a logical and of clauses, or when the operation to be performed by each clause is pre-defined we can use the DataFilterBuilder extension method.

The DataFilterBuilder extension method returns an HtmlHelper to build the whole filter. THe definition of the filter expression is provided by passing it an implementation of the IFilterDescription<T> interface:

public interface IFilterDescription<TData>
{
    Expression<Func<TData, bool>> GetExpression();
}

The only member to implent is GetExpression that returns the LinQ expression already filled with the user input. Below an implementation of IFilterDescription<ToDoView> interface for the same example we already implemented with the DataFilterClauseFor extension method:

public class ToDoGeneralFilter :
        IFilterDescription<ToDoView>
    {
        [Display(Prompt = "chars the name of item starts with")]
        public string Name { get; set; }
        public bool NameEnabled { get; set; }
        [Display(Name = "From Date")]
        public DateTime FromDate { get; set; }
        public bool FromDateEnabled { get; set; }
        [Display(Name = "To Date")]
        public DateTime ToDate { get; set; }
        public bool ToDateEnabled { get; set; }
        public FilterCondition NameContition { get; set; }
        public ToDoGeneralFilter()
        {
            NameContition=FilterCondition.StartsWith;
        }
        public System.Linq.Expressions.Expression<Func<ToDoView, bool>> GetExpression()
        {
            System.Linq.Expressions.Expression<Func<ToDoView, bool>> res = new FilterBuilder<ToDoView>(FilterLogicalOperator.And)
                .Add(NameEnabled && (!string.IsNullOrWhiteSpace(Name)), NameContition, m => m.Name, Name)
                .Add(FromDateEnabled, m => m.DueDate >= FromDate)
                .Add(ToDateEnabled, m => m.DueDate <= ToDate)
                .Get();
            return res;
        }
    }

The public properties of the ToDoGeneralFilter class are filled by the user with the input fileds rendered by the HtmlHelper returned by the DataFilterBuilder extension method. Then, they are used to build the LinQ expression with the help of the FilterBuilder class. The first Add method adds the Name filter by taking the filter condition from the NameCondition public property. Then, the other two clauses are added with a different overload of the Add method that accepts a LinQ expression since the data filter operation is predefined.

All LinQ expressions supplied by the three Add calls are combined with a logical and, since we specified a logical and in as value of the parameter of the FilterBuilder constructor.

Below the code to render the input fields of the ToDoGeneralFilter class:

@{var filter = Html.DataFilterBuilder(m => m.ToDoFilter, new ToDoGeneralFilter());}
             <div>
              
              @filter.CheckBoxFor(m => m.NameEnabled,
                new Dictionary<string, object>() { { "class", "NameGroup_checkbox" } })
              @filter.LabelFor(m => m.Name)
              @filter.FilterClauseSelect(
                filter.ViewData.Model.NameContition,
                     (Expression<Func<ToDoView, string>>)(m => m.Name),
                     conditions: CanSortAttribute.AllowedForProperty(
                        FilterCondition.Equal |
                        FilterCondition.StartsWith |
                        FilterCondition.GreaterThanOrEqual |  
                        FilterCondition.LessThanOrEqual,                             
                        typeof(ToDoView), "Name"))
              @filter.TypedTextBoxFor(m => m.Name,
                new Dictionary<string, object> (){{"class", "NameGroup"}},
                watermarkCss: "watermark")
                @filter.ViewsOnOff("NameGroup", false)
             </div>
             <div >
                
                @filter.CheckBoxFor(m => m.FromDateEnabled,
                    new Dictionary<string, object>() { { "class", "FromGroup_checkbox" } })
                @filter.LabelFor(m => m.FromDate)
                
             
                 <span class ="FromGroup" >
                        @filter.DateTimeFor(m => m.FromDate,
                            DateTime.Now.AddMonths(-6),
                            true).DateCalendar(inLine:false, calendarOptions:
                                new CalendarOptions{
                                       ChangeYear = true,
                                       ChangeMonth =true
                                })
                 </span>     
                 @filter.ViewsOnOff("FromGroup", false)
             </div>
             
             <div>
                 
                 @filter.CheckBoxFor(m => m.ToDateEnabled,
                    new Dictionary<string, object>() { { "class", "ToGroup_checkbox" } })
                 @filter.LabelFor(m => m.ToDate)
                 
             
                <span class ="ToGroup" >
                         @filter.DateTimeFor(m => m.ToDate,
                                DateTime.Now.AddMonths(6),
                                true).DateCalendar(inLine: false, calendarOptions:
                                    new CalendarOptions{
                                           ChangeYear = true,
                                           ChangeMonth =true
                                    })
                    
                 </span>  
                 @filter.ViewsOnOff("ToGroup", false)
             </div>

Complex expression trees can be obtained by using the Open and Close methods of the FilterBuilder object that open and close brackets in the expression being built. A parameter of the Open method specifies the logical operator for combining all expressions within the brackets. Both the Open and the Close methods have a boolean parameter to enable or disable them. Since brackets must balance two corresponding brackets either are both enabled or both disabled. When both are disabled the expression they enclose is not inserted in the whole expression being built.

In the previous example suppose that we would like to provide two date ranges in logical or. Our GetExpression() method becomes:

public System.Linq.Expressions.Expression<Func<ToDoView, bool>> GetExpression()
        {
            System.Linq.Expressions.Expression<Func<ToDoView, bool>> res =
                new FilterBuilder<ToDoView>(FilterLogicalOperator.And)
                .Add(NameEnabled && (!string.IsNullOrWhiteSpace(Name)), NameContition, m => m.Name, Name)
                .Open(true, FilterLogicalOperator.Or)
                    .Open(true, FilterLogicalOperator.And)
                        .Add(FromDate1Enabled, m => m.DueDate >= FromDate1)
                        .Add(ToDate1Enabled, m => m.DueDate <= ToDate1)
                    .Close(true)
                    .Open(true, FilterLogicalOperator.And)
                        .Add(FromDate2Enabled, m => m.DueDate >= FromDate2)
                        .Add(ToDate2Enabled, m => m.DueDate <= ToDate2)
                    .Close(true)
                .Close(true)
                .Get();
            return res;
        }

While the code in the View becomes:

@{var filter = Html.DataFilterBuilder(m => m.ToDoFilter, new ToDoTwoDateRangesFilter());}
             <div>
              
              @filter.CheckBoxFor(m => m.NameEnabled,
                new Dictionary<string, object>() { { "class", "NameGroup_checkbox" } })
              @filter.LabelFor(m => m.Name)
              @filter.FilterClauseSelect(
                filter.ViewData.Model.NameContition,
                     (Expression<Func<ToDoView, string>>)(m => m.Name),
                     conditions: CanSortAttribute.AllowedForProperty(
                        FilterCondition.Equal |
                        FilterCondition.StartsWith |
                        FilterCondition.GreaterThanOrEqual |  
                        FilterCondition.LessThanOrEqual,                             
                        typeof(ToDoView), "Name"))
              @filter.TypedTextBoxFor(m => m.Name,
                new Dictionary<string, object> (){{"class", "NameGroup"}},
                watermarkCss: "watermark")
                @filter.ViewsOnOff("NameGroup", false)
             </div>
             <div >
                
                @filter.CheckBoxFor(m => m.FromDate1Enabled,
                    new Dictionary<string, object>() { { "class", "FromGroup1_checkbox" } })
                @filter.LabelFor(m => m.FromDate1)
                
             
                 <span class ="FromGroup1" >
                        @filter.DateTimeFor(m => m.FromDate1,
                            DateTime.Now.AddMonths(-6),
                            true).DateCalendar(inLine:false, calendarOptions:
                                new CalendarOptions{
                                       ChangeYear = true,
                                       ChangeMonth =true
                                })
                 </span>     
                 @filter.ViewsOnOff("FromGroup1", false)
             </div>
             
             <div>
                 
                 @filter.CheckBoxFor(m => m.ToDate1Enabled,
                    new Dictionary<string, object>() { { "class", "ToGroup1_checkbox" } })
                 @filter.LabelFor(m => m.ToDate1)
                 
             
                <span class ="ToGroup1" >
                         @filter.DateTimeFor(m => m.ToDate1,
                                DateTime.Now.AddMonths(6),
                                true).DateCalendar(inLine: false, calendarOptions:
                                    new CalendarOptions{
                                           ChangeYear = true,
                                           ChangeMonth =true
                                    })
                    
                 </span>  
                 @filter.ViewsOnOff("ToGroup1", false)
             </div>
             <div >
                
                @filter.CheckBoxFor(m => m.FromDate2Enabled,
                    new Dictionary<string, object>() { { "class", "FromGroup2_checkbox" } })
                @filter.LabelFor(m => m.FromDate2)
                
             
                 <span class ="FromGroup2" >
                        @filter.DateTimeFor(m => m.FromDate2,
                            DateTime.Now.AddMonths(-6),
                            true).DateCalendar(inLine:false, calendarOptions:
                                new CalendarOptions{
                                       ChangeYear = true,
                                       ChangeMonth =true
                                })
                 </span>     
                 @filter.ViewsOnOff("FromGroup2", false)
             </div>
             
             <div>
                 
                 @filter.CheckBoxFor(m => m.ToDate2Enabled,
                    new Dictionary<string, object>() { { "class", "ToGroup2_checkbox" } })
                 @filter.LabelFor(m => m.ToDate2)
                 
             
                <span class ="ToGroup2" >
                         @filter.DateTimeFor(m => m.ToDate2,
                                DateTime.Now.AddMonths(6),
                                true).DateCalendar(inLine: false, calendarOptions:
                                    new CalendarOptions{
                                           ChangeYear = true,
                                           ChangeMonth =true
                                    })
                    
                 </span>  
                 @filter.ViewsOnOff("ToGroup2", false)
             </div>

We can use simultaneously the DataFilterClauseFor and DataFilterBuilder extension methods. In such a case the clauses provided by all DataFilterClauseFor calls are added with a logical and to the single LinQ expression provided by the DataFilterBuilder call:

@{var filter = Html.DataFilterBuilder(m => m.ToDoFilter, new ToDoByDateFilter());}
     
      <div >
         
         @filter.CheckBoxFor(m => m.FromDateEnabled,
             new Dictionary<string, object>() { { "class", "FromGroup_checkbox" } })
         @filter.LabelFor(m => m.FromDate)
         
      
          <span class ="FromGroup" >
                 @filter.DateTimeFor(m => m.FromDate,
                     DateTime.Now.AddMonths(-6),
                     true).DateCalendar(inLine:false, calendarOptions:
                         new CalendarOptions{
                                ChangeYear = true,
                                ChangeMonth =true
                         })
          </span>     
          @filter.ViewsOnOff("FromGroup", false)
      </div>
      
      <div>
          
          @filter.CheckBoxFor(m => m.ToDateEnabled,
             new Dictionary<string, object>() { { "class", "ToGroup_checkbox" } })
         @filter.LabelFor(m => m.ToDate)
          
      
         <span class ="ToGroup" >
                  @filter.DateTimeFor(m => m.ToDate,
                         DateTime.Now.AddMonths(6),
                         true).DateCalendar(inLine: false, calendarOptions:
                             new CalendarOptions{
                                    ChangeYear = true,
                                    ChangeMonth =true
                             })
             
          </span>  
          @filter.ViewsOnOff("ToGroup", false)
      </div>

That’s all! The full code used in this tutorial can be found in the Mvc3 Razor – Filtering folder of the zipped file BasicTutorialsCode here 

Stay Tuned !

Francesco

Tags: , , , , ,

Comments