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:
- ToDoGrid: it displays the general container where all data items will be inserted. In our case it is just a table.
- ToDoDisplayItem: it displays a row of data when the grid is in display mode
- ToDoEditItem: it displays a row of data when the grid is in edit mode
- 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: Pager, DataBinder, MVC Controls Toolkit, DataGrid, Paging, MVC Controls, MVCControlsToolkit