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 :-).

Advertisements

The differences between SharpLite and SharpArchitecture 1.6

I am currently writing a small simple website in order to try out SharpLite. SharpLite is a new framework which is designed to cut out a lot of the things from Sharp Architecture that most projects would not use. My site has about 6 maintenance pages, a few miscellaneous pages, an interface to a web service, 10 database tables and no out-of-the-ordinary business logic other than a rather convoluted data search mechanism. These are my notes about the practical differences (for our team) between SharpLite and Sharp Architecture 1.6. I’ll update this if I come across any other glaring differences.

For my team I am not going to recommend trying to update existing Sharp 1.6 projects to SharpLite, because as you will see a lot has changed and some things would not map to the new version very well. Upgrades will have to be to Sharp 2.0 instead which hopefully still supports the things we have done in 1.6 (looks like it does – with reservations). However SharpLite looks good for small, new projects.

My notes:

Slightly different project layout. ‘Core’ and ‘Data’ are replaced by ‘Domain’ as the data layer has almost disappeared (see later). ‘ApplicationServices’ is replaced by ‘Tasks’ and includes ViewModels by default. Controllers has been merged into the Web project. There is new Infrastructure project which just contains a few config-type classes.

No NHibernate.config file. Connectionstring is now in Web.config, and the initialisation is done in Infrastructure..NHibernateProvider.NHibernateInitializer (might need to change SQL Server version here).

If you want the auto-generated database tests to run you need to add your connectionstring into the App.config file in the Tests project too.

No bespoke repositories, instead just use queries on the IRepository. To be fair you can do this in Sharp Architecture too but we didn’t. Testing of queries does not require RepositoriesTestBase, which is lucky as that does not exist anymore. Instead just create an IQueryable() list of the entities you need. Your bespoke queries should be recognised by the Tests project automatically as they are just extensions to IQueryable().

No Fluent mapping. Generally speaking you need to use conventions for table and column names. You can override it but it is a little tedious and uses NHibernate’s ModelMapper instead of Fluent, which is a slightly different syntax. By default their convention for a foreign keyed column is to have an ‘Fk’ suffix which is unusual – the convention is usually ‘Id’ wherever I have worked or whatever I have read. So if you want to change it go to Infrastructure..NHibernateProvider.Conventions.

No Check class and no PreconditionException class which is slightly annoying – I would have thought these were pretty basic. Had to write my own replacement.

Routing is now just in Global.asax rather than RouteRegister class – I expect to fall into line with vanilla MVC.

They have provided a non-mandatory basic CUD service class (in the Tasks project) that you can extend for each entity which is useful. They suggest separating querys from CUD tasks though. I’m not entirely sure why, I think it’s just the latest fashion. It means that if you follow this pattern (you don’t have to), you need to inject both the IRepository and service class into your controller rather than just the service class. Also, this CUD service class needs to be injected as a concrete class rather than an interface. I can see that this is reasonable most of the time, but I can also see the odd scenario where this might be a pain. I just went with their method for this project to see how it went.

If you do create new interfaces for dependency injection then you now need to explicitly declare their mapping to concrete instance in Infrastructure..Init.DependencyResolverInitialiser which is less convenient than Sharp Architecture but then I don’t suppose they are expecting many more interfaces if you follow their ‘CudTasks’ pattern, and are not having to interface with stuff outside the application much.

A new MVC3 thing (as opposed to SharpLite) is Razor view engine which is the default when creating a view now. I’m not sure what advantages this gives you; I think it is just a different syntax, although presumably there was a better reason than that for spending time developing it. Seems to just be a matter of personal preference. There also seems to be a pretty random bug with the paste when editing a .cshtml file – in that it can take 20-30 seconds to paste. No obvious pattern. You can still use the old ASPX view engine but by default SharpLite creates a few Razor files for you so you are being pushed in that direction.

Need to add a reference to SharpLite.Domain dll into Tests project. I think that ideally this should be added for you.

Need to add a reference to Rhino.Mocks into Tests project. Can’t remember if you need to do this in Sharp Architecture or not, but it would be useful if it was there to start with. I suppose this is where the ‘Lite’ kicks in though – it only takes a second to install Rhino via Nuget if you need it.

Need to remember to include the ajax javascript file in your layout (master page) or view in order for the Ajax helpers to work. This is a different file than MVC2, called jquery.unobtrusive-ajax.min.js. Depending on where you put it, it might be best to wrap this include with ‘!Request.IsAjaxRequest()’ too, otherwise the Ajax calls will get slower and slower and…

If you follow their CUD pattern you need to remember about MVC conventions when posting the table data. Your controller post method needs a parameter of your Entity type, however the View uses a ViewModel that has a property of your Entity type. The parameter to the controller post method needs to be the same name as your property or it will not post anything – note that you cannot post the ViewModel itself if you want to use the HasUniqueDomainSignature annotation on your Entity.

I found a bug with HasUniqueDomainSignature annotation fairly early on in my project: if you have a validation error from an annotation on an Entity, and that Entity has another entity as a property (i.e. signifying a foreign key link between tables), it will trigger the HasUniqueDomainSignature error message from the linked Entity, overriding the one that should be shown, which is confusing! This has been fixed but not officially released yet. You need to download the latest version via the ‘zip’ button on one of the code pages and copy the binaries into your project if you come across this problem. Only a problem if you are using HasUniqueDomainSignature though!

Updated: When you deploy your site you will also need to add the MVC assemblies to your site’s bin directory (if your server does not already have them installed). Sharp Architecture does this automatically but Sharp Lite does not. In order to make sure I had the .Net 4 assemblies I downloaded Sharp Architecture 2.0 and in the ‘Referenced Assemblies’ folder you will find all the relevant assemblies.