Meet Spine.js – My framework of choice for client side MVC (Part 2)

In the last post, we have used Html5 local storage for persistence, in this post we are going to integrate it with ASP.NET MVC. Before moving forward, let me tell you that frameworks like backbone.js and spine.js expects your server endpoints are RESTful and do utilize the http verbs, for example the same endpoint can perform different things based upon the http verb. Unlike Rails, the default Controller and routes that ASP.NET MVC generates does not have those features. So we need to tweak the ASP.NET MVC to improve its “RESTful-ness”. In case, if you are wondering about the url structures, here is the list of defaults of ASP.NET MVC:

ASP.NET MVC Default Routes
Verb Url Action
GET /{resource} Index
GET /{resource}/details/{id} Details
GET /{resource}/create Create
POST /{resource}/create Create
GET /{resource}/edit/{id} Edit
POST /{resource}/edit/{id} Edit
GET /{resource}/delete/{id} Delete
POST /{resource}/delete/{id} Delete

And here are the changes that we would like to apply:

Better RESTful urls
Verb Url Action
GET /{resource} Index
GET /{resource}/{id} Show
GET /{resource}/new New
POST /{resource} Create
GET /{resource}/{id}/{edit} Edit
PUT /{resource}/{id} Update
GET /{resource}/{id}/delete Delete
DELETE /{resource}/{id} Destroy

In the above the New, Edit and Delete are used to show an UI which is somewhat redundant in ajax driven application. The first thing we will do create an IRouteConstraint which will match/reject the route depending upon the http verb, there is already a built in constraint in the System.Web.Routing but it does not take the http-method-override into consideration. Here is the code:

RESTfulHttpVerbConstraint

              public class RESTfulHttpVerbConstraint : IRouteConstraint
              {
                  private static readonly IEnumerable<HttpVerbs> knownVerbs =
                      Enum.GetValues(typeof(HttpVerbs)).Cast<HttpVerbs>();
              
                  private readonly IEnumerable<string> acceptVerbs;
              
                  public RESTfulHttpVerbConstraint(HttpVerbs acceptVerbs)
                  {
                      this.acceptVerbs = ConvertToStringList(acceptVerbs);
                  }
              
                  public bool Match(
                      HttpContextBase httpContext,
                      Route route,
                      string parameterName,
                      RouteValueDictionary values,
                      RouteDirection routeDirection)
                  {
                      var originalVerb = httpContext.Request.HttpMethod;
              
                      if ((originalVerb != null) &&
                          acceptVerbs.Contains(
                          originalVerb, StringComparer.OrdinalIgnoreCase))
                      {
                          return true;
                      }
              
                      var overriddenVerb = httpContext.Request.GetHttpMethodOverride();
              
                      var matched = overriddenVerb != null &&
                                    acceptVerbs.Contains(
                                    overriddenVerb, StringComparer.OrdinalIgnoreCase);
              
                      return matched;
                  }
              
                  private static IEnumerable<string> ConvertToStringList(HttpVerbs verb)
                  {
                      var list = new List<string>();
              
                      Action<HttpVerbs> append = matching =>
                      {
                          if ((verb & matching) != 0)
                          {
                              list.Add(matching.ToString().ToUpperInvariant());
                          }
                      };
              
                      foreach (var known in knownVerbs)
                      {
                          append(known);
                      }
              
                      return list;
                  }
              }
              

Next, we will create a helper method which will register the RESTful routes of a given controller with some reflection magic:

