Nov 15 2011

Mvc Controls Toolkit Datagrid Updated Tutorial

Category: MVC | Entity Framework | Asp.netFrancesco @ 23:42

 

This post is an updated  tutorial on how to use the Update/Delete/Insert Templated datagrid of the MVC Controls Toolkit. It contains information already published in previous tutorials updated to the new versions of the MVC Controls Toolkit. The full code used in this tutorial can be found in the Mvc3 Razor folder of the zipped file BasicTutorialsCode  here

The commercial version of the toolkit contains datagrids with several more options, and that may be configured and styled more quickly with a fluent interface.

Our Data model is simply a Todo List with four fields: Name(varchar), Description(varchar), DueDate(date), and the principal key id of type identity. First, we define the Database, and a table named ToDo with the previous four fields.

Next, we create an Entity Framework(EF) model based on this database. We then create a MetaClass to handle the constraints on our ToDo items. Let say that the DueDate can range from 3  months before today to 6 months after today. Since today is not an absolute value such a constraint cannot be interpreted as a Data Layer constraint, therefore it sholdn’t be put with the other data constraints of the data layer. We will take care of it in a few minutes ! First lets take care of the other simpler constraints, and add to our project a new file named Todo.cs just under the Models folder. What we put in this file is quite standard:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel.DataAnnotations;
using MVCControlsToolkit.DataAnnotations;

namespace Mvc_Examples.Models
{
    [MetadataType(typeof(MetaToDo))]
    public partial class ToDo
    {

    }
    public class MetaToDo
    {
        
