27

I have a large model (large I mean model class contains a lot of fields/properties and each has at least one validation attribute (such as Required, MaxLength, MinLength etc)). Instead of creating one view with a lot of inputs for user to fill model with data I want to create several views where user will fill part of model fields and go to the next step (some kind of "wizard"). While redirecting between steps I store not fullfilled model object in Session. Something like below:

Model:

public class ModelClass
{
    [MaxLength(100)] ...
    public string Prop1{get;set;}
    [MaxLength(100)] ...
    public string Prop2{get;set;}
    ...
    [Required][MaxLength(100)] ...
    public string Prop20{get;set;}
}

Controller:

[HttpPost]
public ActionResult Step1(ModelClass postedModel)
{    
    // user posts only for example Prop1 and Prop2
    // so while submit I have completly emty model object
    // but with filled Prop1 and Prop2
    // I pass those two values to Session["model"]
    var originalModel = Session["model"] as ModelClass ?? new ModelClass();
    originalModel.Prop1 = postedModel.Prop1;
    originalModel.Prop2 = postedModel.Prop2;
    Session["model"] = originalModel;

    // and return next step view
    return View("Step2");
}

[HttpPost]
public ActionResult Step2(ModelClass postedModel)
{
    // Analogically the same
    // I have posted only Prop3 and Prop4

    var originalModel = Session["model"] as ModelClass;
    if (originalModel!=null)
    {
        originalModel.Prop3 = postedModel.Prop3;
        originalModel.Prop4 = postedModel.Prop4;
        Session["model"] = originalModel;

        // return next step view
        return View("Step3");
    }
    return View("SomeErrorViewIfSessionBrokesSomeHow")
}

Step1 view has inputs only for Prop1 and Prop2, Step2 view contains inputs for Prop3 and Prop4 etc.

BUT HERE IS THE THING

When user is on, for example, step 1, and fills Prop1 with value more than 100 characters length client side validation works fine. But, of course , I have to validate this value and on the server side in controller. If I had full model I'd just do the following:

if(!ModelState.IsValid) return View("the same view with the same model object");

so user has to fill the form again and correct. BUT on step 1 user has filled only 2 properties of 20, and I need to validate them. I can't use ModelState.IsValid because model state will be invalid. As You can see Prop20 is marked with [Required] attribute, when user submits Prop1 and Prop2, Prop20 is null and that's why ModelState is invalid. Of course I could allow user to go to step2, fill all of the steps and validate model state only on the last step but I don't want to allow user to go to step 2 if he filled step 1 incorrect. And I want this validation in controller. So the question is: How can I validate only part of the model? How can I verify that only some of the model properties match their validation attributes?

3
  • 8
    You could create a different view model for each step which could be a seperate view. Like ProductStep1ViewModel and ProductStep1View, but I would name them better than that. Commented Jan 11, 2013 at 15:06
  • @NickBray, ModelClass is view model class, mapped from original model, so if I create ModelClass as composition of other particular step classes I will have to add about 40 mapping rules for mapper to map those model classes, and it will be a bunch of code. I've already thought about that, but thanks for advice. Commented Jan 11, 2013 at 15:10
  • 1
    @DmytroTsiniavsky You could use something like Value Injecter instead to avoid having to set up mapping rules explicitly. Commented Jan 11, 2013 at 15:31

4 Answers 4

19

One possible solution:

  1. Use ModelState.IsValidField(string key);

    if (ModelState.IsValidField("Name") && ModelState.IsValidField("Address"))
    { ... }
    

Then at the end when everything is done use:

if(ModelState.IsValid) { .. }
Sign up to request clarification or add additional context in comments.

1 Comment

If you fill in a non excisting string it wil always return, make sure that you type it correctly.
11

I think the most elegant way is to do it like that:

List<string> PropertyNames = new List<string>()
{
    "Prop1",
    "Prop2"
};

if (PropertyNames.Any(p => !ModelState.IsValidField(p)))
{
    // Error
}
else
{
    // Everything is okay
}

or:

List<string> PropertyNames = new List<string>()
{
    "Prop1",
    "Prop2"
};

if (PropertyNames.All(p => ModelState.IsValidField(p)))
{
    // Everything is okay
}
else
{
    // Error
}

Comments

9

In MVC Core, this will be the equivalent of:

if (ModelState.GetFieldValidationState("Name") == Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Valid)
{
    // do something
}

However, I would recommend simply creating a separate view model in this instance.

Your partial view model could be inherited by your larger view model so you won't have to repeat yourself in code (DRY principal).

It's better to avoid hard-coding the property names!

3 Comments

It works in my Core 2.1 API project - since MVC and Web API now share the same ControllerBase class. You can see that ModelState live in ControllerBase in Core 2 as well learn.microsoft.com/en-us/dotnet/api/…
There are plenty of examples of using ModelState validation in .NET Core Web API. learn.microsoft.com/en-us/aspnet/web-api/overview/… Just play with it until you get it
I have raised my own question link .. See if you and me are on same page.. i have already seen the link you have suggested..
5

Just to add to the existing answers for this. Rather than hardcoding the property names I would use an attribute to be added along with the rest of your validation attributes along the lines of:

public class ValidationStageAttribute : Attribute
{
    public int StageNumber { get; private set; }
    public ValidationStageAttribute(int stageNumber)
    {
        StageNumber = stageNumber;
    }
}

Now that we can get the property names without knowledge of the model itself the partial validation can be pulled into a method (if you use it a lot, your base controller would be a good spot).

protected bool ValidateStage(object viewModel, int stageToValidate)
{
    var propertiesForStage = viewModel.GetType()
        .GetProperties()
        .Where(prop => prop.GetCustomAttributes(false).OfType<ValidationStageAttribute>().Any(attr => attr.StageNumber == stageToValidate))
        .Select(prop => prop.Name);

    return propertiesForStage.All(p => ModelState.IsValidField(p));
}

Now all you'd need to do in your post action would be to call ValidateStage(viewModel, 1)

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.