Apr 24 2011

Designing a Themed Control in the Mvc Controls Toolkit

Category: MVCFrancesco @ 07:09

Mvc Controls Toolkit Datagrid Updated Tutorial (more recent version of Defining MVC Controls 2: Using the DataGrid)

Advanced Data Filtering Techniques in the Mvc Controls Toolkit

Defining MVC Controls 3: Datagrid, Sorting, and Master-Detail Views

All controls of the Mvc Controls Toolkit are designed to allow the maximum flexibility. For instance, the DataGrid is not table based but allows a generic template to be used as item template. Other controls,such as the DateTimeInput, and the DualSelectBox are composed of several parts that can be rendered separately. Features like, Sorting, Paging and Filtering are not included in controls like the DataGrid and the SortableList but are provided through separate helpers. Finally, all controls can be styled with Css. While this approach ensures an high level of flexibility, the time needed to set-up completely a control is higher compared to the time needed to setup a pre-configured control.

In order to promote re-usability of efforts made to set up a control, the Mvc Controls Toolkit allow the definition of Themes. Each Theme is composed of a Css enriched with an image folder, and of a set of Partial Views. Each Partial View features an already set-up control. While the layout and all parts each control is composed of are defined in the Partial Views, the Css define completely the style of all controls. Thus, the controls of each theme are ready to be used, with no further effort; we just need to pass data to them! Better configuration and style options are offered by the commercial version of the Mvc Controls Toolkit.

In this post I will show how to design a control to be inserted into a theme. I assume a basic knowledge of the Theming features of the Mvc Controls Toolkit. Therefore, readers are encouraged to read the documentation page about theming here before reading this post.

We will call “Test” the theme we are going to define, and we will analyze just how to design a Table based Themed Datagrid offering paging, sorting and column filtering services.,

As a first step we set-up our folders as shown below:

css_folder_treefolder_tree

Now let open the DataGrid.cshtml file. The Themed Datagrid is invoked by the helper:

  1. public static MvcHtmlString ThemedDataGridFor<M, TItem>(
  2.             this HtmlHelper<M> htmlHelper,
  3.             Expression<Func<M, List<Tracker<TItem>>>> expression,
  4.             GridFeatures gridFeatures,
  5.             Columns<TItem> fields,
  6.             Expression<Func<M, List<KeyValuePair<LambdaExpression, OrderType>>>> orderExpression = null,
  7.             Expression<Func<M, int>> page=null,
  8.             Expression<Func<M, int>> prevPage=null,
  9.             Expression<Func<M, int>> pageCount=null,
  10.             Expression<Func<M, Expression<Func<TItem, bool>>>> filter=null,
  11.             string title=null,
  12.             string name = "DataGrid"
  13.             )
  14.             where TItem:class, new()

expression defines the collection to be rendered in the Grid, filter will collect the filtering criteria (see here), orderExpression the sorting criteria(see here), and page, prevPage, and pageCount the paging (see here). gridFeatures is a bit flag enumeration that defines the desired features selected by the user of the Themed DataGrid:

[Flags]
    public enum GridFeatures {None = 0, Edit=1, Display=2, Insert=4, Delete=8, ResetRow=16, UndoEdit=32, InsertOne=64, Paging=128, Sorting=256, Filtering=512}

For more information see the Documentation page about theming.

While fields is a collection of column definitions. Each column contains the column features(if sorting, and filtering are allowed, and an optional column header to override the one provided through DataAnnotations), and possibly Edit and Display templates for the column. One can also define custom columns that are not connected with specific fields.For more information see the Documentation page about theming.

The ThemedDataGridFor helper just packs all information received into a GridDescription object defined as:

public class GridDescriptionBase
    {
        public GridFeatures Features { get; set; }
        public List<Column> Fields { get; set; }   
    }
    
    public class GridDescription:GridDescriptionBase
    {
        public dynamic ToShow {get; set;}
        public dynamic ToOrder { get; set; }
        public string Title { get; set; }
        public dynamic HtmlHelper { get; set; }
        public dynamic Page { get; set; }
        public dynamic PrevPage { get; set; }
        public dynamic PageCount { get; set; }
        public dynamic Filter { get; set; }

    }