        [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 Display attribute is  a standard DataAnnotation attribute and it just declares the string to be used in the labels for our properties. The CanSort attribute comes from the MVC Controls Toolkit and it declares that the field can be used in sorting operations. We will come back on this later on, when we will speak about the sorting helpers of the MVC Controls Toolkit.

Now let go to the View Model! What do we need to put into our View Model? For sure the ToDo items extracted from the database. However, we would like the items to be pageable, so we have to insert at least a Page property containing the current page. A single Page int is enough for the MVC Controls Toolkit pager to work; for a better paging experience you can supply the total number of pages.

Now…the user may edit some fields and then he may change page. What happens if there are validation errors in his editing? A good option is to go back to the previous page and force him to correct the errors. Therefore, we have to remember what page we came from. Thus we need also a previous page property in our View Model. Luckily we don’t need to put it in a hidden field because our pager already offers this type of service when a “previous page” property is available.

Summing Up, our View Model has the following properties: CurrPage, PrevPage, the optional TotalPages, plus a ToDoList property containing all ToDo items.

Now we have to decide how to handle the strange constraint on the DueDate. One way to handle it is to define a View Model version of the ToDo class where we can apply this Presentation Layer specific constraint. We can use the Mvc Controls Toolkit DateRangeAttribute, that offers the ability to define constraints based on expressions containing “Today” or “Now”:

[MetadataType(typeof(MetaToDo))]
    public partial class ToDoView
    {
        public int? id { get; set; }

        public string Name { get; set; }

        public string Description { set; get; }

        [DateRange(SMinimum = "Today-3M", SMaximum = "Today+6M")]
        public DateTime DueDate {set;get;}

       

    }

Note that we have used the same MetaData class of the original ToDo class. This is important in order to avoid code duplications! As a general rule when we build a View Model either we use  the original data classes, or for  each of the original data classes we define a ViewModel version of it and use it as a child of the page ViewModel. This way, we can use the same metaclasses. You should avoid putting fields from more than one data class into a single View Model Class. An exception to this rule is if data are someway aggregated and transformed into other properties. It is preferred to keep a one to one correspondence between the original data classes and the classes used in the View Model; this way we increase modularity and reuse the metadata classes of the original data classes(duplicating code often causes a lot of problems…).

Our next step is the design of the data access procedures. A best practices is to not insert them into the controller methods, because they might be useful to more than one controller, and because it is always better to keep the data layer separate from the controller layer. We create them as static methods of our View Model. This approach is the standard for accessing ToDo items (we will use it each time paged ToDo items are needed). This choice is acceptable for this simple application, but in different situations we might have introduced a new Repository class.

Summing up our View Model contains:

public int TotalPages { get; set; }
public int CurrPage { get; set; }
public int PrevPage { get; set; }
public List<Tracker<ToDoView>> ToDoList {get; set;}

Plus two static methods: GetToDoPage and UpdatePage, for retrieving one page of data, and for passing to the database the updates made by the user to one page of data.

In order to help passing changes to the database the MVC Controls Toolkit provides the class Tacker<T> that is a wrapper put around a data item. It maintains two versions of the data item, the original version and the one with the changes applied by the user. It also has a Boolean value Changed to signal that the two versions are different.

Comparison between the two versions yields the operation to be done on the database:

  • Old version null and new version non-null: Insert
  • Old version non-null and new version null: Delete
  • Both versions non-null: we have an update

Lets analyze the GetToDoPage method:

public static List<Tracker<ToDoView>> GetToDoPage(int pageDim, out int totalPages, ref List<KeyValuePair<LambdaExpression, OrderType>> order, int page = 1)
{
    List<Tracker<ToDoView>> result;
    if (order == null)
    {
        order = new List<KeyValuePair<LambdaExpression, OrderType>>();
       
    }
    if (order.Count == 0)//paging require ordering! Therefore we always need to add a default oredering
    {
        Expression<Func<ToDoView, DateTime>> defaultOrder = m => m.DueDate;

        order.Add(new KeyValuePair<LambdaExpression, OrderType>(defaultOrder, OrderType.Descending));
    }
    using (SiteDbEntities context = new SiteDbEntities())
    {
        int rowCount = context.ToDo.Count();
        
        if (rowCount == 0)
        {
            totalPages=0;
            return new List<Tracker<ToDoView>>();
        }
        totalPages = rowCount / pageDim;
        if (rowCount % pageDim > 0) totalPages++;
        if (page > totalPages) page = totalPages;
        if (page < 1) page = 1;
        int toSkip = (page-1) * pageDim;

        
            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();
        
    }
    return result;
}

The return is of type List<Tracker<ToDoView>> since we have put our wrapper around each data item. We create a context, with the using keyword to guarantee it will be disposed at the end of the operation (because it contains a connection to the database that is not a managed object).

As a first operation we count the total rows, then we do some mathematics to compute the pages: the remainder operation % is needed to take into account partially filled pages. In order to reach the request page we use the Skip and Take methods. In the select construct we create our wrapper and fill it with two copies of the same data item; one for the previous and one for the updated version of the data item. Note we transfer data from the ToDo object into a fresh ToDoView object.

The order parameter contains field sorting information that are passed as a list of couples. Each couple contains a lambda expression that specifies a field, and an OrderType value that specifies the kind of sorting (ascending or descending). The sorting information is passed to the controller by the EnableSortingFor helper (discussed later in in this post). The MVC Controls Toolkit defines extension methods to apply the sorting information to any IEnumerable or IQueryable. You only need 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.

The full code contains also the possibility for filter data. The interested reader may refer to my new tutorial on Data Filtering: Advanced Data Filtering Techniques in the Mvc Controls Toolkit and to my previous tutorial: Data Filtering in the New Mvc 3 Version of The Mvc Controls Toolkit

The UpdatePage method is a little bit more complex, but quite easy, too:

public static void UpdatePage(List<Tracker<ToDoView>> items)
{
    if (items == null) return;
    using (SiteDbEntities context = new SiteDbEntities())
    {
        bool aChange = false;
        foreach (Tracker<ToDoView> item in items)
        {
            if (item.Changed)
            {
                if (item.OldValue == null) //insertion
                {
                    if (item.Value != null)
                    {
                        ToDo curr=new ToDo()
                                 { Name = item.Value.Name, Description = item.Value.Description, DueDate = item.Value.DueDate };
                        aChange = true;
                        context.ToDo.AddObject(curr);
                    }
                }
                else if (item.Value == null) //deletion
                {
                    ToDo curr=new ToDo() { Name = item.OldValue.Name, Description = item.OldValue.Description, DueDate = item.OldValue.DueDate, id=item.OldValue.id.Value };
                    context.ToDo.Attach(curr);
                    context.ObjectStateManager.ChangeObjectState(curr, System.Data.EntityState.Deleted);
                    aChange = true;
                }
                else//update
                {
                    ToDo curr = new ToDo() { Name = item.Value.Name, Description = item.Value.Description, DueDate = item.Value.DueDate, id=item.Value.id.Value };
                    context.ToDo.Attach(curr);
                    context.ObjectStateManager.ChangeObjectState(curr, System.Data.EntityState.Modified);
                    aChange = true;
                }
            }
        }
        if (aChange)
        {
            try
            {
                context.SaveChanges();
                items.ForEach((item) => { item.Confirm(); });//confirm changes have been passed
            }
            catch
            {
            }
        }
    }
}

We have a loop on all modified items where we verify if the item has changed, and if it has changed we analyze what operation needs to be passed to the database as previously.explained. Note the use of the ObjectStateManger to set the correct state of the various objects in the different cases. Insertion is the only case that doesn’t require manually setting the state of the object.

If at least a data item has changed we do a SubmitChanges() that passes all changes in a single transaction to the database. If no exception occurs we call the Confirm() method of the Tracker<T> wrapper that sets the old version of the data item  equal to the new version of it since all changes have been persisted in the database. In case of exceptions an error message should be returned to the controller that instructs the user to make corrections and retry the post. We have not handled this to keep the sample simple.

Now we can go to the controller that has just two Action methods, one for handling the initial HttpGet and the second for handling the subsequent HttpPost:

public const int PageDim=5;//in actual application this should be put in a config file
public ActionResult Index()
{
    int totalPages;
    List<KeyValuePair<LambdaExpression, OrderType>> order = null;
    ToDoViewModel result = new ToDoViewModel()
        {
            ToDoList = ToDoViewModel.GetToDoPage(PageDim, out totalPages, ref order),
            TotalPages = totalPages,
            CurrPage=1,
            PrevPage=1,
            ToDoOrder=order
        };
    return View(result);
}

[HttpPost, System.Web.Mvc.OutputCache(NoStore = true, Duration = 0, VaryByParam = "*")]
public ActionResult Index(ToDoViewModel model)
{
    if (!ModelState.IsValid)
    {
        
        model.CurrPage=model.PrevPage; //cancel possible page change and force correcting errors
        return View(model);
    }
    else
    {
        ModelState.Clear();
        ToDoViewModel.UpdatePage(model.ToDoList);
        int totalPages;
        if (model.CurrPage < 1) model.CurrPage = 1;
        List<KeyValuePair<LambdaExpression, OrderType>> order = model.ToDoOrder;
        ToDoViewModel result = new ToDoViewModel()
        {
            ToDoList = ToDoViewModel.GetToDoPage(PageDim, out totalPages, ref order, model.CurrPage, model.ToDoFilter),
            TotalPages = totalPages,
            CurrPage = Math.Min(model.CurrPage, totalPages),
            PrevPage = Math.Min(model.CurrPage, totalPages),
            ToDoFilter = model.ToDoFilter,
            ToDoOrder=order
        };
        return View(result);
    }
}

The first method just displays the first page and it is quite trivial. The second method handles validation errors. If there are validation errors it cancels the page change by resetting the current page to the previous page, and then returns the same View Model it received in order to let the user corrects the errors. If everything is ok it passes the changes to the database, and retrieves the new page requested by the user. That’s all!

We finally arrived to the datagrid. In order to have the datagrid working we need to prepare 4 templates:

  1. displayTemplate: it displays a row of data when the grid is in display mode
  2. editTemplate: it displays a row of data when the grid is in edit mode
  3. addDisplayTemplate: it defines the look of the insert new row component, normally it just displays an insert button.
  4. gridTemplate: it displays the general container where all data items will be inserted. In our case it is just a table.

As discussed in the documentation about templates, Templates can be passed both as: Partial Views names, Razor Templates, Razor in-line templates, or Lambda Expressions. Here, we will use Razor in-line templates. The reader interested in Partial Views may refer to my previous post: Defining Mvc Controls 2: Using the DataGrid.

Razor in-line templates can be passed directly in the DatagridFor call as arguments. They receive an HtmlHelper<ToDoView> object as the value of a standard parameter called item. We can use this item  variable in exactly the same way we use the Html variable in a standard View.

Lets see in detail each template.

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

The display item View Model is just a data item, it is not a Tracker<T> wrapper. The wrapper is handled automatically by the DataGrid.

We don’t need to put the <tr> tag in each data item: the container tag for each item is defined in the DataGrid helper and it is automatically inserted by the DataGrid. We have also the option to supply a delegate that returns a different item container as a function of the data item and of its position in the DataGrid.

It is worth discussing the two data buttons: the first one switches the row to edit mode, while the second one just deletes the row. In the example below an image button is used, but you can also use link and button helpers.

editTemplate
_S.H<ToDoView>(
@<text>         
     <td class="ToDo">
        @item.ValidationMessageFor(m => m.Name, "*")
        @item.TextBoxFor(m => m.Name)
     </td>
     <td class="ToDo">
        @item.ValidationMessageFor(m => m.DueDate, "*")
        @item.DateTimeFor(m => m.DueDate, DateTime.Today).Date()
     </td>
     <td class="ToDo">
        @item.ValidationMessageFor(m => m.Description, "*")
        @item.TextBoxFor(m => m.Description)
     </td>
     <td class="ToDoTool" colspan="2">
        @item.HiddenFor(m => m.id)
        @item.ImgDataButton(DataButtonType.Cancel, "../../Content/undo.jpg", null)
     </td>
</text>
)

The edit template is completely analogous to the display template. The only difference being that it contains input field allowing you to edit fields.

Please, notice the Hidden field containing the key! It is necessary! Here we have a cancel button that undoes all changes done to the data item and put the row in display mode again.

Last thing worth pointing out is the DateTimeFor helper that is able to take as input, date, time or date and time. It is able to read the DateRange attribute and to enforce its constraints. This means the user is allowed to insert just dates that conforms with the constraints. More information about the DateTimeFor Helper can be found here.

addDisplayTemplate
_S.H<ToDoView>(
               @<td colspan="5" class="ToDo">@item.ImgDataButton(DataButtonType.Insert, "../../Content/add.jpg", null)</td>
                  )

The insert template just contains an insert button that when clicked causes a new row to appear in edit mode.

gridTemplate
   _S.H<ToDoView>(
     @<table class="ToDo" >
       <tr>
       <td class="ToDoHeader"><strong>@item.SortButtonFor(m => m.Name, sortButtonStyle: SortButtonStyle.Button)</strong></td>
       <td class="ToDoHeader"><strong>@item.SortButtonFor(m => m.DueDate, sortButtonStyle: SortButtonStyle.Button)</strong></td>
       <td class="ToDoHeader"><strong>@item.ColumnNameFor(m => m.Description)</strong></td>
       <td class="ToDoHeader"><strong></strong></td>
       <td class="ToDoHeader"><strong></strong></td>
       </tr>
       @item.ViewData["Content"]
   </table>
)

It defines the look of the container of all items. In our case it just displays the <table> tag and the header of the table. The ColumnNameFor helper displays the name of a column, taking it from the Display attribute(ShortName if available, otherwise Name).

The SortButtonFor helper renders sort buttons on the columns where sorting is allowed. They can be used only together with the EnableSortingFor helper that we will discuss below.

The @item.ViewData[“Content”] construct  defines where all data items have to be inserted. It is a kind of placeholder and it needs to be inserted “as it is” in any template that describes a datagrid container.

It is worth pointing out that this template is passed an empty data item object to help the automatic construction of the container. Specifically, we can use reflection to extract all columns and we can also automatically decide some facts about the look of the container by extracting the attributes of each data item property (as for instance the display attribute).

The Datagrid

Summing up the whole call to the DataGridFor helper is:

@Html.DataGridFor(m => m.ToDoList, ItemContainerType.tr,
            _S.H<ToDoView>(
            @<text>         
                 <td class="ToDo">
                    @item.ValidationMessageFor(m => m.Name, "*")
                    @item.TextBoxFor(m => m.Name)
                 </td>
                 <td class="ToDo">
                    @item.ValidationMessageFor(m => m.DueDate, "*")
                    @item.DateTimeFor(m => m.DueDate, DateTime.Today).Date()
                 </td>
                 <td class="ToDo">
                    @item.ValidationMessageFor(m => m.Description, "*")
                    @item.TextBoxFor(m => m.Description)
                 </td>
                 <td class="ToDoTool" colspan="2">
                    @item.HiddenFor(m => m.id)
                    @item.ImgDataButton(DataButtonType.Cancel, "../../Content/undo.jpg", null)
                 </td>
            </text>
            ),
             _S.H<ToDoView>(
            @<text>         
                <td class="ToDo">
                    @item.ValidationMessageFor(m => m.Name, "*")
                    @item.DisplayField(m => m.Name)
                </td>
                <td class="editor-field">
                    @item.ValidationMessageFor(m => m.DueDate, "*")
                    @item.DisplayField(m => m.DueDate)
                </td>
                <td class="ToDo">
                    @item.ValidationMessageFor(m => m.Description, "*")
                    @item.DisplayField(m => m.Description)
                </td>
                <td class="ToDoTool">
                    @item.DetailLink(Ajax, "Edit Details", DetailType.Edit, "ToDoSubTasks", "Home",
                        new {
                            id = item.ViewData.Model.id},
                                            null)
                    @item.ImgDataButton(DataButtonType.Edit, "../../Content/edit.jpg", null)
                
                </td>
                <td class="ToDoTool">
                    @item.ImgDataButton(DataButtonType.Delete, "../../Content/delete.jpg", null)
                </td>
            </text>
            ),
            _S.H<ToDoView>(
              @<table class="ToDo" >
                <tr>
                <td class="ToDoHeader"><strong>@item.SortButtonFor(m => m.Name, sortButtonStyle: SortButtonStyle.Button)</strong></td>
                <td class="ToDoHeader"><strong>@item.SortButtonFor(m => m.DueDate, sortButtonStyle: SortButtonStyle.Button)</strong></td>
                <td class="ToDoHeader"><strong>@item.ColumnNameFor(m => m.Description)</strong></td>
                <td class="ToDoHeader"><strong></strong></td>
                <td class="ToDoHeader"><strong></strong></td>
                </tr>
                @item.ViewData["Content"]
            </table>
         ),
         _S.H<ToDoView>(
               @<td colspan="5" class="ToDo">@item.ImgDataButton(DataButtonType.Insert, "../../Content/add.jpg", null)</td>
                  ))

The first argument of the helper, as usual, defines the property to display in the DataGrid. The second property defines the kind of item container to be used (in our case a <tr>), and the last ones are the templates we discussed before.

There are also optional arguments to define html attributes(also as a function of the data item) and to pass a function to change dynamically the item container.

Finally, the pager is composed of various parts that can be used also  separately. Here I used link buttons but one can use also image or normal buttons:

  <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)
             @pager.PageButton("Go To", PageButtonType.GoTo, PageButtonStyle.Button)
             @pager.GoToText(new { style = "width:50px;" })
</div>

The first call gets an object to be used to render all parts. The PageChoice method displays five page links before the current page, 5 page links after the current page and the number of the current page. There are buttons for the next page, last page, etc., and also a button plus a textbox to jump to any page.

Sorting

The first step to enable sorting with a mouse click on the columns is the insertion of a new property in our ViewModel to receive sorting information:

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

The toolkit datagrid is able to exchange directly the lambda expressions to be used in LINQ queries with the Controller. Thus, we use a list of couples LambdaExpression OrderType. Where OrderType is an enumeration that specifies ascending or descending order. The whole list specifies a lexicographic sorting that can be based also on several columns. As previously discussed, sorting information can be used directly with LINQ expressions by means of the IEnumerable and IQueryable extension ApplyOrder defined in the namespace MVCControlsToolkit.Linq , .

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

Sorting is defined by the user through the column sort buttons we have already seen in the grid header. By default, sort buttons do not cause a post. However they have a parameter to require an immediate post. When a new sorting is applied if there is a pager operating on the grid the page is automatically reset to the first page.

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

@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)

In order to do its job the EnableSortingFor helper needs the collection to be sorted, the property that will contain all sorting information 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 interesting feature is the ManipulationButtonType.ResetGrid that undoes all changes done to the datagrid that are not yet committed. Here commitment means commitment to a database or to any other storage, not simply a post: the grid remembers its previous values through several posts! 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..).

