Nov 15 2010

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

Category: Asp.net | Entity Framework | MVCFrancesco @ 00:15

Defining MVC Controls 1

Mvc Controls Toolkit Datagrid Updated Tutorial

Advanced Data Filtering Techniques in the Mvc Controls Toolkit

I am back with the second tutorial about using the Datagrid of the  MVC Controls Toolkit . We are going to modify the the example of my previous post to include sorting on a mouse click on the columns.

The code of this tutorial is containe in the download page of MVC Controls Toolkit, that is here and is named BasicTutorialsCode.

As a first step we need a new parameter in our View Model to exchange sorting information with the View:

public List<KeyValuePair<LambdaExpression, OrderType>> ToDoOrder { get; set; }

The toolkit datagrid is able to exchange directly the lambda expressions to be used in the Linq queries with the Controller. Thus, we use a list of couples Lambda Expression OrderType. Where OrderType is an enumeration that specifies ascending or descending order. The whole list specifies a lexicographic sorting that is possibly based on several columns.

MVC Controls Toolkit defines extension methods to apply directly the above list either to  an IEnumerable or to an IQueryable.It is just to include the namespace: MVCControlsToolkit.Linq and any IEnumerable or IQueryable will be enriched with the method ApplyOrder that accepts the above list as argument. If an ordering is already defined on either the IEnumerable or the IQueryable the new sorting will be chained Lexicographically with it.  

Now our paged search query becomes:

result = context.ToDo.Select(item =>
                    new ToDoView() { Name = item.Name, Description = item.Description, DueDate = item.DueDate, id = item.id }).ApplyOrder(order).Select(viewItem =>
                    new Tracker<ToDoView>
                            {
                                Value = viewItem,
                                OldValue = viewItem,
                                Changed = false
                            }).Skip(toSkip).Take(pageDim).ToList();

It is worth to point out that the sorting information comes from the client, therefore a malicious user might try a denial of service attack by sending a manipulated request of sorting on a column that is too  difficult to sort (a column with no index defined on it, for instance). In order to defend ourselves from a similar attack the Transformation Handler that receives the data from the DataGrid automatically discard columns that are not decorated with the CanSortAttribute that is defined in the MVCControlsToolkit.DataAnnotations namespace. In our case we have the following Data annotations:

        [Required, CanSort, Display(Name="Name")]
        public object Name { get; set; }
        [Required, Display(ShortName = "Description")]
        public object Description { get; set; }
        [CanSort, Display(ShortName = "Due Date"), Format(DataFormatString="{0:D}")]
        public object DueDate { get; set; }

The DisplayAttribute is a standard .Net. Our DataGrid uses ShortName or Name (if ShortName is not specified) for the header of both the unsortable and the sortable columns.The FormatAttribute  inherits from the standard .Net DisplayAttribute and extends it with some properties that we will use in the 0.8 release of the MVC Controls Toolkit, where we will define typed input fields. At moment it has the same behavior of its parent. In this example we use it to specify a nice format for the date.

Now our datagrid template becomes:

<table class="ToDo" >
<tr>
<td class="ToDoHeader"><strong><%:Html.SortButtonFor(m => m.Name, sortButtonStyle: SortButtonStyle.Button) %></strong></td>
<td class="ToDoHeader"><strong><%:Html.SortButtonFor(m => m.DueDate, sortButtonStyle: SortButtonStyle.Button) %></strong></td>
<td class="ToDoHeader"><strong><%: Html.ColumnNameFor(m => m.Description) %></strong></td>
<td class="ToDoHeader"><strong></strong></td>
<td class="ToDoHeader"><strong></strong></td>
</tr>
<%:ViewData["Content"] as MvcHtmlString %>
</table>

Where we can see the use of the sort buttons, and of the ColumnNameFor helper. As default, sort buttons do not  cause post-backs, however they have a parameter to require immediate post-back. Personally I prefer avoiding immediate post-back because this way I can set up my complete lexicographic sorting and only then I require a post-back to update my grid. In any case if there is a pager operating on the grid the change of the sorting causes the page to be reset to the first page automatically.

The up and down arrows of the sort buttons are defined in CSS classes specified in the sorting helper:

  <%:Html.DataGridFor(m => m.ToDoList, ItemContainerType.tr,  "ToDoEditItem",  "ToDoDisplayItem", "ToDoGrid", "ToDoInsertItem")%>
    </div>
    <div class="ToDoPager">
                    <% var pager = Html.PagerFor(m => m.CurrPage, m => m.PrevPage, m => m.TotalPages); %>
                    <%:pager.PageButton("<<", PageButtonType.First, PageButtonStyle.Link) %>
                    <%:pager.PageButton("<", PageButtonType.Previous, PageButtonStyle.Link) %>
                    <%:pager.PageChoice(5) %>
                    <%:pager.PageButton(">", PageButtonType.Next, PageButtonStyle.Link) %>
                    <%:pager.PageButton(">>", PageButtonType.Last, PageButtonStyle.Link) %>
                </div>
    <div>
    <input type="submit" value="Save" />
    <%:Html.ManipulationButton(ManipulationButtonType.ResetGrid, "Reset", m => m.ToDoList, null, ManipulationButtonStyle.Button) %>
    <%: Html.EnableSortingFor(m => m.ToDoList, m => m.ToDoOrder, "NormalHeaderToDo", "AscendingHeaderToDo", "DescendingHeaderToDo", page: m => m.CurrPage) %>
    <%:Html.HiddenFor(m => m.TotalPages) %>
To do its job the EnableSortingFor helper needs the collection, the sorting property and also the page property. As the sort behavior of the column changes, the three CSS classes specified in the EnableSortingFor helper are applied to the buttons in order to change their look.

Another new feature is the ManipulationButtonType.ResetGrid that undos all changes done to the datagrid and not yet committed. Here commitment means commitment to a database or to any other structure, not simply a post-back: the grid remembers its previous values through post-backs! We declare that commitment took place either by calling the Confirm method of a Tracker<Item> associated to a row or by simply reloading the grid with fresh data.

In order to undo a single row delete we have introduced the undelete data button and a template to substitute deleted rows. The undelete is easy: just put the undelete button in this template! However I prefer seeing my rows disappear completely! Therefore I have not used this feature in this example (no panic….in case of errors we have the reset button..).

The edit row template has not changed from our previous tutorial, but there are some changes in the display row template:

            <td class="ToDo">
                <%: Html.ValidationMessageFor(m => m.Name, "*")%><%: Html.DisplayField(m => m.Name) %>
            </td>
            <td class="editor-field">
                <%: Html.ValidationMessageFor(m => m.DueDate, "*")%><%: Html.DisplayField(m => m.DueDate)%>
            </td>
            <td class="ToDo">
                <%: Html.ValidationMessageFor(m => m.Description, "*")%><%:  Html.DisplayField(m => m.Description)%>
            </td>
            <td class="ToDoTool">
                <%:Html.DetailLink(Ajax, "Edit Details", DetailType.Edit, "ToDoSubTasks", "Home",
                    new { 
                        id = Model.id},
                                        null)%>
                <%: Html.ImgDataButton(DataButtonType.Edit, "../../Content/edit.jpg", null)%>
                
            </td>

The use of the DisplayField helper to display not editable fields not only ensures right formatting as the use of the DisplayFor standard Mvc helper, but also it enables fields representing the same data be synchronized automatically in the view! In particular it enables detail data retrieved via Ajax not only to display in a separate form but also to update the corresponding data in the master grid.

We have just introduced our next feature: the Master-Detail Ajax helper. Next to the Edit button that performs on line editing, there is also a Detail Link helper. In order to do its job this helper needs the Ajax object of the View, a name to put on the link, the name of an Action method returning a Partial View, the name of the associated Controller, and the route parameters. Moreover we need to specify also if the detail form we are going to render is a display or an edit form.

The Partial View returned by the Action method must contain neither an Ajax  nor a standard form: an Ajax form is already supplied by the Master Detail helper(this way we enhance re-usability because the same Partial View can be used with both Ajax and normal child calls):

<% Html.DetailFormFor(Ajax, m => m.ToDoList, ExternalContainerType.div,
           "ToDoSubTasks", "Home", null, "isChangedToDo", "isDeletedToDo");%>

The arguments passed to the helper are: the View Ajax object, the Master collection, the type of container where we woluld like to receive the Ajax content, an Action method and  a Controller to receive the post, some Html attributes and two CSS classes. The first class (in our case “isChangedToDo") is applied to the display data-field(the ones defined with the DisplayField helper) of the Master grid when they are changed because of the synchronization between detail form and Master data. The edit field of the grid are not updated because they contains new data provided by the user that this way can decide if continuing with  the change in the Master grid or if accepting the changes provided by the Ajax call.

Please note that the DetailFormHelper needs to be put out of the main form, don’t forget it!!!

The grid accepst also another optional row template to be used with rows that are updated because of the synchronization between detail and master View.

The second CSS class (in our case “isDeletedToDo")  is applied to a whole row when a data item retrieved by the Ajax call is discovered to be already deleted. If the user updates a deleted row a new row is created. Obviously the final choice is left to the Controller that may decide to abort the changes in a similar case.

In order to enhance re-usability of controllers (in particular of child controllers that returns Partial Views) we defined the [AcceptViewHint] Action Filters that enables a father view to require an action method of a controller uses a particular Partial View, in a way that is analogous to a client requiring that a Web Service uses a specific protocol or a client selecting a specific endpoint. The Partial View Hint is transmitted in either a Route parameter or a post parameter whose name is ViewHint.The name passed to the filter is scanned for acceptable length and may contain only alphanumeric characters in order to prevent attacks from malicious users.

I will not go into the code of the Detail View of the example that is completely analogous to the one of the Main View that we already discussed in detail in my previous post. The only things worth to note is the use of the SortableListFor helper to allows editing of the list of subtasks associated with a ToDo item. The SortableListFor helper allows delettion/in place editing/insertion of subtasks and allows also sorting of the subtasks by simply dragging them with the mouse .  The SortableListFor uses JQuery Sortable UI element, but transferring the order from the DOM into a collection of complex type on the server side….is not an easy task to accomplish.

Below the use of the helper:

<%: Html.SortableListFor(m => m.SubTasks, "SubTasksToSort", "SubTasksToSortAddItem", 0.7f,
                                        htmlAttributesContainer: new Dictionary<string, object> { { "class", "SortableList" } }
                    )%>

0.7f is the opacity used by the JQuery Sortable, the first argument is the collection to operate on, while “SubTasksToSort” is the Item template and “SubTasksToSortAddItem” is the template for inserting a new element.

Below the Item template:

<div id='<%: Html.PrefixedId("InnerContainer") %>'>
<%: Html.TextBoxFor(m => m.Name, new Dictionary<string, object> { { "class", "ToDoDetailName" } })%><%: Html.ValidationMessageFor(m => m.Name, "*") %>
<%: Html.TextBoxFor(m => m.WorkingDays, new Dictionary<string, object> {{"class", "ToDoDetailDuration"}})%><%: Html.ValidationMessageFor(m => m.WorkingDays, "*") %>
<%: Html.SortableListDeleteButton("Delete",  ManipulationButtonStyle.Link) %>
</div>

As one can see the SortableListFor helper has it own delete buttons.

Finally the Add Item template:

<div >
<%: Html.SortableListAddButton("Insert New", ManipulationButtonStyle.Link)%>
<span id='<%: Html.SortableListNewName() %>' style="visibility:hidden">
<%: Html.TextBoxFor(m => m.Name, new Dictionary<string, object> { { "class", "ToDoDetailName" } })%><%: Html.ValidationMessageFor(m => m.Name, "*") %>
<%: Html.TextBoxFor(m => m.WorkingDays, new Dictionary<string, object> {{"class", "ToDoDetailDuration"}})%><%: Html.ValidationMessageFor(m => m.WorkingDays, "*") %>
<%: Html.SortableListDeleteButton("Delete",  ManipulationButtonStyle.Link) %>
</span>
</div>

The Add Button of the SortableListFor needs a target container that it will make visible when it is pressed. We can specify it by giving the id returned by the helper

 Html.SortableListNewName() to a span, div or other container.

At moment we have finished……Next time we will speak about typed input fields and about the advanced filtering capabilities of the 0.8 release of the MVC Controls Toolkit.

                                          Stay Tuned……

                                           Francesco

For more information or consulences feel free to contact me,

Tags: , , , , ,

Comments

1.
Giovanni Giovanni Italy says:

The SortableFor helper is Great! Great also the fact that you pass Lambda Expressions from Javascript to the Controller

2.
mvcsurfer mvcsurfer United States says:

How are you able to built a complex .Net data structure as a lambda expression in Javascript? I mean I thought about it and appears impossible to me.Are you using undocumented MVC methods?

3.
francesco francesco Italy says:

Hi mvcsurfer, very simple! I don't do it in Javascript! Each control has a Transformation Handler that is invoked on postback by the Model Binder...The magic takes place there! The approach followed by most of developers of doing everything in Javascript has big limits: while it is easy producing ANY Javascript code from the .Net server, the converse is FALSE for two reasons: 1) possible code from Javascript should be compiled in MSIL...complex and not efficient 2) It is not possible to compile and execute code coming from a possibly malicious Javascript client.