Harness the Power of ASP.NET MVC, Web Api, OData, Kendo UI & RequireJS to Build an Easy & Maintainable SPA (for the .NET Developer) – Published

Apologize for the delay, the original article: “Harness the Power of ASP.NET MVC, Web Api, OData, Kendo UI & RequireJS to Build an Easy & Maintainable SPA (for the .NET Developer)” has now been published as “A .NET Developer Primer to Single-Page Applications (SPA)“.

Download: http://lelong37.files.wordpress.com/2014/03/msdn-mag-1403.pdf
Online: http://msdn.microsoft.com/en-us/magazine/dn605877.aspx
Source code: https://easyspa.codeplex.com
Live Demo: http://easyspa.azurewebsites.net/home/spa#/customer/index

3-4-2014 1-36-58 AM

Upgrading to Async with Entity Framework, Web Api, OData AsyncEntitySetController, Kendo UI, Glimpse & Generic Unit of Work Repository Framework v2.0

Update [11/18/2013]: Added mocked DbContext and DbSet and example Unit Tests using the mocks, download v2.1 https://genericunitofworkandrepositories.codeplex.com.

Thanks to everyone for allowing us to give back to the .NET community, we released Generic Unit of Work and Repository Framework v1.0 for four weeks and received 655 downloads and 4121 views. This post will also serve as the documentation for release v2.0. Thanks to Ivan (@ifarkas) for helping out on the Async development, Ken for debugging the Unit of Work life cycle management for use in web applications with DI & IoC (specifically with Entlib Unity v3.0) and scaling the framework to handle Bounded DbContexts, and to the Glimpse Team (@nickswan ) for helping out on getting Glimpse MVC4 working with MVC5, and providing guidance on how to leverage Glimpse EF6 to view SQL queries from EF.

This will be part six of a six part series of blog posts.

  1. Modern Web Application Layered High Level Architecture with SPA, MVC, Web API, EF, Kendo UI, OData
  2. Generically Implementing the Unit of Work & Repository Pattern with Entity Framework in MVC & Simplifying Entity Graphs
  3. MVC 4, Kendo UI, SPA with Layout, View, Router & MVVM
  4. MVC 4, Web API, OData, EF, Kendo UI, Grid, Datasource (CRUD) with MVVM
  5. MVC 4, Web API, OData, EF, Kendo UI, Binding a Form to Datasource (CRUD) with MVVM
  6. Upgrading to Async with Entity Framework, MVC, OData AsyncEntitySetController, Kendo UI, Glimpse & Generic Unit of Work Repository Framework v2.0

We’ll continue on from the most recent post in this series, you can do a quick review of it here http://blog.longle.net/2013/06/19/mvc-4-web-api-odata-ef-kendo-ui-binding-a-form-to-datasource-crud-with-mvvm-part. Now let’s get right into it, by first taking a look at what was all involved on the server side.

First off let’s take a quick look and the changes we made to our DbContextBase to support Async.

Repository.DbContextBase.cs

Before


    public class DbContextBase : DbContext, IDbContext
    {
        private readonly Guid _instanceId;

        public DbContextBase(string nameOrConnectionString) : base(nameOrConnectionString)
        {
            _instanceId = Guid.NewGuid();
        }

        public Guid InstanceId
        {
            get { return _instanceId; }
        }

        public void ApplyStateChanges()
        {
            foreach (var dbEntityEntry in ChangeTracker.Entries())
            {
                var entityState = dbEntityEntry.Entity as IObjectState;
                if (entityState == null)
                    throw new InvalidCastException("All entites must implement the IObjectState interface, " +
                                                   "this interface must be implemented so each entites state can explicitely determined when updating graphs.");

                dbEntityEntry.State = StateHelper.ConvertState(entityState.State);
            }
        }

        public new IDbSet<T> Set<T>() where T : class
        {
            return base.Set<T>();
        }

        protected override void OnModelCreating(DbModelBuilder builder)
        {
            builder.Conventions.Remove<PluralizingTableNameConvention>();
            base.OnModelCreating(builder);
        }

        public override int SaveChanges()
        {
            ApplyStateChanges();
            return base.SaveChanges();
        }
    }

After:


    public class DbContextBase : DbContext, IDbContext
    {
        private readonly Guid _instanceId;

        public DbContextBase(string nameOrConnectionString) : base(nameOrConnectionString)
        {
            _instanceId = Guid.NewGuid();
        }

        public Guid InstanceId
        {
            get { return _instanceId; }
        }

        public void ApplyStateChanges()
        {
            foreach (DbEntityEntry dbEntityEntry in ChangeTracker.Entries())
            {
                var entityState = dbEntityEntry.Entity as IObjectState;
                if (entityState == null)
                    throw new InvalidCastException("All entites must implement the IObjectState interface, " +
                                                   "this interface must be implemented so each entites state can explicitely determined when updating graphs.");

                dbEntityEntry.State = StateHelper.ConvertState(entityState.State);
            }
        }

        public new IDbSet<T> Set<T>() where T : class
        {
            return base.Set<T>();
        }

        public override int SaveChanges()
        {
            ApplyStateChanges();
            return base.SaveChanges();
        }

        public override Task<int> SaveChangesAsync()
        {
            ApplyStateChanges();
            return base.SaveChangesAsync();
        }

        public override Task<int> SaveChangesAsync(CancellationToken cancellationToken)
        {
            ApplyStateChanges();
            return base.SaveChangesAsync(cancellationToken);
        }

        protected override void OnModelCreating(DbModelBuilder builder)
        {
            builder.Conventions.Remove<PluralizingTableNameConvention>();
            base.OnModelCreating(builder);
        }
    }

All that was needed here was to expose all the DbContext Async save operations so that we could use with our IUnitOfWork implementation, and also not forgetting to invoke our ApplyStateChanges so that we are managing the different states each entity could have when dealing with graphs.

Next up, are the enhancements made to our Repository.cs, so that our generic repositories can leverage the Async goodness as well.

Repostiory.Repository.cs

Before:


 public class Repository<TEntity> : IRepository<TEntity> where TEntity : class
    {
        private readonly Guid _instanceId;
        internal IDbContext Context;
        internal IDbSet<TEntity> DbSet;

        public Repository(IDbContext context)
        {
            Context = context;
            DbSet = context.Set<TEntity>();
            _instanceId = Guid.NewGuid();
        }

        public Guid InstanceId
        {
            get { return _instanceId; }
        }

        public virtual TEntity FindById(object id)
        {
            return DbSet.Find(id);
        }

        public virtual void InsertGraph(TEntity entity)
        {
            DbSet.Add(entity);
        }

        public virtual void Update(TEntity entity)
        {
            DbSet.Attach(entity);
        }

        public virtual void Delete(object id)
        {
            var entity = DbSet.Find(id);
            ((IObjectState) entity).State = ObjectState.Deleted;
            Delete(entity);
        }

        public virtual void Delete(TEntity entity)
        {
            DbSet.Attach(entity);
            DbSet.Remove(entity);
        }

        public virtual void Insert(TEntity entity)
        {
            DbSet.Attach(entity);
        }

        public virtual IRepositoryQuery<TEntity> Query()
        {
            var repositoryGetFluentHelper = new RepositoryQuery<TEntity>(this);
            return repositoryGetFluentHelper;
        }

        internal IQueryable<TEntity> Get(
            Expression<Func<TEntity, bool>> filter = null,
            Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
            List<Expression<Func<TEntity, object>>> includeProperties = null,
            int? page = null,
            int? pageSize = null)
        {
            IQueryable<TEntity> query = DbSet;

            if (includeProperties != null)
                includeProperties.ForEach(i => query = query.Include(i));

            if (filter != null)
                query = query.Where(filter);

            if (orderBy != null)
                query = orderBy(query);

            if (page != null && pageSize != null)
                query = query
                    .Skip((page.Value - 1)*pageSize.Value)
                    .Take(pageSize.Value);

            var results = query;

            return results;
        }
    }

After:


    public class Repository<TEntity> : IRepository<TEntity> where TEntity : class
    {
        private readonly Guid _instanceId;
        private readonly DbSet<TEntity> _dbSet;

        public Repository(IDbContext context)
        {
            _dbSet = context.Set<TEntity>();
            _instanceId = Guid.NewGuid();
        }

        public Guid InstanceId
        {
            get { return _instanceId; }
        }

        public virtual TEntity Find(params object[] keyValues)
        {
            return _dbSet.Find(keyValues);
        }

        public virtual async Task<TEntity> FindAsync(params object[] keyValues)
        {
            return await _dbSet.FindAsync(keyValues);
        }

        public virtual async Task<TEntity> FindAsync(CancellationToken cancellationToken, params object[] keyValues)
        {
            return await _dbSet.FindAsync(cancellationToken, keyValues);
        }

        public virtual IQueryable<TEntity> SqlQuery(string query, params object[] parameters)
        {
            return _dbSet.SqlQuery(query, parameters).AsQueryable();
        }

        public virtual void InsertGraph(TEntity entity)
        {
            _dbSet.Add(entity);
        }

        public virtual void Update(TEntity entity)
        {
            _dbSet.Attach(entity);
            ((IObjectState)entity).State = ObjectState.Modified;
        }

        public virtual void Delete(object id)
        {
            var entity = _dbSet.Find(id);
            Delete(entity);
        }

        public virtual void Delete(TEntity entity)
        {
            _dbSet.Attach(entity);
            ((IObjectState)entity).State = ObjectState.Deleted;
            _dbSet.Remove(entity);
        }

        public virtual void Insert(TEntity entity)
        {
            _dbSet.Attach(entity);
            ((IObjectState)entity).State = ObjectState.Added;
        }

        public virtual IRepositoryQuery<TEntity> Query()
        {
            var repositoryGetFluentHelper = new RepositoryQuery<TEntity>(this);
            return repositoryGetFluentHelper;
        }

        internal IQueryable<TEntity> Get(
            Expression<Func<TEntity, bool>> filter = null,
            Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
            List<Expression<Func<TEntity, object>>> includeProperties = null,
            int? page = null,
            int? pageSize = null)
        {
            IQueryable<TEntity> query = _dbSet;

            if (includeProperties != null)
            {
                includeProperties.ForEach(i => query = query.Include(i));
            }

            if (filter != null)
            {
                query = query.Where(filter);
            }

            if (orderBy != null)
            {
                query = orderBy(query);
            }

            if (page != null && pageSize != null)
            {
                query = query
                    .Skip((page.Value - 1)*pageSize.Value)
                    .Take(pageSize.Value);
            }
            return query;
        }

        internal async Task<IEnumerable<TEntity>> GetAsync(
                    Expression<Func<TEntity, bool>> filter = null,
                    Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
                    List<Expression<Func<TEntity, object>>> includeProperties = null,
                    int? page = null,
                    int? pageSize = null)
        {
            return Get(filter, orderBy, includeProperties, page, pageSize).AsEnumerable();
        }
    }

Here we’ve exposed the FindAsync methods from DbSet, so our Repositories can make use of them, and we’ve also wrapped implemented an Async implementation of our Get() method so that we can use it in our new Web Api ProductController.cs later.

Important note: here is that although our method is named GetAsync, it is not truly performing an Async interaction, this is due to the fact that if we were to use ToListAsync(), we would already executed the the query prior to OData applying it’s criteria to the execution plan e.g. if the OData query was requesting 10 records for page 2 of a grid from a Products table that had 1000 rows in it, ToListAsync() would have actually pulled a 1000 records from SQL to the web server and at that time do a skip 10 and take 20 from the collection of Products with 1000 objects. What we want is for this to happen on the SQL Server, meaning, SQL query the Products table, skip the first 10, and take next 10 records and only send those 10 records over to the web server, which will eventually surface into the Grid in the user’s browsers. Hence we are favoring payload size (true SQL Server side paging) going over the wire, vs. a true Async call to SQL.

Northwind.Web.Areas.Spa.Api.ProductController.cs

Before:


    public class ProductController : EntitySetController<Product, int>
    {
        private readonly IUnitOfWork _unitOfWork;

        public ProductController(IUnitOfWork unitOfWork)
        {
            _unitOfWork = unitOfWork;
        }

        public override IQueryable<Product> Get()
        {
            return _unitOfWork.Repository<Product>().Query().Get();
        }

        protected override Product GetEntityByKey(int key)
        {
            return _unitOfWork.Repository<Product>().FindById(key);
        }

        protected override Product UpdateEntity(int key, Product update)
        {
            update.State = ObjectState.Modified;
            _unitOfWork.Repository<Product>().Update(update);
            _unitOfWork.Save();

            return update;
        }

        public override void Delete([FromODataUri] int key)
        {
            _unitOfWork.Repository<Product>().Delete(key);
            _unitOfWork.Save();
        }

        protected override void Dispose(bool disposing)
        {
            _unitOfWork.Dispose();
            base.Dispose(disposing);
        }
    }
 

After:

Note: Don’t be overwhelmed by how much more code there is in the “After” for our new ProductController that now inherits AsyncEntitySetController. I’ll explain later, what all the other Actions are there for. For now, please keep in mind there are only a few of these Actions that are actually required for the use case on the live demo site. The only Actions (methods) that are needed for our use case are as follows:

  • Task<IEnumerable> Get()
  • Task Get([FromODataUri] int key)
  • Task UpdateEntityAsync(int key, Product update)
  • Task Delete([FromODataUri] int key)

[ODataNullValue]
public class ProductController : AsyncEntitySetController<Product, int>
{
    private readonly IUnitOfWork _unitOfWork;

    public ProductController(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }

    protected override void Dispose(bool disposing)
    {
        _unitOfWork.Dispose();
        base.Dispose(disposing);
    }

    protected override int GetKey(Product entity)
    {
        return entity.ProductID;
    }

[Queryable]
public override async Task<IEnumerable<Product>> Get()
{
return await _unitOfWork.Repository<Product>().Query().GetAsync();
}

    [Queryable]
    public override async Task<HttpResponseMessage> Get([FromODataUri] int key)
    {
        var query = _unitOfWork.Repository<Product>().Query().Filter(x => x.ProductID == key).Get();
        return Request.CreateResponse(HttpStatusCode.OK, query);
    }

    ///// <summary>
    ///// Retrieve an entity by key from the entity set.
    ///// </summary>
    ///// <param name="key">The entity key of the entity to retrieve.</param>
    ///// <returns>A Task that contains the retrieved entity when it completes, or null if an entity with the specified entity key cannot be found in the entity set.</returns>
    [Queryable]
    protected override async Task<Product> GetEntityByKeyAsync(int key)
    {
        return await _unitOfWork.Repository<Product>().FindAsync(key);
    }

    protected override async Task<Product> CreateEntityAsync(Product entity)
    {
        if (entity == null)
            throw new HttpResponseException(HttpStatusCode.BadRequest);
            
        _unitOfWork.Repository<Product>().Insert(entity);
        await _unitOfWork.SaveAsync();
        return entity;
    }

    protected override async Task<Product> UpdateEntityAsync(int key, Product update)
    {
        if (update == null)
            throw new HttpResponseException(HttpStatusCode.BadRequest);
            
        if (key != update.ProductID)
            throw new HttpResponseException(Request.CreateODataErrorResponse(HttpStatusCode.BadRequest, new ODataError { Message = "The supplied key and the Product being updated do not match." }));

        try
        {
            update.State = ObjectState.Modified;
            _unitOfWork.Repository<Product>().Update(update);
            var x = await _unitOfWork.SaveAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            throw new HttpResponseException(HttpStatusCode.BadRequest);
        }
        return update;
    }

    // PATCH <controller>(key)
    /// <summary>
    /// Apply a partial update to an existing entity in the entity set.
    /// </summary>
    /// <param name="key">The entity key of the entity to update.</param>
    /// <param name="patch">The patch representing the partial update.</param>
    /// <returns>A Task that contains the updated entity when it completes.</returns>
    protected override async Task<Product> PatchEntityAsync(int key, Delta<Product> patch)
    {
        if (patch == null)
            throw new HttpResponseException(HttpStatusCode.BadRequest);

        if (key != patch.GetEntity().ProductID)
            throw Request.EntityNotFound();

        var entity = await _unitOfWork.Repository<Product>().FindAsync(key);

        if (entity == null)
            throw Request.EntityNotFound();

        try
        {
            patch.Patch(entity);
            await _unitOfWork.SaveAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            throw new HttpResponseException(HttpStatusCode.Conflict);
        }
        return entity;
    }

    public override async Task Delete([FromODataUri] int key)
    {
        var entity = await _unitOfWork.Repository<Product>().FindAsync(key);

        if (entity == null)
            throw Request.EntityNotFound();

        _unitOfWork.Repository<Product>().Delete(entity);

        try
        {
            await _unitOfWork.SaveAsync();
        }
        catch (Exception e)
        { 
            throw new HttpResponseException(
                new HttpResponseMessage(HttpStatusCode.Conflict)
                {
                    StatusCode = HttpStatusCode.Conflict, 
                    Content = new StringContent(e.Message), 
                    ReasonPhrase = e.InnerException.InnerException.Message
                });
        }
    }

    #region Links
    // Create a relation from Product to Category or Supplier, by creating a $link entity.
    // POST <controller>(key)/$links/Category
    // POST <controller>(key)/$links/Supplier
    /// <summary>
    /// Handle POST and PUT requests that attempt to create a link between two entities.
    /// </summary>
    /// <param name="key">The key of the entity with the navigation property.</param>
    /// <param name="navigationProperty">The name of the navigation property.</param>
    /// <param name="link">The URI of the entity to link.</param>
    /// <returns>A Task that completes when the link has been successfully created.</returns>
    [AcceptVerbs("POST", "PUT")]
    public override async Task CreateLink([FromODataUri] int key, string navigationProperty, [FromBody] Uri link)
    {
        var entity = await _unitOfWork.Repository<Product>().FindAsync(key);

        if (entity == null)
            throw Request.EntityNotFound();
            
        switch (navigationProperty)
        {
            case "Category":
                var categoryKey = Request.GetKeyValue<int>(link);
                var category = await _unitOfWork.Repository<Category>().FindAsync(categoryKey);

                if (category == null)
                    throw Request.EntityNotFound();
                    
                    entity.Category = category;
                break;

            case "Supplier":
                var supplierKey = Request.GetKeyValue<int>(link);
                var supplier = await _unitOfWork.Repository<Supplier>().FindAsync(supplierKey);

                if (supplier == null)
                    throw Request.EntityNotFound();
                    
                    entity.Supplier = supplier;
                break;

            default:
                await base.CreateLink(key, navigationProperty, link);
                break;
        }
        await _unitOfWork.SaveAsync();
    }

    // Remove a relation, by deleting a $link entity
    // DELETE <controller>(key)/$links/Category
    // DELETE <controller>(key)/$links/Supplier
    /// <summary>
    /// Handle DELETE requests that attempt to break a relationship between two entities.
    /// </summary>
    /// <param name="key">The key of the entity with the navigation property.</param>
    /// <param name="relatedKey">The key of the related entity.</param>
    /// <param name="navigationProperty">The name of the navigation property.</param>
    /// <returns>Task.</returns>
    public override async Task DeleteLink([FromODataUri] int key, string relatedKey, string navigationProperty)
    {
        var entity = await _unitOfWork.Repository<Product>().FindAsync(key);

        if (entity == null)
            throw Request.EntityNotFound();

        switch (navigationProperty)
        {
            case "Category":
                entity.Category = null;
                break;

            case "Supplier":
                entity.Supplier = null;
                break;

            default:
                await base.DeleteLink(key, relatedKey, navigationProperty);
                break;
        }

        await _unitOfWork.SaveAsync();
    }

    // Remove a relation, by deleting a $link entity
    // DELETE <controller>(key)/$links/Category
    // DELETE <controller>(key)/$links/Supplier
    /// <summary>
    /// Handle DELETE requests that attempt to break a relationship between two entities.
    /// </summary>
    /// <param name="key">The key of the entity with the navigation property.</param>
    /// <param name="navigationProperty">The name of the navigation property.</param>
    /// <param name="link">The URI of the entity to remove from the navigation property.</param>
    /// <returns>Task.</returns>
    public override async Task DeleteLink([FromODataUri] int key, string navigationProperty, [FromBody] Uri link)
    {
        var entity = await _unitOfWork.Repository<Product>().FindAsync(key);

        if (entity == null)
            throw Request.EntityNotFound();

        switch (navigationProperty)
        {
            case "Category":
                entity.Category = null;
                break;

            case "Supplier":
                entity.Supplier = null;
                break;

            default:
                await base.DeleteLink(key, navigationProperty, link);
                break;
        }

        await _unitOfWork.SaveAsync();
    }
    #endregion Links

    public override async Task<HttpResponseMessage> HandleUnmappedRequest(ODataPath odataPath)
    {
        //TODO: add logic and proper return values
        return Request.CreateResponse(HttpStatusCode.NoContent, odataPath);
    }

    #region Navigation Properties
    public async Task<Category> GetCategory(int key)
    {
        var entity = await _unitOfWork.Repository<Product>().FindAsync(key);

        if (entity == null)
            throw Request.EntityNotFound();
            
        return entity.Category;
    }

    public async Task<Supplier> GetSupplier(int key)
    {
        var entity = await _unitOfWork.Repository<Product>().FindAsync(key);

        if (entity == null)
            throw Request.EntityNotFound();
            
        return entity.Supplier;
    }
    #endregion Navigation Properties
}

