Как проверить мою модель в пользовательской привязке модели?

Я спросил о проблеме с цифрами здесь.

Учитывая некоторые ответы, я попытался попытаться реализовать свое собственное связующее устройство следующим образом:

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.

Update:

Итак, я попробовал решение @mare на qaru.site/info/363752/... и обновил мое связующее устройство следующим образом:

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. На моем конце wit.

Update:

Чтобы протестировать специализированное связующее устройство с составными типами. Я создал следующие классы модели представлений:

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

Может быть, я делаю что-то неправильно или недостаточно. Я заметил следующие проблемы:

  • Хотя мне нужно только изменить значение цены объекта свойства, кажется, мне нужно утомительно заполнить все другие свойства в связующем устройстве. Я должен сделать то же самое для свойства AnnualSales свойства агента. Так или иначе, этого можно избежать в модельном связующем?
  • Я думал, что метод BindModel по умолчанию будет выполнять нашу аннотацию свойств наших объектов и проверять их соответственно после вызова GetPropertyValue, но это не так. Если я ввожу какой-то параметр значения вне диапазона для параметра Цена объекта "Свойство" или "Годовые продажи агента", модель возвращается как действительная. Другими словами, аннотации Range игнорируются. Я знаю, что могу проверить их, переопределив OnModelUpdated в настраиваемом подменю модели, но это слишком много работы, и, кроме того, у меня есть аннотации на месте, почему реализация по умолчанию для имплантата модели не почитает их только потому, что я преобладаю часть из этого?

@dotnetstep: Не могли бы вы рассказать об этом? Спасибо.

Ответ 1

    [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 для свойства и агента, вы можете сделать это следующим образом.

//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 на уровне класса.

Ответ 2

Это не будет точно отвечать на ваш вопрос, но я все равно даю ему ответ, потому что я думаю, что он затрагивает то, о чем люди спрашивали вас в предыдущем вопросе.

В этом конкретном случае это действительно звучит так, как будто вы хотите строку. Да, я знаю, что значения - это, в конечном счете, числовые (целочисленные, кажется) значения, но когда вы работаете с чем-то вроде MVC, вы должны помнить, что модели, которые используются вашим представлением, не должны совпадать с моделями которые являются частью вашей бизнес-логики. Я думаю, что вы столкнулись с этой проблемой, потому что пытаетесь их смешивать.

Вместо этого я бы рекомендовал создать модель (ViewModel), которая специально предназначена для представления, которое вы показываете конечному пользователю. Добавьте к нему различные аннотации данных, которые помогут подтвердить STRING, которая составляет цену. (Вы можете легко выполнить всю необходимую проверку с помощью простой аннотации данных регулярного выражения. Затем, как только модель будет отправлена ​​на ваш контроллер (или любые другие данные передаются вашему контроллеру), и вы знаете, что оно действительно (через аннотацию данных), вы можете затем преобразовать его в целое число, которое требуется для модели, которую вы используете с вашей бизнес-логикой, и продолжать использовать ее как вы бы (как целое число).

Таким образом, вы избегаете всей этой ненужной сложности, о которой вы просили (у которой есть решение, но это не очень подходит мышлению за MVC) и позволяет вам достичь гибкости, которую вы ищете в ваше представление с жестким требованием в вашей бизнес-логике.

Надеюсь, это имеет смысл. Вы можете искать в Интернете для ViewModels в MVC. Хорошим местом для начала является учебники ASP.NET 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;