代码之家  ›  专栏  ›  技术社区  ›  Chris

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

  •  9
  • Chris  · 技术社区  · 16 年前

    我试着在一个新的项目中使用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 回复  |  直到 16 年前
        1
  •  5
  •   OdeToCode    16 年前

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

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

    托马斯给了你一个1的例子。

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

    也就是说,模型绑定(查找、转换和将数据重组为对象)和验证(数据符合我们的规范)是两个独立的关注点。您可以认为它们应该是独立的阶段/组件/可扩展性点,但是我们有我们所拥有的,尽管DefaultModelBinder在这两个职责之间做了一些区分。如果您只想为特定类型的对象提供一些验证,则可以从DefaultModelBinder派生,并为属性级验证重写OnPropertyValidating方法,或者在需要整体视图时重写OnModelUpdated。

    这是我现在的密码,但它 我就是觉得不对劲。似乎 肮脏或有异味。

    对于您的特定代码,我将尝试只为datetime编写一个模型绑定器。默认模型绑定器可以负责绑定firstname、lastname等,并在自定义模型绑定器到达概要文件上的datetime属性时委托给它。此外,尝试在bindingContext中使用valueProvider,而不是直接转到表单。这些东西可以给你更多的灵活性。

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

        2
  •  4
  •   Thomas Eyde    16 年前

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

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

    public ActionResult SomeAction(ViewModel vm)
    {
        if (vm.IsValid)
        {
            var dm = repositoryOrSomething.GetDomainModel();
            vm.Update(dm);
        }
    
        // more code...
    }
    
        3
  •  2
  •   John    16 年前

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

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

    注意:我在绑定Context.FallbackToEmptyPrefix时遇到了一些问题,它总是错误的……找不到任何有用的信息,但是您得到了这个想法。

        4
  •  1
  •   Craig Stuntz    16 年前

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

    像XVAL这样的验证框架使这个过程简单得多。

        5
  •  0
  •   datacop    16 年前

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

    很好,读一读

        6
  •  0
  •   Community CDub    8 年前

    我厌倦了创建一些小用途的视图模型,这些模型只涉及我的领域模型的一部分。

    因此,我想出了自己的方法来解决这个问题。我的viewModel是一种类型的域模型,我使用 custom model binder 为了确保其标识属性首先加载-一旦设置了标识,它将触发DomainModel.Load,而绑定活动的其余部分基本上执行“合并”。

    同样,当我的viewModel被绑定(例如在表单帖子上)时,在设置了包含ID的基本字段之后,它会立即从数据库加载域模型。我只需要为defaultmodelbinder设计一个替换程序。我的自定义模型活页夹 posted here on StackOverflow 允许您控制属性的绑定顺序。

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

    基本上,我可以拥有所有不同的Razor视图,无论它们公开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
    
    推荐文章