Thursday, November 5, 2009 5:41 PM Kazi Manzur Rashid

Using Telerik MVC Grid in CRUD Scenario

I am proud to inform you that yesterday we released our Q3 2009 version of Telerik Extensions for ASP.NET MVC. As promised this release includes:

  • Grid
  • Menu
  • PanelBar
  • TabStrip

You can find the live version and source codes in the following locations:

Also checkout the product home page and part-II of Tod’s unofficial faq.

In this post, I will show you how to create a basic CRUD(Create/Read/Update/Delete) application with our new MVC Grid. I will be using both Entity Framework v1.0 with the default web form view engine and NHibernate with Spark to create the CRUD screens for the Customer table of Northwind database.

Lets start with the Entity Framework and Default View Engine.

First, lets create a new ASP.NET MVC application and name it as Northwind, when Visual Studio prompts you for the Unit Test project, just skip it. Now, right click the Models folder and add an Entity Data Model and name it as Database. Next, drag the Customers table of the Northwind database from Server Explorer in the VS design surface.

Now, we will create a generic repository to access the database, lets define the interface first.

public interface IRepository<TEntity, TId>
{
    void Add(TEntity entity);

    void Delete(TId id);

    TEntity Get(TId id);

    IEnumerable<TEntity> All();
}

And the implementation:

public class Repository<TEntity, TId> : IRepository<TEntity, TId>
{
    private readonly Database database;

    public Repository() : this(EntityFrameworkObjectContextPerRequest.CurrentDatabase)
    {
    }

    public Repository(Database database)
    {
        this.database = database;
    }

    public void Add(TEntity entity)
    {
        database.AddObject(TypeName(), entity);
    }

    public void Delete(TId id)
    {
        database.DeleteObject(Get(id));
    }

    public TEntity Get(TId id)
    {
        var typeName = TypeName();

        var keyName = database.MetadataWorkspace
                              .GetItems<EntityType>(DataSpace.CSpace)
                              .Single(meta => meta.Name == typeName)
                              .KeyMembers[0].Name;

        var param = Expression.Parameter(typeof(TEntity), "x");
        var left = Expression.Property(param, keyName);
        var right = Expression.Constant(id);
        var equal = Expression.Equal(left, right);

        var predicate = Expression.Lambda<Func<TEntity, bool>>(equal, param).Compile();

        return All().SingleOrDefault(predicate);
    }

    public IEnumerable<TEntity> All()
    {
        return database.CreateQuery<TEntity>("[" + TypeName() + "]");
    }

    private static string TypeName()
    {
        return typeof(TEntity).Name;
    }
}

Very basic stuff,except the Get() method, since we are creating a generic repository which entity’s identifier is not known, we are using the Entity Framework meta data for building the identifier lambda expression. Please note that the stuffs that I have shown in the above has nothing to do with our Grid, we can directly use the Entity Framework ObjectContext in our Controller instead of this repository.

Now, right click the Controllers folder and a new Controller named CustomerController with the following code:

[HandleError]
public class CustomerController : Controller
{
    private readonly IRepository<Customers, string> repository;

    public CustomerController() : this(new Repository<Customers, string>())
    {
    }

    public CustomerController(IRepository<Customers, string> repository)
    {
        this.repository = repository;
    }

    public ActionResult Index()
    {
        return View(repository.All());
    }

    public ActionResult Details(string id)
    {
        return View(repository.Get(id));
    }

    public ActionResult Create()
    {
        return View();
    }

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Create([Bind(Exclude = "CustomerId")]Customers customer)
    {
        customer.CustomerID = CreateNewId();
        repository.Add(customer);

        return RedirectToAction("Index");
    }

    public ActionResult Edit(string id)
    {
        return View(repository.Get(id));
    }

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Edit(string id, FormCollection collection)
    {
        var customer = repository.Get(id);

        UpdateModel(customer, collection.ToValueProvider());

        return RedirectToAction("Index");
    }

    public ActionResult Delete(string id)
    {
        return View(repository.Get(id));
    }

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Delete(string id, string confirm)
    {
        repository.Delete(id);

        return RedirectToAction("Index");
    }