That’s all! The deveoper’s that don’t want to add each time, pagers, sort buttons, filters, etc to their DataGrids can use Theming to define once and for all the look and features of their DataGrids,…or they can simply modify the DataGrid Theme I already defined in the RazorThemedGrid file in the download area. A tutorial on theming is here.

Stay Tuned !

Francesco

Tags: , , , , , , ,

Oct 30 2010

Defining MVC Controls 2: Using the DataGrid

Category: MVC | Entity Framework | Asp.netFrancesco @ 00:46

See a more recent version of this tutorial: Mvc Controls Toolkit Datagrid Updated Tutorial

Defining MVC Controls 1

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

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

This post is a tutorial un how to use the Update/Delete/Insert Templated datagrid of the MVC Controls Toolkit. You can download the full code used in this tutorial here (the package is named BasicTutorialsCode and it contains also the database)..

Our Data model is simply a Todo List with three fields: Name(varchar), Description(varchar) and DueDate(date), plus the principal key id of type identity. As first step we define the Database, and a table named ToDo  with the previous four fields.

Then, we create an entity framework model based on the database just created: I called it MeetingsModel because in a future post I will extend it with the management of a meeting room.

Now it is time to create a MetaClass to handle the constraints on our ToDo items. Let say that the Name and Description fields are required (i.e they can’t be null), while for the date we have a strange constraints: the DueDate can range from 1 month before today to 6 months after today. Since today is not a definite value such constraints can not be interpreted as a Data constraints, thus its natural place is not together with the other data constraints of the data layer: we will take care of it in a few minutes! First let take care of the the other simpler constraints, and add to our project a new file named Todo.cs just under the Models folder. What we put in this file is quite standard:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel.DataAnnotations;
using MVCControlsToolkit.DataAnnotations;

