ASP.NET MVC - 复杂模型验证

时间:2022-09-18 08:19:47

I have a ViewModel class like this:

我有一个像这样的ViewModel类:

class CaseModel {
    public Boolean     ClientPresent { get; set; }
    public ClientModel Client        { get; set; }
}

class ClientModel {
    [Required]
    public String      FirstName     { get; set; }
    [Required]
    public String      LastName      { get; set; }
}

The view page consists of a <input type="checkbox" name="ClientPresent" /> and a Html.EditorFor( m => m.Client ) partial view.

视图页面由和Html.EditorFor(m => m.Client)局部视图组成。

The idea being that when the user if providing information about a case (a business-domain object) that they can choose to not specify any information about the client (another biz object) by unchecking the ClientPresent box.

这个想法是当用户提供关于案例(业务域对象)的信息时,他们可以通过取消选中ClientPresent框来选择不指定有关客户端的任何信息(另一个商业对象)。

I want ASP.NET MVC to not perform any validation of the child ClientModel object - however the CaseModel.Client property is automatically populated when a form is POSTed back to the server, but because FirstName and LastName aren't (necessarily) provided by the user it means it fails the [Required] validation attributes, consequently ViewData.ModelState.IsValid returns false and the user gets a validation error message.

我希望ASP.NET MVC不对子ClientModel对象执行任何验证 - 但是当表单POST回服务器时会自动填充CaseModel.Client属性,但是因为FirstName和LastName不是(必然)由user表示它未通过[Required]验证属性,因此ViewData.ModelState.IsValid返回false并且用户收到验证错误消息。

How can I get it so CaseModel.Client will not be validated if CaseModel.ClientPresent is false?

我怎样才能得到它,如果CaseModel.ClientPresent为false,则不会验证CaseModel.Client?

Note that ClientModel is a fully independent ViewModel class and is used elsewhere in the application (such as in the ClientController class which lets the user edit individual instances of Clients).

请注意,ClientModel是一个完全独立的ViewModel类,在应用程序的其他地方使用(例如在ClientController类中,允许用户编辑客户端的各个实例)。

2 个解决方案

#1


3  

I recognise that my problem is not to do with binding but actually with validation: by keeping the values it means the same form fields will be populated when the user reloads the page, I just needed the validation messages to be discarded as they weren't applicable.

我认识到我的问题不是与绑定有关,而是与验证有关:通过保持值意味着当用户重新加载页面时将填充相同的表单字段,我只需要丢弃验证消息,因为它们不是适用。

To that end I realised I can perform the model property validation, but then use some custom logic to remove the validation messages. Here's something similar to what I did:

为此,我意识到我可以执行模型属性验证,但随后使用一些自定义逻辑来删除验证消息。这与我的做法类似:

public class CaseModel {
    public void CleanValidation(ModelStateDictionary dict) {
        if( this.ClientPresent ) {
            dict.Keys.All( k => if( k.StartsWith("Client") dict[k].Errors.Clear() );
        }
    }
}

(Obviously my actual code is more robust, but you get the general idea)

(显然我的实际代码更健壮,但你得到了一般的想法)

The CleanValidation method is called directly by the controller's action method:

CleanValidation方法由控制器的action方法直接调用:

public void Edit(Int64 id, CaseModel model) {
    model.CleanValidation( this.ModelState );
}

I can probably tidy this up by adding CleanValidation as a method to a new interface IComplexModel and having a new model binder automatically call this method so the controller doesn't need to call it itself.

我可以通过将CleanValidation作为方法添加到新接口IComplexModel并让新模型绑定器自动调用此方法以使控制器不需要自己调用它来整理它。

Update:

I have this interface which is applied to any ViewModel that requires complicated validation:

我有这个接口,适用于任何需要复杂验证的ViewModel:

public interface ICustomValidation {

    void Validate(ModelStateDictionary dict);
}

In my original example, CaseModel now looks like this:

在我的原始示例中,CaseModel现在看起来像这样:

 public class CaseClientModel : ICustomValidation {

      public Boolean ClientIsNew { get; set; } // bound to a radio-button
      public ClientModel ExistingClient { get; set; } // a complex viewmodel used by a partial view
      public ClientModel NewClient { get; set; } // ditto

      public void Validate(ModelStateDictionary dict) {

          // RemoveElementsWithPrefix is an extension method that removes all key/value pairs from a dictionary if the key has the specified prefix.
          if( this.ClientIsNew ) dict.RemoveElementsWithPrefix("ExistingClient");
          else                   dict.RemoveElementsWithPrefix("NewClient");
      }
 }

The validation logic is invoked by OnActionExecuting in my common BaseController class:

验证逻辑由OnActionExecuting在我的公共BaseController类中调用:

protected override void OnActionExecuting(ActionExecutingContext filterContext) {
    base.OnActionExecuting(filterContext);
    if( filterContext.ActionParameters.ContainsKey("model") ) {

        Object                    model = filterContext.ActionParameters["model"];
        ModelStateDictionary modelState = filterContext.Controller.ViewData.ModelState; // ViewData.Model always returns null at this point, so do this to get the ModelState.

        ICustomValidation modelValidation = model as ICustomValidation;
        if( modelValidation != null ) {
            modelValidation.Validate( modelState );
        }
    }
}

#2


2  

You have to create a custom model binder by inheriting from the default model binder.

您必须通过继承默认模型绑定器来创建自定义模型绑定器。

