Skip to content

Instantly share code, notes, and snippets.

@PaulStovell
Created April 13, 2012 20:42
Show Gist options
  • Save PaulStovell/2380031 to your computer and use it in GitHub Desktop.
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();
}
}
@PaulStovell
Copy link
Author

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.

@bigjump
Copy link

bigjump commented Apr 13, 2012

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.

@PaulStovell
Copy link
Author

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?

@LAGuy
Copy link

LAGuy commented Apr 27, 2012

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

@dario-l
Copy link

dario-l commented Jun 7, 2013

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment