Showing posts with label MVC3. Show all posts
Showing posts with label MVC3. Show all posts

Wednesday, June 6, 2012

Editing complex entities within an MVC3 view

Recently I have been busy at work building this site to help us manage all our buildings access cards, helping assign/re-assign and generally keep track of who can access various levels of our building etc. We decided to go down the route of constructing the site using a combination of MVC3, Entity Framework, sql server 2008 and some other front end client side libraries such as bootstrap to help speed up the page design (we were told to build it quick with as less $$ as possible). Given that I myself have just recently started using the framework, I ran into a small issue to do with representing complex entities within our MVC views.
Our Db schema resembled something of the sort shown below (note: I have toned it down quite a bit to keep it simple):
Create Table Cards(
id int  Identity(1,1) Constraint pk_card_id primary key
, AssignedTo varchar(50));

create table Locations
(
id int Identity(1,1) Constraint pk_locations_id primary key
, Name varchar(20));

create table CardLocations
(
 CardId int not null Constraint fk_card_id Foreign key references Cards(id)
, LocationId int not null Constraint fk_location_id Foreign key references Locations(id)
, Constraint pk_CardLocations_id primary key(CardId, LocationId)
);
We have 2 primary entities, Cards which maintains a list of all access cards handed out to people and Locations which is a list of all locations in our building and other facilities. The CardLocations table maintains a mapping of all the Locations that a Card has access to (note: both columns of the table are part of a composite primary key). Let us assume that we have the following sample data inserted in the DB:

Cards
id          AssignedTo
----------- --------------------------------------------------
1           Employee A
2           Employee B
3           Employee C

Locations
id          Name
----------- --------------------
1           Level 1
2           Level 2
3           Level 3
4           Level 4
5           Level 5


CardLocations
CardId      LocationId
----------- -----------
1           1
1           2
2           3
2           4
3           5

Hence if we use EF to generate an Entity model for this DB schema we end up with the following:

image

Entity framework automatically detects the many-many association between a Card and a Location entity and does not see the need to create a separate entity for the CardLocations table instead maintaining this as an Entity Association. Let us assume that we wish to allow the user to edit an access card’s details such as who it is assigned to and what locations it has access to. The next listing shows the mock-up of a simple controller used to edit a Card:
   1:  public class CardsController : Controller
   2:      {
   3:   
   4:          CardsEntities model; //The Entity Model
   5:   
   6:          public CardsController()
   7:          {
   8:              model = new CardsEntities();
   9:          }
  10:   
  11:          public ViewResult Edit(int? id)
  12:          {
  13:              if (id == null)
  14:                  throw new ArgumentNullException("id");
  15:              var card = model.Cards.Single(c => c.id == id);
  16:              return View(card);
  17:          }
  18:      }
As seen, the Edit action method takes the id of the card that we wish to edit and using the reference to the entity model, retrieves the card and passes it to the strongly typed view Edit.cshtml (scripted below).
   1:  @model ComplexMvcEf.Library.Model.Card
   2:  @{
   3:      ViewBag.Title = "Edit";
   4:  }
   5:  @using (Html.BeginForm())
   6:  {
   7:      <h2>
   8:          Card</h2>
   9:   
  10:      @Html.HiddenFor(model => model.id)
  11:   
  12:      @Html.LabelFor(model => model.AssignedTo) @Html.EditorFor(model => model.AssignedTo)
  13:   
  14:   
  15:      <h2>
  16:          Locations</h2>
  17:      foreach (var location in Model.Locations)
  18:      {
  19:          @Html.EditorFor(model => location);
  20:      }
  21:   
  22:      <input type="submit" name="Submit" value="Edit" />
  23:  }

Now, does anyone see what is wrong here? The HTML helpers all do their jobs, we have created a strongly typed model (for the type of the Card entity object) where we simply create editors allowing the user to edit who the card is assigned to. However our problems start with being able to assign locations to this card. Navigating to this view renders the following:

image

Obviously binding to the list of locations only allows us bind to and edit the Location entity object, but it is only the 2 location entities that are associated with the card. What about adding a new location? Also in the true spirit of the view we really don't want to be able to edit a location and that functionality should be restricted to a different page. We would basically like some means of selecting from a list of locations, which ones we wish the card to have access to. I know a whole heap of you HTML cowboys out there are probably coming up with a zillion flash ideas constituting fancy JQuery dropdowns with a whole lot of Ajax magic to dynamically manage a cards location. Any other time, I would love to go down that path, however that would simply eat up project hours like popcorn. In retrospect, the number of locations in our building are always going to be few and finite, hence simply listing them on the page is not going to do anybody any harm. Our primary problem here is that we have tried to use what are primarily, our domain models (the Card and Location entities) as a view model, something which may not always work out, especially since without writing some unnecessary boilerplate code, there is no easy way we can represent that a location is selected/ not selected and not to mention any extra effort required to have the mvc model binders work correctly etc (remember that we are dealing with entity objects and model contexts). To rectify the issue, we introduce a couple of classes to use as our view-model

   1:  public class CardEditModel
   2:      {
   3:          //the card being edited
   4:          public Card Card { get; set; }
   5:   
   6:          //the list of locations to display on the view
   7:          public List<CardLocationModel> LocationsModel { get; set; }
   8:      }
   9:   
  10:      public class CardLocationModel 
  11:      {
  12:          //The location Id
  13:          public int Id { get; set; }
  14:          
  15:          //The location name
  16:          public string Name { get; set; }
  17:          
  18:          //Flag to indicate if card can access this location
  19:          public bool Selected { get; set; }
  20:      }

Notice that there are 2 classes created. A collection of CardLocationModel objects is maintained by the CardEditModel class. The use for this will become more apparent in the following sections. We can now integrate this within our Edit action method

   1:  public ViewResult Edit(int? id)
   2:          {
   3:              if (id == null)
   4:                  throw new ArgumentNullException("id");
   5:              //Create a new viewModel
   6:              var viewModel = new CardEditModel();
   7:              //read the card from the DB
   8:              var card = model.Cards.Single(c => c.id == id);
   9:              if (card != null)
  10:              {
  11:                  viewModel.Card = card;
  12:                  //set the locations
  13:                  viewModel.LocationsModel = model.Locations
  14:                      .AsEnumerable() 
  15:                      .Select(location => new CardLocationModel()
  16:                      {
  17:                          Id = location.id
  18:                          //Set flag if the access card has access to this location
  19:                          ,Selected = card.Locations.Any(loc => loc.id == location.id)
  20:                          ,Name = location.Name
  21:                      }).ToList();
  22:              }
  23:   
  24:              return View(viewModel);
  25:          }

The logic for the Edit action method in the shown above basically involves reading the card entity from the models context (line 8) and referencing it via our view model of type CardEditModel (line 11). Lines 13-21 basically involve selecting all the locations from the model’s context. We then use this list to generate a list of CardLocationModel objects which is then referenced by our view-model. Hence we now have a complete list of locations to display to the user. The Selected boolean flag of the CardLocationModel class allows us to represent if the access card being edited is allowed to access this location(line 19). Hence incorporating this into our Edit.cshtml view

   1:  @model ComplexMvcEf.Models.CardEditModel
   2:  @{
   3:      ViewBag.Title = "Edit";
   4:  }
   5:  @using (Html.BeginForm())
   6:  {
   7:      <h2>Card</h2>
   8:      @Html.HiddenFor(model => model.Card.id)
   9:      @Html.LabelFor(model => model.Card.AssignedTo) @Html.EditorFor(model => model.Card.AssignedTo)
  10:      
  11:      <h2>Locations</h2>
  12:      <p>
  13:      @for (int i = 0; i < Model.LocationsModel.Count; i++)
  14:              {
  15:               @Html.HiddenFor(model => model.LocationsModel[i].Id)
  16:               @Html.CheckBoxFor(model =>  model.LocationsModel[i].Selected) @(Model.LocationsModel[i].Name)
  17:              }
  18:      </p>
  19:      <input type="submit" name="Submit" value="Edit" />
  20:  }

Hence, our new view-model allows us to logically represent all the locations available to the user and easily change the mappings as part of the form

image

Additionally to handle the post-back we simply add another Edit action method to the CardController class like so

   1:         [HttpPost]
   2:          public ActionResult Edit(
   3:              int? id
   4:              , List<CardLocationModel> locationsModel)
   5:          {
   6:              //ensure that the entity object is attached to the model context
   7:              var card = model.Cards.Single(c => c.id == id);
   8:   
   9:              if (card != null
  10:                  //update the name if edited in the view
  11:                  && TryUpdateModel(card)
  12:                  )
  13:              {
  14:                  //remove all the cards locations that are no longer selected
  15:                  card.Locations
  16:                      .RemoveAll(l => !locationsModel.Any(crdLocationModel => crdLocationModel.Id == l.id && crdLocationModel.Selected));
  17:   
  18:                  card.Locations.AddAll(
  19:                      model.Locations
  20:                      .AsEnumerable()
  21:                      .Where(
  22:                      l => //ensure that the location is selected by user
  23:                          locationsModel.Any(crdModelLocation => crdModelLocation.Id == l.id && crdModelLocation.Selected)
  24:                          &&
  25:                          //ensure that the location is not already set
  26:                          !card.Locations.Contains(l)));
  27:   
  28:                  model.SaveChanges();
  29:              }
  30:              //simply redirecting to the same edit window
  31:              return RedirectToAction("edit");
  32:          }

Couple of things to note here. The action method is decorated with an mvc HttpPostAttribute filter (line 1), which ensures that this action only gets called on a post request. Also note that as part of our method parameter’s, we have a collection of CardLocationModel objects (line 4). This allows mvc’s model binders to search for and automatically populate the list from the form values. It may seem a little weird, but I purposely read out the card that we are interested in editing from the context first (line 7) followed by making a call to manually ask the controller to try and update the model’s values. This is done with the intent of ensuring that the Card entity object is always bound to the model’s context, and avoids any problems associated with attaching to the context. I am not suggesting that this is a recommended practise but I found that it saved me a lot of time.

Line’s 15-26 represent the code to synchronize the locations represented in our view-model with the actual Locations mapped to the card. The logic is quite simple. I simply remove any Location entity associated with the Card entity that does not have an equivalent CardLocationModel object in our locationsModel list (line 15-16) with its flag set to true (implying selected). I then go about adding mappings for all newly selected Locations to the Card (line 18-26). Note that I am using the following extension methods

   1:  public static class EntityCollectionExtensions
   2:      {
   3:          public static void RemoveAll<E>(this EntityCollection<E> collection
   4:              , Func<E, bool> itemSelector) where E : EntityObject
   5:          {
   6:              foreach (E item in collection.Where(entity => itemSelector(entity)).ToArray())
   7:                  collection.Remove(item);
   8:          }
   9:   
  10:          public static void AddAll<E>(this EntityCollection<E> collection
  11:              , IEnumerable<E> itemsToAdd) where E : EntityObject
  12:          {
  13:              foreach (var item in itemsToAdd)
  14:                  collection.Add(item);
  15:          }
  16:      }

We conclude by saving the changes made to the model context and because our application only has one view, we simply redirect back to the edit screen.

I hope this demonstrates the advantage behind using a view model inside all your MVC views. Binding directly to objects that represent your domain logic may not always be feasible and may limit the ability to logically represent a view to the user (for example, we were unable to provide a complete list of locations to the user). By creating a custom view model, we now have a clear control over how our domain objects can be represented to the user and also allows for the flexibility to represent the intent behind our view logically to the user.

Wednesday, April 18, 2012

VS2010: Using javascript intellisense for third party javascript libraries with MVC razor

I have been spending a ton of time learning MVC3 of late. Its great how well the framework has been designed to work hand in hand with jQuery with respect to unobtrusive client side validation. Obviously one of the things that makes Visual studio (or any decent IDE for that matter) great for any .net developer, is the awesome intellisense support. However I ran into a tiny snag while trying to get visual studio to render any intellisense for jquery. Let me demonstrate using the example below:
Assume I have the following _Layout.cshtml file:

   1:  <!DOCTYPE html>
   2:  <html>
   3:  <head>
   4:      <meta charset="utf-8" />
   5:      <title>@ViewBag.Title</title>
   6:      <link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
   7:     <script src="@Url.Content("~/Scripts/jquery-1.7.1.min.js")" type="text/javascript"></script>
   8:      <script src="@Url.Content("~/Scripts/modernizr-1.7.min.js")" type="text/javascript"></script>
   9:  </head>
  10:   
  11:  <body>
  12:      @RenderBody()
  13:  </body>
  14:  </html>

ok great I have all my jquery libraries defined and now I am ready to create MVC views that will utilize this layout. Here's a partial excerpt from a simple view ~/Home/Sample.cshtml that utilizes the above layout page:
   1:  @{
   2:      ViewBag.Title = "Sample";
   3:  }
   4:   
   5:  <h2>Sample</h2>
   6:   
   7:  <script type="text/javascript">
   8:      $(document).
   9:  </script>
  10:   
  11:   
  12:  @using(Html.BeginForm())
  13:  {
  14:      @:Enter your name: @Html.TextBox("name")
  15:      <input type="submit" value='Submit Name'/>
  16:  }
 
As you do, my view is laden with common html script mixed with some mvc3 razor syntax. Now notice how in line 8 I have begun using a jquery selector on the JS document object. One would assume that one hit of the compile project button should automatically warrant visual studio to render all sorts of jquery related intellisense. Nope, that does not happen. And quite frankly I assumed that wouldn't work. If you think about it, in the context of editing the view, VS cannot work out during design time which layout file this view is going to use. Of course there could be some awesome “behind the scenes” code to automatically work this out based on the layout set in the _ViewStart.cshtml file but perhaps this is something for the future (hint hint).

A bit of googling pointed out what I had suspected all along. Visual studio does not have a notion of all the scripts that will be loaded client side for this view (account for the existence of mvc partial views as well). Turns out, if you want to use javascript intellisense feature for any third party scripts, then you need to manually reference the jquery Visual studio 2010 intellisense documentation script file into every view that you work like so:
   1:  @{
   2:      ViewBag.Title = "Sample";
   3:  }
   4:   
   5:  <h2>Sample</h2>
   6:   
   7:  @if (false)
   8:  {
   9:      <script type="text/javascript" src="@Url.Content("~/Scripts/jquery-1.7.1-vsdoc.js")"></script>
  10:  }
  11:   
  12:  <script type="text/javascript">
  13:      $(document).
  14:  </script>
  15:   
  16:   
  17:  @using (Html.BeginForm())
  18:  {
  19:      @:Enter your name: @Html.TextBox("name")
  20:      <input type="submit" value='Submit Name'/>
  21:  }

Couple of things to note here. Lines 7-10 show a javascript node referencing the the jquery-1.7.1-vsdoc.js encapsulated inside a razor if statement. This contains VS2010 specific vsdoc that aides visual studio’s intellisense. Since we are already referencing the core jquery libraries in our _Layout.cshtml file, having clients download this script every time this view is rendered as part of a request is unnecessary. Hence having the if block always evaluate to false ensures that this script element never gets rendered as part of a HTTP response. However even though we now have our reference, visual studio still does not render any JS intellisense for my jquery selector (line 13). As part of my orientation with the MVC framework, I have become accustomed to using the plethora of awesome MVC helper functions within my views to generate url’s, action-links, forms etc. We use the Url.Content helper to generate our jquery script reference in line 9. An unfortunate trade –off with this is that visual studio cannot seem to work out the script reference path during design time. The only remedy to this is to hard code a relative path to the script in line-9 of the Sample.cshtml view like so:

<script type="text/javascript" src="../../Scripts/jquery-1.7.1-vsdoc.js"></script>

This does the trick and enables jquery intellisense. I hate having to hardcode the script reference but I guess as long as we ensure that this reference is never included as part of the response, its a small price to pay for having intellisense enabled. I hope the ASP.Net team sort this out soon
Technorati Tags: ,,,,,

Saturday, October 1, 2011

Defining metadata for Entities created using Entity Framework

Ive been using Entity framework for a couple of months now. I had this issue a while back, where I had an entity model designed using the entity framework designer  (note: I am currently using the 4.1 update). My task was to extend the entity model so that I could decorate an entity class with some data annotations.

For the sake of simplicity, let us assume that our entity data model has only the following entity, Product, defined like so:

If this product class was used, say as a model for an MVC3 controller, calling an html helper method to automatically render an editor for the model, (as shown below):


@model MyDomain.Entities.Product
 
@{
    ViewBag.Title = "Edit";
}
 
<h1>Edit @Model.Name</h2>
 
@using (Html.BeginForm())
{
    @Html.EditorForModel()
    <input type="submit" value="save" />
}

However this would result in the view being rendered with an html input element for each data member of the Product entity, including the Id field (of type int) which also happens to be the primary key. This may not be desired, as typically users should not be able to control setting the primary key for an entity. However we cannot simply extend the partial class and overwrite the field either. We could simply edit the code generated by EF for the Product EntityObject class, but any changes to the model would result in your data annotations being wiped, so this is not recommended either. As such the easiest way to solve this issue is to decorate your entity class with the System.ComponentModel.DataAnnotations.MetadataTypeAttribute 






First, we create a new class called ProductMetaData within which we define a single property called Id. Notice that the Id property is defined to be the same type as that of the Id field in the Product entity object, i.e. they are both of type Int. Once this is done, we can add a data annotation to instruct MVC's html helper methods to omit this field when rendering the view (note I am using the HiddenInput data annotation defined in the System.Web.Mvc namespace for this example):


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
 
namespace MyDomain.Entities.Metadata
{

    public class ProductMetadata
    {
        [HiddenInput(DisplayValue=false)]
        public int Id { getset; }
    }
}


Following this, we can then simply create a partial class for the entity that we wish to annotate, in this case the Product entity. Once this is done, all that is left is to annotate the class with a MetadataTypeAttribute, passing in the type of the ProductMetaData class in the constructor and recompile.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace MyDomain.Entities
{
    [MetadataType(typeof(MyDomain.Entities.Metadata.ProductMetadata))]
    public partial class Product
    {
 
    }
}


And that's basically it. During runtime, the html helper methods will now omit generating an editor for the Id field every time the view is rendered like so: