【问题标题】:How to validate my model in a custom model binder?如何在自定义模型绑定器中验证我的模型?
【发布时间】:2017-02-15 21:02:22
【问题描述】:

我询问了一个关于逗号分隔数值 here 的问题。

鉴于一些回复,我尝试按如下方式实现我自己的模型绑定器:

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;
        //}
    }
}

并将我的控制器类更新为:

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();
        }
    }
}

现在,如果我用逗号输入价格,我的自定义模型绑定器将删除逗号,这就是我想要的,但验证仍然失败。所以,问题是:如何在我的自定义模型绑定器中进行自定义验证,从而可以避免使用逗号捕获的价格值?换句话说,我怀疑我需要在我的自定义模型绑定器中做更多事情,但不知道如何以及做什么。谢谢

更新:

所以,我在https://stackoverflow.com/a/2592430/97109 尝试了@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;
        //}
    }
}

它有点工作,但如果我输入 0 作为价格,模型返回为有效,这是错误的,因为我有一个 Range 注释,它说最低价格是 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; }
    }
}

这里是控制器:

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);
        }
    }
}

这是自定义模型绑定器的实现:

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);
        }
    }
}

这是 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" />
}

这是我连接自定义模型绑定器的 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());
        }
    }
}

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

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

@dotnetstep:您能对此发表一些见解吗?谢谢。

【问题讨论】:

标签: c# validation asp.net-mvc-4


【解决方案1】:
    [HttpPost]
    public ActionResult Edit([ModelBinder(typeof(PropertyModelBinder))]PropertyModel model)
    {
        ModelState.Clear();
        TryValidateModel(model);
        if (ModelState.IsValid)
        {
            //Save property info.              
        }

        return View(model);
    }

希望这会有所帮助。

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

这可能是您的自定义 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);
    }       
}

因为您已经更新了绑定范围。我在 cmets 中提供了我的建议。此外,如果您将 ModelBinder 用于 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);
    }
} 

另外我想说的是,您可以找到许多与此相关的信息,并且您可以通过多种方式做同样的事情。就像你引入新的属性应用于类属性以进行绑定一样,你在类级别应用 ModelBinder。

【讨论】:

  • 你摇滚。我需要做的就是重写GetPropertyValue。我的情况甚至不需要覆盖 BindModel。这个答案会帮助很多人。我已经挣扎了3天。非常感谢。 @Ryan 的解决方案不起作用,我在他建议之前尝试过。该技巧基本上试图从 ModelState 中获取 Price 的验证信息,这是不可取的。
  • 我意识到 bindingContext.ValueProvider.GetValue(key) 只能检索简单类型的属性的值。如果我将房地产经纪人定义为自己的类,例如 public Agent { public string FirstName {get;放; } 公共字符串姓氏{get;放; } 公共字符串电话号码 {get;放; } } 和我的 PropertyModel 将有这个属性: public Agent Agent{get;放; }。假设我还想从 GetPropertyValue 中的表单集合中修改代理原始信息,如何在 GetPropertyValue 中重建代理对象?谢谢。
  • 这取决于您如何构建视图。请提供一些示例代码,以便我提供帮助。
  • 我建议您应该对 Property 和 Agent 类使用 modelbinder 而不是 CompositeViewModel。
  • 您不必这样做。即使在您已在 Global.asax 中配置的更新示例中,也无需再次在 Index 方法中指定。除此之外,还有另一种应用 binder 的方法,它是通过 class 的属性。
【解决方案2】:

这并不能完全回答你的问题,但我还是把它作为一个答案,因为我认为它解决了人们在你之前的问题中问你的问题。

在这个特定的例子中,听起来好像你想要一个字符串。是的,我知道这些值最终是数字(似乎是整数)值,但是当您使用 MVC 之类的东西时,您必须记住,您的视图使用的模型不必与模型匹配这是您的业务逻辑的一部分。我认为您遇到此问题是因为您试图将两者混为一谈。

相反,我建议创建一个专门用于您向最终用户显示的视图的模型(ViewModel)。向其中添加各种数据注释,这将有助于验证构成价格的字符串。 (您可以通过简单的regular expression data annotation 轻松完成所有您想要的验证。然后,一旦模型实际提交给您的控制器(或任何其他数据提交给您的控制器)并且您知道它是有效的(通过数据注释),然后您可以将其转换为您在业务逻辑中使用的模型所需的整数,并继续使用它(作为整数)。

通过这种方式,您可以避免遇到您一直在询问的所有这些不必要的复杂性(它确实有一个解决方案,但它并不真正符合 MVC 背后的思维方式),并允许您实现您正在寻找的灵活性你的观点对你的业务逻辑有严格的要求。

希望这是有道理的。您可以在 Web 上搜索 MVC 中的 ViewModel。一个不错的起点是ASP.NET MVC tutorials

【讨论】:

  • 谢谢。我确实觉得我已经在撞墙了。我一直希望在自定义模型绑定器中,我可以处理原始值 (650,000) ,将其转换为 650000,然后以某种方式对其进行验证以使其通过。但 MVC 似乎在构建模型之前进行验证。我认为问题是:如果可能的话,在我以我的方式构建模型之后如何验证它?您是否有指向您暗示的解决方案的链接?
  • 看起来您已经找到了您正在寻找的答案,这很好。但是,我真的建议您理解上述问题(按照我提供给本教程的链接,因为它有一个很好的例子,说明您需要做什么我的建议)。
  • 感谢您的跟进。但我不清楚该音乐商店教程与我想要解决的问题有何关联,以及我想要解决的方式。
  • 这就是问题所在 - 我的解决方案不会像您想要解决问题的方式那样解决它(这就是为什么我的警告是在我的回答开头)。相反,我建议重新审视你的设计,就像你目前正在做的那样,当你让它工作时,可能不是最好的方法。本教程向您展示了如何构建该设计并更好地分离关注点。 (具体来说,本节首先讨论如何解决您试图解决的问题。)
  • 我确实测试了该策略,它似乎是一个不错的解决方案,没有自定义模型绑定器带来的麻烦。似乎如果我们使用自定义模型绑定器,我们的对象属性注释将被忽略,我们还必须在自定义模型绑定器中手动验证我们的模型。如果我错了,请纠正我。我真的希望我是错的。
【解决方案3】:

通过更改 BindModel 触发的时间,我让您的验证正常工作。在您的代码中,PropertyModelBinder 中有这些行:

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;

我将base.BindModel 移动到在返回对象之前立即触发(在重建上下文之后),现在验证按预期工作。这是新代码:

newBindingContext.ModelState.Remove("Price");
newBindingContext.ModelState.Add("Price", new ModelState());
newBindingContext.ModelState.SetModelValue("Price", new ValueProviderResult(price, price, null));
object o = base.BindModel(controllerContext, newBindingContext);
return o;

【讨论】:

  • 我刚试过。这没用。问题是,一旦我们调用 base.BindModel(...),它再次使用带有逗号的旧值进行验证并导致验证错误。
猜你喜欢
  • 2012-01-29
  • 1970-01-01
  • 1970-01-01
  • 2019-11-16
  • 1970-01-01
  • 2012-11-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多