.NET Unplugged

July 25, 2009

ASP.NET MVC Grid – Part 7 – Reusable control

Filed under: ASP.NET MVC — Eric P @ 10:53 am

At this point all the features I planned in Part 1 have been implemented. It is time to make grid re-usable.

Click on image to view the whole screenshot

Click on image to view the whole screenshot

You can see demo here:
http://samples.entechsolutions.com/MvcGridSample/Part7

Code is available here:
http://samples.entechsolutions.com/Downloads/MvcGridSample/MvcGridSample_Part7.zip


So how does one make a grid into reusable control. There is no standard approach in MVC to separate both Logic and Presentation, similar to User Controls in web forms. I hope one will be provided in MVC 2, but for now it is up to a developer to pick from several options.

A found a good article on consolidating HTML for re-usability here: http://www.ajaxprojects.com/ajax/tutorialdetails.php?itemid=487#start. It includes various approaches and strengths and weaknesses of each one.

I considered the following options that would allow me to separate both Grid logic and UI.

a. Extending HtmlHelper

An example of using HtmlHelper to generate a simple pager is here: http://blogs.taiga.nl/martijn/?s=pager
I think helpers are to be better left for building very small HTML pieces, like action links. Even a pager in the example above may be too complex for HtmlHelper.

b. RenderAction

This was mentioned by Rob Connery here:
http://blog.wekeroad.com/blog/asp-net-mvc-preview-4-componentcontroller-is-now-renderaction/

This approach mimics User Control in WebForms, but instead of code behind – you need provide a separate controller to handle Grid Actions. There are some reservations to using this approach described here:
http://haacked.com/archive/2008/07/16/aspnetmvc-codeplex-preview4.aspx

Some people comment that this is not a “Valid” MVC approach, since view would have knowledge of Controllers.

This approach works best for controls like Login form which exist on multiple pages or master page, but the functionality is always the same. In case of the Grid control – the functionality will be custom depending on the entity being desplayed, so RenderAction doesn’t exactly fit here (even though it could work).

c. Partial views
In this approach partial views are used to encapsulate common HTML. All the logic goes into the parent controller. This approach may be the best option for now.


Implementation

The first thing to do to make control re-usable is “Find What Is Varying and Encapsulate It”. The simplest way to do that is to create another grid for a different entity. Then look for code duplication and encapsulate it into shared views/classes.

Add Product Entity

I created a new Product entity that looks like this:


public class Product
{
	[Required(ErrorMessage = "Required.")]
	public int ID { get; set; }

	[Required(ErrorMessage = "Required.")]
	public string Name { get; set; }

	[Required(ErrorMessage = "Required.")]
	[StringLength(10, ErrorMessage = "Must be 10 characters.", MinimumLength = 10)]
	public string SKU { get; set; }

	public string Description { get; set; }

	[Required(ErrorMessage = "Required.")]
    public decimal Price { get; set; }
}

Then I created ProductService class, ProductController and all the Views to List and Edit products

\Models
     Product.cs
     ProductService.cs -  Add/Update/GetByID for product

\Controllers
     ProductController.cs

\Views
    \Product
        List.aspx
        Edit.aspx
        _Grid.aspx

Finding what varies

In Each Grid – the following pieces will work and look the same:

  • Keyword Search and switching search modes
  • Pager navigation and size
  • Sorter
  • CSS and JS

Mvc_Grid_SamePieces
And the following pieces will vary:

  • Column and row data in grid table
  • Advanced Search form

Mvc_Grid_VariedPieces

Here is the screen shot of product list for comparison
Mvc_Grid_Products_List

Extracting Partial views

To incapsulate common UI, while allowing users to only implement pieces that vary I separated Grid html into 4 partials:

_Grid.aspx – generic grid display
_SearchForm.aspx – generic search form
_GridData.aspx – custom display of Grid Data
_AdvancedSearchForm.aspx – custom advanced search form

The Views directory structure looks like:


\Views
    \Customer
        List.aspx  - includes RenderPartial for _Grid and _SearchForm in /Views/Shared
        Edit.aspx
        _GridData.aspx - data table for displaying Customer entries
        _AdvancedSearchForm.aspx - avanced search fields to find Customers
    
    \Product
        List.aspx
        Edit.aspx
        _GridData.aspx - data table for displaying Product entries
        _AdvancedSearchForm.aspx - avanced search fields to find Products

    \Shared
        _Grid.aspx 
             - generic grid display - that provide Grid structure with header and paging, 
                but leaves it up to custom  _GridData (for ex. in \Views\Customer) to display columns and rows
        _SearchForm.aspx 
             - generic search form with Keyword search and link to switch to Advanced.  For custom Advanced Search it 
               will use _AdvancedSearchForm located in appropriate Views directory like (Views\Customer) 
        _Pager.aspx -  to be able to repeat pager on top and bottom of the grid

