Certainly for the scale of application I've built so far this works just fine. In cases where I want to use additional information passed in the loosely typed ViewData dictionary, I'm able to cast to a typed variable at the top of the view in order to use it in a strongly typed manner from then on. And if I've needed to pass additional information back from my form posts, these can be added as additional parameters to the controller action set up to receive the post.
The following code illustrates this. The first method retrieves a User object and passes it in a strongly typed manner to the view, along with a select list for the different options for the user's Role. On the view, an additional field to indicate whether to send out a welcome email to the user can be checked. This isn't part of the model (it's not a field that needs to be persisted back to the database) and so it is passed as an additional parameter to the second method that responds to the form post.
1 public class UserController : Controller
2 {
3 private UserService userService = new UserService();
4 public ActionResult Edit(int id)
5 {
6 //Get user
7 User user = userService.GetUser(id);
8
9 //Get list of roles
10 IList<Role> roles = userService.GetRoles();
11
12 //Add list of roles to ViewData dictionary
13 ViewData["RoleList"] = new SelectList(roles, "Id", "Name", user.Role.Id);
14
15 //Pass the user as the model to the strongly typed view
16 return View(user);
17 }
18
19 [AcceptVerbs(HttpVerbs.Post)]
20 public ActionResult Edit(User user, bool sendEmail)
21 {
22 if (ModelState.IsValid)
23 {
24 //Save user details
25 userService.SaveUser(user);
26
27 //Send email flag set, so send welcome email
28 if (sendEmail)
29 userService.SendWelcomeEmail(user);
30 return RedirectToAction("Index");
31 }
32 else
33 {
34 IList<Role> roles = userService.GetRoles();
35 ViewData["RoleList"] = new SelectList(roles, "Id", "Name", user.Role.Id);
36 return View(user);
37 }
38 }
39 }
It's clear though that in some ways these techniques aren't ideal, and it could be argued are really workarounds for not having a model that perfectly represents the data that the view is intended to display. In the example above it's really only a minor part of the view's data that doesn't come from the domain model, but in other cases the situation can be more obvious (perhaps you want a page that displays lists of two model objects - only one of which can be selected as the type for the strongly-typed view).
Which is where View Models come in. View Models aren't part of the Domain Model, and instead are created from them via composition or mapping. They can be designed to present only the information that the view requires, in a strongly typed manner.
There are a number of transformations you might want to carry out on the Domain Model object that has been designed to precisely represent the business entity or process, and the View Model object that you wish to pass to the view:
- Additional Information - as noted above, you will often need to pass additional details to the view (e.g. to populate drop-down lists or breadcrumb trails), which can be made part of the view model.
- Simplification - you may choose to present only the sub-set of fields that are required for display or to flatten the model
- Logic - the view model could implement some custom logic on the base fields (e.g. presenting a calculated value to the view)
- Formatting - the output of the view will eventually be a string (usually as part of an HTML response), and hence for consistency and in order to follow the policy of keeping the views as "dumb" as possible, you could carry out your date and number field formatting in order to present a pre-formatted string field to the view.
So sounds ideal... but of course the downside here is that it creates more work. Instead of simply passing the Domain Model object to the view and reinstantiating it using the model binder for data updates, it is necessary in convert between view and domain model and vice versa. And if in most cases there aren't hugely persuasive factors for implementation it can be that this extra layer, which can be fairly tedious and error-prone to code, doesn't appear to add too much value for the effort required.
View Model Techniques
In reading around on the various techniques for implementing view models the most useful and recommended information I found was on Steve Michelotti's blog and his post on ASP.NET MVC View Model Patterns. He distinguishes between two methods of creating view models - using composition and mapping.
The first using composition is where the Domain Model object is effectively contained by a View Model object, with the former being accessed via a field of the latter. Additional fields can be added to represent the extra information that needs to be passed to a view. The following code illustrates this idea based on the User Domain Model object described above.
1 public class UserView
2 {
3 public User User { get; set; }
4 public SelectList Roles { get; set; }
5 public bool SendEmail { get; set; }
6 }
1 public ActionResult EditVM1(int id)
2 {
3 //Get user
4 User user = userService.GetUser(id);
5
6 //Get list of roles
7 IList<Role> roles = userService.GetRoles();
8
9 //Create view model
10 UserView userView = new UserView();
11 userView.User = user;
12 userView.Roles = new SelectList(roles, "Id", "Name", user.Role.Id);
13
14 //Pass the user as the model to the strongly typed view
15 return View(userView);
16 }
17
18 [AcceptVerbs(HttpVerbs.Post)]
19 public ActionResult EditVM1(UserView userView)
20 {
21 if (ModelState.IsValid)
22 {
23 //Save user details
24 userService.SaveUser(userView.User);
25
26 //Send email flag set, so send welcome email
27 if (userView.SendEmail)
28 userService.SendWelcomeEmail(userView.User);
29 return RedirectToAction("Index");
30 }
31 else
32 {
33 return View(userView);
34 }
35 }
On the plus side it can be seen that this technique is relatively straightforward to implement and doesn't require too much additional work. However whilst it achieves the aim of providing all information to the view in a strongly typed manner, it doesn't do anything for the other potential benefits of simplification, logic or formatting.
The other technique is to implement a completely separate View Model class designed purely for the purposes of representing the particular data required for display in the view. Here the fields are mapped from the Domain Model object before presentation to the view. This code sample illustrates how this can be done for display of information in a form and it's subsequent processing after the post.
1 public class UserView2
2 {
3 public int Id { get; set; }
4 public string FirstName { get; set; }
5 public string LastName { get; set; }
6 public string Email { get; set; }
7 public Role Role { get; set; }
8
9 public SelectList Roles { get; set; }
10 public bool SendEmail { get; set; }
11 }
1 public ActionResult EditVM2(int id)
2 {
3 //Get user
4 User user = userService.GetUser(id);
5
6 //Get list of roles
7 IList<Role> roles = userService.GetRoles();
8
9 //Create view model
10 UserView2 userView = new UserView2();
11 userView.Id = user.Id;
12 userView.FirstName = user.FirstName;
13 userView.LastName = user.LastName;
14 userView.Email = user.Email;
15 userView.Role = user.Role;
16 userView.Roles = new SelectList(roles, "Id", "Name", user.Role.Id);
17
18 //Pass the user as the model to the strongly typed view
19 return View(userView);
20 }
21
22 [AcceptVerbs(HttpVerbs.Post)]
23 public ActionResult EditVM2(UserView2 userView)
24 {
25 if (ModelState.IsValid)
26 {
27 //Save user details
28 User user = new User();
29 user.Id = userView.Id;
30 user.FirstName = userView.FirstName;
31 user.LastName = userView.LastName;
32 user.Email = userView.Email;
33 user.Role = userView.Role;
34 userService.SaveUser(user);
35
36 //Send email flag set, so send welcome email
37 if (userView.SendEmail)
38 userService.SendWelcomeEmail(user);
39 return RedirectToAction("Index");
40 }
41 else
42 {
43 return View(userView);
44 }
45 }
Enter Automapper...
Kind of ugly? It's certainly fairly tedious code to write mapping each of the properties - particularly when there's actually not a whole lot of difference between the object fields. At the very least we would want to factor out the property-to-property mapping from our controller to a supporting builder class, but nonetheless we would still have to handle to left/right transformation of the properties.
In these cases it's worth considering the use of Automapper - an object-to-object mapper that operates on the basis of conventions.
Using this the code can be simplified so long as you follow a few simple (and sensible) conventions in the design of your View Model fields.
The first step is to download the dll from the link above and add a reference to it from the application. Mappings only need to be created once in the application lifecycle, and hence you can do this in the global.asax file:
1 protected void Application_Start()
2 {
3 RegisterRoutes(RouteTable.Routes);
4 Mapper.CreateMap<User, UserView2>();
5 }
To create a map between objects you call the Map method, indicating the source and destination types:
1 public ActionResult EditVM3(int id)
2 {
3 //Get user
4 User user = userService.GetUser(id);
5
6 //Get list of roles
7 IList<Role> roles = userService.GetRoles();
8
9 //Create view model
10 UserView2 userView = Mapper.Map<User, UserView2>(user);
11 userView.Roles = new SelectList(roles, "Id", "Name", user.Role.Id);
12
13 //Pass the user as the model to the strongly typed view
14 return View(userView);
15 }
16
17 [AcceptVerbs(HttpVerbs.Post)]
18 public ActionResult EditVM3(UserView2 userView)
19 {
20 if (ModelState.IsValid)
21 {
22 //Save user details
23 User user = Mapper.Map<UserView2, User>(userView);
24 userService.SaveUser(user);
25
26 //Send email flag set, so send welcome email
27 if (userView.SendEmail)
28 userService.SendWelcomeEmail(user);
29 return RedirectToAction("Index");
30 }
31 else
32 {
33 return View(userView);
34 }
35 }
Because the fields have the same name, Automapper can on the basis of conventions carry out the property-to-property mapping without having to explicity code this on a per field basis.
Conclusion
Generally I'm keen to ensue that an application code-base is internally consistent, which would mean if using View Models I would like to do so throughout, and to implement them using the same technique. And hence I'm a little reluctant at this stage to take this additional step on with the applicatons I am building - for most examples I look at, the benefit doesn't as yet seem to to outweigh the cost.
Whilst I can see the value and appreciate the best practice of their use with the mapped custom model technique, for now I expect to stick with the ViewData dictionary, but will be certainly be giving them due consideration as I take on new projects and perhaps larger projects using MVC.
UPDATE 15th April: further thought and work has lead me to change my conclusion on this...
How do you handle scenarios in which your view (and consequently your view model) includes only a subset of the properties in your model? For example, let's say you have a view to allow your users update some but not all of their personal information (i.e. Salary). In other words, UserView2 doesn't have a Salary property, but User does. In lines 23 and 24 of your last listing, you are mapping the information the user posted into a new instance of User. This will result in a User with a null Salary, and when you save this, it will result in the Salary column being lost, unless I'm missing something...
ReplyDeleteHi Dani - I'd suggest that would be handled in the service layer rather than the controller code.
ReplyDeleteSo perhaps where I'm calling SaveUser() above, this would be implemented by retrieving a new instance from the database, updating the profile fields that are available in the view and doing a full update to the database. Alternatively you could create something like SaveUserProfile() method that calls a stored procedure which only updates the appropriate fields (i.e. leaving Salary untouched).