Then it passes this object to our DataGrid.cshtml Partial View into: ViewData["ThemeParams"]

Thus our first step is to “unpack” this object:

@{
    var options = ViewData["ThemeParams"] as GridDescription;

    Func<dynamic, HelperResult> editTemplate = null;
    if ((options.Features & GridFeatures.Edit) == GridFeatures.Edit)
    {
        editTemplate = EditItem;
    }
    Func<dynamic, HelperResult> displayTemplate = DisplayItem;
    Func<dynamic, HelperResult> insertTemplate = null;
    if ((options.Features & GridFeatures.Insert) == GridFeatures.Insert)
    {
        insertTemplate = InsertItem;
    }
}

The above instruction just verifies if the Insert and Edit services are required, and if they are required assign the name of the Insert template(InsertItem), and of the Edit template(EditItem) to the variables inserItem, and editItem. The Display template, that is not optional, is always put into the displayItem variable.

Templates are defined through Razor helpers(see the documentation about templates) that use the columns information provided by the GridDescription class to render each single item.

Below the Insert template that just renders an insert button:

@helper InsertItem(dynamic htmlHelper)
    {
        var options = htmlHelper.ViewData["ThemeParams"] as GridDescription;
    int count = 0;
    bool hasCustomField = false;
    foreach (Column column in options.Fields)
    {
        if ((column.Features & FieldFeatures.Hidden) != FieldFeatures.Hidden)
        {
            count++;
        }
        else if (column.DispalyTemplate != null)
        {
            hasCustomField = true;
        }
    }
    if ((options.Features & GridFeatures.Edit) == GridFeatures.Edit
        || hasCustomField
        || (options.Features & GridFeatures.Delete) == GridFeatures.Delete) { count++; }
    <td colspan='@count' class="Theme-DataGrid-Column">
        @DataGridHelpers.ImgDataButton(
            htmlHelper, DataButtonType.Insert,
            Url.Content("~/Content/themes/test/images/add.jpg"), null)</td>
}

 

 

 

Let’s forget for just a minute about the hasCustomField variable, we will discuss about it when analyzing the Display template.The foreach loop just counts the number of columnsto be inserted in the colspan to format properly the table.If the grid is required to work also in Edit mode or if it is required to allow row deletes we adds one column more for the some command buttons. Columns marked as hidden are jumped since they contains just hidden field that will not be rendered together with the other columns.

The imgDataButton helper that renders the insert button takes an image form the Css that is part of the “Test” theme. Its is rendered in a quite strange way, namely,  it is not invoked as an extension method but just as a normal static method of the DataGridHelpers class. This is because we are using dynamic  variables(see the GridDescription definition) to handle Types that will be known just a run-time since our DataGrid is required to work with any class. In such a case extension methods cannot be used!

In the Display template we do some initialization and then we start a foreach loop on all columns:

