how can I bind a complex class to a view, while preserving my custom validation attributes?

In my project I have a model class that uses another class, like in the sample below. One of the properties in the model depends for validation on on of the properties of a child object -- in this sample, LastName property depends for validation on the value of the Address.PostalCode property. I implemented a custom validation attribute to validate my LastName property and it works great.

public class User
{
    public static ValidationResult ValidateLastName(string lastName, ValidationContext context)
    {
        // Grab the model instance
        var user = context.ObjectInstance as User;
        if (user == null)
            throw new NullReferenceException();

        // Cross-property validation
        if (user.Address.postalCode.Length < 10000)
            return new ValidationResult("my LastName custom validation message.");

        return ValidationResult.Success;
    }

    [Display(Name = "Last name")]
    [CustomValidationAttribute(typeof(User), "ValidateLastName")]
    public string LastName { get; set; }

    [Display(Name = "First name")]
    public string FirstName { get; set; }

    [Display(Name = "Address:")]
    [CustomValidationAttribute(typeof(User), "ValidateAddress")]
    public AddressType Address { get; set; }
}

public class AddressType
{
    public string streetName = "";

    public string streetNumber = "";
    public string postalCode = "";
}

The problem is in the controller the Address property does not get constructed from the view, and it is always null. In this sample, user.Address is always null, regardless of what I send in the view. Here is the controller code.

    [HttpPost]
    public ActionResult Create(User user)
    {
        if (ModelState.IsValid)
        {
            // creation code here
            return RedirectToAction("Index");
        }
        else
        {
            return View(user);
        }
    }

Here is the view:

        <div class="editor-label">
            @Html.LabelFor(model => model.Address.postalCode)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Address.postalCode)
            @Html.ValidationMessageFor(model => model.Address.postalCode)
        </div>

To resolve this, I created a custom dummy binder to map the fields in the view to the properties in the model like so:

public class UserBinder : IModelBinder
{
    private string GetValue(ModelBindingContext bindingContext, string key)
    {
        var result = bindingContext.ValueProvider.GetValue(key);
        return (result == null) ? null : result.AttemptedValue;
    }

    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        User instance = new User();
        instance.FirstName = GetValue(bindingContext, "FirstName"); //controllerContext.HttpContext.Request["FirstName"];
        instance.LastName = GetValue(bindingContext, "LastName"); //controllerContext.HttpContext.Request["LastName"];

        instance.Address = new AddressType();
        string streetName = controllerContext.HttpContext.Request["Address.streetName"];

        //ModelStateDictionary mState = bindingContext.ModelState;
        //mState.Add("LastName", new ModelState { });
        //mState.AddModelError("LastName", "There's an error.");

        instance.Address.streetName = streetName;
                    ...
        return instance;
    }

The binder works fine, but the validation attributes do not work anymore. I think there must be a better way to do the binding than this, is there?

This binder is just mapping LastName to LastName and Address.streetName to Address.streetName, I imagine there should be a way to accomplish this without having to write all this tedious code and without breaking the custom validation mechanism.

Answers


You need to use properties instead of public fields in order for the default model binding to work properly.

Change your AddressType class to:

public class AddressType
{
    public string streetName { get; set; }
    public string streetNumber { get; set; }
    public string postalCode { get; set; }
}

Need Your Help

How do I remove an item from a Python list by value?

python list

If I wanted to remove an item by its value instead of its index what would I do?

Algorithm for creating shapes out of elements

javascript algorithm

I got a script that generates 100 elements and appends them to the site.

About UNIX Resources Network

Original, collect and organize Developers related documents, information and materials, contains jQuery, Html, CSS, MySQL, .NET, ASP.NET, SQL, objective-c, iPhone, Ruby on Rails, C, SQL Server, Ruby, Arrays, Regex, ASP.NET MVC, WPF, XML, Ajax, DataBase, and so on.