0

I have an Model with 20 fields.

My view is split in 4 panels, each has 5 fields; the user can save the data separately one panel at a time.

I have created 4 views, one per panel. The views all use the same model, since this data is all in the same database table, so I have a single CRUD set of API.

My question is how can I turn on/off Data Annotation for required fields only when these are displayed in their specific panel?

If I declare them all in the 20 fields model attributes, they will invalidate the model even if they are not displayed on the screen...

Erik Philips
  • 53,428
  • 11
  • 128
  • 150
MaurGi
  • 1,698
  • 2
  • 18
  • 28
  • There is no such a thing as ASP.NET MVC 6 (yet). Please check the version number of the technologies you think you're using... – Charles Apr 08 '14 at 22:23
  • Hehe you are correct, it's MVC5 (got confused with Entity Framework verion 6.1)- will update the post -thx. – MaurGi Apr 08 '14 at 23:37
  • There may be good reasons for these fields to be required and any effort to circumvent validation may cause more problems than it solves. Having said that, have you considered using default values? – RyanCJI Apr 09 '14 at 00:14

4 Answers4

1

There really is no easy way to make this work. My only recommendation is a custom model validator that takes into account the previous field in each partial view. For example:

class LabRatModel
{
    public int a { get; set; }
    public int b { get; set; } // Say the partial splits right here
    public int c { get; set; }
    public int d { get; set; }
}

In your custom validator, try:

public override bool IsValid(object model)
{
    var labrat = model as LabRatModel;
    return labrat.b > 0 && labrat.c > 0;
}

The point is to check the previous field and go from there.

beautifulcoder
  • 10,832
  • 3
  • 19
  • 29
1

You would be better off moving your data annotations to view models. You can create a different model for each view, so that only the annotations affected by that view are triggered.

That being said, sometimes its difficult to reengineer your data model. In that case, you could add properties that trigger portions of the data validation using a custom requiredif annotation. See RequiredIf Conditional Validation Attribute for specifics.

I ran into this myself on my first MVC project. Below is how I resolved the issue. Note that I place these in a Model\DataValidations.cs file to keep my generated classes clean.

[MetadataType(typeof(Location_Validation))]
public partial class Location
{
    public bool DisableValidation { get; set; }
}

public class Location_Validation : HomeIndex_Validation
{
    [RequiredIf(DependentProperty = "DisableValidation", TargetValue = false, ErrorMessageResourceType = typeof(Language), ErrorMessageResourceName = "FieldRequired")]
    [MinLength(2, ErrorMessageResourceType = typeof(Language), ErrorMessageResourceName = "FieldTooShort")]
    [MaxLength(50, ErrorMessageResourceType = typeof(Language), ErrorMessageResourceName = "FieldTooLong")]
    [Display(ResourceType = typeof(Language), Name = "City")]
    public string City { get; set; }

    [RequiredIf(DependentProperty = "DisableValidation", TargetValue = false, ErrorMessageResourceType = typeof(Language), ErrorMessageResourceName = "FieldRequired")]
    [MinLength(2, ErrorMessageResourceType = typeof(Language), ErrorMessageResourceName = "FieldTooShort")]
    [MaxLength(2, ErrorMessageResourceType = typeof(Language), ErrorMessageResourceName = "FieldTooLong")]
    [ReadOnly]
    [Display(ResourceType = typeof(Language), Name = "State")]
    public string State { get; set; }

    [RequiredIf(DependentProperty = "DisableValidation", TargetValue = false, ErrorMessageResourceType = typeof(Language), ErrorMessageResourceName = "FieldRequired")]
    [MinLength(2, ErrorMessageResourceType = typeof(Language), ErrorMessageResourceName = "FieldTooShort")]
    [MaxLength(50, ErrorMessageResourceType = typeof(Language), ErrorMessageResourceName = "FieldTooLong")]
    [Display(ResourceType = typeof(Language), Name = "County")]
    public string County { get; set; }

    [Required(ErrorMessageResourceType = typeof(Language), ErrorMessageResourceName = "FieldRequired")]
    [MaxLength(5, ErrorMessageResourceType = typeof(Language), ErrorMessageResourceName = "FieldInvalid")]
    [MinLength(5, ErrorMessageResourceType = typeof(Language), ErrorMessageResourceName = "FieldInvalid")]
    [RegularExpression(@"^\d+$", ErrorMessageResourceType = typeof(Language), ErrorMessageResourceName = "FieldInvalid")]
    [Display(ResourceType = typeof(Language), Name = "ZipCode")]
    public string ZipCode { get; set; }

}

Not the best solution, but it works, and the data model is fully validated as the user progresses through the web site. Of course, if the user enters a value, the formatting validations are enforced.

