Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET MVC3 Automapper Viewmodel/Model View validation

(Again, an MVC validation question. I know, I know...)

I'd like to use AutoMapper (http://automapper.codeplex.com/) to validate fields in my Create Views for fields that are not in my database (and thus not in my DataModel).

Example: I have an Account/Create View for users to create a new account and I want both a Password and ConfirmPassword field so users have to enter their password twice for confirmation.

The Account table in the database looks like this:

Account[Id(PK), Name, Password, Email]

I've generated an ADO.NET Entity Data Model and from that, I generated the Models using an ADO.NET Self-Tracking Entity Generator.

I've also written a custom AccountViewModel for validation annotations like [Required].

So, to summarize, this is my project structure:

Controllers:
   AccountController

Models:
   Database.edmx (auto-generated from database)
   Model.Context.tt (auto-generated from edmx)
   Model.tt (auto-generated from edmx)
   AccountViewModel.cs

Views:
   Account
      Create.cshtml

The code of my AccountViewModel looks like this:

public class AccountViewModel
    {
        public int Id { get; set; }

        [Required]
        public string Name { get; set; }

        [Required]
        public string Password { get; set; }

        [Required]
        [Compare("Password")]
        public string ConfirmPassword { get; set; }
    }

Now, my Create View looks like this:

@model AutoMapperTest.Models.Account
<script src="@Url.Content("~/Scripts/jquery-1.4.4.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
@using (Html.BeginForm())
{
    @Html.ValidationSummary(true)
    <fieldset>
        <legend>Account</legend>
        <div class="editor-label">
            Name
        </div>
        <div class="editor-field">
            @Html.TextBox("Name")
            @Html.ValidationMessageFor(model => model.Name)
        </div>
        <div class="editor-label">
            Email
        </div>
        <div class="editor-field">
            @Html.TextBox("Email")
            @Html.ValidationMessageFor(model => model.Email)
        </div>
        <div class="editor-label">
            Password
        </div>
        <div class="editor-field">
            @Html.TextBox("Password")
            @Html.ValidationMessageFor(model => model.Password)
        </div>
        <div class="editor-label">
            Confirm your password
        </div>
        <div class="editor-field">
            @Html.TextBox("ConfirmPassword");
            @Html.ValidationMessageFor(model => model.ConfirmPassword)
        </div>
        <p>
            <input type="submit" value="Create" />
        </p>
    </fieldset>
}
<div>
    @Html.ActionLink("Back to List", "Index")
</div>

My code fails because my Model does not contain the ConfirmPassword field of course. Now, a little bird whispered to me the AutoMapper could fix that for me. But I can't figure it out... Can someone please tell me what I have to do to make this work? My AccountController looks like this now:

private readonly AccountViewModel _viewModel = new AccountViewModel();
private readonly DatabaseEntities _database = new DatabaseEntities();

//
        // GET: /Account/Create

        public ActionResult Create()
        {
            Mapper.CreateMap<AccountViewModel, Account>();
            return View("Create", _viewModel);
        } 

        //
        // POST: /Account/Create

        [HttpPost]
        public ActionResult Create(AccountViewModel accountToCreate)
        {
            try
            {
                if (ModelState.IsValid)
                {
                    var newAccount = new Account();
                    Mapper.Map(accountToCreate, newAccount);
                   _database.Account.AddObject(newAccount);
        _database.SaveChanges();
                }

                return RedirectToAction("Index");
            }
            catch
            {
                return View();
            }
        }

But this doesn't work... (Got the example from http://weblogs.asp.net/shijuvarghese/archive/2010/02/01/view-model-pattern-and-automapper-in-asp-net-mvc-applications.aspx)

Can anyone please enlighten me on this matter? Thank you very much, and my apologies for the wall of text and the hundreds of questions about the same subject...

like image 741
Matthias Avatar asked May 14 '11 19:05

Matthias


2 Answers

Few remarks about your code:

  1. Your view is strongly typed (@model declaration) to the Account model whereas it should be typed to the AccountViewModel view model (there is no point in declaring a view model if you don't use it in the view).
  2. AutoMapper is not used for validation, only for converting between types
  3. You don't need to declare a readonly field for your view model (AccountViewModel) inside the controller. You could instantiate the view model inside the GET action and leave the default model binder instantiate it as action argument for the POST action.
  4. You should do the AutoMapper configuration (Mapper.CreateMap<TSource, TDest>) only once for the entire application (ideally in Application_Start) and not inside a controller action
  5. There is no Email field on your view model which might be the reason for the update to fail (especially if this field is required in your database)

So here's how your code might look like:

public ActionResult Create()
{
    var model = new AccountViewModel();
    return View("Create", model);
} 

[HttpPost]
public ActionResult Create(AccountViewModel accountToCreate)
{
    try
    {
        if (ModelState.IsValid)
        {
            var newAccount = Mapper.Map<AccountViewModel, Account>(accountToCreate);
           _database.Account.AddObject(newAccount);
           _database.SaveChanges();
        }
        return RedirectToAction("Index");
    }
    catch
    {
        return View();
    }
}
like image 126
Darin Dimitrov Avatar answered Oct 31 '22 10:10

Darin Dimitrov


replace the first line of you view with

@model AutoMapperTest.AccountViewModel

Also you only need to call Mapper.CreateMap once for app lifetime (e.g. at app start)

like image 43
Necros Avatar answered Oct 31 '22 11:10

Necros