Is it correct way to use ModelState.Remove to deal

2019-01-17 21:14发布

问题:

Im working on a big MVC3 web application and have an annoyance regarding the ModelState.IsValid method.

ModelState is being used in nearly all of my controllers so to validate the data being posted. The views are all based on ViewModels which contain different classes and these classes obviously contain properties which could be marked as [Required].

The problem i am having is the required properties are sometimes not required and im having to use the ModelState.Remove method so that ModelState.IsValid becomes true.

My question is by using ModelState.Remove, is this the correct way of doing things or is there a more efficient approach.

回答1:

If you're using the same view model with a [Required] property in two different contexts, one where the property is required and one where it isn't, then you'll need to manually alter the ModelState as you're doing.

An alternative is to use a different view model. Perhaps have a base class with all properties except the required property in question. Then derive two view models from it, one with the property where is it required and one with the property where it is not (it's duplication, I know). You may decide to keep them entirely separate altogether and not use inheritance.



回答2:

Here's my solution - a RemoveFor() extension method on ModelState, modelled after MVC HTML helpers:

    public static void RemoveFor<TModel>(this ModelStateDictionary modelState, 
                                         Expression<Func<TModel, object>> expression)
    {
        string expressionText = ExpressionHelper.GetExpressionText(expression);

        foreach (var ms in modelState.ToArray())
        {
            if (ms.Key.StartsWith(expressionText + ".") || ms.Key == expressionText)
            {
                modelState.Remove(ms);
            }
        }
    }

Here's how it's used :

if (model.CheckoutModel.ShipToBillingAddress == true) 
{
    // REUSE BILLING ADDRESS FOR SHIPPING ADDRESS
    ShoppingCart.ShippingAddress = ShoppingCart.BillingAddress;

    // REMOVE MODELSTATE ERRORS FOR SHIPPING ADDRESS
    ModelState.RemoveFor<SinglePageStoreModel>(x => model.CheckoutModel.ShippingAddress);
}

So in answer to your question I believe there are definitely use-cases where this is the right way to do it, and a strongly typed helper like this makes it much nicer to look at - and easier to justify if you're concerned about lots of magic strings.



回答3:

Fundamentally, your issue is that while your classes are decorated with [Required], it's not always true. If you're operating in a context where it isn't true, you should really be using a class that doesn't define the property as being [Required].

You should really use a ViewModel that is correctly defined for its specific usage and that may mean duplicating some classes. A ViewModel is associated with the implementation of the UI and while it may use classes from your domain model, it's not always the right thing to do.

Failing that, the options are to either not use ModelState.IsValid, or to continue to use ModelState.Remove.

But logically, it makes sense for your ViewModel to be 'validatable', not to have to ignore certain validation errors.



回答4:

I'm totally with Mr.Steve Morgan

So if your ViewModel doesn't always need some property to be Required then you shouldn't decorate it as Required.

I don't know why you wanna this issue but I suppose that in some cases you need PropertyOne to be Required if PropertyTwo has value. In this case you may need to make your CustomValidationAttribute to check these two properties.

I'm using something like this :

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public class PropertyNeededAttribute : ValidationAttribute
{
    private const string defaultErrorMessage = "'{0}' needs '{1}' to be valid.";

    public PropertyNeededAttribute(string originalProperty, string neededProperty)
        : base(defaultErrorMessage)
    {
        NeededProperty = neededProperty;
        OriginalProperty = originalProperty;
    }

    public string NeededProperty { get; private set; }
    public string OriginalProperty { get; private set; }

    public override object TypeId
    {
        get { return new object(); }
    }

    public override string FormatErrorMessage(string name)
    {
        return String.Format(CultureInfo.CurrentUICulture, ErrorMessageString,
                             OriginalProperty, NeededProperty);
    }

    public override bool IsValid(object value)
    {
        object neededValue = Statics.GetPropertyValue(value, NeededProperty);
        object originalValue = Statics.GetPropertyValue(value, OriginalProperty);
        if (originalValue != null && neededValue == null)
            return false;
        return true;
    }
}

note: Statics.GetPropertyValue(...) do nothing but get the value from the property to compare it.

Hope this helped :)



回答5:

If your property is not always required you should not decorate it with [Required].

A good alternative to do your validation is to implement the interface IValidatableObject.

For example, let's say that you want to make the field State required only when the country is United States. You could do it that way :

public class AddressModel : IValidatableObject
{
    [Required]
    public string Country { get; set; }
    public string State { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if(Country == "United States" && String.IsNullOrEmpty(State))
        {
            yield return new ValidationResult("State is required for United States", new [] { nameof(State) });
        }
    }
}

Note : This kind of validation only works on the server side.

Other alternatives?

As mentioned in other answers, it's sometimes a good idea to create 2 or more models if the views and validations are very different.