ASP.Net MVC2 CustomModelBinder 不工作...从 MVC1 更改

发布于 2024-09-02 10:27:49 字数 8032 浏览 7 评论 0原文

(如果这看起来很冗长,我很抱歉 - 试图提供所有相关代码)

我刚刚升级到 VS2010,现在在尝试让新的 CustomModelBinder 工作时遇到困难。

在 MVC1 中,我会编写类似的内容

public class AwardModelBinder: DefaultModelBinder
{
    :
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        // do the base binding to bind all simple types
        Award award = base.BindModel(controllerContext, bindingContext) as Award;

        // Get complex values from ValueProvider dictionary
        award.EffectiveFrom = Convert.ToDateTime(bindingContext.ValueProvider["Model.EffectiveFrom"].AttemptedValue.ToString());
        string sEffectiveTo = bindingContext.ValueProvider["Model.EffectiveTo"].AttemptedValue.ToString();
        if (sEffectiveTo.Length > 0)
            award.EffectiveTo = Convert.ToDateTime(bindingContext.ValueProvider["Model.EffectiveTo"].AttemptedValue.ToString());
        // etc

        return award;
    }
}

当然,我会在 Global.asax.cs 中注册自定义绑定器:

    protected void Application_Start()
    {
        RegisterRoutes(RouteTable.Routes);

        // register custom model binders
        ModelBinders.Binders.Add(typeof(Voucher), new VoucherModelBinder(DaoFactory.UserInstance("EH1303")));
        ModelBinders.Binders.Add(typeof(AwardCriterion), new AwardCriterionModelBinder(DaoFactory.UserInstance("EH1303"), new VOPSDaoFactory()));
        ModelBinders.Binders.Add(typeof(SelectedVoucher), new SelectedVoucherModelBinder(DaoFactory.UserInstance("IT0706B")));
        ModelBinders.Binders.Add(typeof(Award), new AwardModelBinder(DaoFactory.UserInstance("IT0706B")));
    }

现在,在 MVC2 中,我发现对 base.BindModel 的调用返回一个对象一切都是空的,我只是不想迭代新 ValueProvider.GetValue() 函数显示的所有表单字段。

谷歌找不到此错误的匹配项,因此我认为我做错了什么。

这是我的实际代码:

我的域对象(推断您喜欢封装的子对象的哪些内容 - 我知道我也需要这些自定义绑定器,但是三个“简单”字段(即基本类型)Id、TradingName 和 BusinessIncorporated 是也返回空):

public class Customer
{
    /// <summary>
    /// Initializes a new instance of the Customer class.
    /// </summary>
    public Customer() 
    {
        Applicant = new Person();
        Contact = new Person();
        BusinessContact = new ContactDetails();
        BankAccount = new BankAccount();
    }

    /// <summary>
    /// Gets or sets the unique customer identifier.
    /// </summary>
    public int Id { get; set; }

    /// <summary>
    /// Gets or sets the applicant details.
    /// </summary>
    public Person Applicant { get; set; }

    /// <summary>
    /// Gets or sets the customer's secondary contact.
    /// </summary>
    public Person Contact { get; set; }

    /// <summary>
    /// Gets or sets the trading name of the business.
    /// </summary>
    [Required(ErrorMessage = "Please enter your Business or Trading Name")]
    [StringLength(50, ErrorMessage = "A maximum of 50 characters is permitted")]
    public string TradingName { get; set; }

    /// <summary>
    /// Gets or sets the date the customer's business began trading.
    /// </summary>
    [Required(ErrorMessage = "You must supply the date your business started trading")]
    [DateRange("01/01/1900", "01/01/2020", ErrorMessage = "This date must be between {0} and {1}")]
    public DateTime BusinessIncorporated { get; set; }

    /// <summary>
    /// Gets or sets the contact details for the customer's business.
    /// </summary>
    public ContactDetails BusinessContact { get; set; }

    /// <summary>
    /// Gets or sets the customer's bank account details.
    /// </summary>
    public BankAccount BankAccount { get; set; }
}

我的控制器方法:

    /// <summary>
    /// Saves a Customer object from the submitted application form.
    /// </summary>
    /// <param name="customer">A populate instance of the Customer class.</param>
    /// <returns>A partial view indicating success or failure.</returns>
    /// <httpmethod>POST</httpmethod>
    /// <url>/Customer/RegisterCustomerAccount</url>
    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult RegisterCustomerAccount(Customer customer)
    {
        if (ModelState.IsValid)
        {
            // save the Customer

            // return indication of success, or otherwise
            return PartialView();
        }
        else
        {
            ViewData.Model = customer;

            // load necessary reference data into ViewData
            ViewData["PersonTitles"] = new SelectList(ReferenceDataCache.Get("PersonTitle"), "Id", "Name");

            return PartialView("CustomerAccountRegistration", customer);
        }
    }