  public class CustomModelBinder: DefaultModelBinder
  {
    protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor)
    {
      if (propertyDescriptor.Name == "Client")
      {
          var clientPresent = bindingContext.ValueProvider.GetValue("ClientPresent");

          if (clientPresent == null || 
                string.IsNullOrEmpty(clientPresent.AttemptedValue))
              return;
      }

      base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
    }
  }

Global.asax.cs

ModelBinders.Binders.Add(typeof(CaseModel), new CustomModelBinder());

#1


3  

I recognise that my problem is not to do with binding but actually with validation: by keeping the values it means the same form fields will be populated when the user reloads the page, I just needed the validation messages to be discarded as they weren't applicable.

我认识到我的问题不是与绑定有关,而是与验证有关:通过保持值意味着当用户重新加载页面时将填充相同的表单字段,我只需要丢弃验证消息,因为它们不是适用。

To that end I realised I can perform the model property validation, but then use some custom logic to remove the validation messages. Here's something similar to what I did:

为此,我意识到我可以执行模型属性验证,但随后使用一些自定义逻辑来删除验证消息。这与我的做法类似:

public class CaseModel {
    public void CleanValidation(ModelStateDictionary dict) {
        if( this.ClientPresent ) {
            dict.Keys.All( k => if( k.StartsWith("Client") dict[k].Errors.Clear() );
        }
    }
}

(Obviously my actual code is more robust, but you get the general idea)

(显然我的实际代码更健壮,但你得到了一般的想法)

The CleanValidation method is called directly by the controller's action method:

CleanValidation方法由控制器的action方法直接调用:

public void Edit(Int64 id, CaseModel model) {
    model.CleanValidation( this.ModelState );
}

I can probably tidy this up by adding CleanValidation as a method to a new interface IComplexModel and having a new model binder automatically call this method so the controller doesn't need to call it itself.

我可以通过将CleanValidation作为方法添加到新接口IComplexModel并让新模型绑定器自动调用此方法以使控制器不需要自己调用它来整理它。

Update:

I have this interface which is applied to any ViewModel that requires complicated validation:

我有这个接口,适用于任何需要复杂验证的ViewModel:

public interface ICustomValidation {

    void Validate(ModelStateDictionary dict);
}

In my original example, CaseModel now looks like this:

在我的原始示例中,CaseModel现在看起来像这样:

 public class CaseClientModel : ICustomValidation {

      public Boolean ClientIsNew { get; set; } // bound to a radio-button
      public ClientModel ExistingClient { get; set; } // a complex viewmodel used by a partial view
      public ClientModel NewClient { get; set; } // ditto

      public void Validate(ModelStateDictionary dict) {

          // RemoveElementsWithPrefix is an extension method that removes all key/value pairs from a dictionary if the key has the specified prefix.
          if( this.ClientIsNew ) dict.RemoveElementsWithPrefix("ExistingClient");
          else                   dict.RemoveElementsWithPrefix("NewClient");
      }
 }

The validation logic is invoked by OnActionExecuting in my common BaseController class:

验证逻辑由OnActionExecuting在我的公共BaseController类中调用:

protected override void OnActionExecuting(ActionExecutingContext filterContext) {
    base.OnActionExecuting(filterContext);
    if( filterContext.ActionParameters.ContainsKey("model") ) {

        Object                    model = filterContext.ActionParameters["model"];
        ModelStateDictionary modelState = filterContext.Controller.ViewData.ModelState; // ViewData.Model always returns null at this point, so do this to get the ModelState.

        ICustomValidation modelValidation = model as ICustomValidation;
        if( modelValidation != null ) {
            modelValidation.Validate( modelState );
        }
    }
}

#2


2  

You have to create a custom model binder by inheriting from the default model binder.

您必须通过继承默认模型绑定器来创建自定义模型绑定器。

  public class CustomModelBinder: DefaultModelBinder
  {
    protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor)
    {
      if (propertyDescriptor.Name == "Client")
      {
          var clientPresent = bindingContext.ValueProvider.GetValue("ClientPresent");

          if (clientPresent == null || 
                string.IsNullOrEmpty(clientPresent.AttemptedValue))
              return;
      }

      base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
    }
  }

Global.asax.cs

ModelBinders.Binders.Add(typeof(CaseModel), new CustomModelBinder());