Passing view models

To be able to pass information from \Customer\List.aspx to \Shared\_Grid.asmx and then back to \Customer\_GridData.aspx – I created an interface that would allow me to box and un-box grid data. I extracted IGrid interface from Grid and passed to shared views:

public interface IGrid
{
	Pager Pager { get; set; }
	Sorter Sorter { get; set; }
	GridAction GridAction { get; set; }
	bool IsEmpty { get; }
	SelectList PageSizeSelectList();
	string PageNavActionLink(string linkText, int page);
	string SortActionLink(string linkText, string sortField);
}

Here is the whole workflow:

a. Customer\List.aspx renders two partial views “_SearchForm.aspx” and “_Grid.aspx”

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<CustomerGrid>" %>
...
<div id="search-form">
	<% Html.RenderPartial("_SearchForm", Model.SearchForm); %>
</div> <!-- search-form -->
	
<div style="clear: both"></div>
		
<div id="grid">
	<% Html.RenderPartial("_Grid", Model); %>
</div>  <!-- grid -->	
...

b. _Grid.aspx takes IGrid as ViewModel and passes it on to \Shared\_Pager.ascx and \Customer\_GridData.ascx

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<IGrid>" %>
...
<div class="pager-nav">			
	<% Html.RenderPartial("_PagerNav", Model); %>
</div>
...
<div id="grid-data">
	<% Html.RenderPartial("_GridData", Model); %>
</div> <!-- data -->
...

c. Partial Customer\_GridData.ascx automatically unboxes IGrid into CustomerGrid

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<CustomerGrid>" %>
...

d. Move the search logic outside of controllers

When going through ProductController and CustomerController code – I noticed that both contained a lot of LINQ search logic. For better separation of concerns, I moved the search logic into Model/Domain classes: CustomerCriteria and ProductCriteria. This leaves controllers to do what they do best: display logic.


Observations

Conclusion and what is next…

In this part, I was able to re-use the Grid for both Customers and Products by moving all the common functionality into shared partial views.

There is still a bit of code that needs to go into the Controller to fill Grid data and save/load grid state. I was thinking of moving this code into base controller class, but this could introduce some coupling. I would like to see if RenderAction may be a better solution.

In the near future I will be using Grid control in several client projects. That will be a great opportunity to smooth out any rough edges. I also plan to introduce some of the more advanced features like: Batch Actions with check boxes, multi-column sorting and inline editing. So stay tuned…

In the near future I will add the following bug fixes/enhancements:
1. Use namespace for Grid’s javascript to provide for a better encapsulation
2. Fix a bug where user can’t switch to advanced search if JS is disabled
3. Fix a bug where server side errors are not displayed for Advanced Search, when date or numeric fields are invalid
4. Introduce unit tests for grid actions (other then Paging which already has unit tests)
5. CSS Bug fix: There is a white dot that appears in FireFox right under header row.

About these ads

27 Comments »

  1. This has been a really good series. You are really close to a very nice control. The only thing I noticed is the back button does not work. I saw comments in Part 3. This really is a nasty issue as it causes web usability issues with many users – and I can’t ‘deep’ link into the table. To me this is a huge value add – otherwise I would just use a javascript table like ext-js which is very powerful. Also have you considered export capabilities?

    Comment by glenedwards — July 26, 2009 @ 11:54 am | Reply

    • Glen,

      It seems to be a pretty common issue for AJAX driven pages. Luckily there are many solutions.

      Solution 1: Don’t cache the page, so it will need to re-render when you click back. In ASP.NET MVC it would look like this:

      [OutputCache(NoStore = true, Duration = 0, VaryByParam = "*")]
      public ActionResult List()

      This fix does introduce an extra server request which can probably result in one or more Database queries, so use it with caution.

      Solution 2: A browser history object like YUI Browser History Manager (http://developer.yahoo.com/yui/history/)or JQuery History Plugin (http://plugins.jquery.com/project/history) to keep track of page state including any changes made with AJAX.

      I need to research this further, but it does seem to be a better solution then 1. ExtJS uses same approach to implement its back button support: http://extjs.com/deploy/dev/examples/history/history.html#tab1:subtab1

      Solution 3: Switch to using GET for form submission and always update the URL query string on any change to the Grid. For this approach I may need to change the way AJAX works – since I have to reload the page each time there is an action on the Grid. It could be really fast though, if I load the page without Grid first and then use AJAX to load the grid data asynchronously.

      I will look into solutions 2 & 3, but for now solution 1 should work as a temporary fix.

      Eric

      Comment by Eric P — July 26, 2009 @ 1:05 pm | Reply

  2. Good ideas there. I would lean towards 3 personally as that means I can bookmark the page and keep state. I have used that style with ext-js tables – but there it has been using a cookie for state – which will not scale when my site has a lot of tables. Also means the user is getting some sort of feedback when there is a lot of data

    Comment by glenedwards — July 27, 2009 @ 8:04 am | Reply

  3. I found this thread which has a very interesting solution http://extjs.com/forum/showthread.php?t=66075&highlight=grid+browser+history

    Comment by glenedwards — July 27, 2009 @ 8:27 am | Reply

  4. Wonderful series. I learn a lot from you.At the moment,it goes to Customer Edit page when you click the row on Product List page.

    Comment by toau2009 — July 29, 2009 @ 9:32 pm | Reply

    • Good catch! The onRowClick pointed to “customer/edit” in grid.js:
      function onRowClick(id) {
      document.location = resolveUrl(“~/Customer/Edit/”) + id;
      }
      Needs to be changed to:
      function onRowClick(id) {
      document.location = “Edit/” + id;
      }
      so it works for both products and customers.

      Comment by Eric P — July 30, 2009 @ 10:37 am | Reply

      • Thank you very much.
        Could you please help me to answer two more questions?
        1. I want put multiple tables/queries in the same page. How to use this grid control to show them in the same page?

        2. How to process multiple forms in the same page? There are small login box and search box in my page, but only the first form could be submit, how to submit second or third form in the same page?

        Thanks you again.

        Comment by toau2009 — August 6, 2009 @ 2:12 am

  5. Hello,
    It’s me again.
    I tried to put three tables/queries in the same page. I create a FormViewModle with three grid controls on it.
    It does work. But one more issue, pageSizeDownList doesn’t work. I don’t know to deal with the pageSizeDownList. Could you help me?

    Thanks a lot.

    Comment by toau2009 — August 6, 2009 @ 7:31 am | Reply

  6. This is a very nice control and great article series. I really enjoyed reading. One major thing I am not sure anyone has noticed yet. In IE7, using the sample page as well as the downloaded code on my own environment, the grid has serious issues. For one, clicking a column twice, once to sort one way, then a second time to sort the opposite direction, causes the grid to show up multiple times within the page. Every sort operation thereafter, creates yet another grid within the page. This does not seem to happen in firefox.

    Comment by mvcjunkie — August 10, 2009 @ 3:38 pm | Reply

    • Found the fix for IE issue. The problem had to do with the following line in _Grid.ascx:

      <div style="clear: both;"/>
      

      Seems like IE doesn’t know how to handle self-closing div tags. It was ignoring the closing of the div, so from IEs perspective – AJAX was returning malformed HTML – which resulted in the multiplicity.

      Changing the line to:

      <div style="clear: both;"></div>
      

      fixes the issue and I will include it in upcoming update.

      A simple google search showed that there are other people who experienced the same problem. For ex. here:
      http://blog.seanmcg.com/?p=176

      Comment by Eric P — August 26, 2009 @ 8:19 pm | Reply

  7. I love your blog series and have learned a lot from you. I’m looking to use your grid in my next project. I’ve also noticed the bug mentioned by mvcjunkie, as well as another. When I delete the last row on a page, rather than seeing the prior page, I see the message “There are no customers that match specified criteria.” I have to manually click “Search” again with an empty search box to see the prior page.

    I personally think you should package the code components of this project as a DLL, with a guideline of how to create the ASCX / ASPX file required to have a grid. The JS and CSS files could be packed as required components to import.

    All in all, this is fantastic and could become a huge MVC component in the open source community. Thanks for taking the time to code it.

    Comment by Chris F — August 12, 2009 @ 3:01 pm | Reply

    • I wanted to point out a couple of other things.

      This specific design works very well for in-memory objects, but not well with database driven apps. At the moment, I’m trying to integrate this into a multi-layered application built on of S#arp Architecture. The EntityCriteria that builds the query that allows for all of the paging, sorting, and searching works off an in-memory object. When integrating with NHibernate, the code needs to be changed to build an NH query accordingly so that only the data that’s necessary for this page, with these search parameters, in this order are fetched. I’ve yet to do this and it’s sitting on my todo list.

      Furthermore, the separation of classes in various namespaces and layers get more complicated. At the moment, I’ve appended the Grid, IGrid, Pager, SearchForm, and Sorter classes with “ViewModel” and put them in my Web.Controllers project in a \ViewModels\Shared folder. I’ve striped out the binders from Utils and put them into GridBinders.cs, and I renamed GridStateService to GridStateManager and put this all in \Grid in the same project. The other utility classes landed in my Utility project.

      Then there’s the challenge: where to put the EntityCriteria and its inheriting classes. They may belong in the Core (Domain) project, which is where the domain entities reside, but with the data access modifications mentioned above, they really need to be with the data repositories. So a natural fit seems the Data project. The Controllers should also access them via an Application Service, rather than directly. This poses the separation of concerns and loose coupling challenge. Application Services should have Data related objects injected (persistence ignorance), and Controllers should have Application Services injected, which begs the question, how do you expose the functionality of EntityCriteria that reside in the Data project? For example, the SortDirection enum will need to be moved or duplicated.

      I’m currently toying with some ideas for using interfaces and such to see what I can figure out. This is my very next todo. Then I’ll have to spend some time unit testing this infrastructure.

      I really like the base design, thanks for sharing, and please feel free to discuss implications such as the ones I’ve mentioned.

      Comment by Chris F — August 13, 2009 @ 12:04 pm | Reply

      • Hi Chris,

        I keep EntityCriteria and related classes with Repositories.

        If I would be a strict subscriber to DDD, I would use DTOs to pass/retrieve data from the application servie. So I would create a ProductCriteriaDTO, then I would call:

        ProductService.FindByCriteria(ProductCriteriaDTO dto)
        {
        ProductCriteria criteria = new ProductCriteria();
        mapper.Map(dto, criteria);
        return criteria.Find();
        }

        This would allow me to keep Controllers totally separate from Data layer.

        Since I am not too strict with DDD, I allow my controller to pass ProductCriteria directly. So there is no extra step of DTO mapping.
        This does require a reference to Data Project from my Controllers.

        I actually don’t use a separate Data layer in my project. Instead I use Module based approach (mentioned by Eric Evans), so in my CORE library I have:

        \ProductModule
        –\DataAccess
        —–\ProductRepository.cs
        —–\ProductCriteria.cs
        –\Model
        —–\Product.cs
        –\Services
        ——\ProductService.cs

        This way any change (like adding new method) – that spans 2 or more tiers can be done in one module with all the files in the same place. There are some disadvantages to this approach, but it has worked well for me.

        Eric

        Comment by Eric P — January 12, 2010 @ 6:57 am

  8. Hello,

    This is really a very good component. Thanks to shared it for everyone.

    I noticed an issue in the demo page: click on the search button several times causes the grid to show up multiple times within the page.

    Any idea why ?

    Thanks.

    Comment by dejoie01 — September 8, 2009 @ 2:00 am | Reply

    • It’s me again to give you more detail about my browser: I use IE8.

      Thanks.

      Comment by dejoie01 — September 8, 2009 @ 2:02 am | Reply

  9. The fix on pager.cs (//handle a case when user deleted all rows on last page) do not really works when u pass in an impossible Pager.CurrentPage value.

    This is because the in the CustomerController.cs -> FillGridData() function, grid was bind to criteria before _grid.Pager.Init(totalRows) is called. The fix does change the CurrentPage value in Pager but not the criteria.PageIndex value. In result, a error message shows (There are no customers that match specified criteria).

    To fix this problem, just insert this line after _grid.Pager.Init(totalRows):
    criteria.PageIndex = _grid.Pager.CurrentPage;

    Now, the last page will always shows regardless what ur Pager.CurrentPage value is.

    Comment by exiang — October 5, 2009 @ 3:29 am | Reply

  10. Eric,

    Great series and you even mentioned Eric Clapton.

    If this project has been discontinued or is on
    hold then the following question is a mute point.

    I noticed in your comments your approach would
    need to be changed for a data-base driven vs. in-memory application. Any plans for using your reusable grid against LING TO SQL, LightSpeed
    or the Entity Framework ?

    Thanks for Sharing.

    Comment by dc7048 — January 9, 2010 @ 10:04 pm | Reply

    • I took the original code and changed it to work with NHibernate. It is doable, though a fair bit of change is required, especially if you want to use a proper multi-layered design. Some of the NH Criteria ends up being quite ugly and the fact that you’re limited to only a single Entity (and any Entities that are associated with it) makes the grid somewhat limiting. I have yet to tackle a multi-tiered design, which will be even more complicated given the detachment requirements of entities across tiers. Still, since I don’t use the Grid to do updates, I imagine it wouldn’t be too complicated to detach the entities or to build the Grid around DTOs that get translated to entities at some point along the way.

      Still, the original project was an excellent design that suites 95% of my needs with the changes I employed and I work around the rest. Making it work with LINQ to SQL or Entity Framework would be near identical to making it work with NHibernate. I’m unfamiliar with LightSpeed, but I figure it’s another ORM, so it too should be doable.

      If you want, I can give you the basic structure of how to accomplish it, but the code would be too complex to include here.

      Cheers.

      Comment by Chris F — January 9, 2010 @ 10:49 pm | Reply

    • Hi,

      I would like to continue working on expanding this grid. I was able to use it on several client projects, so I think it is time to start adding some new features.
      I did find some issues that I would like to bring up in the next post.

      As to making the grid reusable againist LINQ to SQL or Ligth Speed:
      I tried to keep most of the data access logic outside controllers and UI. It should all be in Services and Criteria classes.
      Current Linq code – should work for both Linq to Sql and Entity Framework. It just needs to be hooked up to appropriate data context.
      As to LightSpeed, I am not too familiar with that.

      Eric

      Eric

      Comment by Eric P — January 12, 2010 @ 7:10 am | Reply

  11. Eric,

    Are the modifications mentioned above included
    in the latest source code download ?

    FYI,

    LightSpeed is an ORM which allows you to use LINQ since Microsoft has no LINQ to ORACLE support. Their site has some examples using MVC.
    I think there’s a free version which allows
    a limited number of tables.

    Thanks

    Comment by dc7048 — January 12, 2010 @ 11:35 am | Reply

  12. I noticed a lot of people have mentioned the idea of retrofitting this grid control to use a dynamic data source such as a relational database. I have used this very excellent grid control within a few small projects and modified the source code to utilize my custom data repository classes, which primarily implement LINQ to SQL for dynamic data access. You can see a LIVE example of this grid framework worked into a registered members search page here: http://wikilang.com/Members/Search. This website, Wikilang.com is entirely developed in ASP.NET MVC. Another example of how I have used the grid in a live environment can be seen at: http://wikilang.com/Review/Vote. I am happy to post my updates if anyone is interested. Thanks again for the great work!

    Comment by mvcjunkie — January 13, 2010 @ 7:50 pm | Reply

    • @mvcjunkie: Well, how did you do it?

      Thanks for the great tutorial. I’ve been searching forever for it. Haven’t quite got it working yet, but it’s getting there.

      Cheers

      Comment by dirkle — July 14, 2010 @ 8:05 pm | Reply

  13. Woow this 7 part grid story is just brilliant!!
    Why don’t you put it up on CodePlex.com? You would get a lot of attention ;)

    Comment by depechie — January 26, 2010 @ 2:45 am | Reply

  14. Great Tutorial Eric.
    Howeever i am facing problem using it in vs 2010 as it migrates it to MVC2 and uses paging or sorting features, it gives error at the following line

    ModelBinders.Binders.DefaultBinder = new Microsoft.Web.Mvc.DataAnnotations.DataAnnotationsModelBinder();

    This is because Microsoft.Web.MVC.DataAnnotations dll is missing in MVC2.

    Do you have any alternative to this. I want to use this grid after reading through your comparison between Jgrid,MVCCOntrib.
    Please help.

    Cheers
    Rajiv

    Comment by rajiv13579 — September 1, 2010 @ 8:03 am | Reply

    • Hello Eric, I’m just trying to learn your grid and it is difficult because I can’t get it to compile.

      Error 1 The type or namespace name ‘DataAnnotations’ does not exist in the namespace ‘Microsoft.Web.Mvc’ (are you missing an assembly reference?) …\Visual Studio 2010\Projects\MVCGridSample7\MvcGridSample\Global.asax.cs 7 25 MvcGridSample

      Would you please make the changes to the sample project so that it works with the released to manufacturing version of Visual Studio 2010 and repost for download.
      Thank you.

      Comment by dacke9000 — October 4, 2010 @ 11:55 am | Reply

  15. Will there be a part 8 that integrates things such as checkboxes and/or drop-down selectors for batch edits?

    Comment by Stephen Long — October 10, 2011 @ 2:25 pm | Reply

  16. Is it OK to use this code in a commercial app for work? Are there any licensing terms for it?

    Comment by welcometotherungle — June 26, 2012 @ 3:40 pm | Reply


RSS feed for comments on this post. TrackBack URI

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

The Rubric Theme Blog at WordPress.com.

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: