Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

A Lightweight AjaxGrid User Control for MVC

5.00/5 (7 votes)
2 Jan 2013CPOL14 min read 31.6K   1.9K  
Implementing a grid user control in MVC4/Razor.

The grid in action

Introduction

Selecting and querying data from a list is one of the core parts of any web application, most commercial web applications will have dozens or even hundreds of lists and a pressing need to develop them quickly and efficiently. There are a number of different ways to implement such lists: traditionally templates have been implemented server-side on .aspx pages and bound to data controls; more recently MVC has supported the paradigm of templating client-side, supported by JSON feeds from the server. This project extends an approach pioneered by my previous implementation in CodeProject for ASP.NET A lightweight AJAX.NET-enabled grid control of combining the definition of the data source and the templating in a single XML configuration file.

The AjaxGrid uses AJAX (of course) to manage the flow of large amounts of grid data in the most efficient way possible - building HTML server-side and copying the resultant string directly into innerHTML. In this way, data transfer size and JavaScript runtime are both minimised. Furthermore, the grids are optimised for progressively providing large amounts of information server-side and supporting user strategies to quickly drill-down to the required subset by easy-to-use sort and filter functionality. Lastly, the grids also provide the ability to generate simple, user-customisable reports which can be exported for further work or future reference.

The grid supports the following user functionality:

  • Built-in sort and filter
  • "Auto-complete" and free-text filtering options
  • "Infinite scroll" support for large datasets, without paging
  • Export to CSV and JSON
  • Optional create new row, either in-place or through MVC routing
  • Optional data edit, either in-place or through MVC routing
  • Built-in undo/redo for in-place edits
  • Integrated with MVC routing

There is a lot of functional detail in supporting an effective user interface and some sort of user control structurally guarantees a compatible user interface throughout the application and that usability enhancements are consistently applied. MVC/Razor does not really support user controls, so part of this project has been to see how to integrate this functionality within the framework. Even if you are not going to use the grid functionality, you may be interested in how I have worked with MVC to provide such shared functionality.

Background

The simple approach to implementing shared functionality in multiple views and Controller classes is to implement a class derived from System.MVC.Controller and to derive the "page controller" classes from that. This is simple and works well in a demonstration project but is not really practical in the real world due to the unfortunate fact that you cannot derive a C# class from multiple classes, making it an administrative nightmare to derive selectively from more than one control. A more sustainable approach is to use an HtmlHelper to delegate the shared functionality to a shared Controller, which is hidden from the end-user.

The task in hand is to generate HTML dynamically without having to configure or repeat boilerplate or custom code in each view and controller that wishes to use the functionality; the configuration is entirely defined in an XML control file. The dynamic nature of this requirement has a number of consequences:

  • The AjaxGrid Controller renders output dynamically - apart from a single containing DIV - and so does not use views to render (although it could)
  • There are no models because the data is extracted dynamically as directed by the configuration file (although the code can generate and export JSON models dynamically)
  • LINQ is not a suitable vehicle for dynamically generating data queries, so ADO.NET is used
  • The shared functionality must be able to integrate with more "normal" MVC implementation and routing

Overall the functionality of the user control is managed by a static AjaGrid class. Its core functions are split into defining and loading data structures from the XML file in AjaxGrid.cs, generating SQL queries in AjaxGrid.DataAccess.cs, rendering HTML from the resultant SQL data source and the current user options in AjaxGrid.Render.cs and generating export formats for CSV and JSON in AjaxGrid.Export.cs. I think that there are arguments that using Asynchronous Controllers for database operations is not cost-effective, see: Should my database calls be asynchronous?. However, it should be quite easy to upgrade the AjaxGridController to asynchronous usage if you disagree.

Using the code

This implementation has been targeted at MVC4 on .NET4.5, but it should be easily rejigged to work with other versions of MVC. The attached sample project has had its packages folder emptied to save space in the zip file - download the project and then copy in the packages files from one of your default projects.

