Editable data grid in ASP.NET MVC

I was asked by a colleague recently how to create an editable data grid in MVC, as that is quite easy in Webforms. I was surprised to find that there is not much in the way of examples on the web at the moment; most seem to say download a 3rd party helper which is a bit of a cheat. :) So I decided to post my notes about how I generally do it. This example assumes you are a reasonably experienced programmer – I don’t explain the all the bits that aren’t relevant to the topic! Also, I am using Sharp Lite in this example, which is a framework that sets up a Visual Studio template for MVC3/Razor and NHibernate. This more-or-less follows their recommended pattern in terms of CRUD. I dare say that there are dozens of more elegant ways to do this, but this method seems to work well for very simple sites at least.

This example is very simple – a list of ‘Special Codes’, each record in the underlying database table has just 2 fields. The grid itself has no paging or user-controlled sorting but hopefully it should be reasonably easy to see how to add it. Each item on the list has an Edit and Delete button, and there is a Create button at the bottom of the list. Editing is done ‘inline’, but depending on how you style the edit form it could just as easily be turned into a dialog box.

First of all the NHibernate database entity and its simple SharpLite-style ViewModel that we are going to list in the grid:

    [HasUniqueDomainSignature(ErrorMessage="2 Special Codes may not have the same code")]
    public class SpecCode:Entity
    {
        public SpecCode() { }

        public SpecCode(string _code)
        {
            Check.Require(!String.IsNullOrEmpty(_code), "_code must not be null or empty");
            Code = _code;
        }

        [DomainSignature]
        [Display(Name = "Code")]
        [Required(ErrorMessage = "Code is required")]
        [StringLength(2, ErrorMessage = "Code must be a maximum of 2 characters")]
        public virtual string Code { get; set; }

        [Display(Name = "Description")]
        [Required(ErrorMessage = "Description is required")]
        [StringLength(100, ErrorMessage = "Description must be a maximum of 100 characters")]
        public virtual string Description { get; set; }
    }

    public class SpecCodeEditViewModel
    {
        public SpecCode SpecCode { get; set; }
    }

This is going to be an AJAX grid accessible via the URL /SpecCode/List. We have 3 HTML pages. The first one is a View that just contains any calls to the Layout view and the relevant Javascript libraries. The second is a PartialView that contains the grid itself (and is within a div to allow easy update via the MVC AJAX helpers). The third is another PartialView containing the edit form.

the next 3 listings are the views mentioned above – the first one being the main ‘List’ View:

@model IEnumerable<MyProject.Domain.SpecCode>

@if (!Request.IsAjaxRequest()) 
    {
        /* Putting this inside an IsAjaxRequest() block prevents reloading of the JS libraries on every Ajax call.*/
     <script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
     <script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>

     <script src="@Url.Content("~/Scripts/jquery.unobtrusive-ajax.min.js")" type="text/javascript"></script>
     <script src="@Url.Content("~/Scripts/list-specialcode.js")" type="text/javascript"></script>
    }
     
@Html.Partial("Listing", Model)

Next is the PartialView that contains the datatable itself. The Ajax.ActionImageLink() method is not a standard MVC helper – it is one of my own. You can use the standard Ajax.ActionLink() to achieve the same thing. As you can hopefully see, all that the Edit or Create buttons do is to load the ‘Item’ PartialView into the ‘divEditItem_*’ div. The Javascript is listed further down.

@model IEnumerable<MyProject.Domain.SpecCode>
@using MyProject.Web.Helpers

<div id="ListView">

<h2>Special Codes</h2>
   
    <table>
       <tr>
        <th>
        <span class="codeStyle">Code</span>
        <span class="descStyle">Description</span>
        
        </th>
       </tr>

    @foreach (var item in Model)
    {
        
        <tr>
            <td style="width:400px">

                <div id='@String.Concat("divEdit_", item.Id.ToString())'>
                    <div id='@String.Concat("divDisplayItem_", item.Id.ToString())'>
                        <span class="codeStyle"> @Html.DisplayFor(x => item.Code)</span>  <span class="descStyle"> @Html.DisplayFor(x => item.Description) </span>
                    </div>

                    <div id='@String.Concat("divEditItem_", item.Id.ToString())' >
         
        
                    </div>

                </div>
    
            </td>
            <td style="vertical-align:top">
          
                 @Ajax.ActionImageLink(Url.Content("~/Content/images/edit24.png"), "Edit", "Edit", "SpecCode", new { id = item.Id }, new AjaxOptions { UpdateTargetId = "divEditItem_" + item.Id, OnBegin = "showEdit(" + item.Id + ")" })

                 <a href='@String.Concat(new string[]{"javascript:delete_item(", item.Id.ToString(), ")"} )' ><img src='@Url.Content("~/Content/images/delete16.png")' alt="Delete" title="Delete" height="24px" width="24px"/></a>
            </td>
        </tr>
        
    }
    <tr>
            <td style="width:500px">
                <div id='divEdit_0'>
                    <div id='divDisplayItem_0'>
                    </div>

                    <div id='divEditItem_0' >
                
        
                    </div>
                </div>
            </td>
            <td style="vertical-align:top">
          
              @Ajax.ActionImageLink(Url.Content("~/Content/images/add32.png"), "Create New", "Edit", "SpecCode", new { id = 0 }, new AjaxOptions { UpdateTargetId = "divEditItem_0", OnBegin = "showEdit(0)" })  
            </td>
    </tr>

    </table>

 </div>

