【问题标题】:ASP.NET MVC model binding and validation questionASP.NET MVC 模型绑定和验证问题
【发布时间】:2013-12-20 23:39:26
【问题描述】:

在了解了所有示例和教程等之后,我正在尝试将 MVC 用于一个新项目。但是,我很难确定某些事情应该在哪里发生。

例如,我有一个名为 Profile 的实体。此实体包含正常的配置文件类型内容以及 DateTime 类型的 DateOfBirth 属性。在 HTML 表单中,出生日期字段分为 3 个字段。现在,我知道我可以使用自定义模型绑定器来处理这个问题,但是如果输入的日期不是有效日期怎么办?我应该在模型活页夹中检查吗?我的所有验证都应该放在模型活页夹中吗?是否可以只在模型绑定器中验证少数内容,而在控制器或模型本身中验证其余内容?

这是我现在拥有的代码,但它对我来说看起来不正确。看起来又脏又臭。

namespace WebSite.Models
{
    public class ProfileModelBinder : IModelBinder
    {
        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            DateTime birthDate;

            var form = controllerContext.HttpContext.Request.Form;
            var state = controllerContext.Controller.ViewData.ModelState;

            var profile = new Profile();
            profile.FirstName = form["FirstName"];
            profile.LastName = form["LastName"];
            profile.Address = form["Address"];
            profile.Address2 = form["Address2"];
            profile.City = form["City"];
            profile.State = form["State"];
            profile.Zip = form["Zip"];
            profile.Phone = form["Phone"];
            profile.Email = form["Email"];
            profile.Created = DateTime.UtcNow;
            profile.IpAddress = controllerContext.HttpContext.Request.UserHostAddress;

            var dateTemp = string.Format("{0}/{1}/{2}",
                form["BirthMonth"], form["BirthDay"], form["BirthYear"]);

            if (string.IsNullOrEmpty(dateTemp))
                state.AddModelError("BirthDate", "Required");
            else if (!DateTime.TryParse(dateTemp, out birthDate))
                state.AddModelError("BirthDate", "Invalid");
            else
                profile.BirthDate = birthDate;

            return profile;
        }        
    }
}

基于上面的示例代码,您将如何为 3 部分字段生成验证消息?在上述情况下,我使用了一个完全独立的键,它实际上并不对应于表单中的字段,因为我不希望在所有 3 个字段旁边出现错误消息。我只希望它出现在 Year 字段的右侧。

【问题讨论】:

  • 您在寻找模型验证还是表单验证?我会推荐两者。通过这种方式,您可以定制模型并在前端提供丰富的 UI。

标签: asp.net asp.net-mvc


【解决方案1】:

我认为在模型绑定器中进行验证是合理的。正如 Craig 指出的那样,验证主要是您的业务领域的属性,但是:

  1. 有时您的模型只是一个愚蠢的演示模型,而不是业务对象
  2. 您可以使用多种机制将验证知识呈现到模型绑定器中。

Thomas 为您提供了#1 的示例。

#2 的一个示例是,当您使用属性(如 DataAnnotation 属性 [必需])以声明方式描述验证知识,或将一些业务层验证服务注入自定义模型绑定器时。在这些情况下,模型绑定器是进行验证的理想场所。

话虽如此,模型绑定(查找、转换和混洗数据到对象)和验证(数据符合我们的规范)是两个独立的问题。您可能会争辩说它们应该是单独的阶段/组件/可扩展点,但我们拥有我们所拥有的,尽管 DefaultModelBinder 对这两个职责做了一些区分。如果您只想为特定类型的对象提供一些验证,您可以从 DefaultModelBinder 派生并覆盖 OnPropertyValidating 方法以进行属性级别验证或 OnModelUpdated 如果您需要整体视图。

这是我现在拥有的代码,但它 只是在我看来不合适。似乎 脏或臭。

对于您的特定代码,我会尝试仅为 DateTime 编写模型绑定器。默认模型绑定器可以处理绑定名字、姓氏等,并在它到达 Profile 上的 DateTime 属性时委托给您的自定义模型绑定器。另外,尝试在 bindingContext 中使用 valueProvider,而不是直接进入表单。这些东西可以给你更多的灵活性。

更多想法在这里:6 Tips for ASP.NET MVC Model Binding

【讨论】:

  • 这应该适用于服务器端验证(当表单发布时),但是客户端呢?我的情况与 OP 相同,但无法弄清楚如何将新规则插入客户端验证引擎(而不是进行三个验证 - 每个字段一个)。想法?
  • 是的,扩展客户端验证有点复杂,但可能。我会考虑构建一个 UI,使用户很难输入无效值(可能使用微调器或滑块),然后仅在服务器上捕获边缘情况。
【解决方案2】:

有时模型是视图模型,而不是域模型。在这种情况下,您可以受益于将这两者分开并设计视图模型以匹配您的视图。

现在您可以让视图模型验证输入并将三个字段解析为DateTime。然后它可以更新领域模型:

public ActionResult SomeAction(ViewModel vm)
{
    if (vm.IsValid)
    {
        var dm = repositoryOrSomething.GetDomainModel();
        vm.Update(dm);
    }

    // more code...
}

【讨论】:

    【解决方案3】:

    前几天我也遇到了同样的情况……下面是我的模型绑定代码。基本上它绑定了所有的日期时间?模型的字段到表单的月/日/年字段(如果可能)所以,是的,我确实在此处添加了验证,因为这样做似乎很合适。

    public class DateModelBinder : DefaultModelBinder  
        {
    
            protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor)
            {
    
                if (propertyDescriptor.PropertyType == typeof(DateTime?))
                {
                    string DateMonth = _GetDateValue(bindingContext, propertyDescriptor.Name + "Month");
                    string DateDay = _GetDateValue(bindingContext, propertyDescriptor.Name + "Day");
                    string DateYear = _GetDateValue(bindingContext, propertyDescriptor.Name + "Year");
                    // Try to parse the date if we have at least a month, day or year
                    if (!String.IsNullOrEmpty(DateMonth) || !String.IsNullOrEmpty(DateDay) || !String.IsNullOrEmpty(DateYear))
                    {
                        DateTime fullDate;
                        CultureInfo enUS = new CultureInfo("en-US");
                        // If we can parse it, set the model property
                        if (DateTime.TryParse(DateMonth + "/" + DateDay + "/" + DateYear,
                                             enUS,
                                             DateTimeStyles.None, out fullDate))
                        {
                            SetProperty(controllerContext, bindingContext, propertyDescriptor, (DateTime?)fullDate);
                        }
                        // The date is invalid, so we need to add a model error
                        else
                        {
                            string ModelPropertyName = bindingContext.ModelName;
                            if(ModelPropertyName != "")
                            {
                                ModelPropertyName += ".";
                            }
                            ModelPropertyName += propertyDescriptor.Name;
                            bindingContext.ModelState.AddModelError(ModelPropertyName, "Invalid date supplied for " + propertyDescriptor.Name);
                        }
                    }
                    return;
                }
                base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
            }
    
            // Get a property from binding context
            private string _GetDateValue(ModelBindingContext bindingContext, string key)
            {
                ValueProviderResult valueResult;
                bindingContext.ValueProvider.TryGetValue(bindingContext.ModelName + "." + key, out valueResult);
                //Didn't work? Try without the prefix if needed...  
                // && bindingContext.FallbackToEmptyPrefix == true
                if (valueResult == null)
                {
                    bindingContext.ValueProvider.TryGetValue(key, out valueResult);
                }
                if (valueResult == null)
                {
                    return null;
                }
                return (string)valueResult.ConvertTo(typeof(string));
            }
    
        }
    

    注意:我遇到了一些问题,bindingContext.FallbackToEmptyPrefix 总是错误的......找不到任何有用的信息,但你明白了。

    【讨论】:

    【解决方案4】:

    应根据每个地方的功能在多个地方进行验证。例如,如果您的模型绑定器无法将提交的值找到正确的 DateTime 值,则绑定器可以添加模型状态错误。另一方面,如果您的业务逻辑要求日期在一定范围内,则不适合这样做并且模型绑定器;它应该在业务逻辑层。例如,如果无法将编辑模型转换为实体模型,控制器也可能会添加验证错误。

    诸如 xVal 之类的验证框架使这变得更加简单。

    【讨论】:

      【解决方案5】:

      http://www.asp.net/mvc 站点上的 Contact Manager 示例应用程序很好地描述了将验证逻辑从控制器和模型中分离到服务层中。

      好好读书

      【讨论】:

      • 是的,我看过这个应用程序。这对我提出的特定问题没有任何帮助。我的问题是模型包含一个 DateTime 字段。但是,表单将其表示为 3 个文本框。因此,我必须在模型绑定之前进行某种形式的验证,并且我正在尝试找出最好的方法。
      • 看看这篇文章:hanselman.com/blog/…
      【解决方案6】:

      我厌倦了创建只涉及我一英里范围内的部分领域模型的小型、小型 ViewModel。

      因此,我制定了自己的方法来解决这个问题。我的 ViewModel 是一个 typeOf DomainModel,我使用 custom model binder 来确保首先加载它的身份属性 - 一旦设置了身份 - 它会触发 DomainModel.Load,并且绑定活动的其余部分基本上执行“合并”。

      再次,当我的 ViewModel 被绑定时(例如,在表单 POST 上),在设置了包含 ID 的基本字段之后 - 它会立即从数据库加载域模型。我只需要想出一个替代 DefaultModelBinder 的方法。我的自定义模型绑定器posted here on StackOverflow 允许您控制属性的绑定顺序。

      一旦我可以保证身份属性被绑定,(我的视图模型的内部监听身份设置器的完成)我触发我的域模型的加载,因为其余的属性被绑定,它们被覆盖,即“合并”到加载的域模型中。

      基本上,我可以拥有所有各种剃须刀视图,无论它们是公开 5 个表单字段还是模型的 50 个字段。所有这些都提交给一个看起来像这样的控制器操作(当然,我仍然在需要的地方进行单独的操作适当的自定义业务内容.. 但重点是,我的控制器操作集中且简洁)

      <HttpPost()>
      <Authorize(Roles:="MYCOMPANY\activeDirRoleForEditing")>
      Function Edit(<Http.FromBody()> ByVal mergedModel As OrderModel) As ActionResult
          'notice: NO loading logic here - it already happened during model binding
          'just do the right thing based upon resulting model state
          If Me.ModelState.IsValid Then
      
              mergedModel.SaveAndReload("MyServiceWebConfigKey")
      
              ViewBag.SuccessMessage = String.Format("You have successfully edited the order {0}", mergedModel.Id)
      
              Return View("Edit", mergedModel)
          Else
              ViewBag.ErrorText = String.Format("Order {0} not saved. Check for errors and correct.", mergedModel.Id)
              Return View("Edit", mergedModel)
          End If
      End Function
      

      【讨论】:

        猜你喜欢
        • 2013-12-16
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-03-08
        • 1970-01-01
        • 1970-01-01
        • 2011-04-27
        相关资源
        最近更新 更多