To simplify, I have included a zip of the source files with a folder structure as laid out in a default project, and a sample project implementation based on the default internet implementation without rewriting irrelevant parts such as the AccountController and Views. I also don't want to prejudge application-specific issues such as authentication, use of Entity Framework, bundling strategies for JavaScript and CSS, application build procedures or the inclusion of other frameworks - whether client-side or server-side. Also, I haven't implemented Create or Edit Actions to the Home Controller of sample program (I don't want to presume about how you scaffold model objects) and I changed the default font size to 60% for aesthetic reasons.

I have also included two grids based on the NorthWind sample database, one very simple and one with a bit more complexity in the SQL and supporting inline edits.

For the web application as a whole, you will need to include the following files:

  • App_Data/AjaxGrid.xml: the configuration file for your application
  • Controllers/AjaxGridController.cs: the Controller
  • AjaxGrid folder: contains the AjaxGrid class in four partial files for clarity
  • Scripts/AjaxGrid.js: you will need to include this (and jQuery+jQueryUI) in relevant views or bundle them in App_Start/Bundle.config (NET 4.5)
  • Content/AjaxGrid.css: you will need to include this in relevant views (and datepicker CSS for jQueryUI) or bundle it in App_Start/Bundle.config (NET 4.5)
  • Images/ajaxgrid: folder holding the image files
  • global.asax.cs: add AjaxGrid.Load(); to Application_Start().
  • web.config:Include a connection string "DataConnection" in web.config (as below)
XML
add name="DataConnection" connectionString="Trusted_Connection=true;
          MultipleActiveResultSets=True;Server=.\SQLEXPRESS;Database=NorthWind" 
    providerName="System.Data.SqlClient"

...note MultipleActiveResultSets=True. Ideally share this connection string with the rest of your application for effective caching of SQL connections.

Each participating view can then render its grid with a one-liner HtmlHelper as follows:

C#
@Html.DisplayAjaxGrid(@Url, 1, "Edit","Create",(AjaxGrid.ValidateEventHandler)ViewBag.Validator)

where @Url is the current URL, 1 is the gridId (as set in AjaxGrid.xml), "Edit" and "Create" are the optional calling Controller's Action Methods and ViewBag.Validator contains an optional delegate for an editable grid to perform custom data validation as below (note that an explicit cast was required).

C#
AjaxGrid.ValidateEventHandler v = (fieldname, value) =>
{
   if (fieldname == "book" && value.Length == 0)
            return fieldname + ": A valid string must be entered\r\n";
   return "";
};
ViewBag.Validator = v;

It can also be useful to set the gridId as a ViewBag parameter, if you want to vary the grid shown dynamically depending on user permissions or application state.

The XML configuration file

AjaxGrid.xml contains two main sections, a section defining system texts (such as static arrays to populate SELECT options) and a section to define the grids themselves. The XML format should make it quick to create large numbers of grids and reports.

Grids count up from zero as each one is defined, so you shouldn't move the grid definitions around if you want easy maintenance. Each grid is defined with the following attributes:

  • Id: numeric grid id for information only (counts up from zero)
  • Name: for documentation purposes only
  • DbTable: the core database table for the query
  • DbKey: the column containing the primary key for the DbTable
  • Height: to contain the grid in a scrolling div
  • NewRow: indicates if an icon to create a new row is desired

Within each grid are elements defining the query SQL and a list of column definitions.

The query SQL is the core definition of the grid contents, once that is in place it is straightforward to manage the column definitions. The query SQL should begin with either SELECTx or SELECTDISTINCTx, automatically converted to "SELECT TOP n" or "SELECT DISTINCT TOP n" to support infinite scroll. The SQL should also terminate with a WHERE clause to which extra filters can be tacked on, even if the clause is a dummy such as WHERE 1=1.

Each table column selected in the SQL is then defined (in the order determined by the SELECT statement) with the following attributes or sub-elements:

  • Header: the user-displayed header (also the column name in the database by default)
  • ColumnName: used where user-displayable header does not match the column name (for example, "Company Name" vs. "CompanyName")
  • SortName: used where a custom Sort column is required
  • Width: gives a fixed layout for the column
  • Editable: optional for in-place edit for this column
  • SelectOptionsArray: for SELECT dropdowns, name of static options array in Systems Text section
  • SelectOptionsSQL: for SELECT dropdowns, SQL to generate dataset of Id and Value for non-static options to be read from the database
  • ColumnDbKey: optional for in-place edit where the edited column is connected by a JOIN to another table
  • UpdateTable: optional for in-place edit where the edited column is connected by a JOIN to another table
  • CustomUpdateSQL: optional for in-place edit where custom update SQL is required
  • FreeTextSql: I haven't included a free text user interface, but this defines the free-text search columns in the database

Most columns don't need all of these parameters, a quick look at the two sample templates will give you a feel how all this works in practice.

Points of Interest

Enabling fast user sort and filter

As soon as a sort or filter operation has been performed by the end user, the operation is notified by an AJAX call and applied to the displayed grid. One desirable feature is the ability to "auto-complete" when a free-form input filter is being typed into; rather than provide a list of possible options, the grid is automatically filtered as the filter string is entered.

For this to work efficiently, two mechanisms are in force: firstly, there is a delay of half a second from the last keystroke before the filter is AJAX'ed to the server - this enables multiple keystrokes entered quickly to be packaged up and sent in a single AJAX request. Users quickly get a feel for this and type in only as many characters as are needed to create an efficient filter string. The exception is where a user presses TAB or ENTER, in those cases we can assume that filter input for that column is complete and the request is sent immediately. Secondly multiple AJAX filter calls are optimised at the client because once an AJAX filter request has been sent it cannot be recalled, even if more characters are subsequently entered. The grid JavaScript maintains a counter for filter requests and only renders the response to the last filter to be sent, eliminating surplus client-side work so that the client quickly renders when the most recent data arrives.

Free-text search is also easy to implement if your database has been set up to support it, I haven't included an user interface although there is a sample definition in AjaxGrid.xml of how to define the target columns for a free-text search. Note that "auto-complete" doesn't really work for free-text search because it operates on whole words and so a conventional Search button is probably best to launch a free-text filter AJAX command with text from an associated input field.

Data Security

Two key areas of data security are required, preventing SQL injection and XSS (Cross-Site Scripting). Any data which is entered by a user and which will be included in a SQL statement, either by data entry or by being used as a filter, must be attached to the SQL query by a parameter. XSS attacks are prevented by sanitising all outgoing user-generated data with HttpUtility.HtmlEncode.

Note that SQL is sanitised incoming, it must be to protect its SQL queries, while html is sanitised outgoing as is standard practice for MVC. It is probably more efficient, although maybe more risky, to sanitise user data one time incoming rather than many times outbound - but this depends on the confidence you have that text fields in your database are always pre-sanitised; the safer option of always sanitising outgoing user data is used here.

Most of the methods in AjaxGridController can only be called by AJAX, and this is enforced by a custom attribute [AjaxOnly], ensuring that the information that they deliver is restricted to the local referer. All AjaxGrid methods also enforce whether a GET, POST or DELETE operations are used to access their functionality.

The CSV export method, however, must be obtained by a postback - which makes it potentially callable from any referer: checking the referer is not the solution because referrers can be spoofed. If data security is important and this is implemented in the calling controller, then it is recommended to apply the [Authorize]attribute and the [ValidateAntiForgeryToken]attribute to this method. These attributes need application authentication and/or the view or controller setup (Html.AntiForgeryToken()) to work. Refer to this blog for a discussion of anti-forgery issues, including how to read anti-forgery tokens from jQuery, and here for information on how to automatically secure all controllers with the [Authorize] attribute.

AjaxGrid uses either strings or JSON to respond to its clients. However it is only in the case of Export to JSON that the JSON response can contain an array and this is a potential security risk using a GET, prevention of XSS attacks requires that a POST operation is mandated for the AjaxGrid's JSON export action.

The use of jQuery

Using jQuery, in line with good MVC practice, has much simplified the JavaScript for this module; you will need to include jQuery and jQueryUI (for the datepicker). No assumptions have been made as to how you should bundle up and/or use CDNs to manage JavaScript, that's very much an application choice.

Unobtrusive jQuery event management is great for documenting in one place where all the handled events are, but slightly fiddly for managing unbind without memory leaks - especially for server-generated html; keeping C# and JavaScript event management synchronized is a challenge (unless you were to generate JavaScript dynamically at the server also). I have ensured that all events in the header row are added and removed dynamically, but some of the content event handler interfaces are still server-created.

To compress the content data as much as possible, events are delegated to the content TD elements; column-specific flags are kept as "data-" attributes in the header cells - these can be found by matching the cellindex of the TD with the cellindex of the header row TDs.

Design patterns for inline edits

Almost by definition, a web application will be active for multiple, simultaneous users.

Editing individual fields or inserting complete new rows are generally low-risk in this environment, although be aware that if editing individual fields amounts to incrementing a count then you should generate some custom SQL to call a stored procedure implementing a lock-increment-unlock sequence to prevent races. Updating multiple fields from a model which is potentially "stale" is more complex - let Entity Framework manage that, although perhaps not so efficiently.

Because end users can make updates directly onto the database as each field is changed, undo/redo functionality is essential for a good user experience. This will automatically pop up when edits have been made.

If you configure the grid, or other unrelated applications using the same database tables, to allow inserts or deletes there is a risk that "pages" can become stale while performing infinite scroll - leading to missed or duplicate entries at "page" boundaries. This is why each paging action (scrolling to the end of the grid) initiates a complete re-rendering of the grid rather than just tacking a few more rows on each time. This is, I think, justified because users quickly learn that sorting and filtering is a much more efficient way of extracting data than wading through pages of data. Nevertheless you could opt for the latter strategy, in which case I would recommend the use of SQL Server 2012 OFFSET/FETCH rather than SELECT TOP.

Extending AjaxGrid

AjaxGrid is definitely built with extensions in mind. At the moment there are a relatively small range of column types supported (text, dropdown, date, link, delete, etc). To create a custom column type simply add a new name to the AjaxGrid.columnType enum. The column type is then available to be selected in the XML file. The new columntype will default to text format, but may vary in how it is filtered or rendered in the header or in the contents, the simplest way to catch each point of customisation is to search for a type such as columnType.Date and add a new case into the switch statements where the columnType appears where you want to add a new behaviour or rendering for your new columnType. You may also have to look at AjaxGrid.js if the column is to be editable and custom editing is required.

Conclusions

MVC has provided a framework which has enabled a surprisingly terse implementation of the AjaxGrid user control, given that it does not natively support user controls. The main problem encountered to providing a clean generic interface is that the control depends on the calling Controller as to whether the [Authorize] and/or the [ValidateAntiForgeryToken] attributes can (and therefore should) be applied.

History

Version 1.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)