In this article we are going to examine how to take advantage of the excellent feature provided by the Telerik RadGrid control to implement fast and responsive grids in Sitefinity modules. As you may have noticed, in Sitefinity 3.6 all grids displaying content are using this approach, making them highly performing controls which work (e.g. sort, page, delete) without postbacks. As our sample contacts pluggable module demonstrated, this feature is available to all modules, not only Generic Content based ones.
General overview of the implementation
Before we dig into the actual implementation details, let us examine the steps we will need to complete in order to have a client side data bound grid control in our module. Note that we will describe what we believe to be best practice at the moment and that not all steps are required. As long you understand the principle you are free to modify it as you wish.
- Implement a simplified version of the persistent data class to which you are going to bind
- Implement a webservice which the grid will use to work with data
- Implement the RadGrid, ClientSide templates and JavaScript for binding
Implement a simplified version of the persistent data class to which you are going to bind
The purpose of the client side data binding is to increase responsiveness and performance of the user interface. We are doing that by reducing the amount of information that needs to be sent from the server to the client (browser). With classic, server side data binding, every time a command on the grid is performed (e.g. user sorts the grid), server prepares the whole page and sends the whole page (including navigation and all the other controls that stay unchanged between the two commands) to the client. With client side data binding, server will send only the data used by the grid, thus making the whole process much faster.
Since the purpose of the whole implementation is to increase the performance, it is only natural to wonder if we can speed things up even more. Namely, perhaps we don’t need all the data on the client - perhaps we can send only the data that grid actually uses. This is where the simplified data classes come into the play. In our sample contacts module we have Contact class (Sample.Contacts.Data namespace) which implements IContact interface. The IContact interface implementation looks like this:
/// <summary> |
/// Defines what properties every Contacts object should implement |
/// </summary> |
public interface IContact |
{ |
/// <summary> |
/// Gets the ID. |
/// </summary> |
/// <value>The ID.</value> |
Guid ID { get; } |
/// <summary> |
/// Gets or sets the first name. |
/// </summary> |
/// <value>The first name.</value> |
string FirstName { get; set; } |
/// <summary> |
/// Gets or sets the last name. |
/// </summary> |
/// <value>The last name.</value> |
string LastName { get; set; } |
/// <summary> |
/// Gets or sets the title. |
/// </summary> |
/// <value>The title.</value> |
string Title { get; set; } |
/// <summary> |
/// Gets or sets the phone. |
/// </summary> |
/// <value>The phone.</value> |
string Phone { get; set; } |
/// <summary> |
/// Gets or sets the E mail. |
/// </summary> |
/// <value>The E mail.</value> |
string EMail { get; set; } |
/// <summary> |
/// Gets or sets the cell phone. |
/// </summary> |
/// <value>The cell phone.</value> |
string CellPhone { get; set; } |
/// <summary> |
/// Gets or sets the responsibilities. |
/// </summary> |
/// <value>The responsibilities.</value> |
string Responsibilities { get; set; } |
/// <summary> |
/// Gets or sets the photo. |
/// </summary> |
/// <value>The photo.</value> |
string Photo { get; set; } |
/// <summary> |
/// Gets or sets the department ID. |
/// </summary> |
/// <value>The department ID.</value> |
Guid DepartmentID { get; set; } |
/// <summary> |
/// Gets the department. |
/// </summary> |
/// <value>The department.</value> |
IDepartment Department { get; } |
/// <summary> |
/// Gets the name of the department. |
/// </summary> |
/// <value>The name of the department.</value> |
string DepartmentName { get; } |
} |
/// <summary> |
/// Lightweight representation of IContact object used for webservice data transfer |
/// </summary> |
public class SimpleContactItem |
{ |
/// <summary> |
/// Initializes a new instance of the <see cref="SimpleContactItem"/> class. |
/// </summary> |
/// <param name="contact">The contact.</param> |
public SimpleContactItem(IContact contact) |
{ |
this.id = contact.ID; |
this.firstName = contact.FirstName; |
this.lastName = contact.LastName; |
this.title = contact.Title; |
this.departmentName = contact.DepartmentName; |
} |
/// <summary> |
/// Gets or sets the ID. |
/// </summary> |
/// <value>The ID.</value> |
public Guid ID |
{ |
get { return this.id; } |
set { this.id = value; } |
} |
/// <summary> |
/// Gets or sets the first name. |
/// </summary> |
/// <value>The first name.</value> |
public string FirstName |
{ |
get { return this.firstName; } |
set { this.firstName = value; } |
} |
/// <summary> |
/// Gets or sets the last name. |
/// </summary> |
/// <value>The last name.</value> |
public string LastName |
{ |
get { return this.lastName; } |
set { this.lastName = value; } |
} |
/// <summary> |
/// Gets or sets the title. |
/// </summary> |
/// <value>The title.</value> |
public string Title |
{ |
get { return this.title; } |
set { this.title = value; } |
} |
/// <summary> |
/// Gets or sets the name of the department. |
/// </summary> |
/// <value>The name of the department.</value> |
public string DepartmentName |
{ |
get { return this.departmentName; } |
set { this.departmentName = value; } |
} |
private Guid id; |
private string firstName; |
private string lastName; |
private string title; |
private string departmentName; |
} |
Implement a webservice which the grid will use to work with data
Since we will bind our data to the grid on the client side we need to implement the webservice which we will call from the javascript. The purpose of the webservice is very similar to the manager class - but while manager class is used from our server side code, webservice will be used from our client side code.
If you are unfamiliar with webservices, please follow these steps to create a new webservice:
- In the Sitefinity website, navigate to ~/Sitefinity/Admin/Services folder
- Right click on the Services folder and select “Add new item”
- From the “Add new item” dialog, select “Web Service”, give your service a name and uncheck the “Place code in separate file” checkbox
- Delete all the code that Visual Studio generated and leave only the first line (declaration)
- Set the Class attribute to the full name of the class that will provide code for this webservice (we will implement this class in a moment). The webservice file for the Contacts module (ContactsService.asmx) looks like this:
<%@ WebService Language="C#" Class="Sample.Contacts.Services.ContactsService, Sample.Contacts" %> - Now we need to implement the webservice class. In your pluggable module project, create a new class and make sure its name corresponds to the name of the class you have defined in the .asmx file. For example in our sample contacts module this class is located in the Sample.Contacts project in the Services folder and has the name of ContactsServices.cs
- In this class you should implement all the methods that you will need to make your grid work properly (for example method for retrieving data, deleting data and so on). The webservice class implemented in ContactsService webservice looks like this:
/// <summary> /// Provides web service methods for Contacts module. /// </summary> [ScriptService] public class ContactsService { #region Properties /// <summary> /// Gets or sets the filter. /// </summary> public string FirstLetterFilter { get { return this.firstLetterFilter; } set { this.firstLetterFilter = value; } } /// <summary> /// Gets or sets the provider name. /// </summary> public string ProviderName { get { return this.providerName; } } #endregion /// <summary> /// Gets contacts for client-side data binding. /// </summary> /// <param name="firstLetterFilter">The first letter filter.</param> /// <param name="providerName">Name of the provider.</param> /// <returns>GridBindingData object.</returns> [WebMethod] public GridBindingData GetContacts(string firstLetterFilter, string providerName) { this.firstLetterFilter = firstLetterFilter; this.providerName = providerName; return this.GetContacts(firstLetterFilter, new ContactsManager(providerName)); } /// <summary> /// Gets the contacts. /// </summary> /// <param name="firstLetterFilter">The first letter filter.</param> /// <param name="manager">The manager.</param> /// <returns></returns> public GridBindingData GetContacts(string firstLetterFilter, ContactsManager manager) { IList list; if (!string.IsNullOrEmpty(firstLetterFilter)) { list = manager.GetContacts(firstLetterFilter); } else { list = manager.GetContacts(); } int count = list.Count; List<object> dataList = new List<object>(count); foreach (IContact contact in list) { dataList.Add(new SimpleContactItem(contact)); } return new GridBindingData(dataList, count); } ///<summary> /// Deletes a contact. ///</summary> ///<param name="contentId">Id of the contact to be deleted</param> ///<param name="providerName">Name of the provider to which contact belongs to</param> [WebMethod] public void DeleteContact(Guid contactId, string providerName) { ContactsManager manager = new ContactsManager(providerName); manager.DeleteContact(contactId); } #region Private fields private string firstLetterFilter; private string providerName; #endregion }
- In order to make a class a webservice class you need to declare ScriptService attribute on it.
- Every method that should be accessible from the client side must have a WebMethod attribute declared on it.
- The data is being returned through GridBindingData class, which has our data source and count arguments.
Implement the RadGrid, ClientSide templates and JavaScript for binding
When it comes to implementing RadGrid, there are two basic approaches. You can go the standard way as it is demonstrated in the Telerik RadGrid demos or you can go the Sitefinity way where we provide some Sitefinity specific enhancements. In this article we will illustrate the Sitefinity way.
Let us start by opening a template where we wish to have the grid and declare the control. Note that if you are going the Sitefinity way all your columns should be declared as template columns. Here is how the RadGrid declaration looks like in the ContactsItemList.ascx template of our sample contacts module.
<telerik:RadGrid ID="contactsGrid" runat="server" AutoGenerateColumns="false" EnableViewState="false" Skin="SitefinityItems" EnableEmbeddedSkins="false"> |
<MasterTableView AllowMultiColumnSorting="false" CssClass="listItems listItemsBindOnClient" Width="98%"> |
<Columns> |
<telerik:GridTemplateColumn UniqueName="LastName" HeaderText="Last Name" /> |
<telerik:GridTemplateColumn UniqueName="FirtsName" HeaderText="First Name" /> |
<telerik:GridTemplateColumn UniqueName="Title" HeaderText="Title" /> |
<telerik:GridTemplateColumn UniqueName="DepartmentName" HeaderText="Department" /> |
<telerik:GridTemplateColumn UniqueName="Edit" HeaderText="Edit" ItemStyle-CssClass="gridActions edit" /> |
<telerik:GridTemplateColumn UniqueName="Delete" HeaderText="Delete" ItemStyle-CssClass="gridActions delete" /> |
</Columns> |
</MasterTableView> |
<ClientSettings> |
<ClientEvents OnCommand="RadGrid_Command" OnRowDataBound="RadGrid_RowDataBound" /> |
</ClientSettings> |
</telerik:RadGrid> |
<telerik:ClientTemplatesHolder ID="GridTemplates" runat="server"> |
<telerik:ClientTemplate Name="LastName" runat="server"> |
{#LastName#} |
</telerik:ClientTemplate> |
<telerik:ClientTemplate Name="FirtsName" runat="server"> |
{#FirstName#} |
</telerik:ClientTemplate> |
<telerik:ClientTemplate Name="Title" runat="server"> |
{#Title#} |
</telerik:ClientTemplate> |
<telerik:ClientTemplate Name="DepartmentName" runat="server"> |
{#DepartmentName#} |
</telerik:ClientTemplate> |
<telerik:ClientTemplate Name="Edit" runat="server"> |
<a href="<%= Parent.Parent.ContactEditUrl %>">Edit</a> |
</telerik:ClientTemplate> |
<telerik:ClientTemplate Name="Delete" runat="server"> |
<a href="javascript:if(confirm('Are you sure you want to delete this contact?')) DeleteContent('{#ID#}')">Delete</a> |
</telerik:ClientTemplate> |
</telerik:ClientTemplatesHolder> |
Inside of every client template we are free to implement any kind of html we wish to (not that server controls are not allowed). Once the grid is bound to data, it will take a look at the ClientTemplatesHolder and replace its cells with the one defined in the ClientTemplates.
You may have noticed a special kind of syntax in the client templates: {#LastName#}. Namely, if you go back to the SimpleContactItem class implementation, you will notice that we have a property LastName in that class. Namely, {#PropertyName#} syntax is used to designate where the value from the data item should be placed when data is bound. You can think of it as an equivalent of the <%# Eval(“PropertyName”) %>.
Finally, we come to the JavaScript functions which do the actual binding. In the ContactsItemList.ascx template you can find all the JavaScript used for contacts module, but for the purpose of this article we will extract (and simplify) only the functions that are really needed.
var dataProviderName = "<%= Parent.Parent.ProviderName %>"; |
var gridTemplates = ClientTemplates.GetSet("<%= GridTemplates.ClientID %>"); |
function loadData() { |
var firstLetterFilterField = document.getElementById('<%= FirstLetterFilterField.ClientID %>'); |
DataBindGrid(firstLetterFilterField.value); |
} |
Sys.Application.add_load(loadData); |
function DataBindGrid(firstLetterFilterAsSQL) { |
Sample.Contacts.Services.ContactsService.GetContacts(firstLetterFilterAsSQL, dataProviderName, updateGrid, OnFailed); |
} |
function updateGrid(result) { |
var tableView = $find("<%= contactsGrid.ClientID %>").get_masterTableView(); |
tableView.set_virtualItemCount(result.Count); |
tableView.set_dataSource(result.Data); |
tableView.dataBind(); |
} |
function RadGrid_RowDataBound(sender, args) { |
var dataItem = args.get_dataItem(); |
var item = args.get_item(); |
var columns = item.get_owner().get_columns(); |
var cells = args.get_item().get_element().cells; |
for (var i = 0; i < cells.length; i++) { |
var cell = cells[i]; |
var html = gridTemplates.Replace(columns[i].get_element().UniqueName, dataItem); |
if (html != null) |
if( html != "") |
cell.innerHTML = html; |
else |
cell.innerHTML = " "; |
} |
} |
function OnFailed(error) { |
alert("Stack Trace: " + error.get_stackTrace() + "/r/n" + |
"Error: " + error.get_message() + "/r/n" + |
"Status Code: " + error.get_statusCode() + "/r/n" + |
"Exception Type: " + error.get_exceptionType() + "/r/n" + |
"Timed Out: " + error.get_timedOut()); |
} |
Sys.Application.add_load(loadData);
Next, in the loadData function we check if there is a first letter filter present (remember that the contacts module supports filtering contacts by the first letter of the contact’s last name) and call DataBindGrid function.
DataBindGrid function calls the method on our webservice which we have defined at the beginning of this article and specifies successful callback function (“updateGrid”) and failure callback function (“OnFailed”).
The updateGrid function (successful callback) will return us the result object (GridBindingData object, check the webservice implementation if unsure) in which we have Data and Count objects. We’ll take advantage of the RadGrid client side API then and bind this data to the grid.
Finally, we also have a client side handler for the RadGrid’s RowDataBound client side event. In this function, we are simply performing the substitution of the grid’s template column content with the one defined in our ClientTemplatesHolder.
Conclusion
While the approach may seem a bit exhaustive at the moment, you will soon notice that after the first implementation it becomes very natural. The benefits of this approach will surely overshadow the effort invested.