5

There are some properties in my view model that are optional when saving, but required when submitting. In a word, we allow partial saving, but the whole form is submitted, we do want to make sure all required fields have values.

The only approaches I can think of at this moment are:

Manipulate the ModelState errors collection.

The view model has all [Required] attributes in place. If the request is partial save, the ModelState.IsValid becomes false when entering the controller action. Then I run through all ModelState (which is an ICollection<KeyValuePair<string, ModelState>>) errors and remove all errors raised by [Required] properties.

But if the request is to submit the whole form, I will not interfere with the ModelState and the [Required] attributes take effect.

Use different view models for partial save and submit

This one is even more ugly. One view model will contain all the [Required] attributes, used by an action method for submitting. But for partial save, I post the form data to a different action which use a same view model without all the [Required] attributes.

Obviously, I would end up with a lot of duplicate code / view models.

The ideal solution

I have been thinking if I can create a custom data annotation attribute [SubmitRequired] for those required properties. And somehow make the validation ignores it when partial saving but not when submitting.

Still couldn't have a clear clue. Anyone can help? Thanks.

5
  • 3
    You could make use of a foolproof [RequiredIfTrue] or similar attribute (based on an additional bool SubmitRequired property in your view model. If the value is false then no validation will be performed on the properties decorated with [RequiredIfTrue "SubmitRequired"], otherwise the properties will be validated. Commented Jun 17, 2015 at 12:46
  • Thanks, Stephen. That package is a beta with a last update in 2012. But this RequiredIf did point to the right direction. Really appreciate. Commented Jun 17, 2015 at 12:55
  • Let me attach the link to the code I would use: foolproof.codeplex.com/SourceControl/latest#Foolproof/… Commented Jun 17, 2015 at 12:56
  • They are identical :) Commented Jun 17, 2015 at 13:04
  • It's not necessary to use foolproof library. RequiredIf is a pretty common attribute and you can find some code for it here or here. Commented Jun 17, 2015 at 13:07

3 Answers 3

2

This is one approach I use in projects.

Create a ValidationService<T> containing the business logic that will check that your model is in a valid state to be submitted with a IsValidForSubmission method.

Add an IsSubmitting property to the view model which you check before calling the IsValidForSubmission method.

Only use the built in validation attributes for checking for invalid data i.e. field lengths etc.

Create some custom attributes within a different namespace that would validate in certain scenarios i.e. [RequiredIfSubmitting] and then use reflection within your service to iterate over the attributes on each property and call their IsValid method manually (skipping any that are not within your namespace).

This will populate and return a Dictionary<string, string> which can be used to populate ModelState back to the UI:

var validationErrors = _validationService.IsValidForSubmission(model);

if (validationErrors.Count > 0)
{
    foreach (var error in validationErrors)
    {
        ModelState.AddModelError(error.Key, error.Value);
    }
}
Sign up to request clarification or add additional context in comments.

Comments

1

I think there is more precise solution for your problem. Lets say you're submitting to one method, I mean to say you are calling same method for Partial and Full submit. Then you should do like below:

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult YourMethod(ModelName model)
        {
          if(partialSave) // Check here whether it's a partial or full submit
          {
            ModelState.Remove("PropertyName");
            ModelState.Remove("PropertyName2");
            ModelState.Remove("PropertyName3");
          }

          if (ModelState.IsValid)
          {
          }
        }

This should solve your problem. Let me know if you face any trouble.

Edit:

As @SBirthare commented that its not feasible to add or remove properties when model get updated, I found below solution which should work for [Required] attribute.

 ModelState.Where(x => x.Value.Errors.Count > 0).Select(d => d.Key).ToList().ForEach(g => ModelState.Remove(g));

Above code will get all keys which would have error and remove them from model state. You need to place this line inside if condition to make sure it runs in partial form submit. I have also checked that error will come for [Required] attribute only (Somehow model binder giving high priority to this attribute even you place it after/below any other attribute). So you don't need to worry about model updates anymore.

3 Comments

This is actually the first solution I proposed - manipulating ModelState in the controller action. But thanks for the input.
Your proposed solution is not cost effective dear. You proposed to loop through all the properties. While I am proposing to remove those which are required but not at the time of partial post.
While this may be a simple solution and work well in cases when you have few properties, it's not elegant. No offense. It just breaks OCP for one i.e. a new property added into model, you have to come here and add into the list.
0

My approach is to add conditional checking annotation attribute, which is learned from foolproof.

Make SaveMode part of the view model.

Mark the properties nullable so that the values of which are optional when SaveMode is not Finalize.

But add a custom annotation attribute [FinalizeRequired]:

[FinalizeRequired]
public int? SomeProperty { get; set; }

[FinalizeRequiredCollection]
public List<Item> Items { get; set; }

Here is the code for the Attribute:

[AttributeUsage(AttributeTargets.Property)]
public abstract class FinalizeValidationAttribute : ValidationAttribute
{
    public const string DependentProperty = "SaveMode";

    protected abstract bool IsNotNull(object value);

    protected static SaveModeEnum GetSaveMode(ValidationContext validationContext)
    {
        var saveModeProperty = validationContext.ObjectType.GetProperty(DependentProperty);

        if (saveModeProperty == null) return SaveModeEnum.Save;

        return (SaveModeEnum) saveModeProperty.GetValue(validationContext.ObjectInstance);
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var saveMode = GetSaveMode(validationContext);

        if (saveMode != SaveModeEnum.SaveFinalize) return ValidationResult.Success;

        return (IsNotNull(value))
            ? ValidationResult.Success
            : new ValidationResult(string.Format("{0} is required when finalizing", validationContext.DisplayName));
    }
}

For primitive data types, check value!=null:

[AttributeUsage(AttributeTargets.Property)]
public class FinalizeRequiredAttribute : FinalizeValidationAttribute
{
    protected override bool IsNotNull(object value)
    {
        return value != null;
    }
}

For IEnumerable collections,

[AttributeUsage(AttributeTargets.Property)]
public  class FinalizeRequiredCollectionAttribute : FinalizeValidationAttribute
{
    protected override bool IsNotNull(object value)
    {
        var enumerable = value as IEnumerable;
        return (enumerable != null && enumerable.GetEnumerator().MoveNext());
    }
}

This approach best achieves the separation of concerns by removing validation logic out of controller. Data Annotation attributes should handle that kind of work, which controller just need a check of !ModelState.IsValid. This is especially useful in my application, because I would not be able to refactor into a base controller if ModelState check is different in each controller.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.