    // Not a bullet proof method, but it should work for the demo
    private string CreateNewId()
    {
        Func<int, string> generateId = length =>
                                       {
                                           string generatingId = string.Empty;
                                           Random rnd = new Random();

                                           for (int i = 1; i <= length; i++)
                                           {
                                               int characterCode = rnd.Next(65, 90); // Only uppercase;
                                               generatingId += Convert.ToChar(characterCode).ToString();
                                           }

                                           return generatingId;
                                       };

        string id = generateId(5);

        while (repository.Get(id) != null)
        {
            id = generateId(5);
        }

        return id;
    }
}

There are one more thing we have to do before we start working on the Views, as you can see in the above the Repository uses a special class EntityFrameworkObjectContextPerRequest in the constructor to get the Database reference. EntityFrameworkObjectContextPerRequest is a HttpModule which creates a new instance of the Database in the BeginRequest and stores it in the HttpContext.Items to reuse it in the same request and in the EndRequest it commit the changes and Dispose the Database instance. Here is the code:

public class EntityFrameworkObjectContextPerRequest : IHttpModule
{
    private static readonly string key = typeof(Database).FullName;

    public static Database CurrentDatabase
    {
        get
        {
            return HttpContext.Current.Items[key] as Database;
        }
    }

    public void Init(HttpApplication context)
    {
        context.BeginRequest += OnBeginRequest;
        context.EndRequest += OnEndRequest;
    }

    public void Dispose()
    {
    }

    private static void OnBeginRequest(object sender, EventArgs e)
    {
        var database = HttpContext.Current.Items[key] as Database;

        if (database == null)
        {
            database = new Database();
            HttpContext.Current.Items[key] = database;
        }
    }

    private static void OnEndRequest(object sender, EventArgs e)
    {
        var database = HttpContext.Current.Items[key] as Database;

        if (database != null)
        {
            database.SaveChanges();
            database.Dispose();
        }
    }
}

Now, add the Telerik.Web.Mvc.dll from the binary folder that you have downloaded previously.

Once you include the dll, the first thing you should do is register the HttpHandler for the Web Assets, lets put these lines in the web.config file (I am only showing the relevant section of the web.config):

<?xml version="1.0"?>
<configuration>
    <system.web>
        <httpHandlers>
            <add verb="GET,HEAD" path="asset.axd" validate="false" type="Telerik.Web.Mvc.WebAssetHttpHandler, Telerik.Web.Mvc" />
        </httpHandlers>
    </system.web>
    <system.webServer>
        <handlers>
            <remove name="AssetHandler" />
            <add name="AssetHandler" preCondition="integratedMode" verb="GET,HEAD" path="asset.axd" type="Telerik.Web.Mvc.WebAssetHttpHandler, Telerik.Web.Mvc" />
        </handlers>
    </system.webServer>
</configuration>

Now, Copy the telerik.common.min.css, telerik.vista.min.css and Vista from the download Content folder to your project Content folder. We also have to copy few javascripts files, please copy the telerik.common.min.js, telerik.grid.min.js and telerik.grid.filtering.min.js from the downloaded Scripts folder to your project Scripts folder. Once you are done, it will look very similar to the following:

SE

Now open the Site.Master from Views/Shared directory and put the following lines in the head section:

<head runat="server">
    <title><asp:ContentPlaceHolder ID="TitleContent" runat="server" /></title>
    <%= Html.Telerik().StyleSheetRegistrar()
                      .DefaultGroup(group => group.Add("Site.css")
                                                  .Add("telerik.common.css")
                                                  .Add("telerik.vista.css")
                                                  .Combined(true)
                                   ) %>
</head>

This will ensure the stylesheets files that we have added in the previous steps will be included when the page renders. We have do the same for javascript files, but this time we will only add a ScriptRegistrar at the bottom of the page, but we will not add the javascript files like we did for the stylesheet files. Lets put the following code at the bottom of the page:

    <% Html.Telerik().ScriptRegistrar()
                     .DefaultGroup(group => group.Combined(true).Compress(true))
                     .Render(); %>
</body>
</html>

Now, go back to the Index method of the CustomerController, right click and select Add View, this will show the Add View Dialog. When the dialog appears, turn on the strongly typed view and select the Customers class as the view data class and finally select the List in View content drop down.

AV

Once the view is generated replace the generated code with the following:

<% Html.Telerik()
       .Grid(Model)
       .Name("customers")
       .PrefixUrlParameters(false)
       .Columns(columns =>
                {
                    columns.Add(c =>
                                {
                                    %>
                                        <%= Html.ActionLink("Edit", "Edit", new { id = c.CustomerID })%>
                                        <%= Html.ActionLink("Delete", "Delete", new { id = c.CustomerID })%>
                                    <%
                                }).Title("Action");

                    columns.Add(c => c.CompanyName).Width(200);
                    columns.Add(c => c.ContactName);
                    columns.Add(c => c.Address);
                    columns.Add(c => c.City);
                    columns.Add(c => c.PostalCode);
                    columns.Add(c => c.Country);
                    columns.Add(c => c.Phone);
                    columns.Add(c => c.Fax);
                })
        .Filterable()
        .Sortable(sort => sort.SortMode(GridSortMode.MultipleColumn))
        .Pageable()
        .Scrollable(scrolling => scrolling.Height(250))
        .Render(); %>

Now press F5 and navigate to /Customers, you will find a nice looking Grid like the following:

GRID

As you can see the Grid has out of the box support for data sorting/filtering/paging, all you have to do is set an IEnumerable<T> as its DataSource, the rest is taken care by itself. If the IEnumerable<T> has a Linq Provider the data will be processed at the database level. If you run the SQL Profiler for the above example and navigate to page 2 you will find the following SQL is generated:

SELECT TOP (10) 
[Project1].[CustomerID] AS [CustomerID], 
[Project1].[CompanyName] AS [CompanyName], 
[Project1].[ContactName] AS [ContactName], 
[Project1].[ContactTitle] AS [ContactTitle], 
[Project1].[Address] AS [Address], 
[Project1].[City] AS [City], 
[Project1].[Region] AS [Region], 
[Project1].[PostalCode] AS [PostalCode], 
[Project1].[Country] AS [Country], 
[Project1].[Phone] AS [Phone], 
[Project1].[Fax] AS [Fax]
FROM ( SELECT [Project1].[C1] AS [C1], [Project1].[CustomerID] AS [CustomerID], [Project1].[CompanyName] AS [CompanyName], [Project1].[ContactName] AS [ContactName], [Project1].[ContactTitle] AS [ContactTitle], [Project1].[Address] AS [Address], [Project1].[City] AS [City], [Project1].[Region] AS [Region], [Project1].[PostalCode] AS [PostalCode], [Project1].[Country] AS [Country], [Project1].[Phone] AS [Phone], [Project1].[Fax] AS [Fax], row_number() OVER (ORDER BY [Project1].[C1] ASC) AS [row_number]
	FROM ( SELECT 
		CASE WHEN ([Extent1].[CustomerID] IS NOT NULL) THEN [Extent1].[CustomerID] END AS [C1], 
		[Extent1].[CustomerID] AS [CustomerID], 
		[Extent1].[CompanyName] AS [CompanyName], 
		[Extent1].[ContactName] AS [ContactName], 
		[Extent1].[ContactTitle] AS [ContactTitle], 
		[Extent1].[Address] AS [Address], 
		[Extent1].[City] AS [City], 
		[Extent1].[Region] AS [Region], 
		[Extent1].[PostalCode] AS [PostalCode], 
		[Extent1].[Country] AS [Country], 
		[Extent1].[Phone] AS [Phone], 
		[Extent1].[Fax] AS [Fax]
		FROM [dbo].[Customers] AS [Extent1]
	)  AS [Project1]
)  AS [Project1]
WHERE [Project1].[row_number] > 10
ORDER BY [Project1].[C1] ASC

The rest of the screens are pretty simple, just use the ASP.NET MVC Add View dialog to implement them and that is it for the Entity Framework and Web form view engine.

Now, lets do the same for NHibernate and Spark.

First, Create a new ASP.NET MVC Project named AltNorthwind and skip the Unit Test Project. Now, the add following the references:

  • NHibernate
  • NHibernate.Linq
  • FluentNHibernate
  • Spark.

Now, right click the Models folder add a new class named Customers and create simple getter/setter properties for the Customers table fields, also ensure the properties are declared as virtual.Next, Add a new class named CustomerMap which will be used by the NHibernate to map the Customers class with the Customer table.

