-
-
Save PaulStovell/2380031 to your computer and use it in GitHub Desktop.
@model UserViewModel | |
@using (Html.BeginForm()) | |
{ | |
<div> | |
@Html.HiddenFor(m => m.Id) | |
@Html.TextBoxFor(m => m.FirstName) | |
@Html.TextBoxFor(m => m.LastName) | |
@Html.DropDownListFor(m => m.CountryId, Model.Countries) | |
<input type="submit" /> | |
</div> | |
} |
interface IModelBuilder<TViewModel, TEntity> | |
{ | |
TViewModel CreateFrom(TEntity entity); | |
TViewModel Rebuild(TViewModel model); | |
} |
interface IModelCommand<TInput> | |
{ | |
void Execute(TInput model); | |
} |
class UserEditCommand : IModelCommand<UserEditModel> | |
{ | |
readonly ISession session; | |
public UserEditCommand(ISession session) | |
{ | |
this.session = session; | |
} | |
public void Execute(UserEditModel model) | |
{ | |
var user = string.IsNullOrEmpty(model.Id) ? new User() : session.Find<User>(model.Id); | |
session.Store(user); | |
user.FirstName = model.FirstName; | |
user.LastName = model.LastName; | |
user.Country = session.Find<Country>(model.CountryId); | |
session.SaveChanges(); | |
// Auditing and other interesting things can happen here | |
} | |
} |
class UserController : Controller | |
{ | |
UserViewModelBuilder builder = new UserViewModelBuilder(); | |
UserEditCommand saveCommand = new UserEditCommand(); | |
public ActionResult Edit(string id) | |
{ | |
var user = session.Find<User>(id) ?? new User(); | |
return View(model, builder.CreateFrom(user)); | |
} | |
[HttpPost] | |
public ActionResult Edit(UserViewModel model) | |
{ | |
if (!ModelState.IsValid) | |
{ | |
return View(builder.Rebuild(model); | |
} | |
saveCommand.Execute(model); | |
return RedirectToAction("Index"); | |
} | |
} |
class UserEditModel | |
{ | |
public string Id { get; set; } | |
public string FirstName { get; set; } | |
public string LastName { get; set; } | |
public string CountryId { get; set; } | |
} | |
class UserViewModel : UserEditModel | |
{ | |
public ICollection<SelectListItem> Countries { get; set; } | |
} |
class UserViewModelBuilder : IModelBuilder<UserViewModel, User> | |
{ | |
readonly ISession session; | |
public UserViewModelBuilder(ISession session) | |
{ | |
this.session = session; | |
} | |
public UserViewModel CreateFrom(User user) | |
{ | |
var model = new UserViewModel(); | |
model.Id = user.FirstName; | |
model.FirstName = user.FirstName; | |
model.LastName = user.LastName; | |
model.Country = user.Country.Id; | |
model.Countries = GetCountries(); | |
return model; | |
} | |
public UserViewModel Rebuild(UserViewModel model) | |
{ | |
model.Countries = GetCountries(); | |
return model; | |
} | |
ICollection<SelectListItem> GetCountries() | |
{ | |
var countries = session.FindAll<Country>(); | |
return countries.Select(c => new SelectListItem { Value = c.Id, Text = c.Name }).ToList(); | |
} | |
} |
Looking good to me - we tend to postfix models with either "ViewModel" or "PostModel" (eg. EditCustomerViewModel and EditCustomerPostModel) which helps my brain grasp which one I'm working with. Our commands also tend to have a parameterless Execute() method - we simply set properties on the command, but passing the PostModel works well too.
Most of our GET actions follow this pattern:
[HttpGet]
public ActionResult Edit(int id) {
var builder = new EditCustomerViewModelBuilder();
var model = builder.Build(id);
return View(model);
}
Our POST actions looks pretty similar to yours - the commands can throw CommandFailureExceptions etc, which allow you to add them back into the collection of ModelState errors.
Thanks Jay, you're right that the naming convention could be clearer - I've renamed them.
I like the idea of being able to raise exceptions from the command. Do you try/catch within the controller, or do you have an action filter that handles it?
Hi Paul,
Thanks for this ! :)
As I'm learning MVC and EF right now, I have a question regarding the ISession interface.
class UserViewModelBuilder : IModelBuilder<UserViewModel, User>
{
readonly ISession session;
public UserViewModelBuilder(ISession session)
{
this.session = session;
}
Can you post the EF layer code and injection into the controller ?
How is the constructor fired passing the session ?
UserViewModelBuilder(ISession session)
Thanks, LA Guy
Hi Paul,
I know it is old but still active :)
When you use PRG pattern and copy ModelState between Redirect->Get then you don't needed Rebuild method anymore. :)
Regards
Darek
I like this because the 'saving' part is separated from the 'viewing' part. The command is a great place to add permission checks and all kinds of things and test them independently of the view model.
Using inheritance between the ViewModel and the raw data to be saved is a nice way of separating those concerns too.