Community
  • 1
  • 1
B2K
  • 2,541
  • 1
  • 22
  • 34
  • One last point on this, I have a localized application. If you don't need that, use ErrorMessage = "..." instead. – B2K Apr 09 '14 at 00:57
  • Thanks - this is what I was looking for - yeah this is a database first approach so it's not easy to change the DB.. – MaurGi Apr 09 '14 at 02:59
1

As B2K Answer pointed out, using a ViewModel has advantages. I am not a fan of consistently mapping fields back and forth over and over, so I like to use MetaData and Validation classes to encapsulate that concept.

public TheMainClass
{
    public string Prop1 { get; set; }
    public string Prop2 { get; set; }
    public string Prop3 { get; set; }
    public string Prop4 { get; set; }
} 

If I only display and validate each prop for each view, then I would create 4 models designed specifically for validation:

[Metadata(typeof(Main1Model.IView1Validation))]
public Main1Model : TheMainClass
{
  internal interface IView1Validation
  {
    [Required]
    string Prop1 { get; set; }

    [ScaffoldColumn(false)]
    string Prop2 { get; set; }

    //etc
  }
}

ViewModel2:

[Metadata(typeof(IView2Validation))]
public Main2Model : TheMainClass
{
  internal interface IView1Validation
  {
    [HiddenInput(DisplayValue = false)]
    [Required]
    string Prop1 { get; set; }

    [Required]
    string Prop2 { get; set; }

    //etc
  }
}

And so forth. As the views progress if someone decides, using a browser tool, to fudge a hidden field, you still have the validation.

One ViewModel will encapsulate all views models: (I always do this as B2K recommends as well)

public MainViewModel
{
  public MainModel { get; set; }
}

etc..

Then Actions would look something like:

[HttpPost]
public ActionResult View1(MainViewModelmodel)
{
  if (ModelState.IsValid)
  {
    TempData["View1"] = model;
    return this.RedirectToAction("View2");
  }

  return this.View(model);
}

public ActionResult View2()
{
  model = TempData["View"] as Main1ViewModel;
  if (model == null)
  {
    return this.RedirectToAction("View1");
  }

  return this.View(model)
}

[HttpPost]
public ActionResult View2(MainViewModel model)
{
  // and so on (like HttpPost View1
}

Then you might be asking.. but wait how does this work? The ViewModel is using the base type, and that is the magic of MVC. You'll notice that View2.cshtml is strongly-typed to MainViewModel but Views use the Concrete Type passed in, not the defined typed passed in.

All views would look something like (might even be able to be the same view):

@model MainViewModel

  @EditFor(m => m.MainModel.Prop1)
  @EditFor(m => m.MainModel.Prop2)
  @EditFor(m => m.MainModel.Prop3)
  @EditFor(m => m.MainModel.Prop4)

If Model1View is passed in with scaffold false, the editfor()s won't create any html elements.

As the user progresses and you've specified [HiddenInput] the editfor()s create a hidden field.

The last bit of awesome-sauce is two-fold; first you don't have to persist the model in memory (because each model is fully-passed in to the view), and secondly the back button works because the model is stored in the view inputs (including hidden fields)

Community
  • 1
  • 1
Erik Philips
  • 53,428
  • 11
  • 128
  • 150
  • Thanks - I thought about a similar approach with subclasses, but I think RequiredIf is a simpler solution in my case. – MaurGi Apr 09 '14 at 03:01
0

In keeping with 'DRY' principles, I would consider it harmful to map all your entities to viewmodels when your domain entities could neatly contain all the validation meta-data annotation required. In looking for a more satisfactory solution to this problem of partial validation I keep a "step" count in the viewmodel and add my domain entities as sub-components within that view. I found that I could retain the annotation on the domain entities by checking this step count prior to my if(Model.IsValid) { and removing the offending entries. eg.

public MainViewModel
{
  public ModelType MainModel { get; set; }//Domain entity containing my validation metadata annotations
  public int StepCount { get; set; }
}

then my action

public ActionResult SubmitProcess(MainViewModel model)
{
  int NextStep = model.StepCount+1;
  if (NextStep <= 4)//I am not ready to perform the below validation at this time.
    ModelState.Remove("MainModel.Prop1");//So remove the key!

  if (!ModelState.IsValid)//Now validate and allow next step only if valid
    NextStep = model.StepCount;//Failed so retract movement to NextStep.

  if (NextStep != model.StepCount)
  {
    ModelState.Remove("StepCount");//Another 'gotcha' - model needs a 'nudge' to refresh hidden fields!
    model.StepCount = NextStep;
  }

  return View(model)//model.StepCount now incremented only if partial validation passed - so do an action based on that.
}