@helper DisplayItem(dynamic htmlHelper)
    {
        var options = htmlHelper.ViewData["ThemeParams"] as GridDescription;
        bool hasCustomField=false;
        foreach (Column column in options.Fields)

We will see what the hasCustomField variable is needed for in a short time, let’s see first how “normal” columns(the ones that are not marked as Hidden) are dealt with:

if ((column.Features & FieldFeatures.Hidden) != FieldFeatures.Hidden)
            {
            <td class="Theme-DataGrid-Column">
            @{
                if (column.DispalyTemplate == null)
                {
                      @DisplayExtensions.DisplayFor(htmlHelper, column.Field)
                }
                else
                {
                      @CoreHTMLHelpers.TemplateFor(htmlHelper, column.DispalyTemplate)
                }
             }
            </td>
            }

 

Very simple: if the user has provided a Display template for that column we just invoke it through the TemplateFor helper otherwise we simply invoke the standard Mvc DisplayFor helper. It is worth to point out that column templates are passed information on the whole row, since we passed the htmlHelper for the whole row to the TemplateFor helper. This way, each column can use information about other columns in the rendering. Moreover, we may define “custom” columns that are associated to no specific field by by using  m => m as filed selector.

Now let’s see what happens to fields marked as Hidden:

else if(column.DispalyTemplate != null){
                hasCustomField = true;
            }

If they have a custom display template we just set the hasCustomField variable, defined at the beginning of the helper to true, just to remember we have found one of them. We have done the same thing in the Insert template. Why?? Simple! Because hidden fields are rendered in the edit Templates, since they just store data for a future post, thus a display template associated with an hidden field for sure is needed to render a command button. Probably a command button that uses the principal key of the row to render either a detail view of the record or some other children objects by means of either an action link or an Ajax call. Therefore, in such a case we need to display a further column reserved for all command buttons:

if ((options.Features & GridFeatures.Edit) == GridFeatures.Edit
            || hasCustomField
            || (options.Features & GridFeatures.Delete) == GridFeatures.Delete)
        {
        <td>
            <table>
                <tr>
                    @if ((options.Features & GridFeatures.Edit) == GridFeatures.Edit)
                    {         
                        <td>
                            @DataGridHelpers.ImgDataButton(
                                htmlHelper, DataButtonType.Edit,
                                Url.Content("~/Content/themes/test/images/edit.jpg"), null)
                        </td>
                    }
                    @if ((options.Features & GridFeatures.Delete) == GridFeatures.Delete)
                    {
                        <td>
                            @DataGridHelpers.ImgDataButton(
                                htmlHelper, DataButtonType.Delete,
                                Url.Content("~/Content/themes/test/images/delete.jpg"), null)
                        </td>
                    }
                    @if (hasCustomField)
                    {
                        foreach (Column column in options.Fields)
                        {
                            if ((column.Features & FieldFeatures.Hidden) == FieldFeatures.Hidden &&
                                column.DispalyTemplate != null)
                            {
                                <td>
                                    @CoreHTMLHelpers.TemplateFor(htmlHelper, column.DispalyTemplate)
                                </td>
                            }
                        }
                    }
               </tr>
            </table>
        </td>
        }

The Edit template is completely analogous, just the command buttons are different:

@helper EditItem(dynamic htmlHelper)
    {
        var options = htmlHelper.ViewData["ThemeParams"] as GridDescription;

        foreach (Column column in options.Fields)
        {
            if ((column.Features & FieldFeatures.Hidden) != FieldFeatures.Hidden)
            {
            <td class="Theme-DataGrid-Column">
            @{
                if (column.EditTemplate == null)
                {
                      @EditorExtensions.EditorFor(htmlHelper, column.Field)
                }
                else
                {
                    if (column.EditTemplate is string)
                    {
                        string templateName = column.EditTemplate as string;
                        templateName = "Themes/Test/" + templateName;
                        @CoreHTMLHelpers.TemplateFor(htmlHelper, templateName)
                    }
                    else
                    {     
                        @CoreHTMLHelpers.TemplateFor(htmlHelper, column.EditTemplate)
                    }
                }
             }
            </td>
            }
        }
        <td>
            <table>
                <tr>          
                    <td>
                        @DataGridHelpers.ImgDataButton(
                            htmlHelper,
                            DataButtonType.Cancel,
                            Url.Content("~/Content/themes/test/images/undo.jpg"), null)
                    </td>
                    @if ((options.Features & GridFeatures.UndoEdit) == GridFeatures.UndoEdit)
                    {
                        <td>
                            @DataGridHelpers.LinkDataButton(
                                htmlHelper,
                                DataButtonType.ResetRow,
                                ThemedControlsStrings.Get("Item_ResetRow", "DataGrid"), null)
                        </td>
                    }
                    @foreach (Column column in options.Fields)
                    {
                        if ((column.Features & FieldFeatures.Hidden) == FieldFeatures.Hidden)
                        {
                            if (column.EditTemplate != null)
                            {
                                @CoreHTMLHelpers.TemplateFor(htmlHelper, column.EditTemplate)
                            }
                            else
                            {
                                <td style="width:0px">
                                    @InputExtensions.HiddenFor(htmlHelper, column.Field)
                                </td>
                            }
                        }
                    }
               </tr>
            </table>
        </td>
}

It is convenient to define also a Razor helper that renders the Header of the table, with all column names, sort, and filtering logics:

@helper DisplayHeader(dynamic htmlHelper, GridDescription options)
    {
    <tr>
        @{bool hasCustomField=false;}
        @foreach (Column column in options.Fields)
        {
            if ((column.Features & FieldFeatures.Hidden) != FieldFeatures.Hidden)
            {
            <td class="Theme-DataGrid-Header">
                <table>
                <tr>

The above Razor Helper is completely analogous to the other templates. In each column we have 2 sections. The first one is dedicated to filtering and the second one is dedicated either to a sort button with the column name inside it or just to the column name if the column is not sortable.

Let’s start with the filtering:

@if ((options.Features & GridFeatures.Filtering) == GridFeatures.Filtering && (column.Features & FieldFeatures.Filtering) == FieldFeatures.Filtering)
                {
                    <td >
                    <div class="Theme-DataGrid-Filter">
                        <div class="MvcControlsToolkit-Hover">
                            @{
                            var h1 = DataFilterClauseHelpers.DataFilterClauseFor(
                                htmlHelper,
                                options.Filter,
                                column.Field,
                                "first");
                            var h2 = DataFilterClauseHelpers.DataFilterClauseFor(
                                htmlHelper,
                                options.Filter,
                                column.Field,
                                "second");
                            bool oldClientSetting = ViewContext.ClientValidationEnabled;
                            ViewContext.ClientValidationEnabled = false;
                                }
                              <table>
                                <tr>
                                    <td>
                                        @InputExtensions.CheckBox(h1, "Selected", h1.ViewData.Model.Selected)
                                    </td>
                                    <td>
                                        @DataFilterClauseHelpers.FilterClauseSelect(h1, h1.ViewData.Model.Condition, column.Field)
                                    </td>   
                                </tr>
                                <tr>
                                    <td colspan="2">
                                        @{object val1=h1.ViewData.Model.Search;
                                          if (! h1.ViewData.Model.Selected)
                                          {
                                              val1 = string.Empty;
                                          }
                                        }
                                        @InputExtensions.TextBox(h1, "Search", val1)
                                        @{ValidationExtensions.Validate(h1, "Search"); }
                                    </td>
                                </tr>
                              </table>
                              <table>
                                <tr>
                                    <td>
                                        @InputExtensions.CheckBox(h2, "Selected", h2.ViewData.Model.Selected)
                                    </td>
                                    <td>
                                        @DataFilterClauseHelpers.FilterClauseSelect(h2, h2.ViewData.Model.Condition, column.Field)
                                    </td>   
                                </tr>
                                <tr>
                                    <td colspan="2">
                                        @{object val2=h2.ViewData.Model.Search;
                                          if (!h2.ViewData.Model.Selected)
                                          {
                                              val2 = string.Empty;
                                          }
                                         }
                                        @InputExtensions.TextBox(h2, "Search", val2)
                                        @{ValidationExtensions.Validate(h2, "Search"); }
                                        @{ViewContext.ClientValidationEnabled=oldClientSetting;}
                                    </td>
                                </tr>
                              </table>    
                            </div>
                        </div>
                    </td>
                    
                }

Filters are implemented through an hover menu that appears on mouse hover a filtering icon. All hover menu logic is contained in the Css, so we don’t have to worry about this while coding the filtering logics. Each column may contains two filter clauses, this is the minimum to implement min-max conditions. Accordingly we create the two helpers h1 and h2 to renders the filters by means of the DataFilterClauseFor helper.

We use each helper to render a checkbox to enable the filter criterion, a dropdown to select the filtering condition, and finally a TextBox for inserting the Filter parameter and the associated validation helper. We ensure that if the filter is disabled the TextBox is empty.

What’s beautiful about the DataFilterClauseFor helper is that once we have Rendered input fields, for its “Search” field, for its filter criterion(our DropDown), and for its enabled condition…it handles all filtering logics for us….We will receive the complete filter obtained combining all columns filters in our the filter property of our ViewModel..So we need just to apply it to an IQueryable to filter it (see also here for more information).

Let’s move to the second section (sorting and column names):

<td>
                <strong>
                @if ((column.Features & FieldFeatures.Sort) == FieldFeatures.Sort)
                {
                    @DataGridHelpers.SortButtonForTrackedCollection(
                        htmlHelper,
                        options.ToShow,
                        column.Field,
                        sortButtonStyle: SortButtonStyle.Button)
                }
                else
                {
                    if (column.ColumnHeader != null)
                    {
                        @column.ColumnHeader
                    }
                    else
                    {
                       @DataGridHelpers.ColumnNameForTrackedCollection(
                       htmlHelper,
                       options.ToShow,
                       column.Field)
                    }
                }
            </strong>
            </td>

options.Toshow contains the lambda expression that defines the collection to be shown in the grid. If the column sorting is required we render a sort button otherwise we render just the column name either taken from Data Annotations or from the column definition.

For all columns marked as Hidden as usual we have:

else if (column.DispalyTemplate != null)
            {
                hasCustomField = true;
            }

and finally:

@if ((options.Features & GridFeatures.Edit) == GridFeatures.Edit
            || hasCustomField
            || (options.Features & GridFeatures.Delete) == GridFeatures.Delete)
        {
            <td class="Theme-DataGrid-Header"></td>
        }

If there is the column for the command buttons, we just render an empty <td> to have the right number of <td>.in the header.

Now we are ready to render the DataGrid:

<table>
@DisplayHeader(options.HtmlHelper, options)
@DataGridHelpers.DataGridFor(options.HtmlHelper,
    options.ToShow,
    ItemContainerType.tr,
    editTemplate,
    displayTemplate,
    null,
    insertTemplate,
    enableMultipleInsert: (options.Features & GridFeatures.InsertOne) != GridFeatures.InsertOne,
    itemCss: "Theme-DataGrid-ItemCss",
    altItemCss: "Theme-DataGrid-AlternateItemCss",
    toTrack: options.GetToTrack())
</table>

toTrack contains the list of all properties we need to save in hidden fields for the changes tracking performed by the grid. This argument is optional, but if we don’t pass it the DataGrid helper just save the original values of all properties of each item. Since, the list of all properties we are using can be easily computed from the columns definition, we get it by calling the GridDescription class method GetToTrack().

Finally we add Pager, and Sorting helper if these services are required:

@if ((options.Features & GridFeatures.Paging) == GridFeatures.Paging)
{
    <div class="Theme-DataGrid-Pager">
        @{ var pager = PagerHelper.PagerFor(options.HtmlHelper, options.Page, options.PrevPage, options.PageCount);}
        @pager.PageButton(ThemedControlsStrings.Get("Page_First", "DataGrid"), PageButtonType.First, PageButtonStyle.Link)
        @pager.PageButton(ThemedControlsStrings.Get("Page_Prev", "DataGrid"), PageButtonType.Previous, PageButtonStyle.Link)
        @pager.PageChoice(5)
        @pager.PageButton(ThemedControlsStrings.Get("Page_Next", "DataGrid"), PageButtonType.Next, PageButtonStyle.Link)
        @pager.PageButton(ThemedControlsStrings.Get("Page_Last", "DataGrid"), PageButtonType.Last, PageButtonStyle.Link)
    </div>
}

@if (((options.Features & GridFeatures.Sorting) == GridFeatures.Sorting) && options.ToOrder != null)
{
        @DataGridHelpers.EnableSortingFor(
            options.HtmlHelper, options.ToShow,
            options.ToOrder, "Theme-DataGrid-NormalHeader",
            "Theme-DataGrid-AscendingHeader", "Theme-DataGrid-DescendingHeader",
            page: options.Page)
}

The whole code can be downloaded from the download page of the Mvc Controls Toolkit here.

That’s all! Not so difficult, isn’t it?

The full code is contained in the RazorThemedGrid file in the Mvc Controls Toolkit download page. Download it and enjoy!

                                                    Stay Tuned

                                                                 Francesco

Tags: , , , , ,