RESTfulRouteHelper

              public static class RESTfulRouteHelper
              {
                  private static readonly IEnumerable<string> idBasedActions =
                      new[] { "destroy", "update", "show" };
              
                  public static RouteCollection Resources<TController>(
                      this RouteCollection instance) where TController : Controller
                  {
                      if (instance == null)
                      {
                          throw new ArgumentNullException("instance");
                      }
              
                      var controllerType = typeof(TController);
                      var controllerName = ControllerName(controllerType);
                      var actions = ControllerActions(controllerType);
              
                      foreach (var method in actions)
                      {
                          var actionName = ActionName(method);
                          var routeName = controllerName + "-" + actionName;
              
                          string url;
                          object constraints;
              
                          if (idBasedActions.Contains(actionName,
                              StringComparer.Ordinal))
                          {
                              var httpMethod = HttpVerbs.Get;
              
                              if (actionName.Equals("destroy", StringComparison.Ordinal))
                              {
                                  httpMethod = HttpVerbs.Delete;
                              }
                              else if (actionName.Equals("update",
                                  StringComparison.Ordinal))
                              {
                                  httpMethod = HttpVerbs.Put;
                              }
              
                              url = controllerName;
              
                              var firstParameter = method.GetParameters()
                                                         .FirstOrDefault();
              
                              if (firstParameter != null &&
                                  firstParameter.Name.Equals("id",
                                  StringComparison.OrdinalIgnoreCase))
                              {
                                  url += "/{id}";
                              }
              
                              constraints = new
                                                {
                                                    httpMethod =
                                                    new RESTfulHttpVerbConstraint(
                                                        httpMethod)
                                                };
                          }
                          else if (actionName.Equals("create", StringComparison.Ordinal))
                          {
                              url = controllerName;
              
                              constraints = new
                                                {
                                                    httpMethod =
                                                    new RESTfulHttpVerbConstraint(
                                                        HttpVerbs.Post)
                                                };
                          }
                          else
                          {
                              continue;
                          }
              
                          instance.MapRoute(
                              routeName.ToLower(CultureInfo.CurrentCulture),
                              url.ToLower(CultureInfo.CurrentCulture),
                              new
                                  {
                                      controller = controllerName,
                                      action = actionName
                                  },
                              constraints);
                      }
              
                      return instance;
                  }
              
                  private static string ActionName(MethodInfo methodInfo)
                  {
                      var actionName = (methodInfo.GetCustomAttributes(
                                                   typeof(ActionNameAttribute), true)
                                                  .OfType<ActionNameAttribute>()
                                                  .Select(attribute => attribute.Name)
                                                  .FirstOrDefault() ?? methodInfo.Name)
                                       .ToLower(CultureInfo.CurrentCulture);
              
                      return actionName;
                  }
              
                  private static IEnumerable<MethodInfo> ControllerActions(Type controllerType)
                  {
                      Func<MethodInfo, bool> match = methood => !methood.IsSpecialName &&
                                                                typeof(ActionResult)
                                                                .IsAssignableFrom(
                                                                  methood.ReturnType) &&
                                                                !methood.GetCustomAttributes(
                                                                typeof(
                                                                  NonActionAttribute),
                                                                  true)
                                                                .Any();
              
                      var actions = controllerType.GetMethods().Where(match).ToList();
              
                      return actions;
                  }
              
                  private static string ControllerName(Type controllerType)
                  {
                      var controllerName = controllerType.Name
                                                         .Substring(
                                                              0,
                                                              controllerType.Name
                                                              .IndexOf(
                                                              "Controller",
                                                              StringComparison.Ordinal))
                                                         .ToLower(
                                                              CultureInfo.CurrentCulture);
              
                      return controllerName;
                  }
              }
              

There is one more boilerplate that we need to complete before start writing the actual application code, we need to replace the default JsonResult that comes with the ASP.NET MVC with our version, The issue with the default JsonResult is it does follow the casing style that we use in JavaScript. When writing code in C# we follow Pascal case, but in JavaScript it is Camel casing, we need our JsonResult to do this translation automatically so that do not have to construct anonymous objects each time we need to return Json. I found that Json.NET has this feature built-in so we will use it for our Json serialization. Here is the replaced JsonResult:

JsonNetResult

              public class JsonNetResult : JsonResult
              {
                  public JsonNetResult()
                  {
                      Settings = new JsonSerializerSettings
                      {
                          ContractResolver = new CamelCasePropertyNamesContractResolver()
                      };
              
                      Settings.Converters.Add(new IsoDateTimeConverter
                      {
                          DateTimeFormat = "yyyy-MM-dd"
                      });
                  }
              
                  public int HttpStatusCode { get; set; }
              
                  public Formatting Formatting { get; set; }
              
                  public JsonSerializerSettings Settings { get; private set; }
              
                  public override void ExecuteResult(ControllerContext context)
                  {
                      if (context == null)
                      {
                          throw new ArgumentNullException("context");
                      }
              
                      if (JsonRequestBehavior == JsonRequestBehavior.DenyGet &&
                          string.Equals(context.HttpContext.Request.HttpMethod, "GET",
                          StringComparison.OrdinalIgnoreCase))
                      {
                          throw new InvalidOperationException(
                              "Get must be explicitly specified.");
                      }
              
                      var response = context.HttpContext.Response;
              
                      response.ContentType = string.IsNullOrWhiteSpace(ContentType) ?
                                             "application/json" :
                                             ContentType;
              
                      if (ContentEncoding != null)
                      {
                          response.ContentEncoding = ContentEncoding;
                      }
              
                      if (Data == null)
                      {
                          return;
                      }
              
                      using (var writer = new JsonTextWriter(response.Output)
                                              {
                                                  Formatting = Formatting
                                              })
                      {
                          JsonSerializer.Create(Settings).Serialize(writer, Data);
                      }
              
                      response.StatusCode = HttpStatusCode;
                  }
              }
              

Now, lets start with our application code, we will have the exact same copy of Task that we have in the client:

Task Model

              public class Task
              {
                  public Guid Id { get; set; }
              
                  [Required]
                  public string Name { get; set; }
              
                  public bool Planned { get; set; }
              
                  public string Notes { get; set; }
              
                  public int? Estimation { get; set; }
              
                  public DateTime CreatedAt { get; set; }
              
                  public DateTime? DeadlineAt { get; set; }
              
                  public DateTime? CompletedAt { get; set; }
              }
              

Next, we will use Repository for storing/retrieving the tasks, in this case we will use the ASP.NET Session:

Repository

              public class InMemoryRepository<T> : IRepository<T>
              {
                  protected IList<T> DataStore
                  {
                      get
                      {
                          var key = typeof(T).Name;
              
                          var list = HttpContext.Current.Session[key] as IList<T>;
              
                          if (list == null)
                          {
                              HttpContext.Current.Session[key] = list = new List<T>();
                          }
              
                          return list;
                      }
                  }
              
                  public void Add(T instance)
                  {
                      DataStore.Add(instance);
                  }
              
                  public IEnumerable<T> All()
                  {
                      return DataStore;
                  }
              }
              

Now, it time to create the controller:

TasksController

              public class TasksController : ApplicationController
              {
                  private readonly IRepository<Task> repository;
              
                  public TasksController() : this(new InMemoryRepository<Task>())
                  {
                  }
              
                  public TasksController(IRepository<Task> repository)
                  {
                      this.repository = repository;
                  }
              
                  public ActionResult Index()
                  {
                      return Json(repository.All(), JsonRequestBehavior.AllowGet);
                  }
              
                  public ActionResult Create(Task task)
                  {
                      if (!ModelState.IsValid)
                      {
                          return Json(new { errors = ModelState.Errors() });
                      }
              
                      repository.Add(task);
              
                      return Json(task, 201); // Created
                  }
              }
              

We are using a base controller from which the TasksController inherits, the base controller has few reusable methods, the Index is plain and simple but in the Create we are returning Json serialized errors if the model validation fails, next when the model is created we are returning http status code 201 along with Json serialized task, the http status code is not necessary but it good practice to set proper http status code and our new JsonResult does accept the optional http status code. If are wondering about the ModelState.Errors, here is the complete code:

ModelState.Errors

              public static class ModelStateDictionaryHelper
              {
                  public static object Errors(this ModelStateDictionary instance)
                  {
                      if (instance == null)
                      {
                          throw new ArgumentNullException("instance");
                      }
              
                      return instance.Select(ms => new
                       {
                           Name = ms.Key,
                           Messages = ms.Value
                                        .Errors
                                        .Select(error => (error.Exception == null) ?
                                            error.ErrorMessage :
                                            error.Exception.Message)
                                        .Where(error => !string.IsNullOrWhiteSpace(error))
                                        .ToList()
                       })
                      .Where(ms => ms.Messages.Any())
                      .ToList();
                  }
              }
              

Now, it time to register the routes for this controller, open the route registration and add this routes.Resources<TasksController>, it will register the necessary RESTful routes that we discussed earlier.

Next, open the assets/model/task.coffee and replace the Task.extend Spine.Model.Local with Task.extend Spine.Model.Ajax and that’s it. Now when you run the application it will retrieve/create the tasks from/to the server.

This is just a trivial sample developed in Spine, there are two more important things in Spine that we have not touched yet the Routing and Custom events. The Routing comes in to action when we want to make a Controller active based upon url hash change and usually defined in the startup/root controller, for example the application controller:

Spine Route

              @routes
                  '!/settings'         : => settingsController.activate()
                  '!/reports'          : => reportsController.activate()
                  '!/tasks/:id/edit'   : (params) =>
                      editTaskController.change params.id
                  '!/tasks/new'        : => newTaskController.activate()
                  '!/tasks'            : => tasksController.active
                  ''                   : => dashboardController.activate()
              
              Spine.Route.setup()
              

Next, the Custom events comes into action if you want to communicate between controllers in loosely coupled way, this works pretty much as any other pub/sub components. For example to fire an event you will write @App.trigger 'someEvent', args and to capture the event @App.bind 'someEvent', (args) - > .

Now, what are the motivation to pick Spine over any other framework. The initial reason was Mvc Pattern, although there was Backbone.js which Spine is heavily inspired from. But coming from a conventional Mvc background I found Backbone rather confusing for example what we know the responsibilities of a Controller in Backbone the View does the job and Controller was used for routing, but with the latest release the Controller is renamed to Router. There is one more client side mvc framework that I find interesting the Batman.js but have not had a chance to explore it thoroughly. I have also tried knockout.js which is now shipped with the default asp.net mvc template, although some people claims they serve the same purpose which is not at all correct, other than data binding knockout does not have any out of the box support for object  persistence, routing and  overall a clean client side architecture which I often find necessary for developing a RIA application.

That it for today.

Download: MeetSpine-Part2.zip

Shout it

Comments

blog comments powered by Disqus