namespace Mvc_Examples.Models
{
    [MetadataType(typeof(MetaToDo))]
    public partial class ToDo
    {

    }
    public class MetaToDo
    {
        [Required]
        public object Name { get; set; }
        [Required]
        public object Description { get; set; }
        
    }
}

Now let go to the View Model! What we need to put into the View Model? For sure the ToDo items extracted from the database. However, we would like the items be paged, so we have to insert at least a Page property containing the current page. A single Page int is enough for the MVC Controls Toolkit pager to work, but for a better paging experience it is better to supply also the total number of pages. It is ok, our pager control handle also this information, if available.

Now…the user might edit some fields and then he might change page. What does we do if there are validation errors in its editing? The better thing to do is to go back to the previous page and force him to correct the errors. Therefore, we have to remember what page we come from. Thus we need also a previous page property in our View Model. Luckily we don’t need to put it in a hidden field because our pager already offers  this type of service when a “previous page” property is available.

Summing Up, our View Model needs the following properties: CurrPage, PrevPage, and TotalPages, plus a ToDoList property containing all ToDo items.

Now we have to decide how to handle the strange constraint on the DueDate. One way to do it is to define a View Model version of the ToDo class where we can apply it. In this case we might solve in a simpler way but I prefer to be more general:

using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel.DataAnnotations;
using MVCControlsToolkit.DataAnnotations;
 
