Custom Model Binder Not Validating Model

2019-03-25 20:11发布

问题:

I started to play around with knockout.js and in doing so I used the FromJsonAttribute (created by Steve Sanderson). I ran into an issue with the custom attribute not performing model validation. I put together a simple example-- I know it looks like a lot of code-- but the basic issue is how to force the validation of the model within a custom model binder.

using System.ComponentModel.DataAnnotations;

namespace BindingExamples.Models
{
    public class Widget
    {
        [Required]
        public string Name { get; set; }
    }
}

and here is my controller:

using System;
using System.Web.Mvc;
using BindingExamples.Models;

namespace BindingExamples.Controllers
{
    public class WidgetController : Controller
    {

        public ActionResult Index()
        {
            return View();
        }

        [HttpPost]
        public ActionResult Index(Widget w)
        {
            if(this.ModelState.IsValid)
            {
                TempData["message"] = String.Format("Thanks for inserting {0}", w.Name);
                return RedirectToAction("Confirmation");
            }
            return View(w);
        }

        [HttpPost]
        public ActionResult PostJson([koListEditor.FromJson] Widget w)
        {
            //the ModelState.IsValid even though the widget has an empty Name
            if (this.ModelState.IsValid)
            {
                TempData["message"] = String.Format("Thanks for inserting {0}", w.Name);
                return RedirectToAction("Confirmation");
            }
            return View(w);
        }

        public ActionResult Confirmation()
        {
            return View();
        }

    }
}

My issue is that the model is always valid in my PostJson method. For completeness here is the Sanderson code for the FromJson attribute:

using System.Web.Mvc;
using System.Web.Script.Serialization;

namespace koListEditor
{
    public class FromJsonAttribute : CustomModelBinderAttribute
    {
        private readonly static JavaScriptSerializer serializer = new JavaScriptSerializer();

        public override IModelBinder GetBinder()
        {
            return new JsonModelBinder();
        }

        private class JsonModelBinder : IModelBinder
        {
            public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
            {
                var stringified = controllerContext.HttpContext.Request[bindingContext.ModelName];
                if (string.IsNullOrEmpty(stringified))
                    return null;
                var model = serializer.Deserialize(stringified, bindingContext.ModelType);
                return model;
            }
        }
    }
}

回答1:

Description

The FromJsonAttribute only binds to the model and does, like you said, no validation.

You can add validation to the FromJsonAttribute in order to validate the model's against his DataAnnotations attributes.

This can be done using the TypeDescriptor class.

TypeDescriptor Provides information about the characteristics for a component, such as its attributes, properties, and events.

Check out my solution. I have tested it.

Solution

private class JsonModelBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var stringified = controllerContext.HttpContext.Request[bindingContext.ModelName];
        if (string.IsNullOrEmpty(stringified))
            return null;
        var model = serializer.Deserialize(stringified, bindingContext.ModelType);

        // DataAnnotation Validation
        var validationResult = from prop in TypeDescriptor.GetProperties(model).Cast<PropertyDescriptor>()
                                from attribute in prop.Attributes.OfType<ValidationAttribute>()
                                where !attribute.IsValid(prop.GetValue(model))
                                select new { Propertie = prop.Name, ErrorMessage = attribute.FormatErrorMessage(string.Empty) };

        // Add the ValidationResult's to the ModelState
        foreach (var validationResultItem in validationResult)
            bindingContext.ModelState.AddModelError(validationResultItem.Propertie, validationResultItem.ErrorMessage);

        return model;
    }
}

More Information

  • TypeDescriptor Class
  • System.ComponentModel.DataAnnotations Namespace


回答2:

Thank you, thank you, dknaack!! Your answer was exactly what I was looking for, except I want to validate after each property is bound b/c I have properties that are dependent on other properties, and I don't want to continue binding if a dependent property is invalid.

Here's my new BindProperty overload:

protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor){

    // if this is a simple property, bind it and return
    if(_simplePropertyKeys.ContainsKey(propertyDescriptor.Name)){
        this.BindSimpleProperty(bindingContext, propertyDescriptor);

    // if this is complex property, only bind it if we don't have an error already
    } else if (bindingContext.ModelState.IsValid){
        this.BindComplexProperty(bindingContext, propertyDescriptor);
    }

    // add errors from the data annotations
    propertyDescriptor.Attributes.OfType<ValidationAttribute>()
        .Where(a => a.IsValid(propertyDescriptor.GetValue(bindingContext.Model)) == false)
        .ForEach(r => bindingContext.ModelState.AddModelError(propertyDescriptor.Name, r.ErrorMessage));
}


回答3:

First of all, I'm only starting to learn ASP.NET so don't take my solution seriously. I found this article and as you, tried to do a custom model binder. There was no validation. Then i just replaced IModelBinder interface with DefaultModelBinder and voula, it works. Hope I could help someone