Finally the ‘Item’ PartialView that contains the form for editing. This will simply show two fields side by side with a ValidationSummary helper that may show text underneath those fields.

@model MyProject.Tasks.ViewModels.SpecCodeEditViewModel
@using (Ajax.BeginForm("Edit", new AjaxOptions { UpdateTargetId = "divEditItem_"+  Model.SpecCode.Id, OnSuccess = "hideEditOnSuccess(" + Model.SpecCode.Id + ")" })) 
{
    @Html.AntiForgeryToken();
    @Html.HiddenFor(m => m.SpecCode.Id)                
   <span class="codeStyle"> @Html.EditorFor(m => m.SpecCode.Code, "SmallTextTemplate")  </span>
    
   <span class="descStyle"> @Html.EditorFor(m => m.SpecCode.Description, "LongNameTemplate") </span>
    
    <button title="Save" type="submit" value="Save"  ><img src='@Url.Content("~/Content/images/save.png")' alt="Save" title="Save" height="16px" width="16px"/></button> 
    <button type="button" value="Cancel" onclick="hideEdit(@Model.SpecCode.Id)" title="Cancel" ><img src='@Url.Content("~/Content/images/cancel.png")' alt="Cancel" title="Cancel" height="16px" width="16px"/></button>
    
    @Html.ValidationSummary() 

}

You can see from the last two listings that quite a lot of Javascript calls are made. These reference functions in a JS file called list-specialcodes.js, which is referenced in the ‘List’ View. Below is the Javascript code which should be resonably self-explanatory:


function showEdit(ID) {

    $('#divDisplayItem_' + ID).hide();
    $('#divEditItem_' + ID).show();
}


function hideEdit(ID) {
   
    $('#divEditItem_' + ID).hide();
    $('#divDisplayItem_' + ID).show();
}


function hideEditOnSuccess(ID) {
   
    /* if no errors remove the form from the page and reload the list */
    if ($('#divEditItem_' + ID).find(".validation-summary-errors").size() == 0) {
        $('#divEditItem_' + ID).hide();
        $('#divDisplayItem_' + ID).show();
        reload_listing();
    }
}

function reload_listing() {
    var urlpath = "";
    var urlstr = urlpath + "/SpecCode/List";

    $.ajax({
        cache: false,
        url: urlstr,
        success: function (data) {
            $("#ListView").html(data);

        }
    });
}


function delete_item(ID) {

    if (confirm("Do you want to delete this special code?") == false) {
        return;
    }

    var urlpath = "";
    var urlstr = urlpath + "/SpecCode/Delete/" + ID;

    $.ajax({
        cache: false,
        url: urlstr,
        type: "POST",
        success: function (data) {
            reload_listing();
        }
    });
}

Obviously there needs to be a controller for this – here is the code:

    public class SpecCodeController : Controller
    {
        private IRepository<SpecCode> _rcRepo;
        private SpecCodeCudTasks _cudTasks;

        // SpecCodeCudTasks is a SharpLite CUD service class.
        public SpecCodeController(IRepository<SpecCode> rcRepo, SpecCodeCudTasks cudTasks)
        {
            Check.Require(rcRepo != null, "rcRepo cannot be null");
            Check.Require(cudTasks != null, "cudTask cannot be null");
            _rcRepo = rcRepo;
            _cudTasks = cudTasks;
        }

        [HttpGet]
        public ActionResult List()
        {

            // Obviously you could order by something in a parameter here if you wanted
            List<SpecCode> SpecCodes = _rcRepo.GetAll().OrderBy(x => x.Code).ToList();

            if (Request.IsAjaxRequest()) // From the JS file
                return PartialView("Listing", SpecCodes);
            else // URL called direct
                return View("List", SpecCodes);
        }

        public ActionResult Edit(int id)
        {
            // This is just to stop someone navigating directly, not that necessary really  
            if (!Request.IsAjaxRequest())
                return View("NotFound");

            if (id != 0)
            {
                SpecCode rc = _rcRepo.Get(id);
                return PartialView("Item", _cudTasks.CreateEditViewModel(rc));
            }
            else
            {
                return PartialView("Item", _cudTasks.CreateEditViewModel());
            }
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Edit(SpecCode speccode)
        {
            try
            {
                if (ModelState.IsValid)
                {
                    // Sharp Lite CUD stuff.
                    ActionConfirmation<SpecCode> confirmation;

                    confirmation = _cudTasks.SaveOrUpdate(speccode);
                    if (confirmation.WasSuccessful == false)
                    {
                       ModelState.AddModelError("SpecCode.Code",confirmation.Message);
                    }
                }

                return PartialView("Item", _cudTasks.CreateEditViewModel(speccode));
            }
            catch
            {
                return View();
            }
        }

        [HttpPost]
        public ActionResult Delete(int id)
        {
            _cudTasks.Delete(id);
            return new ContentResult();
        }
    }

I’ve not included any styling here, or the SharpLite CudTasks class, but hopefully they are fairly simple to figure out (think inline-block for the styling!), and are not that relevant to this example anyway. Hope this helps someone, and especially my colleague :-).

About these ads

About Andrew Jerrison
I am a software developer living near Manchester, UK. I work at a UK university creating software for members of the academic staff. In my spare time, amongst my many interests, I am studying via distance learning with De Montfort University.

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 66 other followers

%d bloggers like this: