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

时间:2014-04-18 00:51:55

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

我用逗号分隔的数字值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();
        }
    }
}

现在,如果我用逗号输入价格,我的自定义模型绑定器将删除逗号,这就是我想要的,但验证仍然失败。所以,问题是:如何在我的自定义模型绑定器中进行自定义验证,以便可以避免使用逗号捕获的价格值?换句话说,我怀疑我需要在我的自定义模型绑定器中做更多,但不知道如何和什么。感谢。Open the screen shot in a new tab for a better 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作为价格,模型会返回有效,这是错误的,因为我有一个范围注释,表示最低价格是1.在我的机智结束。< / p>

更新

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

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来验证它们,但是这样做太多了,而且,我有注释,为什么模型绑定器的默认实现不会让他们尊重它们只是因为我压倒了它的一部分?
  3. @dotnetstep:你能否对此提出一些见解?谢谢。

3 个答案:

答案 0 :(得分:21)

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

        return View(model);
    }

希望这会有帮助。

您也可以尝试@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);
    }       
}

您已更新绑定范围。我在评论中提出了我的建议。此外,如果你使用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);
    }
} 

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

答案 1 :(得分:2)

我通过更改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;

答案 2 :(得分:1)

这不能完全回答你的问题,但无论如何我都会把它作为答案,因为我认为它解决了人们在你上一个问题中提出的问题。

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

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

通过这种方式,您可以避免遇到所有这些不必要的复杂性(它确实有一个解决方案,但它并不真正符合MVC背后的思维方式),并且可以让您实现所需的灵活性。根据您的业务逻辑中的严格要求,您的观点。

希望这是有道理的。您可以在Web上搜索MVC中的ViewModel。一个好的开始是ASP.NET MVC tutorials