Quickly looking at this, one can realize there is a lot more code than our pre-Async implementation. Well don’t be alarmed, there’s a lot of code here that wasn’t required to support our use case in the live demo (http://longle.azurewebsites.net), however we wanted to take the extra step so that we can really grasp on how to work with entity graphs with OData by leveraging the ?$expand query string parameter.

We’ll leave all the other that Actions that aren’t actually required for our use case on the live demo SPA as is, so we can see how to deep load your entity graph with OData and Web Api. We’ve included some pre-baked clickable OData URL’s (queries) on the View so that you can actually click and see the response payload in your browser (you’ll have to use Chrome or Firefox, IE has some catching up to do here).

*Click on image
10-9-2013 8-51-43 PM

Now let’s do a deep dive on the our Async Get() Action in our Controller.


[Queryable]
 
public override async Task<IEnumerable<Product>> Get()
{
    return await _unitOfWork.Repository<Product>().Query().GetAsync();
}
 

My initial thought when seeing this this Action (signature) is that it’s not IQueryable?! Which means that the SQL plan from EF has already been executed before OData has an opportunity to apply it’s criteria to the query plan! Well that’s not the case, we outfitted the Project with Glimpse and Glimpse EF6 to actually see what SQL queries were being sent over the wire.

So let’s take a look at the loading up our Kendo UI Grid with the awesomeness of Glimpse running. Since our View is built with Kendo UI, and we know it’s invoking Ajax calls to request data, we’ll click on the Ajax panel on the Glimpse HUD.

*Click on image
10-9-2013 7-30-56 PM

Now with the HUD automatically switching to standard view we can see all the Ajax requests that our View made, we are interested in the OData request that was made to hydrate our Kendo Grid.

*Click on image
10-9-2013 8-04-32 PM

After clicking on Inspect for the Ajax OData request, we see that menu buttons buttons that have tracing data for that request start to actual blink…! One of them being SQL, so let’s click on it.

*Click on image
10-9-2013 8-32-10 PM

Ladies and gentlemen, I kid you not, behold this is the actual SQL query that was from our Unit Of Work -> Repostiory -> Entity Framework 6 -> T-SQL, that was actually sent to SQL Server (actually in our case SQL Server CE, so that the live demo can be complete free with Azure Website without the need to pay for SQL Azure). BTW, we just scratching the surface of what Glimpse can do, the list is pretty much endless e.g. displays MVC Routes, Actions, Tracing, Environment Variables, MVC Views, and performance metrics for pretty much all of them, etc.

Now back to the topic at hand, we can definitively see that although our Action and our Repository are returning IEnumerable:

Get Action the Kendo UI Datasource is calling, which returns IEnumerable.


[Queryable] 
 
public override async Task<IEnumerable<Product>> Get()
{
    return await _unitOfWork.Repository<Product>().Query().GetAsync();
}
 

Repository method the Action is calling, which also returns IEnumerable.


        internal async Task<IEnumerable<TEntity>> GetAsync(
                    Expression<Func<TEntity, bool>> filter = null,
                    Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
                    List<Expression<Func<TEntity, object>>> includeProperties = null,
                    int? page = null,
                    int? pageSize = null)
        {
            return Get(filter, orderBy, includeProperties, page, pageSize).AsEnumerable();
        }
 

The query plan is still valid, meaning it’s selecting only the rows (10 records to be exact) that the Grid is requesting for page one (1) of the Grid. So how is this happening? Well we’ve decorated our action with the [Queryable] attribute, so OData and Web Api is able to perform it’s magic together during run-time in the ASP.NET HTTP pipeline.

T-SQL that’s being sent over the wire, courtesy of Glimpse EF6

 
 
SELECT TOP (10 /* @p__linq__0 */) 
    [Extent1].[Product ID] AS [Product ID], 
    [Extent1].[Product Name] AS [Product Name], 
    [Extent1].[Supplier ID] AS [Supplier ID], 
    [Extent1].[Category ID] AS [Category ID], 
    [Extent1].[Quantity Per Unit] AS [Quantity Per Unit], 
    [Extent1].[Unit Price] AS [Unit Price], 
    [Extent1].[Units In Stock] AS [Units In Stock], 
    [Extent1].[Units On Order] AS [Units On Order], 
    [Extent1].[Reorder Level] AS [Reorder Level], 
    [Extent1].[Discontinued] AS [Discontinued]
    FROM [Products] AS [Extent1]
    ORDER BY [Extent1].[Product ID] ASC
 

Now, let’s cover at a high-level on all the Actions that aren’t required for our live demo use case, which are mostly to support Navigation Properties e.g. Product.Supplier, Product.Category, etc.

The $expand query string parameter allows us to hydrate complex navigation property types. For example in our case when we query for a Product, and Product has a property of Category and we the Category to be hydrated with its data we would leverage the $expand querystring parameter to do this, click this Url : http://longle.azurewebsites.net/odata/Product/?$inlinecount=allpages&$orderby=ProductName&$skip=1&$top=2&$expand=Category&$select=ProductID,ProductName,Category/CategoryID,Category/CategoryName to see the $expand in action.

T-SQL that’s being sent over the wire, again, courtesy of Glimpse EF6

 
 
SELECT TOP (2 /* @p__linq__1 */) 
    [top].[Product ID] AS [Product ID], 
    [top].[C1] AS [C1], 
    [top].[C2] AS [C2], 
    [top].[Product Name] AS [Product Name], 
    [top].[C3] AS [C3], 
    [top].[C4] AS [C4], 
    [top].[C5] AS [C5], 
    [top].[C6] AS [C6], 
    [top].[Category Name] AS [Category Name], 
    [top].[C7] AS [C7], 
    [top].[Category ID] AS [Category ID], 
    [top].[C8] AS [C8]
    FROM ( SELECT [Project1].[Product ID] AS [Product ID], [Project1].[Product Name] AS [Product Name], [Project1].[Category ID] AS [Category ID], [Project1].[Category Name] AS [Category Name], [Project1].[C1] AS [C1], [Project1].[C2] AS [C2], [Project1].[C3] AS [C3], [Project1].[C4] AS [C4], [Project1].[C5] AS [C5], [Project1].[C6] AS [C6], [Project1].[C7] AS [C7], [Project1].[C8] AS [C8]
        FROM ( SELECT 
            [Extent1].[Product ID] AS [Product ID], 
            [Extent1].[Product Name] AS [Product Name], 
            [Extent1].[Category ID] AS [Category ID], 
            [Extent2].[Category Name] AS [Category Name], 
            N'ace5ad31-e3e9-4cde-9bb8-d75fced846fa' AS [C1], 
            N'ProductName' AS [C2], 
            N'ProductID' AS [C3], 
            N'Category' AS [C4], 
            N'ace5ad31-e3e9-4cde-9bb8-d75fced846fa' AS [C5], 
            N'CategoryName' AS [C6], 
            N'CategoryID' AS [C7], 
            CASE WHEN ([Extent1].[Category ID] IS NULL) THEN cast(1 as bit) ELSE cast(0 as bit) END AS [C8]
            FROM  [Products] AS [Extent1]
            LEFT OUTER JOIN [Categories] AS [Extent2] ON [Extent1].[Category ID] = [Extent2].[Category ID]
        )  AS [Project1]
        ORDER BY [Project1].[Product Name] ASC, [Project1].[Product ID] ASC
        OFFSET 1 /* @p__linq__0 */ ROWS 
    )  AS [top]
 

Product results with Categories hydrated

 
  
{
  "odata.metadata":"http://longle.azurewebsites.net/odata/$metadata#Product&$select=ProductID,ProductName,Category/CategoryID,Category/CategoryName","odata.count":"77","value":[
    {
      "Category":{
        "CategoryID":2,"CategoryName":"Condiments"
      },"ProductID":3,"ProductName":"Aniseed Syrup"
    },{
      "Category":{
        "CategoryID":8,"CategoryName":"Seafood"
      },"ProductID":40,"ProductName":"Boston Crab Meat"
    }
  ]
} 
 

We can really see the power of Web Api and OData now, we’re actually able to query for Products (skip the first and take the next two) and request that Category be hydrated but specifically only the CategoryId and Name and none of the other fields.

Sample Application Client Side (Kendo UI) Tweaks

We’ve polished the UI/UX a bit, relocated Edit, Edit Details, and Delete buttons out of the rows into the Grid Toolbar (header) to make better use of the Grid real estate, using Kendo’s Template Framework, which illustrates how flexible Kendo UI can be. The app has been upgraded to, Twitter Bootstrap as by leveraging the new out of the box MVC Project Templates in Visual Studio 2013 (Preview) and changing the Kendo UI theme to Bootstrap to match.

All Kendo Views which are remotely loaded on demand into the SPA are now actually MVC Razor Views, the Kendo Router remotely loads views by traditional MVC routes e.g.
{controller}/{action}/{id} vs. what was in the previous post (http://blog.longle.net/2013/06/17/mvc-4-kendo-ui-spa-with-layout-router-mvvm/) which was just serving up raw *.html pages. This has been a request for devs that are making the transition from server side MVC development into the SPA realm, and had .NET libraries they still wanted to make use of and leverage in their their Controllers, and Razor Views for SPA’s. Obviously, all Views and ViewModel binding on the client-side are done with with Kendo’s MVVM Framework.

Northwind.Web/Areas/Spa/Content/Views/products.html

Before (non Razor, just plain *.html pages were used for SPA):

 
@{
    ViewBag.Title = "Products";
    Layout = "";
}
<div class="row">
    <div class="span5">
        <h2>Technlogy Stack</h2>
        <h3><a href="http://blog.longle.net">blog.longle.net</a></h3>
        <p>ASP.NET MVC 4, Web API, OData, Entity Framework 6 CTP, EntityFramework CE 6 RC1, Visual Studio 2013 Preview, Sql Server CE, Twitter Bootstrap, Kendo UI Web, Azure Website PaaS (<a href="http://www.windowsazure.com/en-us/develop/net/aspnet/" target="blank">free!</a>)</p>
        <br />
        <p><a class="btn" href="http://go.microsoft.com/fwlink/?LinkId=301865">Learn more &raquo;</a></p>
    </div>
</div>

<br /><br />
<div class="k-content" style="width: 100%">
    <div id="view">
        <div id="productGrid"
             data-role="grid"
             data-sortable="true"
             data-pageable="true"
             data-filterable="true"
             data-bind="source: dataSource, events: { dataBound: dataBound, change: onChange }"
             data-editable="inline"
             data-selectable="true"
             data-toolbar='[ { template: $("#toolbar").html() } ]'
             data-columns='[
                    { field: "ProductID", title: "ID", width: "50px" },
                    { field: "ProductName", title: "Name"},
                    { field: "QuantityPerUnit", title: "Quantity", width: "200px" },
                    { field: "UnitsInStock", title: "Stock", width: "90px" },
                    { field: "UnitPrice", title: "Price", format: "{0:c}", width: "100px" },
                    { field: "Discontinued", width: "150px" } ]'>
        </div>
    </div>
    <h3>Use Chrome or Firefox and click on OData (queries) Urls below for example results.</h3>
    <ul>
        <li><a href="/odata/$metadata">/odata/$metadata</a></li>
        <li><a href="/odata/Product">/odata/Product</a></li>
        <li><a href="/odata/Product/?$select=ProductID,ProductName">/odata/Product/?$select=ProductID,ProductName</a></li>
        <li><a href="/odata/Product/?$orderby=ProductName&$skip=1&$top=2">/odata/Product/?$orderby=ProductName&$skip=1&$top=2</a></li>
        <li><a href="/odata/Product/?$orderby=ProductName&$skip=1&$top=2">/odata/Product/?$orderby=ProductName&$skip=1&$top=2</a></li>
        <li><a href="/odata/Product/?$inlinecount=allpages&$filter=UnitPrice ge 20">/odata/Product/?$inlinecount=allpages&$filter=UnitPrice ge 20</a></li>
        <li><a href="/odata/Product/?$expand=Category">/odata/Product/?$expand=Category</a></li>
        <li><a href="/odata/Product/?$expand=Category&$select=ProductID,ProductName,Category/CategoryID,Category/CategoryName">/odata/Product/?$expand=Category&$select=ProductID,ProductName,Category/CategoryID,Category/CategoryName</a></li>
        <li><a href="/odata/Product/?$inlinecount=allpages&$orderby=ProductName&$skip=1&$top=2&$expand=Category&$select=ProductID,ProductName,Category/CategoryID,Category/CategoryName">/odata/Product/?$inlinecount=allpages&$orderby=ProductName&$skip=1&$top=2&$expand=Category&$select=ProductID,ProductName,Category/CategoryID,Category/CategoryName</a></li>
    </ul>
</div>

<script type="text/x-kendo-template" id="toolbar">
    <div class="toolbar">
        <a class="k-button" onclick="edit(event);"><span class="k-icon k-i-tick"></span>Edit</a>
        <a class="k-button" onclick="destroy(event);"><span class="k-icon k-i-tick"></span>Delete</a>
        <a class="k-button" onclick="details(event);"><span class="k-icon k-i-tick"></span>Edit Details</a>
    </div>
    <div class="toolbar" style="display:none">
        <a class="k-button" onclick="save(event);"><span class="k-icon k-i-tick"></span>Save</a>
        <a class="k-button" onclick="cancel(event);"><span class="k-icon k-i-tick"></span>Cancel</a>
    </div>
</script>

<script>
    var lastSelectedDataItem;

    var save = function (event) {
        onClick(event, function (grid) {
            grid.saveRow();
            $(".toolbar").toggle();
        });
    };

    var cancel = function (event) {
        onClick(event, function (grid) {
            grid.cancelRow();
            $(".toolbar").toggle();
        });
    };

    var details = function (event) {
        onClick(event, function (grid, row, dataItem) {
            window.location.href = '#/edit/' + dataItem.ProductID;
        });
    };

    var edit = function (event) {
        onClick(event, function (grid, row) {
            grid.editRow(row);
            $(".toolbar").toggle();
        });
    };

    var destroy = function (event) {
        onClick(event, function (grid, row, dataItem) {
            grid.dataSource.remove(dataItem);
            grid.dataSource.sync();
        });
    };

    var onClick = function (event, delegate) {
        event.preventDefault();
        var grid = $("#productGrid").data("kendoGrid");
        var selectedRow = grid.select();
        var dataItem = grid.dataItem(selectedRow);
        if (selectedRow.length > 0)
            delegate(grid, selectedRow, dataItem);
        else
            alert("Please select a row.");
    };

    var Product = kendo.data.Model.define({
        id: "ProductID",
        fields: {
            ProductID: { type: "number", editable: false, nullable: true },
            ProductName: { type: "string", validation: { required: true } },
            QuantityPerUnit: { type: "string", validation: { required: true } },
            UnitsInStock: { type: "number", validation: { required: true } },
            UnitPrice: { type: "number", validation: { required: true, min: 1 } },
            Discontinued: { type: "boolean" }
        }
    });

    var baseUrl = "/odata/Product";

    var dataSource = new kendo.data.DataSource({
        type: "odata",
        transport: {
            read: {
                url: baseUrl,
                dataType: "json"
            },
            update: {
                url: function (data) {
                    return baseUrl + "(" + data.ProductID + ")";
                },
                dataType: "json"
            },
            destroy: {
                url: function (data) {
                    return baseUrl + "(" + data.ProductID + ")";
                },
                dataType: "json"
            }
        },
        batch: false,
        serverPaging: true,
        serverSorting: true,
        serverFiltering: true,
        pageSize: 10,
        schema: {
            data: function (data) {
                return data.value;
            },
            total: function (data) {
                return data["odata.count"];
            },
            errors: function (e) {
                return e.errors;
            },
            model: Product
        },
        error: function (e) {
            var responseJson = e.xhr.responseJSON;
            if (responseJson != undefined) {
                if (responseJson["odata.error"] != undefined) {
                    var error = responseJson["odata.error"];
                    var message = error.message.value + '\n\n' + error.innererror.message;
                    alert(message);
                }
            } else {
                alert(e.xhr.status + "\n\n" + e.xhr.responseText + "\n\n" + e.xhr.statusText);
            }
            this.read();
        }
    });

    var viewModel = kendo.observable({
        dataSource: dataSource,
        dataBound: function (arg) {
            if (lastSelectedDataItem == null) return; // check if there was a row that was selected
            var view = this.dataSource.view(); // get all the rows
            for (var i = 0; i < view.length; i++) { // iterate through rows
                if (view[i].ProductID == lastSelectedDataItem.ProductID) { // find row with the lastSelectedProductd
                    var grid = arg.sender; // get the grid
                    grid.select(grid.table.find("tr[data-uid='" + view[i].uid + "']")); // set the selected row
                    break;
                }
            }
        },
        onChange: function (arg) {
            var grid = arg.sender;
            lastSelectedDataItem = grid.dataItem(grid.select());
        },
    });

    $(document).bind("viewSwtichedEvent", function (e, args) { // subscribe to the viewSwitchedEvent
        if (args.name == "list") { // check if this view was switched too
            if (args.isRemotelyLoaded) { // check if this view was loaded for the first time (remotely from server)
                kendo.bind($("#view"), viewModel); // bind the view to the model
            } else {// view already been loaded in cache
                viewModel.dataSource.read(); // refresh grid
            }
        }
    });

</script>
<style scoped>
    #productGrid .k-toolbar {
        padding: .7em;
    }

    .toolbar {
        float: right;
    }
</style>
 

10-9-2013 7-11-39 PM

Happy Coding…! :)

Live Demo: http://longle.azurewebsites.net
Download: https://genericunitofworkandrepositories.codeplex.com/

MVC 4, Web API, OData, Entity Framework, Kendo UI, Grid, Datasource (CRUD) with MVVM

This will be part four of a six part series of blog posts.

  1. Modern Web Application Layered High Level Architecture with SPA, MVC, Web API, EF, Kendo UI, OData
  2. Generically Implementing the Unit of Work & Repository Pattern with Entity Framework in MVC & Simplifying Entity Graphs
  3. MVC 4, Kendo UI, SPA with Layout, View, Router & MVVM
  4. MVC 4, Web API, OData, EF, Kendo UI, Grid, Datasource (CRUD) with MVVM
  5. MVC 4, Web API, OData, EF, Kendo UI, Binding a Form to Datasource (CRUD) with MVVM
  6. Upgrading to Async with Entity Framework, MVC, OData AsyncEntitySetController, Kendo UI, Glimpse & Generic Unit of Work Repository Framework v2.0

Update: 09/09/2013 – Sample application and sourcecode has been uploaded to CodePlex: https://genericunitofworkandrepositories.codeplex.com, updated Visual 2013, Twitter Bootstrap, MVC 5, EF6, Kendo UI Bootstrap theme, project redeployed to Windows Azure Website.

Update: 06/18/2013 – Added CRUD actions to Kendo UI Grid & Datasource (read, update, delete) updated sample download and live demo.

Update: 06/20/2013 – Bug fix(es): Fixed View being loaded duplicate times. Enhancement(s): Added state management for Grid, after productEdit View updates (syncs), will auto navigate back to Grid and re-select the last selected row. Updated blog, sample app download, and live demo

Let’s start off where we left off from my previous blog MVC 4, Kendo UI, SPA with Layout, View, Router & MVVM – Part 1. In this post, we’ll cover how to wire up a Kendo UI Grid and Datasource in our SPA with MVVM using OData.

This will be part two of a five part series of blog posts.

  1. Generically Implementing the Unit of Work & Repository Pattern with Entity Framework in MVC & Simplifying Entity Graphs
  2. MVC 4, Kendo UI, SPA with Layout, View, Router & MVVM
  3. MVC 4, Web API, OData, EF, Kendo UI, Grid, Datasource (CRUD) with MVVM
  4. MVC 4, Web API, OData, EF, Kendo UI, Binding a Form to Datasource (CRUD) with MVVM
  5. Upgrading to Async with Entity Framework, MVC, OData AsyncEntitySetController, Kendo UI, Glimpse & Generic Unit of Work Repository Framework v2.0

Taking a look at a high level architecture of this three part series blog: Modern Web Application Layered High Level Architecture with SPA, MVC, Web API, EF, Kendo UI.

For live demo: http://longle.azurewebsites.net, courtesy of Windows Azure free 10 Website’s.

Let’s get Web API setup and configured with OData, you will need the Nuget package Microsoft ASP.NET Web API OData.

6-18-2013 1-32-31 AM

We will use a SQL Server Compact (4.0) Northwind database for this example, you can easily use a full SQL Server Database if you’d like with libraries in this project. I’ve tested them both and they work fine. Both the Northwind SQL Compact and SQL Server database are included in the sample download application that will be available for download in Part 3 of this series. For all of those who are wondering, why did I use (embedded) SQL Server Compact for as web app..?! Well, because I can host the SQL Server Compact database in my Windows Azure Website for free..!

You will need the NuGet package EntityFramework.SqlServerCompact and Microsoft.SqlServerCompact packages. You can read up on some more in-depth details on how to setup ASP.NET MVC with SQL Server Compact Databases here.

6-18-2013 1-38-54 AM

We need to create a ProductController so that we can provide data to our View, this controller will inherit the EntitySetController which inherits the ApiController that we all know so well, so we can serve up our data using OData.

Spa.Controllers.ProductController.cs


    public class ProductController : EntitySetController<Product, int>
    {
        private readonly IUnitOfWork _unitOfWork;

        public ProductController(IUnitOfWork unitOfWork)
        {
            _unitOfWork = unitOfWork;
        }

        public override IQueryable<Product> Get()
        {
            return _unitOfWork.Repository<Product>().Query().Get();
        }

        protected override Product GetEntityByKey(int key)
        {
            return _unitOfWork.Repository<Product>().FindById(key);
        }

        protected override Product UpdateEntity(int key, Product update)
        {
            update.State = ObjectState.Modified;
            _unitOfWork.Repository<Product>().Update(update);
            _unitOfWork.Save();
            return update;
        }
        
        public override void Delete([FromODataUri] int key)
        {
            _unitOfWork.Repository<Product>().Delete(key);
            _unitOfWork.Save();
        }
        
        protected override void Dispose(bool disposing)
        {
            _unitOfWork.Dispose();
            base.Dispose(disposing);
        }
    }

We need to setup OData with the MVC runtime as well as setup the OData endpoint. Not relevant to this post, however, notice that our OData Products Web Api Controller has a dependency for IUnitOfWork and that it is being injected (DI) with an instance of it’s concrete implementation UnitOfWork, courtesy of Unity 3.0. Please read up on post: Generically Implementing the Unit of Work & Repository Pattern with Entity Framework in MVC & Simplifying Entity Graphs, if any clarity is needed for the generic Unit Of Work and Repository pattern used in the ProductController seen here.

Spa.WebApiConfig



    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            ODataModelBuilder modelBuilder = new ODataConventionModelBuilder();
            var entitySetConfiguration = modelBuilder.EntitySet<Product>("Product");
            entitySetConfiguration.EntityType.Ignore(t => t.Order_Details);
            entitySetConfiguration.EntityType.Ignore(t => t.Category);
            entitySetConfiguration.EntityType.Ignore(t => t.Supplier);

            var model = modelBuilder.GetEdmModel();
            config.Routes.MapODataRoute("ODataRoute", "odata", model);

            config.EnableQuerySupport();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new {id = RouteParameter.Optional}
                );
        }
    }

For interest of time we’ll go ahead an ignore all the relational mappings that our Product entity has.

Now let’s test our ProductsController with Fiddler and makes sure everything we’re able to query our Get method on our ProductController using OData queries.

6-18-2013 12-11-03 AM

The raw HTTP response message should look similar to the following:

http://localhost:29622/odata


HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/atomsvc+xml; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
DataServiceVersion: 3.0
X-AspNet-Version: 4.0.30319
X-SourceFiles: =?UTF-8?B?RDpcVXNlcnNcbGxlXERvd25sb2Fkc1xUZW1wMlxTb2x1dGlvblxTcGFcb2RhdGE=?=
X-Powered-By: ASP.NET
Date: Tue, 18 Jun 2013 05:09:41 GMT
Content-Length: 363

<?xml version="1.0" encoding="utf-8"?>
<service xml:base="http://localhost:29622/odata/" xmlns="http://www.w3.org/2007/app" xmlns:atom="http://www.w3.org/2005/Atom">
  <workspace>
    <atom:title type="text">Default</atom:title>
    <collection href="Product">
      <atom:title type="text">Product</atom:title>
    </collection>
  </workspace>
</service>

The response body contains the OData service document in JSON format. The service document contains an array of JSON objects that represent the entity sets. In this case, there is a single entity set, “Product”. To query this set, send a GET request to http://localhost:port/odata/Products. The
response should be similar to the following:

http://localhost:29622/odata/Product


HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; odata=minimalmetadata; streaming=true; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
DataServiceVersion: 3.0
X-AspNet-Version: 4.0.30319
X-SourceFiles: =?UTF-8?B?RDpcVXNlcnNcbGxlXERvd25sb2Fkc1xUZW1wMlxTb2x1dGlvblxTcGFcb2RhdGFcUHJvZHVjdA==?=
X-Powered-By: ASP.NET
Date: Tue, 18 Jun 2013 05:18:27 GMT
Content-Length: 21696

{
  "odata.metadata":"http://localhost:29622/odata/$metadata#Product","value":[
    {
      "ProductID":1,"ProductName":"Chai","EnglishName":"Dharamsala Tea","SupplierID":1,"CategoryID":1,"QuantityPerUnit":"10 boxes x 20 bags","UnitPrice":"20","UnitsInStock":39,"UnitsOnOrder":10,"ReorderLevel":10,"Discontinued":false,"State":"Unchanged"
    },{
      "ProductID":2,"ProductName":"Chang","EnglishName":"Tibetan Barley Beer","SupplierID":1,"CategoryID":1,"QuantityPerUnit":"24 - 12 oz bottles","UnitPrice":"19","UnitsInStock":17,"UnitsOnOrder":40,"ReorderLevel":25,"Discontinued":false,"State":"Unchanged"
    },{
      "ProductID":3,"ProductName":"Aniseed Syrup","EnglishName":"Licorice Syrup","SupplierID":1,"CategoryID":2,"QuantityPerUnit":"12 - 550 ml bottles","UnitPrice":"10","UnitsInStock":13,"UnitsOnOrder":71,"ReorderLevel":25,"Discontinued":false,"State":"Unchanged"
    },{
      "ProductID":4,"ProductName":"Chef Anton's Cajun Seasoning","EnglishName":"Chef Anton's Cajun Seasoning","SupplierID":2,"CategoryID":2,"QuantityPerUnit":"48 - 6 oz jars","UnitPrice":"22","UnitsInStock":53,"UnitsOnOrder":0,"ReorderLevel":0,"Discontinued":false,"State":"Unchanged"
    },{
      "ProductID":5,"ProductName":"Chef Anton's Gumbo Mix","EnglishName":"Chef Anton's Gumbo Mix","SupplierID":2,"CategoryID":2,"QuantityPerUnit":"36 boxes","UnitPrice":"21.35","UnitsInStock":0,"UnitsOnOrder":0,"ReorderLevel":0,"Discontinued":true,"State":"Unchanged"
    },{
      "ProductID":6,"ProductName":"Grandma's Boysenberry Spread","EnglishName":"Grandma's Boysenberry Spread","SupplierID":3,"CategoryID":2,"QuantityPerUnit":"12 - 8 oz jars","UnitPrice":"25","UnitsInStock":120,"UnitsOnOrder":0,"ReorderLevel":25,"Discontinued":false,"State":"Unchanged"
    },{
      "ProductID":7,"ProductName":"Uncle Bob's Organic Dried Pears","EnglishName":"Uncle Bob's Organic Dried Pears","SupplierID":3,"CategoryID":7,"QuantityPerUnit":"12 - 1 lb pkgs.","UnitPrice":"30","UnitsInStock":15,"UnitsOnOrder":0,"ReorderLevel":10,"Discontinued":false,"State":"Unchanged"
    },{
      "ProductID":8,"ProductName":"Northwoods Cranberry Sauce","EnglishName":"Northwoods Cranberry Sauce","SupplierID":3,"CategoryID":2,"QuantityPerUnit":"12 - 12 oz jars","UnitPrice":"40","UnitsInStock":6,"UnitsOnOrder":0,"ReorderLevel":0,"Discontinued":false,"State":"Unchanged"
    }
  ]
}

Great, we have data with OData :P

One of the important take aways here, is by implementing OData with a data provider (e.g. Entity Framework) that supports IQueryable all of our REST HTTP GET queries options (e.g. skip, take, sort, filter, equals, etc.) are automatically translated for to us to Entity Framework, so that we don’t have to wire up any of this.

Let’s launch Fiddler again and see this in action and do a OData HTTP GET request querying for product that has the name equal to “Chai”.

http://localhost:29622/odata/Product?%24inlinecount=allpages&%24top=10&%24filter=ProductName+eq+’chai&#8217;

6-18-2013 1-26-12 PM

Raw Request of OData Query


GET http://localhost:29622/odata/Product?%24inlinecount=allpages&%24top=10&%24filter=ProductName+eq+'chai' HTTP/1.1
X-Requested-With: XMLHttpRequest
Accept: application/json, text/javascript, */*; q=0.01
Referer: http://localhost:29622/index.html#/products
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; WOW64; Trident/6.0)
Host: localhost:29622
DNT: 1
Connection: Keep-Alive

Raw Request of OData Response


HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
DataServiceVersion: 3.0
X-AspNet-Version: 4.0.30319
X-SourceFiles: =?UTF-8?B?RDpcVXNlcnNcbGxlXERvd25sb2Fkc1xUZW1wMlxTb2x1dGlvblxTcGFcb2RhdGFcUHJvZHVjdA==?=
X-Powered-By: ASP.NET
Date: Tue, 18 Jun 2013 18:22:02 GMT
Content-Length: 374

{
  "odata.metadata":"http://localhost:29622/odata/$metadata#Product","odata.count":"1","value":[
    {
      "ProductID":1,"ProductName":"Chai","EnglishName":"Dharamsala Tea","SupplierID":1,"CategoryID":1,"QuantityPerUnit":"10 boxes x 20 bags","UnitPrice":"20","UnitsInStock":39,"UnitsOnOrder":10,"ReorderLevel":10,"Discontinued":false,"State":"Unchanged"
    }
  ]
}

Again, querying is navtively supported out of the box with OData and a data provider that supports IQueryable, however if we re-visit our ProductController, we didn’t have to write up any code for this..!

Spa.Controllers.ProductController


    public class ProductController : EntitySetController<Product, int>
    {
        private readonly IUnitOfWork _unitOfWork;

        public ProductController(IUnitOfWork unitOfWork)
        {
            _unitOfWork = unitOfWork;
        }

        public override IQueryable<Product> Get()
        {
            return _unitOfWork.Repository<Product>().Query().Get();
        }

        protected override Product GetEntityByKey(int key)
        {
            return _unitOfWork.Repository<Product>().FindById(key);
        }

        protected override Product UpdateEntity(int key, Product update)
        {
            update.State = ObjectState.Modified;
            _unitOfWork.Repository<Product>().Update(update);
            _unitOfWork.Save();
            return update;
        }
        
        public override void Delete([FromODataUri] int key)
        {
            _unitOfWork.Repository<Product>().Delete(key);
            _unitOfWork.Save();
        }
        
        protected override void Dispose(bool disposing)
        {
            _unitOfWork.Dispose();
            base.Dispose(disposing);
        }
    }

Now we create a View for the Products listing using Kendo UI Grid, Datasource and use MVVM as the adhesive to bind it all together.

Spa/Content/Views/products.html


<script type="text/x-kendo-template" id="products">
    <section class="content-wrapper main-content clear-fix">    
        <h3>Technlogy Stack</h3>
        <ol class="round">
            <li class="one">
                <h5>.NET</h5>
                ASP.NET MVC 4, Web API, OData, Entity Framework        
            </li>
            <li class="two">
                <h5>Kendo UI Web Framework</h5>
                MVVM, SPA, Grid, DataSource
            </li>
            <li class="three">
                <h5>Patterns</h5>
                Unit of Work, Repository, MVVM
            </li>
        </ol>
        <h3>View Products</h3><br/>
            <div class="k-content" style="width:100%">    
            <div id="productsForm">
            <div id="productGrid" 
                data-role="grid"
                data-sortable="true"
                data-pageable="true"
                data-filterable="true"
                data-bind="source: dataSource, events:{dataBound: dataBound, change: onChange}"
                data-editable = "inline"
                data-selectable="true" 
                data-columns='[
                    { field: "ProductID", title: "Id", width: "50px" }, 
                    { field: "ProductName", title: "Name", width: "300px" }, 
                    { field: "UnitPrice", title: "Price", format: "{0:c}", width: "100px" },                    
                    { field: "Discontinued", width: "150px" }, 
                    { command : [ "edit", "destroy", { text: "Edit Details", click: editProduct } ], title: "Action",  } ]'>
            </div>
            </div>
            </div>
    </section>    
</script>

<script>
    function editProduct(e) {
        e.preventDefault();
        var tr = $(e.currentTarget).closest("tr");
        var dataItem = $("#productGrid").data("kendoGrid").dataItem(tr);
        window.location.href = '#/productEdit/' + dataItem.ProductID;
    }

    var lastSelectedProductId;
                
    var crudServiceBaseUrl = "/odata/Product";
    var productsModel = kendo.observable({
        dataSource: dataSource = new kendo.data.DataSource({
            type: "odata",
            transport: {
                read: {
                    url: crudServiceBaseUrl,
                    dataType: "json"
                },
                update: {
                    url: function (data) {
                        return crudServiceBaseUrl + "(" + data.ProductID + ")";
                    },
                    dataType: "json"
                },
                destroy: {
                    url: function (data) {
                        return crudServiceBaseUrl + "(" + data.ProductID + ")";
                    },
                    dataType: "json"
                }
            },
            batch: false,
            serverPaging: true,
            serverSorting: true,
            serverFiltering: true,
            pageSize: 10,
            schema: {
                data: function (data) {
                    return data.value;
                },
                total: function (data) {
                    return data["odata.count"];
                },
                errors: function (data) {
                },
                model: {
                    id: "ProductID",
                    fields: {
                        ProductID: { type: "number", editable: false, nullable: true },
                        ProductName: { type: "string", validation: { required: true } },
                        UnitPrice: { type: "number", validation: { required: true, min: 1 } },
                        Discontinued: { type: "boolean" },
                        UnitsInStock: { type: "number", validation: { min: 0, required: true } }
                    }
                }
            },
            error: function (e) {
                var message = e.xhr.responseJSON["odata.error"].message.value;
                var innerMessage = e.xhr.responseJSON["odata.error"].innererror.message;
                alert(message + "\n\n" + innerMessage);
            }
        }),
        dataBound: function (arg) {
            if (lastSelectedProductId == null) return; // check if there was a row that was selected
            var view = this.dataSource.view(); // get all the rows
            for (var i = 0; i < view.length; i++) { // iterate through rows
                if (view[i].ProductID == lastSelectedProductId) { // find row with the lastSelectedProductd
                    var grid = arg.sender; // get the grid
                    grid.select(grid.table.find("tr[data-uid='" + view[i].uid + "']")); // set the selected row
                    break;
                }
            }
        },
        onChange: function (arg) {
            var grid = arg.sender;
            var dataItem = grid.dataItem(grid.select());
            lastSelectedProductId = dataItem.ProductID;
        }
    });

    $(document).bind("viewSwtichedEvent", function (e, args) { // subscribe to the viewSwitchedEvent
        if (args.name == "products") { // check if this view was switched too
            if (args.isRemotelyLoaded) { // check if this view was remotely loaded from server
                kendo.bind($("#productsForm"), productsModel); // bind the view to the model
            } else {// view already been loaded in cache
                productsModel.dataSource.fetch(function() {}); // refresh grid
            }
        }
    });

</script>

We wrap our html content in the script tags with type=”text/x-kendo-template”, this is so we can leverage all the goodness that Kendo UI Web Templates bring to the table.

productGrid (Spa/Content/Views/products.html)


            <div id="productGrid" 
                data-role="grid"
                data-sortable="true"
                data-pageable="true"
                data-filterable="true"
                data-bind="source: dataSource, events:{dataBound: dataBound, change: onChange}"
                data-editable = "inline"
                data-selectable="true" 
                data-columns='[
                    { field: "ProductID", title: "Id", width: "50px" }, 
                    { field: "ProductName", title: "Name", width: "300px" }, 
                    { field: "UnitPrice", title: "Price", format: "{0:c}", width: "100px" },                    
                    { field: "Discontinued", width: "150px" }, 
                    { command : [ "edit", "destroy", { text: "Edit Details", click: editProduct } ], title: "Action",  } ]'>
            </div>

Model & Datasource (Spa/Content/Views/products.html)


    var crudServiceBaseUrl = "/odata/Product";
    var productsModel = kendo.observable({
        dataSource: dataSource = new kendo.data.DataSource({
            type: "odata",
            transport: {
                read: {
                    url: crudServiceBaseUrl,
                    dataType: "json"
                },
                update: {
                    url: function (data) {
                        return crudServiceBaseUrl + "(" + data.ProductID + ")";
                    },
                    dataType: "json"
                },
                destroy: {
                    url: function (data) {
                        return crudServiceBaseUrl + "(" + data.ProductID + ")";
                    },
                    dataType: "json"
                }
            },
            batch: false,
            serverPaging: true,
            serverSorting: true,
            serverFiltering: true,
            pageSize: 10,
            schema: {
                data: function (data) {
                    return data.value;
                },
                total: function (data) {
                    return data["odata.count"];
                },
                errors: function (data) {
                },
                model: {
                    id: "ProductID",
                    fields: {
                        ProductID: { type: "number", editable: false, nullable: true },
                        ProductName: { type: "string", validation: { required: true } },
                        UnitPrice: { type: "number", validation: { required: true, min: 1 } },
                        Discontinued: { type: "boolean" },
                        UnitsInStock: { type: "number", validation: { min: 0, required: true } }
                    }
                }
            },
            error: function (e) {
                var message = e.xhr.responseJSON["odata.error"].message.value;
                var innerMessage = e.xhr.responseJSON["odata.error"].innererror.message;
                alert(message + "\n\n" + innerMessage);
            }
        }),
        dataBound: function (arg) {
            if (lastSelectedProductId == null) return; // check if there was a row that was selected
            var view = this.dataSource.view(); // get all the rows
            for (var i = 0; i < view.length; i++) { // iterate through rows
                if (view[i].ProductID == lastSelectedProductId) { // find row with the lastSelectedProductd
                    var grid = arg.sender; // get the grid
                    grid.select(grid.table.find("tr[data-uid='" + view[i].uid + "']")); // set the selected row
                    break;
                }
            }
        },
        onChange: function (arg) {
            var grid = arg.sender;
            var dataItem = grid.dataItem(grid.select());
            lastSelectedProductId = dataItem.ProductID;
        }
    });

Notice how there is very little, if any, of our own code here. All we’ve done, is simply configured (filling in the blanks) the Kendo Datasource.

The important items to note here is that by default our OData enabled ProductController returns the count as data.odata.count and our data set in data.value, with this being the case we will need to help the Datasource by unpacking this and returning it to the Datasource. You can see how this is done in the schema data and total functions defined above.

We also define the model in the Datasource, the model inherits Kendo’s Observable object (class), meaning their is two-way binding with the Grid and Datasource, so anytime something happens on the Grid updates are automatically sent to the Datasource, then sent to our ProductsController.

The productGrid is declaratively attributed using the data- attributes, this is what Kendo UI MVVM uses for binding the View to the Model.

Subscribing to the viewSwitchedEvent Event


    $(document).bind("viewSwtichedEvent", function (e, args) { // subscribe to the viewSwitchedEvent
        if (args.name == "products") { // check if this view was switched too
            if (args.isRemotelyLoaded) { // check if this view was remotely loaded from server
                kendo.bind($("#productsForm"), productsModel); // bind the view to the model
            } else {// view already been loaded in cache
                productsModel.dataSource.fetch(function() {}); // refresh grid
            }
        }
    });

Here we are simply subscribing to viewSwitchedEvent that is published from the host page (Spa\index.html) of our SPA. This event is published (raised) everytime is View switching is complete. Here we check that the View that was switched in place was indeed the products View and that it was the first time is was loaded remotely from the server, if so, we bind the View to the Model. We do this only on the first time it loads from the server because there is really no need to do this more than once.

Now let’s load up the application and see our Products listing.

6-18-2013 3-01-38 PM

Testing Filtering and Sorting

6-18-2013 3-01-27 PM

Testing Inline Grid Editing

6-18-2013 6-31-01 PM

Testing Inline Grid Deleting

6-18-2013 6-57-55 PM

6-18-2013 6-55-22 PM

We see this error:

An error has occurred.

The primary key value cannot be deleted because references to this key still exist. [ Foreign key constraint name = Order Details_FK00 ]

Which is expected since the Order Details still has foreign keys to the Product table.

There you have it MVC 4, Web API, OData, Kendo UI, Grid, Datasource with MVVM. To spice things up, I changed the Kendo UI CSS to the Metro UI theme.

For live demo: http://longle.azurewebsites.net

Stay tuned for Part 3…

Part 3 – MVC 4, Web API, OData, EF, Kendo UI, Binding a Form to Datasource (CRUD) with MVVM

Download sample application: https://genericunitofworkandrepositories.codeplex.com

Happy Coding…! :)

MVC 4, Kendo UI, SPA with Layout, View, Router & MVVM

This will be part three of a six part series of blog posts.

  1. Modern Web Application Layered High Level Architecture with SPA, MVC, Web API, EF, Kendo UI, OData
  2. Generically Implementing the Unit of Work & Repository Pattern with Entity Framework in MVC & Simplifying Entity Graphs
  3. MVC 4, Kendo UI, SPA with Layout, View, Router & MVVM
  4. MVC 4, Web API, OData, EF, Kendo UI, Grid, Datasource (CRUD) with MVVM
  5. MVC 4, Web API, OData, EF, Kendo UI, Binding a Form to Datasource (CRUD) with MVVM
  6. Upgrading to Async with Entity Framework, MVC, OData AsyncEntitySetController, Kendo UI, Glimpse & Generic Unit of Work Repository Framework v2.0

Update: 09/09/2013 – Project has been uploaded to CodePlex: https://genericunitofworkandrepositories.codeplex.com, updated Visual 2013, Twitter Bootstrap, MVC 5, EF6, Kendo UI Bootstrap theme, project redeployed to Windows Azure Website.

Update: 06/20/2013 – Bug fix(es): Fixed View being loaded duplicate times. Enhancement(s): Added View caching. Updated blog, sample app download, and live demo

Wondering how to setup a SPA with MVC 4 project using Kendo UI Web…?! Well let’s cut to the chase and get started. We will start off with a plain old regular ASP.NET MVC 4 Internet project template and convert to a SPA, for the most part this implementation will keep in mind many of us are familiar with MVC 4 so we’ll try to adapt a lot things we are intimate with like the usage and organization of views, layouts, conventions, etc.

This will be part one five of a five part series of blog posts.

  1. Generically Implementing the Unit of Work & Repository Pattern with Entity Framework in MVC & Simplifying Entity Graphs
  2. MVC 4, Kendo UI, SPA with Layout, View, Router & MVVM
  3. MVC 4, Web API, OData, EF, Kendo UI, Grid, Datasource (CRUD) with MVVM
  4. MVC 4, Web API, OData, EF, Kendo UI, Binding a Form to Datasource (CRUD) with MVVM
  5. Upgrading to Async with Entity Framework, MVC, OData AsyncEntitySetController, Kendo UI, Glimpse & Generic Unit of Work Repository Framework v2.0

Taking a look at a high level architecture of this three part series blog: Modern Web Application Layered High Level Architecture with SPA, MVC, Web API, EF, Kendo UI.

Live demo: http://longle.azurewebsites.net.

You’ll need KendoUIWeb NuGet package.

6-18-2013 1-40-51 AM

First let’s create the SPA landing page (index.html), this will be the default page when the site is loaded up and where our Kendo UI Layout object will reside, and where we will swap our different views in.

Index.html


<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>MVC 4 SPA</title>
    <link href="/Content/kendo/2013.1.319/kendo.common.min.css" rel="stylesheet" />
    <link href="/Content/kendo/2013.1.319/kendo.metro.min.css" rel="stylesheet" />
    <link href="/Content/Site.css" rel="stylesheet" />
    <script src="/Scripts/jquery-2.0.2.min.js"></script>
    <script src="/Scripts/kendo/2013.1.319/kendo.web.min.js"></script>
    <script>
        var templateLoader = (function ($, host) {
            return {
                loadExtTemplate: function (name, path) {
                    $.ajax({
                        async: false,
                        url: path,
                        cache: false,
                        success: function (result) {
                            $("body").append(result);
                        },
                        error: function (result) {
                            alert("Error Loading View/Template -- TODO: Better Error Handling");
                        }
                    });
                }
            };
        })(jQuery, document);

        $(function () {
            var views = {};
            templateLoader.loadExtTemplate("layout", "/content/views/layout.html");
            var layout = new kendo.Layout($('#layout').html());
            layout.render($("#app"));

            var router = new kendo.Router();
            var addRoute = function (route, name, path, forceRemoteLoad) {
                forceRemoteLoad = typeof forceRemoteLoad !== "undefined" ? forceRemoteLoad : false;
                router.route(route, function () {
                    kendo.fx($("#body")).slideInRight().reverse().then(function () { // transition, slide view left
                        var isRemotelyLoaded = false;
                        if (views[name] == null || forceRemoteLoad)
                        {   // check if we have already loaded in cache, could store this in browser local storage for larger apps
                            isRemotelyLoaded = true;
                            templateLoader.loadExtTemplate(name, path); // load the view
                            views[name] = new kendo.View(('#' + name)); // add the view to cache
                        }
                        layout.showIn("#body", views[name]); // switch view
                        $(document).trigger("viewSwtichedEvent", { route: route, name: name, path: path, isRemotelyLoaded: isRemotelyLoaded }); // publish event view has been loaded (EventAggregator pattern)
                        kendo.fx($("#body")).slideInRight().play(); // transition, slide view back to the right (center)
                    });
                });
            };

            addRoute("/", "home", "/content/views/home.html");
            addRoute("/about", "about", "/content/views/about.html");
            addRoute("/contact", "contact", "/content/views/contact.html");
            addRoute("/categories", "categories", "/content/views/categories.html");
            addRoute("/customers", "customers", "/content/views/customers.html");
            addRoute("/products", "products", "/content/views/products.html");
            addRoute("/productEdit/:id", "productEdit", "/content/views/productEdit.html");

            router.start();
        });
    </script>
</head>
<body>
    <div id="app"></div>
</body>
</html>

I’ve deliberately left much of the JavaScript code on each of the pages in the interest of easily understanding what’s happening on each of the pages or views, obviously you can extract them into their own .js files however you see fit.

TemplateLoader Helper


        var templateLoader = (function ($, host) {
            return {
                loadExtTemplate: function (name, path) {
                    $.ajax({
                        async: false,
                        url: path,
                        cache: false,
                        success: function (result) {
                            $("body").append(result);
                        },
                        error: function (result) {
                            alert("Error Loading View/Template -- TODO: Better Error Handling");
                        }
                    });
                }
            };
        })(jQuery, document);

The templateLoader helper method was used from here, and has been slightly modified so that we can load Kendo Views remotely from the server in our SPA when navigating through the application.

  1. It’s taking a path to a file
  2. Grabbing the contents with jQuery Ajax
  3. Appending the contents to the body of our document
  4. Then notifying the application that the template has loaded

Kendo UI Web Router


        $(function () {
            var views = {};
            templateLoader.loadExtTemplate("layout", "/content/views/layout.html");
            var layout = new kendo.Layout($('#layout').html());
            layout.render($("#app"));

            var router = new kendo.Router();
            var addRoute = function (route, name, path, forceRemoteLoad) {
                forceRemoteLoad = typeof forceRemoteLoad !== "undefined" ? forceRemoteLoad : false;
                router.route(route, function () {
                    kendo.fx($("#body")).slideInRight().reverse().then(function () { // transition, slide view left
                        var isRemotelyLoaded = false;
                        if (views[name] == null || forceRemoteLoad)
                        {   // check if we have already loaded in cache, could store this in browser local storage for larger apps
                            isRemotelyLoaded = true;
                            templateLoader.loadExtTemplate(name, path); // load the view
                            views[name] = new kendo.View(('#' + name)); // add the view to cache
                        }
                        layout.showIn("#body", views[name]); // switch view
                        $(document).trigger("viewSwtichedEvent", { route: route, name: name, path: path, isRemotelyLoaded: isRemotelyLoaded }); // publish event view has been loaded (EventAggregator pattern)
                        kendo.fx($("#body")).slideInRight().play(); // transition, slide view back to the right (center)
                    });
                });
            };

            addRoute("/", "home", "/content/views/home.html");
            addRoute("/about", "about", "/content/views/about.html");
            addRoute("/contact", "contact", "/content/views/contact.html");
            addRoute("/categories", "categories", "/content/views/categories.html");
            addRoute("/customers", "customers", "/content/views/customers.html");
            addRoute("/products", "products", "/content/views/products.html");
            addRoute("/productEdit/:id", "productEdit", "/content/views/productEdit.html");

            router.start();
        });

We are also putting the Kendo UI Router to good use so that we can navigate between the different views in our SPA while syncing with the browser history so even though we are never refreshing, and our app never does any post backs, the user is still able to navigate back and forward using their browser buttons.

We can see that we are also using the Kendo FX API, this is to polish the UX in our SPA by adding slide transitions when swapping views. We are simply remotely loading the requested View (if it hasn’t already been loaded for the first time) from the server, sliding the #body div to the left, swapping the View with the requested one, sliding the #body div back it’s place (you can click on the live demo link to see this in action). If the view has already been loaded we cache the view in a dictionary, the next time this View is navigated to, we will check cache it it already exist then will load it from there, if not, we’ll load it from the server. Yes, we could have done the transitions this with a jQuery plug-in or some other tool or framework, however the intent was here was to accomplish this (along with Part 2 and 3 of this blog series) with the use of only framework, Kendo UI Web.

addRoute Helper


            var addRoute = function (route, name, path, forceRemoteLoad) {
                forceRemoteLoad = typeof forceRemoteLoad !== "undefined" ? forceRemoteLoad : false;
                router.route(route, function () {
                    kendo.fx($("#body")).slideInRight().reverse().then(function () { // transition, slide view left
                        var isRemotelyLoaded = false;
                        if (views[name] == null || forceRemoteLoad)
                        {   // check if we have already loaded in cache, could store this in browser local storage for larger apps
                            isRemotelyLoaded = true;
                            templateLoader.loadExtTemplate(name, path); // load the view
                            views[name] = new kendo.View(('#' + name)); // add the view to cache
                        }
                        layout.showIn("#body", views[name]); // switch view
                        $(document).trigger("viewSwtichedEvent", { route: route, name: name, path: path, isRemotelyLoaded: isRemotelyLoaded }); // publish event view has been loaded (EventAggregator pattern)
                        kendo.fx($("#body")).slideInRight().play(); // transition, slide view back to the right (center)
                    });
                });
            };

The addRoute is simply a helper method to register our routes and the with the Router along with the delegate we want invoke when navigating between our Views. Our delegate simply handles the animation transitions, switching out the Views and caching of our Views.

Now our next step is just converting the regular ASP.NET views to pure html which is pretty straight forward. For now, since MVC ignores routes for anything under /Content folder, we’ll just put our views in Spa/Content/views/, you can place them anywhere you see fit, and just have the MVC runtime ignore that path using the routes.IgnoreRoute([path-goes-here]) method in your RoutConfig.cs.

6-17-2013 10-41-19 PM

Spa/Content/views/layout.html


<script type="text/x-kendo-template" id="layout" highlight="31">
    <header>
        <div class="content-wrapper">
            <div class="float-left">
                <p class="site-title"><a href="/">CBRE Architecture & Design Team</a></p>
            </div>
            <div class="float-right">
                <section id="login">
                    <ul>
                        <li><a href="/Account/Register" id="registerLink">Register</a></li>
                        <li><a href="/Account/Login" id="loginLink">Log in</a></li>
                    </ul>
                </section>
                <nav>
                    <ul id="menu">
                        <li><a href="#/" >Home</a></li>
                        <li><a href="#/about" >About</a></li>
                        <li><a href="#/contact">Contact</a></li>
                    </ul>
                </nav>
                <nav>
                    <ul id="menu">
                        <li><a href="#/customers" >Customers</a></li>
                        <li><a href="#/categories" >Categories</a></li>
                        <li><a href="#/products" >Products</a></li>
                    </ul>
                </nav>
            </div>
        </div>
    </header>
    <div id="body">
    </div>
    <footer>
        <div class="content-wrapper">
            <div class="float-left">
                <p>&copy; 2013 - My ASP.NET MVC Application</p>
            </div>
        </div>
    </footer>
</script>

The layout.html View is loaded first, this is pretty much the layout of the application, all the Views will be loaded into the div:

<div d="body">

Spa/Content/views/home.html


<script type="text/x-kendo-template" id="home">
<section class="featured">
    <div class="content-wrapper">
        <hgroup class="title">
            <h1>Home Page.</h1>
            <h2>Modify this template to jump-start your ASP.NET MVC application.</h2>
        </hgroup>
        <p>
            To learn more about ASP.NET MVC visit
                <a href="http://asp.net/mvc" title="ASP.NET MVC Website">http://asp.net/mvc</a>.
                The page features <mark>videos, tutorials, and samples</mark> to help you get the most from ASP.NET MVC.
                If you have any questions about ASP.NET MVC visit
                <a href="http://forums.asp.net/1146.aspx/1?MVC" title="ASP.NET MVC Forum">our forums</a>.
        </p>
    </div>
</section>

<section class="content-wrapper main-content clear-fix">

    <h3>We suggest the following:</h3>
    <ol class="round">
        <li class="one">
            <h5>Getting Started</h5>
            ASP.NET MVC gives you a powerful, patterns-based way to build dynamic websites that
        enables a clean separation of concerns and that gives you full control over markup
        for enjoyable, agile development. ASP.NET MVC includes many features that enable
        fast, TDD-friendly development for creating sophisticated applications that use
        the latest web standards.
        <a href="http://go.microsoft.com/fwlink/?LinkId=245151">Learn more…</a>
        </li>

        <li class="two">
            <h5>Add NuGet packages and jump-start your coding</h5>
            NuGet makes it easy to install and update free libraries and tools.
        <a href="http://go.microsoft.com/fwlink/?LinkId=245153">Learn more…</a>
        </li>

        <li class="three">
            <h5>Find Web Hosting</h5>
            You can easily find a web hosting company that offers the right mix of features
        and price for your applications.
        <a href="http://go.microsoft.com/fwlink/?LinkId=245157">Learn more…</a>
        </li>
    </ol>

</section>
</script>
<script>
    var homeModel = new kendo.data.ObservableObject({});
</script>

Spa/Content/views/contact.html


<script type="text/x-kendo-template" id="contact">
<section class="content-wrapper main-content clear-fix">
    <hgroup class="title">
        <h1>Contact.</h1>
        <h2>Your contact page.</h2>
    </hgroup>

    <section class="contact">
        <header>
            <h3>Phone</h3>
        </header>
        <p>
            <span class="label">Main:</span>
            <span>425.555.0100</span>
        </p>
        <p>
            <span class="label">After Hours:</span>
            <span>425.555.0199</span>
        </p>
    </section>

    <section class="contact">
        <header>
            <h3>Email</h3>
        </header>
        <p>
            <span class="label">Support:</span>
            <span><a href="mailto:Support@example.com">Support@example.com</a></span>
        </p>
        <p>
            <span class="label">Marketing:</span>
            <span><a href="mailto:Marketing@example.com">Marketing@example.com</a></span>
        </p>
        <p>
            <span class="label">General:</span>
            <span><a href="mailto:General@example.com">General@example.com</a></span>
        </p>
    </section>

    <section class="contact">
        <header>
            <h3>Address</h3>
        </header>
        <p>
            One Microsoft Way<br />
            Redmond, WA 98052-6399
        </p>
    </section>
</section>
</script>
<script>
    var contactModel = new kendo.data.ObservableObject({});
</script>

Spa/Content/views/about.html


<script type="text/x-kendo-template" id="about">
<section class="content-wrapper main-content clear-fix">
    <hgroup class="title">
        <h1>About.</h1>
        <h2>Your app description page.</h2>
    </hgroup>

    <article>
        <p>
            Use this area to provide additional information.
        </p>

        <p>
            Use this area to provide additional information.
        </p>

        <p>
            Use this area to provide additional information.
        </p>
    </article>

    <aside>
        <h3>Aside Title</h3>
        <p>
            Use this area to provide additional information.
        </p>
        <ul>
            <li><a href="/">Home</a></li>
            <li><a href="/Home/About">About</a></li>
            <li><a href="/Home/Contact">Contact</a></li>
        </ul>
    </aside>
</section>
</script>
<script>
    var aboutModel = new kendo.data.ObservableObject({});
</script>

We see that each and every View is responsible for the Model that it will use and bind to, so every view will need to have a Model. The convention we are using here is name of View and viewModel, e.g. so if your View is name Product then it should have a model named ProductModel. The code in the SPA/Index.html will handle binding them together, we just need to follow the convention and everything else is handled.

For live demo: http://longle.azurewebsites.net

Download sample application: https://genericunitofworkandrepositories.codeplex.com

Stay tune for Part 2 and 3…

Part 2 – MVC 4, Web API, OData, EF, Kendo UI, Grid, Datasource with MVVM
Part 3 – MVC 4, Web API, OData, EF, Kendo UI, Binding a Form to Datasource (CRUD) with MVVM

Learning Kendo UI Web Development with MVC Book Published…!

Follow up on one of my previous blog post in regards to Kendo UI Web with MVC book project I had the opportunity to be involved with as a Technical Reviewer, the book is complete and has been published..! Kudos to John Adams (author) from RBA for all the late nights, weekends and hard work I know he put into the project.

For those of you looking for a good starting point, incorporating Kendo UI Web into your MVC project or solution the book has some good real world practical implementations with ASP.NET MVC.

Personally and professionally Kendo UI Web is my client side framework of choice. It’s one solution that offers it all, so that you don’t have into incorporate and include several different frameworks to gain some of the fundamental features of Kendo UI Web, especially when working with MVC.

My personal top favorite features of Kendo UI Web Framework

  • Widgets (client-side controls)
  • MVVM Framework (a must have for modern web apps)
  • Validation Framework
  • Templates
  • Built on top of jQuery
  • Custom UI/UX Themes
  • MVC 4 Sever Side Wrappers
  • SPA (Single Page App) Support (a must have for modern web apps)
  • Globalization
  • DataSource
  • List goes on…

http://www.amazon.com/dp/1849694346/?tag=packtpubli-20
http://www.packtpub.com/learning-kendo-ui-web-development/book
http://packtlib.packtpub.com/library/9781849694346

An easy-to-follow practical tutorial to add exciting features to your web pages without being a JavaScript expert with this book and ebook

Overview

Learn from clear and specific examples on how to utilize the full range of the Kendo UI tool set for the web
Add powerful tools to your website supported by a familiar and trusted name in innovative technology
Learn how to add amazing features with clear examples and make your website more interactive without being a JavaScript expert

In Detail

Creating useful and attractive web sites for today’s audiences requires more JavaScript programming than ever before. JavaScript, however, isn’t easy and staying up to date with recent trends makes it even harder. You need a way to integrate the latest technology into your web site without having to start from the beginning.

“Learning Kendo UI Web Development” shows you how to add the latest web features to your site without doing all of the work yourself. The experts at Telerik have created a fully supported code library that lets you focus on what makes your sites special while letting them focus on how that translates into cutting edge JavaScript code.

This book will take you on a survey through Kendo UI for the web from the basic widgets that wrap input fields to the full page scaffolding widgets for setting up your site.

Kendo UI for the web is a rich framework of JavaScript widgets and tools that will add immediate value to your web sites. Learn the full spectrum of what Kendo UI has to offer in specialized widgets that gather input from users by displaying calendars, sliders, or date pickers all the way up to widgets that structure your web pages through data-driven models in accordions, tabs, list views, and tree views.

“Learning Kendo UI Web Development” is the perfect companion for navigating the broad features offered by Telerik in JavaScript web development.

What you will learn from this book

  • Leverage data source objects and JavaScript templates with the TabStrip and Grid widgets
  • Guide users in date selection with the Calendar widget
  • Create fast and fluid word wheels with the AutoComplete widget and date selection with the Calendar widget
  • Take advantage of the powerful MVVM JavaScript architectural pattern
  • Take full HTML input from users with a graphical editor
  • Structure your site with the Menu and ListView widgets
  • Build interactive accordions with the PanelBar widget and a fun way to select numbers with the Slider widget
  • Organize your data with the Splitter and TreeView widgets and Customize pop-up windows and file uploads with the Window and Upload widgets

Approach

A practical tutorial with step-by-step example based approach.

Who this book is written for

This book is for web developers who want to take advantage of cutting edge JavaScript and HTML 5 web site features, but who don’t have the time or the knowledge to write all of that code by hand. The reader should be familiar with basic HTML 5 and JavaScript but does not need to be an expert.

Author:

John Adams currently works as an application development consultant in the Dallas/Fort Worth area for a fantastic company called RBA. He has been developing custom business applications with the Microsoft .NET platform for 6 years and has specialized in development with ASP.NET MVC. He loves writing code and creating solutions. Above all, he loves his wife and children and the lord Jesus Christ.

This book is dedicated to Michell, Samuel, and Sophie whose patience with my late nights made this project possible.

I would also like to thank RBA, especially my manager Will, who introduced me to the project and kicked everything off.

Finally, I would like to thank Kartikey Pandey, Anugya Khurana, Mayur Hule, Ricardo Covo, and Long Le for their oversight and editing skills. Their work has been exceptional and valuable throughout.

Reviewers:

Ricardo Covo has more than a decade of international experience in the Software Development field, with experience in Latin America, California, and Canada. He has a wealth of experience in delivering data-driven enterprise solutions across various industries.

With a Bachelor’s degree in Systems Engineering, complemented with a certification in Advanced Project Management, he has the right combination of technical and leadership skills to build development teams and set them up for efficient execution.

In 2007 he founded (and is the principal of) Web Nodes – Software Development (http://webnodes.ca); a custom software development company, with clients big and small in Canada, United States, and South America.

Prior to Web Nodes, Ricardo spent some years in the corporate world both in Canada and in the U.S., being part of companies such as Loblaws Inc., Trader Corporation, UNX (http://www.unix.com) and Auctiva (http://www.auctiva.com).

Ricardo’s passion for technology goes beyond work; he normally works on personal projects in an effort to always remain on top of the changes in technology. These projects include: http://ytnext.com, http://serversok.com, and http://toystrunk.com.

Long Le is a Principle .NET Architect and ALM Practitioner at CBRE. He also serves as consultant for Thinklabs and spends most of his time developing frameworks and application blocks, providing guidance for best practices and patterns, and standardizing the enterprise technology stack. He has been working with Microsoft technologies for over 10 years.

Le has focused on a wide spectrum of server-side and web technologies, such as ASP.NET Web Forms, ASP.NET MVC, Windows Workflow, LINQ and Entity Framework, DevExpress, and Kendo UI. In his spare time, he enjoys blogging (http://blog.longle.net) and playing Call of Duty on his XBOX. He’s recently became a proud father of his new born daughter Khloe Le. You can reach and follow him on Twitter @LeLong37.

Special thanks to my significant other Tina Le for all your love and support throughout this project and to my wonderful newborn daughter Khloe Le. I love you.

https://twitter.com/LeLong37/status/344402714240897024

Telerik’s HTML5 Kendo UI Web Validator Framework

I recently wrote a blog on Telerik’s Kendo UI Web MVVM Framework. This blog post was intended to illustrate the power of MVVM binding on the client side (declaritive binding).

In this blog post I wanted to demonstrate the ability to do some nice validation with the Kendo UI Web Validator Framework which also let’ you do a quite bit of validtion declaritively. With that beind said let’s get right into it using the MVVM form we created from the previous post.

Let’s take a quick at where we left off at on our last form.


    <div id="formContainer">
        <h1>
            My First MVVM Web View!</h1>
        <form id="myForm" action="">
        <label>
            First Name</label>
        <input type="text" data-bind="value: firstName, disabled:  isDisabled" />
        <label>
            Last Name</label>
        <input type="text" data-bind="value: lastName, disabled:  isDisabled" />
        <label>
            Email</label>
        <input type="text" data-bind="value: email, disabled:  isDisabled" />
        <label>
            Twitter</label>
        <input type="text" data-bind="value: twitter, disabled:  isDisabled" />
        <label>
            Site</label>
        <input type="text" data-bind="value: site, disabled:  isDisabled" />
        <label>
            Address</label>
        <input type="text" data-bind="value: address, disabled:  isDisabled" />
        <label>
            City</label>
        <input type="text" data-bind="value: city, disabled:  isDisabled" />
        <label>
            State</label>
        <input type="text" data-bind="value: state, disabled:  isDisabled" />
        <label>
            Zip</label>
        <input type="text" data-bind="value: zip, disabled:  isDisabled" />
        <label>
            Occupation</label>
        <select data-bind="source: occupations, value: occupation, disabled:  isDisabled">
        </select>
        <br />
        <br />
        <input type="button" value="Load" data-bind="click: load" />
        <input type="button" value="Edit" data-bind="click: edit" />
        <input type="button" value="Cancel" data-bind="click: cancel" />
        <input type="button" value="Reset" data-bind="click: reset" />
        </form>
    </div>

Now let’s add a couple of extra fields and declaritively add some validation to our form. While doing this let’s go ahead and change the types to our input controls with the appropriate HTML5 (http://www.w3schools.com/html5/html5_form_attributes.asp) types e.g. date, number, email, etc.. Quick note, notice that we are using the HTML5 [pattern] attribute for the input field named “Phone”.


    <div id="formContainer">
        <h1>
            My First MVVM Web View <br />
            now with Validation!</h1>
        <form id="myForm" action="">
        <label>
            First Name</label>
        <input type="text" name="firstName" data-bind="value: firstName, disabled:  isDisabled" required validationMessage="please enter a first name" />
        <label>
            Last Name</label>
        <input type="text" name="lastName" data-bind="value: lastName, disabled:  isDisabled" required validationMessage="please enter a last name" />
        <label>
            Email</label>
        <input type="email" name="email" data-bind="value: email, disabled:  isDisabled" required />
        <label>
            Twitter</label>
        <input type="url" name="twitter" data-bind="value: twitter, disabled:  isDisabled" required data-required-msg="please enter a {0}" data-url-msg="please enter a valid url" />
        <label>
            Site</label>
        <input type="url" name="site" data-bind="value: site, disabled:  isDisabled" required validationMessage="please enter a {0}" data-url-msg="please enter a valid url"/>
        <label>
            Address</label>
        <input type="text" name="address" data-bind="value: address, disabled:  isDisabled" required validationMessage="please enter a {0}" />
        <label>
            City</label>
        <input type="text" name="city" data-bind="value: city, disabled:  isDisabled" required validationMessage="please enter a {0}"/>
        <label>
            State</label>
        <input type="text" name="state" data-bind="value: state, disabled:  isDisabled" required validationMessage="please enter a {0}"/>
        <label>
            Zip</label>
        <input type="text" name="zip" data-bind="value: zip, disabled:  isDisabled" required validationMessage="please enter a {0}"/>
        <label>
            Phone</label>
        <input type="tel" name="phone" pattern="\d{10}"  data-bind="value: phone, disabled:  isDisabled" required data-required-msg="please enter a {0} number" data-pattern-msg="please enter a 10 digit phone number"/>
        <label>
            Lucky Number</label>
        <input type="number" name="luckynumber" data-bind="value: luckyNumber, disabled:  isDisabled" required data-required-msg="please enter a lucky number" data-max-msg="please enter a number less than 100" min="1" max="100" />
        <label>
            Birth Date</label>
        <input type="date" name="birthdate" data-bind="value: birthDate, disabled:  isDisabled" required validationMessage="please enter a {0}" />
        <label>
            Occupation</label>
        <select name="occupation" data-bind="source: occupations, value: occupation, disabled:  isDisabled" required valdationMessage="please select a {0}">
        </select>
        <br />
        <br />
        <input type="button" value="Load" data-bind="click: load" />
        <input type="button" value="Edit" data-bind="click: edit" />
        <input type="button" value="Cancel" data-bind="click: cancel" />
        <input type="button" value="Reset" data-bind="click: reset" />
        <button type="submit" value="Submit">Save</button>
        </form>
    </div>

Notice that all we are doing here is really providing the Validator Framework with is validation messages for the type of validation. There is a convention that we need to follow which is marking the control with the attribute: data-[rule]-msg, [rule] just needs to be replaced with actual rule name so for example data-required-msg=”please enter a luckly number” will be the message that is displayed when there is no number in the input field and data-max-message=”please enter a number less than 100″ will be the validation message that is shown if a user enters a number that is greater than 100. Now that all of our validation messages are setup let’s add our one line of script to get this all of our validations wired up (line 45).


    var myViewModel;
    $(document).ready(function () {
        myViewModel = kendo.observable({
            firstName: "Long",
            lastName: "Le",
            email: "lelong37@gmail.com",
            twitter: "twitter.com/lelong37",
            site: "blog.longle.net",
            address: "3737 Galatic Avenue",
            city: "Cloud City",
            state: "Texas",
            occupations: ["", "Hacker", "Jedi", "Ninja"],
            occupation: "Jedi",
            phone: "1111111111",
            luckyNumber: 34,
            birthDate: null,
            isSaved: false,
            isDisabled: true,
            edit: function (e) {
                this.set("isDisabled", false);
            },
            cancel: function (e) {
                this.set("isDisabled", true);
            },
            reset: function (e) {
                this.set("firstName", null);
                this.set("lastName", null);
                this.set("email", null);
                this.set("twitter", null);
                this.set("site", null);
                this.set("address", null);
                this.set("city", null);
                this.set("state", null);
                this.set("zip", null);
                this.set("occupation", "");
                this.set("phone", null);
                this.set("luckyNumber", null);
            },
            load: function (e) {
                LoadJohnDoesInfo();
            }
        });

        kendo.bind($("#myForm"), myViewModel);
        var validator = $("#myForm").kendoValidator().data("kendoValidator");
    });

Now with our declaritive validation and one line of Javascript to wire everything up let’s give our form a spin. Let’s clear out our form by and try saving by clicking [Edit], [Reset] and [Save].

Let’s test out our messsages that are custom to a specific validation type by typing in an invalid Url for our Twitter, Site and invalid email address when filling out our form and clicking [Save].

Notice how are validation messages are not the displaying the standard required messages but now they are validation messages specific to the type of validation. For example with the field “Lucky Number” the standard validation message was “please enter a lucky number” now that we actually typed in a value that is greater than the max attribute that was set to 100 we are getting the declartive validation message “please enter a number less than 100″. Also for the field [email] we also have typed in something however because we changed the input type to email (new with HTML5) out of the box the framework is displaying a standard message for us “email is not a valid email” even though we did not explicitly declare a validation message for email.

Now let’s fix all the fields with the correct values email, twitter, site, phone and lucky number and notice as we tab out of each field our validation messages dissapear and we are able to post the form.

Last but not least we can manually invoke the Validator by invoking the validate() method, let’s go ahead and rewire our click button to go ahead and validate our form as well as display a message to the user.


        $("button").click(function (e) {
            e.preventDefault();
            if (validator.validate()) {
                alert("Your form is golden!");
            } else {
                alert("Your form has errors!");
            }
        });

Let’s give this a try.

Happy Coding! :)

Download:

Telerik’s Kendo UI Web MVVM Framework Rocks!

For those of us that fell in love with the awesome binding power that WPF and Silverlight brought to the masses with MVVM (Model-ViewModel-Model), well I have good news for you folks. Telerik has their Kendo UI Web MVVM Framework so that you can leverage MVVM in your (MVC) web apps!

Quick synopsis on MVVM for those of us that are new to it (especially MVC devs) and in the interest of time I’ll try to do this in a 60 second nutshell. So when working with a view whether it be in WPF, Silverlight, and now MVC, you can declaritively set which controls bind to which properties to your ViewModel (simply a JSON object with properties). So for example I can set a DropDownList (select) control on my view to bind directly to a property of my ViewModel that is a collection.

So let’s get right to it with a few simple examples.

First add a script references for jQuery and Kendo UI, here are some links for Microsoft’s and Teleriks’ CDN’s for these scripts.


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Kendo UI Web MVVM</title>
</head>
<body>
</body>
<script src="http://code.jquery.com/jquery-1.7.1.min.js"></script>
<script src="http://cdn.kendostatic.com/2012.2.710/js/kendo.all.min.js"></script>
</html>

Now let’s create a simple form.


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Kendo UI Web MVVM</title>
    <script src="http://code.jquery.com/jquery-1.7.1.min.js" type="text/javascript"></script>
    <script src="http://cdn.kendostatic.com/2012.2.710/js/kendo.all.min.js" type="text/javascript"></script>
    <style type="text/css">
        body
        {
            font-family: Arial;
        }
        label
        {
            display: block;
            margin-top: 10px;
        }
        #formContainer
        {
            background-color: #F2F7F9;
            width: 400px;
            padding: 20px;
            margin: 50px auto;
            border: 6px solid #8FB5C1;
            border-radius: 15px;
            position: relative;
        }
    </style>
</head>
<body>
    <div id="formContainer">
        <h1>My First MVVM Web View!</h1>
        <form id="myForm" action="">
        <label>
            First Name</label>
        <input type="text" />
        <label>
            Last Name</label>
        <input type="text"  />
        <label>
            Email</label>
        <input type="text"  />
        <label>
            Twitter</label>
        <input type="text"  />
        <label>
            Site</label>
        <input type="text" />
        <label>
            Address</label>
        <input type="text" />
        <label>
            City</label>
        <input type="text" />
        <label>
            State</label>
        <input type="text" />
        <label>
            Zip</label>
        <input type="text" />
        <label>
            Occupation</label>
        <select>
        </select>
        <br />
        <br />
        <input type="button" />
        <input type="button" />
        <input type="button" />
         <input type="button" />
        </form>
    </div>
</body>

</html>

Now let’s create a ViewModel (JavaScript JSON object or class if you will) with some default values for our View (in this case our form) to bind to. We will also add some methods to our ViewModel so that we can bind our buttons that are on our view too.


  <script language="javascript" type="text/javascript">

    var myViewModel;
    $(document).ready(function () {
        myViewModel = kendo.observable({
            firstName: "Long",
            lastName: "Le",
            email: "lelong37@gmail.com",
            twitter: "twitter.com/lelong37",
            site: "blog.longle.net",
            address: "3737 Galatic Avenue",
            city: "Cloud City",
            state: "Texas",
            occupations: ["Please Select", "Hacker", "Jedi", "Ninja"],
            occupation: "Jedi",
        });

        kendo.bind($("#myForm"), myViewModel);
    });

</script>

Note: Line 18 is what will bind our form to our ViewModel, if your wondering why nothing is happening yet it’s because we are missing once more step which is to provide the binding (mapping) information, which will map our controls to our ViewModel. The beauty here is we will do this declaritively with without code.

Our View will have two modes: read-only and edit, so we will bind our controls for where each control will get and set it’s values to and another binding for enabling and disabling them.


    <div id="formContainer">
        <h1>My First MVVM Web View!</h1>
        <form id="myForm" action="">
        <label>
            First Name</label>
        <input type="text" data-bind="value: firstName, disabled:  isDisabled" />
        <label>
            Last Name</label>
        <input type="text" data-bind="value: lastName, disabled:  isDisabled" />
        <label>
            Email</label>
        <input type="text" data-bind="value: email, disabled:  isDisabled" />
        <label>
            Twitter</label>
        <input type="text" data-bind="value: twitter, disabled:  isDisabled" />
        <label>
            Site</label>
        <input type="text" data-bind="value: site, disabled:  isDisabled" />
        <label>
            Address</label>
        <input type="text" data-bind="value: address, disabled:  isDisabled" />
        <label>
            City</label>
        <input type="text" data-bind="value: city, disabled:  isDisabled" />
        <label>
            State</label>
        <input type="text" data-bind="value: state, disabled:  isDisabled" />
        <label>
            Zip</label>
        <input type="text" data-bind="value: zip, disabled:  isDisabled" />
        <label>
            Occupation</label>
        <select data-bind="source: occupations, value: occupation, disabled:  isDisabled">
        </select>
        <br />
        <br />
        <input type="button" value="Load" />
        <input type="button" value="Edit" />
        <input type="button" value="Cancel" />
         <input type="button" value="Reset" />
        </form>
    </div>

Now if we refresh the page we that our View is not bound to our ViewModel…!

Note: Notice that our form is in read-only mode, all of our controls are disabled for editing because the disabled attribute is bound to the isDisabled property in our ViewModel whose default value is set to true.

For those of us that are ASP.NET MVC developers, the answer is yes you can get and post your ViewModel back as JSON using jQuery from and to an actions on your controller!

Now let’s demonstrate binding our buttons to methods on our ViewModel, first let’s add a couple of methods to our ViewModel (edit, cancel, reset, and load)

  • edit, will enabled our View for editing
  • cancel, will set our View back to read-only mode
  • reset, will clear out our View
  • load, will load John Doe’s information into our View (obviously here you could load this from using a “GET” to an action off of your controller)

    var myViewModel;
    $(document).ready(function () {
        myViewModel = kendo.observable({
            firstName: "Long",
            lastName: "Le",
            email: "lelong37@gmail.com",
            twitter: "twitter.com/lelong37",
            site: "blog.longle.net",
            address: "3737 Galatic Avenue",
            city: "Cloud City",
            state: "Texas",
            occupations: ["Please Select", "Hacker", "Jedi", "Ninja"],
            occupation: "Jedi",
            isSaved: false,
            isDisabled: true,
            edit: function (e) {
                this.set("isDisabled", false);
            },
            cancel: function (e) {
                this.set("isDisabled", true);
            },
            reset: function (e) {
                this.set("firstName", null);
                this.set("lastName", null);
                this.set("email", null);
                this.set("twitter", null);
                this.set("site", null);
                this.set("address", null);
                this.set("city", null);
                this.set("state", null);
                this.set("zip", null);
                this.set("occupation", "Please Select");
            },
            load: function (e) {
                LoadJohnDoesInfo();
            }
        });

        kendo.bind($("#myForm"), myViewModel);
    });

    function LoadJohnDoesInfo() {
        myViewModel.set("firstName", "John");
        myViewModel.set("lastName", "Doe");
        myViewModel.set("email", "jdoe@skyranch.com");
        myViewModel.set("twitter", "twitter.com/jedi");
        myViewModel.set("site", "starwars.com");
        myViewModel.set("address",  "1212 SkyRanch");
        myViewModel.set("state", "California");
        myViewModel.set("zip", "98000");
        myViewModel.set("occupation", "Jedi");
    }

Add our binding meta-data to our buttons declaritively.


        <input type="button" value="Load" data-bind="click: load" />
        <input type="button" value="Edit" data-bind="click: edit" />
        <input type="button" value="Cancel" data-bind="click: cancel" />
        <input type="button" value="Reset" data-bind="click: reset" />


So let’s invoke some our methods on our ViewModel and give our button’s a run for their money.

Now let’s click on the [Load] button and here I just wanted to demonstrating that all we need to do here to update the View is interact with the ViewModel, any changes to the ViewModel and the View automatically updates because they are bound together, which is the magic and essense of the MVVM pattern!

Our load method on our ViewModel.


            load: function (e) {
                LoadJohnDoesInfo();
            }

Which turns around and invokes our LoadJohnDoesInfo method and set’s his data on our ViewModel.


    function LoadJohnDoesInfo() {
        myViewModel.set("firstName", "John");
        myViewModel.set("lastName", "Doe");
        myViewModel.set("email", "jdoe@skyranch.com");
        myViewModel.set("twitter", "twitter.com/jedi");
        myViewModel.set("site", "starwars.com");
        myViewModel.set("address", "1212 SkyRanch");
        myViewModel.set("state", "California");
        myViewModel.set("zip", "98000");
        myViewModel.set("occupation", "Jedi");
    }

Notice when the View first loads our entire form is disabled for our read-only mode and when we click [Edit] all of our controls that had the binding for disabled that was bound to the isDisabled property on our ViewModel which be default is set to true. Once we click on the Edit button the isDisabled property is set to true enabling all of our controls for edit mode.

Our declaritive binding on one of our controls.


<input type="text" data-bind="value: firstName, disabled:  isDisabled" />

The Edit method that is invoked when clicking the [Edit] button.


            edit: function (e) {
                this.set("isDisabled", false);
            },

Hitting the [Reset] button will simply invoke the reset method on our ViewModel which clears out all our properties by setting them null values and again our View is automagically updated because it is bound to our ViewModel.


            reset: function (e) {
                this.set("firstName", null);
                this.set("lastName", null);
                this.set("email", null);
                this.set("twitter", null);
                this.set("site", null);
                this.set("address", null);
                this.set("city", null);
                this.set("state", null);
                this.set("zip", null);
                this.set("occupation", "Please Select");
            },

Well great, I know we aren’t doing anything ground breaking here, however this post is really just to illustrate MVVM on the client-side using Telerik’s Kendo UI Web MVVM Framework and ellaborate on how we can really just work with the ViewModel to update the View vice- versa any updates to the View will update our ViewModel because they are “bound” together using MVVM.

Last but not least, even though the Kendo UI Framework was written with jQuery, notice how we didn’t have to explicitly use any jQuery or Javascript to set or get any values from our controls. I’m not at all saying jQuery is a bad thing, however when developing you probably want to be coding something that is immediately adding business value rather than coding jQuery selectors to get and and set values in your View right..? :p

Happy coding…! :)

Download sample code: https://skydrive.live.com/redir?resid=949A1C97C2A17906!1951