我的自定义活页夹:

public class CustomerModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        ValueProviderResult vpResult = bindingContext
            .ValueProvider.GetValue(bindingContext.ModelName);
        // vpResult is null

        // MVC2 - ValueProvider is now an IValueProvider, not dictionary based anymore
        if (bindingContext.ValueProvider.GetValue("Model.Applicant.Title") != null)
        {
            // works
        }

        Customer customer = base.BindModel(controllerContext, bindingContext) as Customer;
        // customer instanitated with null (etc) throughout

        return customer;
    }
}

我的活页夹注册:

    /// <summary>
    /// Application_Start is called once when the web application is first accessed.
    /// </summary>
    protected void Application_Start()
    {
        RegisterRoutes(RouteTable.Routes);

        // register custom model binders
        ModelBinders.Binders.Add(typeof(Customer), new CustomerModelBinder());

        ReferenceDataCache.Populate();
    }

...以及我视图中的片段(这可能是前缀问题吗?)

    <div class="inputContainer">
        <label class="above" for="Model_Applicant_Title" accesskey="t"><span class="accesskey">T</span>itle<span class="mandatoryfield">*</span></label>
        <%= Html.DropDownList("Model.Applicant.Title", ViewData["PersonTitles"] as SelectList, "Select ...", 
            new { @class = "validate[required]" })%>
        <% Html.ValidationMessageFor(model => model.Applicant.Title); %>
    </div>
    <div class="inputContainer">
        <label class="above" for="Model_Applicant_Forename" accesskey="f"><span class="accesskey">F</span>orename / First name<span class="mandatoryfield">*</span></label>
        <%= Html.TextBox("Model.Applicant.Forename", Html.Encode(Model.Applicant.Forename),
                            new { @class = "validate[required,custom[onlyLetter],length[2,20]]", 
                                title="Enter your forename",
                                maxlength = 20, size = 20, autocomplete = "off",
                                  onkeypress = "return maskInput(event,re_mask_alpha);"
                            })%>
    </div>
    <div class="inputContainer">
        <label class="above" for="Model_Applicant_MiddleInitials" accesskey="i">Middle <span class="accesskey">I</span>nitial(s)</label>
        <%= Html.TextBox("Model.Applicant.MiddleInitials", Html.Encode(Model.Applicant.MiddleInitials),
                            new { @class = "validate[optional,custom[onlyLetter],length[0,8]]",
                                  title = "Please enter your middle initial(s)",
                                  maxlength = 8,
                                  size = 8,
                                  autocomplete = "off",
                                  onkeypress = "return maskInput(event,re_mask_alpha);"
                            })%>
    </div>

(My apologies if this seems verbose - trying to provide all relevant code)

I've just upgraded to VS2010, and am now having trouble trying to get a new CustomModelBinder working.

In MVC1 I would have written something like

public class AwardModelBinder: DefaultModelBinder
{
    :
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        // do the base binding to bind all simple types
        Award award = base.BindModel(controllerContext, bindingContext) as Award;

        // Get complex values from ValueProvider dictionary
        award.EffectiveFrom = Convert.ToDateTime(bindingContext.ValueProvider["Model.EffectiveFrom"].AttemptedValue.ToString());
        string sEffectiveTo = bindingContext.ValueProvider["Model.EffectiveTo"].AttemptedValue.ToString();
        if (sEffectiveTo.Length > 0)
            award.EffectiveTo = Convert.ToDateTime(bindingContext.ValueProvider["Model.EffectiveTo"].AttemptedValue.ToString());
        // etc

        return award;
    }
}

Of course I'd register the custom binder in Global.asax.cs:

    protected void Application_Start()
    {
        RegisterRoutes(RouteTable.Routes);

        // register custom model binders
        ModelBinders.Binders.Add(typeof(Voucher), new VoucherModelBinder(DaoFactory.UserInstance("EH1303")));
        ModelBinders.Binders.Add(typeof(AwardCriterion), new AwardCriterionModelBinder(DaoFactory.UserInstance("EH1303"), new VOPSDaoFactory()));
        ModelBinders.Binders.Add(typeof(SelectedVoucher), new SelectedVoucherModelBinder(DaoFactory.UserInstance("IT0706B")));
        ModelBinders.Binders.Add(typeof(Award), new AwardModelBinder(DaoFactory.UserInstance("IT0706B")));
    }

Now, in MVC2, I'm finding that my call to base.BindModel returns an object where everything is null, and I simply don't want to have to iterate all the form fields surfaced by the new ValueProvider.GetValue() function.

Google finds no matches for this error, so I assume I'm doing something wrong.

Here's my actual code:

My domain object (infer what you like about the encapsulated child objects - I know I'll need custom binders for those too, but the three "simple" fields (ie. base types) Id, TradingName and BusinessIncorporated are also coming back null):

public class Customer
{
    /// <summary>
    /// Initializes a new instance of the Customer class.
    /// </summary>
    public Customer() 
    {
        Applicant = new Person();
        Contact = new Person();
        BusinessContact = new ContactDetails();
        BankAccount = new BankAccount();
    }

    /// <summary>
    /// Gets or sets the unique customer identifier.
    /// </summary>
    public int Id { get; set; }

    /// <summary>
    /// Gets or sets the applicant details.
    /// </summary>
    public Person Applicant { get; set; }

    /// <summary>
    /// Gets or sets the customer's secondary contact.
    /// </summary>
    public Person Contact { get; set; }

    /// <summary>
    /// Gets or sets the trading name of the business.
    /// </summary>
    [Required(ErrorMessage = "Please enter your Business or Trading Name")]
    [StringLength(50, ErrorMessage = "A maximum of 50 characters is permitted")]
    public string TradingName { get; set; }

    /// <summary>
    /// Gets or sets the date the customer's business began trading.
    /// </summary>
    [Required(ErrorMessage = "You must supply the date your business started trading")]
    [DateRange("01/01/1900", "01/01/2020", ErrorMessage = "This date must be between {0} and {1}")]
    public DateTime BusinessIncorporated { get; set; }

    /// <summary>
    /// Gets or sets the contact details for the customer's business.
    /// </summary>
    public ContactDetails BusinessContact { get; set; }

    /// <summary>
    /// Gets or sets the customer's bank account details.
    /// </summary>
    public BankAccount BankAccount { get; set; }
}

My controller method:

    /// <summary>
    /// Saves a Customer object from the submitted application form.
    /// </summary>
    /// <param name="customer">A populate instance of the Customer class.</param>
    /// <returns>A partial view indicating success or failure.</returns>
    /// <httpmethod>POST</httpmethod>
    /// <url>/Customer/RegisterCustomerAccount</url>
    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult RegisterCustomerAccount(Customer customer)
    {
        if (ModelState.IsValid)
        {
            // save the Customer

            // return indication of success, or otherwise
            return PartialView();
        }
        else
        {
            ViewData.Model = customer;

            // load necessary reference data into ViewData
            ViewData["PersonTitles"] = new SelectList(ReferenceDataCache.Get("PersonTitle"), "Id", "Name");

            return PartialView("CustomerAccountRegistration", customer);
        }
    }

My custom binder:

public class CustomerModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        ValueProviderResult vpResult = bindingContext
            .ValueProvider.GetValue(bindingContext.ModelName);
        // vpResult is null

        // MVC2 - ValueProvider is now an IValueProvider, not dictionary based anymore
        if (bindingContext.ValueProvider.GetValue("Model.Applicant.Title") != null)
        {
            // works
        }

        Customer customer = base.BindModel(controllerContext, bindingContext) as Customer;
        // customer instanitated with null (etc) throughout

        return customer;
    }
}

My binder registration:

    /// <summary>
    /// Application_Start is called once when the web application is first accessed.
    /// </summary>
    protected void Application_Start()
    {
        RegisterRoutes(RouteTable.Routes);

        // register custom model binders
        ModelBinders.Binders.Add(typeof(Customer), new CustomerModelBinder());

        ReferenceDataCache.Populate();
    }

... and a snippet from my view (could this be a prefix problem?)

    <div class="inputContainer">
        <label class="above" for="Model_Applicant_Title" accesskey="t"><span class="accesskey">T</span>itle<span class="mandatoryfield">*</span></label>
        <%= Html.DropDownList("Model.Applicant.Title", ViewData["PersonTitles"] as SelectList, "Select ...", 
            new { @class = "validate[required]" })%>
        <% Html.ValidationMessageFor(model => model.Applicant.Title); %>
    </div>
    <div class="inputContainer">
        <label class="above" for="Model_Applicant_Forename" accesskey="f"><span class="accesskey">F</span>orename / First name<span class="mandatoryfield">*</span></label>
        <%= Html.TextBox("Model.Applicant.Forename", Html.Encode(Model.Applicant.Forename),
                            new { @class = "validate[required,custom[onlyLetter],length[2,20]]", 
                                title="Enter your forename",
                                maxlength = 20, size = 20, autocomplete = "off",
                                  onkeypress = "return maskInput(event,re_mask_alpha);"
                            })%>
    </div>
    <div class="inputContainer">
        <label class="above" for="Model_Applicant_MiddleInitials" accesskey="i">Middle <span class="accesskey">I</span>nitial(s)</label>
        <%= Html.TextBox("Model.Applicant.MiddleInitials", Html.Encode(Model.Applicant.MiddleInitials),
                            new { @class = "validate[optional,custom[onlyLetter],length[0,8]]",
                                  title = "Please enter your middle initial(s)",
                                  maxlength = 8,
                                  size = 8,
                                  autocomplete = "off",
                                  onkeypress = "return maskInput(event,re_mask_alpha);"
                            })%>
    </div>

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

回忆躺在深渊里 2024-09-09 10:27:49

MVC 2 中的模型绑定发生了显着变化。它充满了“陷阱”——甚至比 MVC 1 中的更多。例如,表单中的空值将使绑定失败。这些都没有详细记录。实际上,诊断这个问题的唯一好方法是 使用 MVC 源代码构建 并通过绑定进行跟踪。

我很高兴源代码可用;没有它我就会迷路。

Model binding changed significantly in MVC 2. It's full of "gotchas" -- even moreso than in MVC 1. E.g., an empty value in your form will make binding fail. None of this is well-documented. Realistically, the only good way to diagnose this stuff is to build with the MVC source code and trace through the binding.

I'm just glad the source code is available; I'd be lost without it.

清风夜微凉 2024-09-09 10:27:49

下载并使用 MVC2 RTM 源代码进行构建后(感谢 Craig 提供的链接),我能够单步执行 MVC 代码,并发现在 BindProperty 方法(位于 DefaultModelBinder.cs 的第 178 行)中,有一个测试:

protected virtual void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor) {
    // need to skip properties that aren't part of the request, else we might hit a StackOverflowException
    string fullPropertyKey = CreateSubPropertyName(bindingContext.ModelName, propertyDescriptor.Name);
    if (!bindingContext.ValueProvider.ContainsPrefix(fullPropertyKey)) {
        return;
    }
    :

... ValueProvider 字典包含带有前缀的键,该前缀本质上是自定义模型绑定程序的 BindingContext 的 ModelName 属性。

就我而言,绑定上下文.ModelName 被推断为“客户”(我猜是从我的域对象类型),因此第 181 行的测试总是失败,因此退出 BindProperty 而不绑定我的表单值。

这是我的新自定义模型活页夹代码:

public class CustomerModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        // vitally important that we set what is the prefix of values specified in view 
        // (usually "Model" if you've rendered a strongly-typed view after setting ViewData.Model)
        bindingContext.ModelName = "Model"; 
        Customer customer = base.BindModel(controllerContext, bindingContext) as Customer;

        return customer;
    }
}

我希望这对遇到类似问题的其他人有所帮助。

非常感谢克雷格的帮助。

After downloading and building with the MVC2 RTM source (thanks to Craig for that link), I was able to step through the MVC code, and discovered that in the BindProperty method (on line 178 of DefaultModelBinder.cs), there is a test:

protected virtual void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor) {
    // need to skip properties that aren't part of the request, else we might hit a StackOverflowException
    string fullPropertyKey = CreateSubPropertyName(bindingContext.ModelName, propertyDescriptor.Name);
    if (!bindingContext.ValueProvider.ContainsPrefix(fullPropertyKey)) {
        return;
    }
    :

... that the ValueProvider dictionary contains keys with the prefix that is essentially the ModelName property of the custom model binder's bindingContext.

In my case, the bindingContext.ModelName had been inferred as "customer" (from my domain object type, I guess) and hence the test at line 181 always failed, therefore exiting BindProperty without binding my form value.

Here's my new custom model binder code:

public class CustomerModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        // vitally important that we set what is the prefix of values specified in view 
        // (usually "Model" if you've rendered a strongly-typed view after setting ViewData.Model)
        bindingContext.ModelName = "Model"; 
        Customer customer = base.BindModel(controllerContext, bindingContext) as Customer;

        return customer;
    }
}

I hope this helps anyone else who's having similar issues.

Many thanks to Craig for his help.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文