如何在自定义模型绑定器中验证我的模型?

Sta*_*low 15 c# validation asp.net-mvc-4

我问一个问题,我有一个逗号分隔的数值在这里.

鉴于一些回复,我试图尝试实现我自己的模型绑定器,如下所示:

namespace MvcApplication1.Core
{
    public class PropertyModelBinder : DefaultModelBinder
    {
        public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            object objectModel = new object();

            if (bindingContext.ModelType == typeof(PropertyModel))
            {
                HttpRequestBase request = controllerContext.HttpContext.Request;
                string price = request.Form.Get("Price").Replace(",", string.Empty);

                ModelBindingContext newBindingContext = new ModelBindingContext()
                {
                    ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(
                        () => new PropertyModel() 
                        {
                            Price = Convert.ToInt32(price)
                        },
                        typeof(PropertyModel)       
                    ),
                    ModelState = bindingContext.ModelState,
                    ValueProvider = bindingContext.ValueProvider
                };

                // call the default model binder this new binding context
                return base.BindModel(controllerContext, newBindingContext);
            }
            else
            {
                return base.BindModel(controllerContext, bindingContext);
            }
        }

        //protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
        //{
        //    //return base.CreateModel(controllerContext, bindingContext, modelType);
        //    PropertyModel model = new PropertyModel();

        //    if (modelType == typeof(PropertyModel))
        //    {
        //        model = (PropertyModel)base.CreateModel(controllerContext, bindingContext, modelType);
        //        HttpRequestBase request = controllerContext.HttpContext.Request;
        //        string price = request.Form.Get("Price").Replace(",", string.Empty);
        //        model.Price = Convert.ToInt32(price);
        //    }

        //    return model;
        //}
    }
}
Run Code Online (Sandbox Code Playgroud)

并更新了我的控制器类,如下所示:

namespace MvcApplication1.Controllers
{
    public class PropertyController : Controller
    {
        public ActionResult Edit()
        {
            PropertyModel model = new PropertyModel
            {
                AgentName = "John Doe",
                BuildingStyle = "Colonial",
                BuiltYear = 1978,
                Price = 650000,
                Id = 1
            };

            return View(model);
        }

        [HttpPost]
        public ActionResult Edit([ModelBinder(typeof(PropertyModelBinder))] PropertyModel model)
        {
            if (ModelState.IsValid)
            {
                //Save property info.              
            }

            return View(model);
        }

        public ActionResult About()
        {
            ViewBag.Message = "Your app description page.";

            return View();
        }

        public ActionResult Contact()
        {
            ViewBag.Message = "Your contact page.";

            return View();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,如果我用逗号输入价格,我的自定义模型绑定器将删除逗号,这就是我想要的,但验证仍然失败.所以,问题是:如何在我的自定义模型绑定器中进行自定义验证,以便可以避免使用逗号捕获的价格值?换句话说,我怀疑我需要在我的自定义模型绑定器中做更多,但不知道如何和什么.谢谢.在新选项卡中打开屏幕截图以获得更好的视图.

更新:

所以,我在/sf/answers/181470131/尝试了@ mare的解决方案并更新了我的模型绑定器,如下所示:

namespace MvcApplication1.Core
{
    public class PropertyModelBinder : DefaultModelBinder
    {
        public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            object objectModel = new object();

            if (bindingContext.ModelType == typeof(PropertyModel))
            {
                HttpRequestBase request = controllerContext.HttpContext.Request;
                string price = request.Form.Get("Price").Replace(",", string.Empty);

                ModelBindingContext newBindingContext = new ModelBindingContext()
                {
                    ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(
                        () => new PropertyModel() 
                        {
                            Price = Convert.ToInt32(price)
                        },
                        typeof(PropertyModel)    
                    ),
                    ModelState = bindingContext.ModelState,
                    ValueProvider = bindingContext.ValueProvider
                };

                // call the default model binder this new binding context
                object o = base.BindModel(controllerContext, newBindingContext);
                newBindingContext.ModelState.Remove("Price");
                newBindingContext.ModelState.Add("Price", new ModelState());
                newBindingContext.ModelState.SetModelValue("Price", new ValueProviderResult(price, price, null));
                return o;
            }
            else
            {
                return base.BindModel(controllerContext, bindingContext);
            }
        }

        //protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
        //{
        //    //return base.CreateModel(controllerContext, bindingContext, modelType);
        //    PropertyModel model = new PropertyModel();

        //    if (modelType == typeof(PropertyModel))
        //    {
        //        model = (PropertyModel)base.CreateModel(controllerContext, bindingContext, modelType);
        //        HttpRequestBase request = controllerContext.HttpContext.Request;
        //        string price = request.Form.Get("Price").Replace(",", string.Empty);
        //        model.Price = Convert.ToInt32(price);
        //    }

        //    return model;
        //}
    }
}
Run Code Online (Sandbox Code Playgroud)

它有点工作,但如果我输入0代价,模型会返回有效,这是错误的,因为我有一个范围注释,表示最低价格是1.在我的机智结束时.

更新:

为了测试具有复合类型的自定义模型绑定器.我创建了以下视图模型类:

using System.ComponentModel.DataAnnotations;

namespace MvcApplication1.Models
{
    public class PropertyRegistrationViewModel
    {
        public PropertyRegistrationViewModel()
        {

        }

        public Property Property { get; set; }
        public Agent Agent { get; set; }
    }

    public class Property
    {
        public int HouseNumber { get; set; }
        public string Street { get; set; }
        public string City { get; set; }
        public string State { get; set; }
        public string Zip { get; set; }

        [Required(ErrorMessage="You must enter the price.")]
        [Range(1000, 10000000, ErrorMessage="Bad price.")]
        public int Price { get; set; }
    }

    public class Agent
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        [Required(ErrorMessage="You must enter your annual sales.")]
        [Range(10000, 5000000, ErrorMessage="Bad range.")]
        public int AnnualSales { get; set; }

        public Address Address { get; set; }
    }

    public class Address
    {
        public string Line1 { get; set; }
        public string Line2 { get; set; }
    }
}
Run Code Online (Sandbox Code Playgroud)

这是控制器:

using MvcApplication1.Core;
using MvcApplication1.Models;
using System.Web.Mvc;

namespace MvcApplication1.Controllers {
    public class RegistrationController : Controller
    {
        public ActionResult Index() {
            PropertyRegistrationViewModel viewModel = new PropertyRegistrationViewModel();
            return View(viewModel);
        }

        [HttpPost]
        public ActionResult Index([ModelBinder(typeof(PropertyRegistrationModelBinder))]PropertyRegistrationViewModel viewModel)
        {
            if (ModelState.IsValid)
            {
                //save registration.
            }

            return View(viewModel);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

以下是自定义模型绑定器实现:

using MvcApplication1.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace MvcApplication1.Core
{
    public class PropertyRegistrationModelBinder : DefaultModelBinder
    {
        protected override object GetPropertyValue(
            ControllerContext controllerContext,
            ModelBindingContext bindingContext,
            System.ComponentModel.PropertyDescriptor propertyDescriptor,
            IModelBinder propertyBinder)
        {
            if (propertyDescriptor.ComponentType == typeof(PropertyRegistrationViewModel))
            {
                if (propertyDescriptor.Name == "Property")
                {  
                    var price = bindingContext.ValueProvider.GetValue("Property.Price").AttemptedValue.Replace(",", string.Empty);
                    var property = new Property();

                    // Question 1: Price is the only property I want to modify. Is there any way 
                    // such that I don't have to manually populate the rest of the properties like so?
                    property.Price = string.IsNullOrWhiteSpace(price)? 0: Convert.ToInt32(price);
                    property.HouseNumber = Convert.ToInt32(bindingContext.ValueProvider.GetValue("Property.HouseNumber").AttemptedValue);
                    property.Street = bindingContext.ValueProvider.GetValue("Property.Street").AttemptedValue;
                    property.City = bindingContext.ValueProvider.GetValue("Property.City").AttemptedValue;
                    property.State = bindingContext.ValueProvider.GetValue("Property.State").AttemptedValue;
                    property.Zip = bindingContext.ValueProvider.GetValue("Property.Zip").AttemptedValue;

                    // I had thought that when this property object returns, our annotation of the Price property
                    // will be honored by the model binder, but it doesn't validate it accordingly.
                    return property;
                }

                if (propertyDescriptor.Name == "Agent")
                {
                    var sales = bindingContext.ValueProvider.GetValue("Agent.AnnualSales").AttemptedValue.Replace(",", string.Empty);
                    var agent = new Agent();

                    // Question 2: AnnualSales is the only property I need to process before validation,
                    // Is there any way I can avoid tediously populating the rest of the properties?
                    agent.AnnualSales = string.IsNullOrWhiteSpace(sales)? 0:  Convert.ToInt32(sales);
                    agent.FirstName = bindingContext.ValueProvider.GetValue("Agent.FirstName").AttemptedValue;
                    agent.LastName = bindingContext.ValueProvider.GetValue("Agent.LastName").AttemptedValue;

                    var address = new Address();
                    address.Line1 = bindingContext.ValueProvider.GetValue("Agent.Address.Line1").AttemptedValue + " ROC";
                    address.Line2 = bindingContext.ValueProvider.GetValue("Agent.Address.Line2").AttemptedValue + " MD";
                    agent.Address = address;

                    // I had thought that when this agent object returns, our annotation of the AnnualSales property
                    // will be honored by the model binder, but it doesn't validate it accordingly.
                    return agent;
                }
            }
            return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);
        }

        protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            var model = bindingContext.Model as PropertyRegistrationViewModel;
            //In order to validate our model, it seems that we will have to manually validate it here. 
            base.OnModelUpdated(controllerContext, bindingContext);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这是Razor视图:

@model MvcApplication1.Models.PropertyRegistrationViewModel
@{
    ViewBag.Title = "Property Registration";
}

<h2>Property Registration</h2>
<p>Enter your property and agent information below.</p>

@using (Html.BeginForm("Index", "Registration"))
{
    @Html.ValidationSummary();    
    <h4>Property Info</h4>
    <text>House Number</text> @Html.TextBoxFor(m => m.Property.HouseNumber)<br />
    <text>Street</text> @Html.TextBoxFor(m => m.Property.Street)<br />
    <text>City</text> @Html.TextBoxFor(m => m.Property.City)<br />
    <text>State</text> @Html.TextBoxFor(m => m.Property.State)<br />
    <text>Zip</text> @Html.TextBoxFor(m => m.Property.Zip)<br />
    <text>Price</text> @Html.TextBoxFor(m => m.Property.Price)<br /> 
    <h4>Agent Info</h4>
    <text>First Name</text> @Html.TextBoxFor(m => m.Agent.FirstName)<br />
    <text>Last Name</text> @Html.TextBoxFor(m => m.Agent.LastName)<br />
    <text>Annual Sales</text> @Html.TextBoxFor(m => m.Agent.AnnualSales)<br />
    <text>Agent Address L1</text>@Html.TextBoxFor(m => m.Agent.Address.Line1)<br />
    <text>Agent Address L2</text>@Html.TextBoxFor(m => m.Agent.Address.Line2)<br />
    <input type="submit" value="Submit" name="submit" />
}
Run Code Online (Sandbox Code Playgroud)

这里是global.asax文件,我连接自定义模型绑定器.顺便说一下,似乎不需要这一步,因为我注意到它没有这一步仍然有用.

using MvcApplication1.Core;
using MvcApplication1.Models;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;

namespace MvcApplication1 {
    // Note: For instructions on enabling IIS6 or IIS7 classic mode, 
    // visit http://go.microsoft.com/?LinkId=9394801

    public class MvcApplication : System.Web.HttpApplication {
        protected void Application_Start() {
            AreaRegistration.RegisterAllAreas();

            WebApiConfig.Register(GlobalConfiguration.Configuration);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);
            AuthConfig.RegisterAuth();
            ModelBinders.Binders.Add(typeof(PropertyRegistrationViewModel), new PropertyRegistrationModelBinder());
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

也许我做错了或做得不够.我注意到以下问题:

  1. 虽然我只需要修改属性对象的Price值,但似乎我不得不繁琐地填充模型绑定器中的所有其他属性.我必须对代理属性的AnnualSales属性执行相同的操作.无论如何,在模型绑定器中可以避免这种情况吗?
  2. 我原以为默认的BindModel方法会尊重我们对象属性的注释,并在调用GetPropertyValue后对其进行相应的验证,但事实并非如此.如果我为Property对象的Price或Agent对象的AnnualSales输入超出范围的某个值,则模型将返回为有效.换句话说,范围注释被忽略.我知道我可以通过覆盖自定义模型绑定器中的OnModelUpdated来验证它们,但这太多了,而且,我有注释,为什么模型绑定器的默认实现不尊重它们只是因为我要覆盖部分它?

@dotnetstep:你能否对此提出一些见解?谢谢.

dot*_*tep 21

    [HttpPost]
    public ActionResult Edit([ModelBinder(typeof(PropertyModelBinder))]PropertyModel model)
    {
        ModelState.Clear();
        TryValidateModel(model);
        if (ModelState.IsValid)
        {
            //Save property info.              
        }

        return View(model);
    }
Run Code Online (Sandbox Code Playgroud)

希望这会有所帮助.

您也可以尝试@Ryan解决方案.

这可能是您的Custom ModelBinder.(在这种情况下,您不需要更新您上面建议的编辑操作结果)

public class PropertyModelBinder : DefaultModelBinder
{     

    protected override object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder)
    {
        if(propertyDescriptor.ComponentType == typeof(PropertyModel))
        {
            if (propertyDescriptor.Name == "Price")
            {
                var obj=   bindingContext.ValueProvider.GetValue("Price");
                return Convert.ToInt32(obj.AttemptedValue.ToString().Replace(",", ""));
            }
        }
        return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);
    }       
}
Run Code Online (Sandbox Code Playgroud)

当您更新了绑定范围时.我在评论中提出了我的建议.此外,如果您使用ModelBinder for Property和Agent,那么您可以这样做.

//In Global.asax
ModelBinders.Binders.Add(typeof(Property), new PropertyRegistrationModelBinder());
ModelBinders.Binders.Add(typeof(Agent), new PropertyRegistrationModelBinder());

//Updated ModelBinder look like this.

 public class PropertyRegistrationModelBinder : DefaultModelBinder
{
    protected override object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder)
    {
        if (propertyDescriptor.ComponentType == typeof(Property) || propertyDescriptor.ComponentType == typeof(Agent))
        {
            if(propertyDescriptor.Name == "Price" || propertyDescriptor.Name == "AnnualSales")
            {                    
                var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName).AttemptedValue.Replace(",", string.Empty);
                return string.IsNullOrEmpty(value) ? 0 : Convert.ToInt32(value);
            }
        }            
        return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);
    }
} 
Run Code Online (Sandbox Code Playgroud)

另外我想说你能找到很多与此相关的信息,你也可以用很多方法做同样的事情.就像你引入适用于类属性的新属性进行绑定一样,你可以在类级别应用ModelBinder.