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.