I'm using ASP.NET MVC 2.0 and I am trying to take advantage of the model binding in my controller and also the modelstate validation. However I have come up against a problem and wanted to share it with people on here to see what you think.
Ok I have my clean User poco in my model class library...
namespace Model
{
public partial class User
{
public virtual int Id { get; private set; }
public virtual string UserName { get; private set; }
public virtual string DisplayName { get; set; }
public virtual string Email { get; set; }
public User(string displayName, string userName)
: this()
{
DisplayName = displayName;
UserName = userName;
}
}
}
The design I have gone for only allows certain properties to be edited, after the object has been constructed. The UserName for example can only be set when the object is constructed, to me this makes OO sense, but is the key to my problem, so I wanted to highlight it here.
I then have a 'buddy class' that defines the validation metadata for my User class...
namespace Model
{
[MetadataType(typeof(UserMetadata))]
public partial class User
{
class UserMetadata
{
[Required]
public virtual int Id { get; set; }
[Required]
public virtual string UserName { get; set; }
[Required]
public virtual string DisplayName { get; set; }
[RegularExpression(@"^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$", ErrorMessage = "Invalid address")]
public virtual string Email { get; set; }
}
}
}
Then up in my web layer I want to allow my users to be able to edit this object. So I have the following two action methods in my profile controller.
namespace Web.Controllers
{
public class ProfileController : Controller
{
[Authorize]
public ActionResult Edit()
{
var user = _session.Single<User>(x => x.UserName == HttpContext.User.Identity.Name );
return View(user);
}
[HttpPost]
[ValidateAntiForgeryToken]
[Authorize]
[TransactionFilter]
public ActionResult Edit(User updatedUser)
{
// Get the current user to update.
var user = _session.Single<User>(x => x.UserName == HttpContext.User.Identity.Name);
if (ModelState.IsValid)
{
TryUpdateModel(user);
// Update store...
}
return View(updatedUser);
}
}
}
This has a strongly typed view to go with it...
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<Model.User>" %>
<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
Edit
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<%=Html.Script("jquery.validate.js")%>
<%=Html.Script("MicrosoftMvcJQueryValidation.js")%>
<%=Html.Script("MvcFoolproofJQueryValidation.js")%>
<div class="container">
<div class="column span-14">
<% using (Html.BeginForm()) {%>
<%= Html.AntiForgeryToken() %>
<fieldset>
<%: Html.DisplayFor(model => model.UserName) %>
<%= Html.Label("Display Name") %>
<%= Html.HiddenFor(model => model.DisplayName)%>
<%= Html.ValidationMessageFor(model => model.DisplayName)%>
<%= Html.Label("Email address") %>
<%= Html.EditorFor(model => model.Email)%>
<%= Html.ValidationMessageFor(model => model.Email)%>
<%= Html.HiddenFor(model => model.UserName)%>
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
</div>
<div class="clear"></div>
<% } %>
</div>
</asp:Content>
Ok so thats all the code out the way!!
So here's the problem, the view is rendered fine after the initial get request. But when the user posts the form back, say after editing their display name, the ModelState is NOT valid. This is because the UserName property has a private setter on it. However this is by design, for security and semantics I don't ever want them to change their username, so the setter is private. However, as I have added the Required attribute to the property, it is failing as it is not set!
The question is should the modelbinding be reporting this as a validation error or not?! As the property is private, I have designed for it to not be set, therefore by design I don't expect the model binder to be setting it, but I don't want a validation error. I think it should only produce validation errors for properties that it CAN set.
Ok so possible solutions I have come up with so far..
Make the property public.
If I do this I open myself up to allowing the username to be changed for existing user's. I would have to add extra logic somewhere to catch this, not really very nice. I would also have to add a Bind Exclude on the action method to stop any naughty people trying to set it via a post.
Remove the Error
I believe I can remove the error from the ModelState dictionary, this would be fine on this occasion, but I think this will introduce some code smell, as I would have to add this for all my objects that have private setters. I would probably forget!!
Strongly type my view against an interface
I have read that some people bind their view to an interface of their model, this is king of a ModelView interface onto the business model object. I like this idea, but I loose the auto binding and would need to duplicate my model objects with their constructors in my web layer, not sure about that?! Some info on this here http://www.codethinked.com/post/2010/04/12/Easy-And-Safe-Model-Binding-In-ASPNET-MVC.aspx.
Use Model Views
This just doesn't seem DRY to me?! I'm happy to use these if I don't have an existing model object that fits (for example I use a signup Model View).
CustomModelBinder
My preferred option, but I'm not sure I know what I'm doing!! If I could just get the binder to only bind to properties it can set, then I would be laughing!!
What do people think? Comments on the above options, any other solutions, am I just off the mark with my architecture?!
Thanks :)