At this point all the features I planned in Part 1 have been implemented. It is time to make grid re-usable.
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

And the following pieces will vary:
- Column and row data in grid table
- Advanced Search form

Here is the screen shot of product list for comparison

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
-
MonoRail has nice functionality called sections. It is described here:
http://ayende.com/Blog/archive/2006/12/10/BuildingReusableUIComponentsInMonoRail.aspxIt would be great if this functionality would exist in MVC, but right now the only way to replicate it is by using several partial views.
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.