namespace Mvc_Examples.Models
{
    [MetadataType(typeof(MetaToDo))]
    public partial class ToDoView
    {
        public int? id { get; set; }
 
        public string Name { get; set; }
 
        public string Description { set; get; }
 
        [DateRange(DynamicMinimum="MinDate", DynamicMaximum="MaxDate")]
        public DateTime DueDate {set;get;}
 
        [MileStone]
        public DateTime MaxDate
        {
            get 
            {
                return DateTime.Today.AddMonths(6);
            }
            set 
            {
            }
        }
 
        [MileStone]
        public DateTime MinDate
        {
            get 
            {
                return DateTime.Today.AddMonths(-1);
            }
            set 
            {
            }
        
        
        }
 
    }
}
 

Please notice that we have used the same MetaData class of the original ToDo class. This is important in order to avoid code duplications! As a general rule when building a View Model one has to use either the original data classes, or for each data class we define a modified version and use it as a son of the View Model: this way, we can use the same metaclasses. Please avoid putting fields from more than one data class into a single View Model Class. This can be done only if data are someway aggregated and transformed into other properties, otherwise it is better to keep a one to one correspondence between data classes and classes used in the View Model: this way we increase modularity and we can reuse the metadata classes of the original data classes(duplicating code always causes a lot of problems…).

We have defined two new properties MinData and MaxData that are always equal to Today minus one month and Today plus 6 months: they define the range of possible values for the DueDate property.

The Milestone attribute that decorates these two properties is defined in the MVCControlsToolkit.DataAnnotations namespace and it do exactly nothing…

It is just an hint for the View not to put these properties  into input fields  because their only purpose in the View is giving help to the user: the view designer can use them to display a message for the user, for example.

Our constraint on the DueDate is enforced by using the DateRange attribute defined in the MVCControlsToolkit.DataAnnotations namespace. This attribute act very similarly to the normal Range validation attribute, but accepts also dynamic minimum and maximum, that are essentially the names of the properties containing the actual minimum and maximum.

Next step is the design of the data access procedures. It is not a good idea to insert them into the controller methods, because they might be useful to more than one controller, and because it is always better to keep the data layer separate from the controllers layer. We will put them as static methods of our View Model that this way becomes a kind of  a standard for accessing ToDo items(we will use it always when paged ToDo items are needed). This choice is acceptable for this application, but in different situations we might have introduced a new class.

Therefore our View Model now contains the properties we discussed before:

        public int TotalPages { get; set; }
        public int CurrPage { get; set; }
        public int PrevPage { get; set; }
        public List<Tracker<ToDoView>> ToDoList {get; set;}

Plus two static methods, say GetToDoPage and UpdatePage, respectively for retrieving one page of data, and for passing to the database the updates made by the user to one page of data.

In order to help passing changes to the database the MVC Controls Toolkit furnishes the class Tacker<T> that is a wrapper to put around a data item. It maintains two versions of the data item, the original version and the one with the changes applied by the user. Moreover, it has also a Boolean value Changed to signal that the two versions are different.

Comparison between the two versions yields easily the operation to be done on the database:

  • Old version null and new version different from null: we have an insertion
  • Old version different from null and new version null: we have a deletion
  • Both versions different from null: we have an update

After this short premise let go analyze the GetToDoPage method:

        public static List<Tracker<ToDoView>> GetToDoPage(int pageDim, out int totalPages, int page = 1) 
        {
            List<Tracker<ToDoView>> result;
            using (SiteDbEntities context = new SiteDbEntities())
            {
                int rowCount = context.ToDo.Count();
                if (rowCount == 0)
                {
                    totalPages=0;
                    return new List<Tracker<ToDoView>>();
                }
                totalPages = rowCount / pageDim;
                if (rowCount % pageDim > 0) totalPages++;
                int toSkip = (page-1) * pageDim;
                result = (from item in context.ToDo
                          orderby item.DueDate ascending
                          select new Tracker<ToDoView>()
                          {
                           Value = new ToDoView() { Name = item.Name, Description = item.Description, DueDate = item.DueDate, id = item.id },
                           OldValue = new ToDoView() { Name = item.Name, Description = item.Description, DueDate = item.DueDate, id=item.id }, 
                          Changed=false }).Skip(toSkip).Take(pageDim).ToList();
            }
            return result;
        }

The result is of type  List<Tracker<ToDoView>> since we have used our wrapper around each data item. We create a context, with the using keyword to be sure it will be disposed at the end of the operation since it contains a connection to the database that is not a managed object.

As first operation we counts the total rows, then we do some mathematics to compute the pages: the remainder operation % is needed to take into account partially filled pages.In order to reach the request page we use the Skip and Take methods. In the select construct we create also our wrapper and fill it with two copies of the same data item, that will become the previous and actual version of the data item. Please notice also that we transfer data from the ToDo object into a fresh ToDoView object.