public class CustomerMap : ClassMap<Customers>
{
    public CustomerMap()
    {
        Id(c => c.CustomerID).Length(5).Not.Nullable();
        Map(c => c.CompanyName).Length(40).Not.Nullable();
        Map(c => c.ContactName).Length(30);
        Map(c => c.ContactTitle).Length(30);
        Map(c => c.Address).Length(60);
        Map(c => c.City).Length(15);
        Map(c => c.Region).Length(15);
        Map(c => c.PostalCode).Length(10);
        Map(c => c.Country).Length(15);
        Map(c => c.Phone).Length(24);
        Map(c => c.Fax).Length(24);
    }
}

Lets create the Repository, we will be using the same IRepository interface that we used in the Entity Framework section.

public class Repository<TEntity, TId> : IRepository<TEntity, TId>
{
    private readonly ISession session;

    public Repository() : this(NHibernateSessionPerRequest.CurrentSession)
    {
    }

    public Repository(ISession session)
    {
        this.session = session;
    }

    public void Add(TEntity entity)
    {
        session.SaveOrUpdate(entity);
    }

    public void Delete(TId id)
    {
        session.Delete(Get(id));
    }

    public TEntity Get(TId id)
    {
        return session.Get<TEntity>(id);
    }

    public IEnumerable<TEntity> All()
    {
        return session.Linq<TEntity>();
    }
}

As we can see that we are using the NHibernate ISession which is passed in the constructor similar to the Entity Framework version, we are also using an HttpModule for managing NHibernate session.

public class NHibernateSessionPerRequest : IHttpModule
{
    private static readonly ISessionFactory sessionFactory = CreateSessionFactory();

    public static ISession CurrentSession
    {
        get
        {
            return sessionFactory.GetCurrentSession();
        }
    }

    public void Init(HttpApplication context)
    {
        context.BeginRequest += OnBeginRequest;
        context.EndRequest += OnEndRequest;
    }

    public void Dispose()
    {
    }

    private static void OnBeginRequest(object sender, EventArgs e)
    {
        var session = sessionFactory.OpenSession();

        session.BeginTransaction();

        CurrentSessionContext.Bind(session);
    }

    private static void OnEndRequest(object sender, EventArgs e)
    {
        var session = CurrentSessionContext.Unbind(sessionFactory);

        if (session != null)
        {
            try
            {
                session.Transaction.Commit();
            }
            catch
            {
                session.Transaction.Rollback();
            }
            finally
            {
                session.Close();
                session.Dispose();
            }
        }
    }

    private static ISessionFactory CreateSessionFactory()
    {
        const string ConnectionStringName = "NorthwindConnectionString";

        var configuration = Fluently.Configure()
                                    .Database(MsSqlConfiguration.MsSql2005.ConnectionString(x => x.FromConnectionStringWithKey(ConnectionStringName)))
                                    .ExposeConfiguration(c => c.SetProperty("current_session_context_class", "web"))
                                    .Mappings(m => m.FluentMappings.AddFromAssemblyOf<NHibernateSessionPerRequest>());

        return configuration.BuildSessionFactory();
    }
}

The Controller is same as the Entity Framework version. Now, lets configure the Spark view engine, first put the following lines in the web.config file.

<configSections>
    <section name="spark" type="Spark.Configuration.SparkSectionHandler, Spark"/>
</configSections>
<spark>
    <compilation debug="true">
        <assemblies>
            <add assembly="AltNorthwind"/>
            <add assembly="System.Web.Mvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
        </assemblies>
    </compilation>
    <pages automaticEncoding="true">
        <namespaces>
            <add namespace="System.Collections.Generic"/>
            <add namespace="System.Linq"/>
            <add namespace="System.Web.Mvc"/>
            <add namespace="System.Web.Mvc.Html"/>
            <add namespace="System.Web.Routing"/>
            <add namespace="Telerik.Web.Mvc.UI"/>
        </namespaces>
    </pages>
</spark>

And register the view engine in the global.asax, like the following:

protected void Application_Start()
{
    RegisterRoutes(RouteTable.Routes);
    SparkEngineStarter.RegisterViewEngine();
}

Now, Copy the telerik.common.min.css, telerik.vista.min.css and Vista from the download Content folder to your project Content folder and  telerik.common.min.js, telerik.grid.min.js and telerik.grid.filtering.min.js from the downloaded Scripts folder to your project Scripts folder, same as we did in the Entity Framework version.

Now, lets add an application.spark file in Views/Shared for the stylesheets and javascripts and add the following code in the head section for the stylesheets registration:

<% Html.Telerik().StyleSheetRegistrar()
                  .DefaultGroup(group => group.Add("Site.css")
                                              .Add("telerik.common.css")
                                              .Add("telerik.vista.css")
                                              .Combined(true)
                               ) 
                   .Render(); %>

And at the bottom for the javascript files:

<% Html.Telerik().ScriptRegistrar()
                 .DefaultGroup(group => group.Combined(true).Compress(true))
                 .Render(); %>

Now, create a new folder named Customer under the Views folder and add new spark file named Index.spark and put the following code:

<content name="TitleContent">
    Customers
</content>
<content name="MainContent">
    <h2>Customers</h2>
    <viewdata model="IEnumerable[[Customers]]"/>
    <% Html.Telerik()
           .Grid(Model)
           .Name("customers")
           .PrefixUrlParameters(false)
           .Columns(columns =>
                    {
                        columns.Add(c =>
                                    {
                                        %>
                                            !{ Html.ActionLink("Edit", "Edit", new { id = c.CustomerID })}
                                            !{ Html.ActionLink("Delete", "Delete", new { id = c.CustomerID })}
                                        <%
                                    }).Title("Action");

                        columns.Add(c => c.CompanyName).Width(200);
                        columns.Add(c => c.ContactName);
                        columns.Add(c => c.Address);
                        columns.Add(c => c.City);
                        columns.Add(c => c.PostalCode);
                        columns.Add(c => c.Country);
                        columns.Add(c => c.Phone);
                        columns.Add(c => c.Fax);
                    })
            .Sortable(sort => sort.SortMode(GridSortMode.MultipleColumn))
            .Pageable()
            .Scrollable(scrolling => scrolling.Height(250))
            .Render(); %>
    <p>!{ Html.ActionLink("Create New", "Create") }</p>
</content>

Once you are done, press F5 and navigate to /Customer, you will find a nice looking Grid. I am skipping the rest of the views as they are pretty easy to implement, but you will find the complete code in the following link.

That’s it for today, I hope I will be posting more on these components, so stay tuned.

Source Codes: TelerikMVCGridCRUDDemo.zip (Sorry for the earlier inconvenience, just made it public, it is accessible now.)

Shout it
Filed under: , , , , , ,

Comments

# re: Using Telerik MVC Grid in CRUD Scenario

Thursday, November 5, 2009 8:04 AM by Erik

The source link is not working :(

Besides that, awesome.

# re: Using Telerik MVC Grid in CRUD Scenario

Thursday, November 5, 2009 8:29 AM by Kazi Manzur Rashid

Thanks Eric for pointing that out, just fixed it, you should be able to download it now.

# re: Using Telerik MVC Grid in CRUD Scenario

Thursday, November 5, 2009 8:55 AM by PB

Awesome post, thanks!

# re: Using Telerik MVC Grid in CRUD Scenario

Friday, November 6, 2009 4:54 AM by Moim Hossain

Looks good. BTW What about grouping? Is there any plan for it?

# re: Using Telerik MVC Grid in CRUD Scenario

Friday, November 6, 2009 8:11 AM by Kazi Manzur Rashid

Grouping, Inline Edit and few other extra enhancements would be added in the 2010 Q1 release.

# re: Using Telerik MVC Grid in CRUD Scenario

Friday, November 6, 2009 5:22 PM by mike kidder

I'm glad I read ahead in the blog post -- I was wasting a lot of time looking for "EntityFrameworkObjectContextPerRequest" in the Entity assemblie thinking I missed something.  :)

Fantastic stuff, keep to the great post and thanks for sharing!

# re: Using Telerik MVC Grid in CRUD Scenario

Wednesday, November 11, 2009 3:10 AM by Gleb

Great sample, but code displayed here is different from what available for download:( So the code as it is works only in its download version - it seems to me you forgot to mention some changes in web.config on this page.

Anyway, thanks

# re: Using Telerik MVC Grid in CRUD Scenario

Friday, November 13, 2009 12:43 PM by Michael Munnis

I've added a dropdownlist helper to my view. When the edit controller code executes, the UpdateModel(Entity, collection.ToValueProvider()) doesn't save the value that changed in the dropdown.  The FormsCollection seems to contain all the updated values.  I must be missing something fundamental here.