ASP.NET MVC模型绑定和验证问题

时间:2009-04-30 21:57:06

标签: asp.net asp.net-mvc

我正在尝试将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个字段旁边显示错误消息。我只希望它出现在年度字段的右侧。

6 个答案:

答案 0 :(得分:5)

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

  1. 有时您的模型只是一个愚蠢的演示模型,而不是业务对象
  2. 您可以使用各种机制将验证知识表示到模型绑定器中。
  3. 托马斯给你一个#1的例子。

    #2的一个例子是当您使用属性(如DataAnnotation属性[Required])声明性地描述验证知识时,或者将一些业务层验证服务注入自定义模型绑定器。在这些情况下,模型装订器是处理验证的理想场所。

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

      

    这是我现在的代码,但它   只是看起来不对我。似乎   肮脏或臭。

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

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

答案 1 :(得分:4)

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

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

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

    // more code...
}

答案 2 :(得分:2)

前几天我的情况完全一样......下面是我的模型绑定代码。基本上它绑定所有DateTime?模型的字段到表单中的月/日/年字段(如果可能的话)所以,是的,我确实在这里添加了验证,因为这样做似乎是合适的。

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总是假的时候遇到了一些问题...找不到任何有用的信息,但你明白了。

答案 3 :(得分:1)

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

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

答案 4 :(得分:0)

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

这是一个很好的阅读

答案 5 :(得分:0)

我厌倦了创建一些只能触及英里范围内部分模型的小型ViewModel。

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

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

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

基本上,我可以拥有所有不同的剃刀视图,无论是暴露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