The UpdatePage method is a little bit more complex, but quite easy, too:

        public static void UpdatePage(List<Tracker<ToDoView>> items)
        {
            using (SiteDbEntities context = new SiteDbEntities())
            {
                bool aChange = false;
                foreach (Tracker<ToDoView> item in items)
                {
                    if (item.Changed)
                    {
                        if (item.OldValue == null) //insertion
                        {
                            if (item.Value != null)
                            {
                                ToDo curr=new ToDo() 
                                         { Name = item.Value.Name, Description = item.Value.Description, DueDate = item.Value.DueDate };
                                aChange = true;
                                context.ToDo.AddObject(curr);
                            }
                        }
                        else if (item.Value == null) //deletion
                        {
                            ToDo curr=new ToDo() { Name = item.OldValue.Name, Description = item.OldValue.Description, DueDate = item.OldValue.DueDate, id=item.OldValue.id.Value };
                            context.ToDo.Attach(curr);
                            context.ObjectStateManager.ChangeObjectState(curr, System.Data.EntityState.Deleted);
                            aChange = true;
                        }
                        else//update
                        {
                            ToDo curr = new ToDo() { Name = item.Value.Name, Description = item.Value.Description, DueDate = item.Value.DueDate, id=item.Value.id.Value };
                            context.ToDo.Attach(curr);
                            context.ObjectStateManager.ChangeObjectState(curr, System.Data.EntityState.Modified);
                            aChange = true;
                        }
                    }
                }
                if (aChange)
                {
                    try
                    {
                        context.SaveChanges();
                        items.ForEach((item) => { item.Confirm(); });//confirm changes have been passed
                    }
                    catch
                    {
                    }
                }
            }
        }
                    

We have a loop an all modified items where we verify if the item has changed, and if changed we analyze what operation needs to be done on the database, as previously.explained. Please notice the use of the ObjectStateManger to set the correct state of the various objects in the different cases.  The case of the insertion is the only one that doesn’t require to set manually the state of the object.

If at least a data item has changed we do a SubmitChanges() that passes all changes in a single transaction to the database. In  case no exception is fired we call the Confirm() method of the Tracker<T> wrapper  that sets the old version equal to the new version of the data item since all changes has been passed to the database.In case of exceptions an error message should be returned to the controller that need to say to the user to retry the post in a few minutes….however we have not handled this for sake of simplicity(a too complex example doesn’t help the understanding).

Now we can go now to the controller that has just two Action methods, one for handling the initial get and the second for handling the subsequent posts:       

         public const int PageDim=5;//into an actual application this should be put in a config file
        public ActionResult Index()
        {
            int totalPages;
            ToDoViewModel result = new ToDoViewModel()
                {
                    ToDoList = ToDoViewModel.GetToDoPage(PageDim, out totalPages),
                    TotalPages = totalPages,
                    CurrPage=1,
                    PrevPage=1
                };
            return View(result);
        }

        [HttpPost]     
        public ActionResult Index(ToDoViewModel model)
        {
            if (!ModelState.IsValid)
            {
                
                model.CurrPage=model.PrevPage; //cancel possible page change and force correcting errors
                return View(model);
            }
            else
            {
                ToDoViewModel.UpdatePage(model.ToDoList);
                int totalPages;
                ToDoViewModel result = new ToDoViewModel()
                {
                    ToDoList = ToDoViewModel.GetToDoPage(PageDim, out totalPages, model.CurrPage),
                    TotalPages = totalPages,
                    CurrPage = model.CurrPage,
                    PrevPage = model.CurrPage
                };
                return View(result);
            }
        }

 

The first method just displays the first page and it is quite trivial. The second method just controls if there are validation errors. In case there are it cancels a possible page change by resetting the current page to the previous page, and then returns the same View Model it received in order to let the user correct the errors. In case everything is ok it passes the changes to the database, and retrieve the new page requested by the user. That’s all!

We finally arrived to the datagrid. In order to have the datagrid working we need to prepare 4 templates:

  1. ToDoGrid: it displays the general container where all data items will be inserted. In our case it is just a table.
  2. ToDoDisplayItem: it displays a row of data when the grid is in display mode
  3. ToDoEditItem: it displays a row of data when the grid is in edit mode
  4. ToDoInsertItem: it defines the look of the insert new row component, normally it just displays an insert button.

Well, Let go see in detail each single template.

ToDoGrid
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<Mvc_Examples.Models.ToDoView>" %>

<table class="ToDo" >

<tr>
<td class="ToDoHeader"><strong>Name</strong></td>
<td class="ToDoHeader"><strong>Due Date</strong></td>
<td class="ToDoHeader"><strong>Description</strong></td>
<td class="ToDoHeader"><strong></strong></td>
<td class="ToDoHeader"><strong></strong></td>
</tr>
<%:ViewData["Content"] as MvcHtmlString %>
</table>

It just displays the <table> tags and the header of the table. The <%:ViewData["Content"] as MvcHtmlString %> construct defines where all data items have to be inserted. It is a kind of placeholder and it needs to be inserted “as it is” in any template that describes a datagrid container.

It is worth to point out that this template is passed an empty data item object to help the automatic construction of the container. Specifically, we can use reflection to extract all columns and we can also automatically decide some facts about the look of the container by extracting the attributes of each data item property (as for instance the display attribute).

ToDoDisplayItem
          <%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<Mvc_Examples.Models.ToDoView>" %>
          <%@ Import Namespace=" MVCControlsToolkit.Core" %>
          <%@ Import Namespace=" MVCControlsToolkit.Controls" %>
          
                      <td class="ToDo">
                          <%: Html.ValidationMessageFor(m => m.Name, "*")%><%: Model.Name %>
                      </td>
                      <td class="editor-field">
                          <%: Html.ValidationMessageFor(m => m.DueDate, "*")%><%: Model.DueDate.ToString("D")%>
                      </td>
                      <td class="ToDo">
                          <%: Html.ValidationMessageFor(m => m.Description, "*")%><%:  Model.Description %>
                      </td>
                      <td class="ToDoTool">
                          <%: Html.ImgDataButton(DataButtonType.Edit, "../../Content/edit.jpg", null)%>
                      </td>
                      <td class="ToDoTool">
                          <%: Html.ImgDataButton(DataButtonType.Delete, "../../Content/delete.jpg", null)%>

          The display item View Model is just a data item, it is not a Tracker<T> wrapper. The wrapper is handled automatically by the DataGrid.

          We don’t need to put the <tr> tag in each data item: the container tag for each item is defined in the DataGrid helper and it is automatically inserted by the DataGrid. We have also the option to supply a delegate that returns a different item container as a function of the data item and of its position in the DataGrid. 

          The last thing worth to discuss are the two data buttons: the first one switches the row in edit mode, while the second one just delete the row. I used here an image button but there are also link and normal buttons.

          ToDoEditItem
          <%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<Mvc_Examples.Models.ToDoView>" %>
          <%@ Import Namespace=" MVCControlsToolkit.Core" %>
          <%@ Import Namespace=" MVCControlsToolkit.Controls" %>
                      
                      <td class="ToDo">
                          <%: Html.ValidationMessageFor(m => m.Name, "*")%><%: Html.TextBoxFor(m => m.Name) %>
                      </td>
                      <td class="ToDo">
                          <%: Html.ValidationMessageFor(m => m.DueDate, "*")%><%: Html.DateTimeFor(m => m.DueDate, DateTime.Today).Date()%>
                      </td>
                      <td class="ToDo">
                          <%: Html.ValidationMessageFor(m => m.Description, "*")%><%: Html.TextBoxFor(m => m.Description) %>
                      </td>
                      <td class="ToDoTool" colspan="2">
                          <%: Html.HiddenFor(m => m.id) %>
                          <%: Html.ImgDataButton(DataButtonType.Cancel, "../../Content/undo.jpg", null)%>
                      </td>

          The edit template is completely analogous to the display template. The only difference being that it contains input field to allow user to edit fields.

          Please, notice the Hidden filed containing the key! It is necessary! Here we have a cancel button that undo all changes done to the data item and put the row in display mode.

          Last thing worth to point out is the DateTimeFor helper that is able to take as input, date, time or date and time. It is able to read the DateRange attribute and to enforce its constraints. This means the user is allowed to insert only dates that conforms with the constraints. More information about the DateTimeFor Helper can be found here.

          ToDoInsertItem
          %@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<Mvc_Examples.Models.ToDoView>" %>
          <%@ Import Namespace=" MVCControlsToolkit.Core" %>
          <%@ Import Namespace=" MVCControlsToolkit.Controls" %>
          
          <td colspan="5" class="ToDo"><%: Html.ImgDataButton(DataButtonType.Insert, "../../Content/add.jpg", null)%></td>
          The insert template just contains an insert button that when clicked causes a new row to appear in edit mode.
          The Datagrid

          Once we have all templates defined we just need to use the datagrid and pager helpers:

          <div>
              <%: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.HiddenFor(m => m.TotalPages) %>

          The first argument of the helper is the definition of the property to display in the DataGrid, as usual, the second defines the item container, and the last ones are the names of the templates defined before.

          There are also optional arguments to define html attributes(also as a function of the data item) and to pass a function to change dynamically the item container.

          The pager is composed of various parts that can be also used separately. Here I used link buttons but one can use also image or normal buttons.

          Well we have finished this short adventure,,,,

          Next post will be about the new features of the new release of the datagrid:, sorting, master-detail helpers…….

                                                                  Stay Tuned !

                                                                  Francesco

          For more information or consulences feel free to contact me

          Tags: , , , , , ,