I know there is a lot of ways of doing Model validation within MVC, and there is quite a lot of documentation regarding this topic. However, I'm not quite sure what's the best approach for validating properties of the Model which are "Sub Model" of same type.
Keep in mind the following
TryUpdateModel/TryValidateModel
methodsMainModel
class that renders the overall display viewIt might sound a little confusing but i'll throw in some code to clarify. Take as example the following classes:
MainModel:
class MainModel{
public SomeSubModel Prop1 { get; set; }
public SomeSubModel Prop2 { get; set; }
}
SomeSubModel:
class SomeSubModel{
public string Name { get; set; }
public string Foo { get; set; }
public int Number { get; set; }
}
MainModelController:
class MainModelController{
public ActionResult MainDisplay(){
var main = db.retrieveMainModel();
return View(main);
}
[HttpGet]
public ActionResult EditProp1(){
//hypothetical retrieve method to get MainModel from somewhere
var main = db.retrieveMainModel();
//return "submodel" to the strictly typed edit view for Prop1
return View(main.Prop1);
}
[HttpPost]
public ActionResult EditProp1(SomeSubModel model){
if(TryValidateModel(model)){
//hypothetical retrieve method to get MainModel from somewhere
var main = db.retrieveMainModel();
main.Prop1 = model;
db.Save();
//when succesfully saved return to main display page
return RedirectToAction("MainDisplay");
}
return View(main.Prop1);
}
//[...] similar thing for Prop2
//Prop1 and Prop2 could perhaps share same view as its strongly
//typed to the same class
}
I believe this code all make sense until now (correct me if it's not the case) because TryValidateModel()
is validating against a model with no ValidationAttribute
.
The problem lies here, where would be the best place, or what would be the best and most elegant way to have different validation constraints for Prop1
and Prop2
while still taking advantage of TryValidateModel()
and not filling the Edit method with conditional statements and ModelState.AddModelError()
Usually you could have validation attributes in the SomeSubModel
class, but it wouldn't work in this case, because there is different constraints for each property.
Other option is that there could be Custom validation attribute in the MainModel
class, but it also wouldn't work in this case because the SomeSubModel
object is passed directly to the view and when validating has no reference to its MainModel
object.
The only left option I can think about is a ValidationModel for each property, but I am not quite what would be the best approach for this.
Here's solution I implemented, based of @MrMindor's answer.
Base ValidationModel class:
public class ValidationModel<T> where T : new()
{
protected ValidationModel() {
this.Model = new T();
}
protected ValidationModel(T obj) {
this.Model = obj;
}
public T Model { get; set; }
}
Validation Model for Prop1
public class Prop1ValidationModel:ValidationModel<SomeSubModel>
{
[StringLength(15)]
public string Name { get{ return base.Model.Name; } set { base.Model.Name = value; } }
public Prop1ValidationModel(SomeSubModel ssm)
: base(ssm) { }
}
Validation Model for Prop2
public class Prop2ValidationModel:ValidationModel<SomeSubModel>
{
[StringLength(70)]
public string Name { get{ return base.Model.Name; } set { base.Model.Name = value; } }
public Prop2ValidationModel(SomeSubModel ssm)
: base(ssm) { }
}
Action
[HttpPost]
public ActionResult EditProp1(SomeSubModel model){
Prop1ValidationModel vModel = new Prop1ValidationModel(model);
if(TryValidateModel(vModel)){
//[...] persist data
//when succesfully saved return to main display page
return RedirectToAction("MainDisplay");
}
return View(model);
}
The following three type of validations we can do in ASP.NET MVC web applications: HTML validation / JavaScript validation. ASP.NET MVC Model validation.
In code we need to check the IsValid property of the ModelState object. If there is a validation error in any of the input fields then the IsValid property is set to false. If all the fields are satisfied then the IsValid property is set to true. Depending upon the value of the property, we need to write the code.
We have a similar situation in one of our applications where each SomeSubModel
represents a parameter setting for a job. As each type of job has a different number and types of parameters, our job model has a collection of these parameters instead of just having set properties.
We have a JobParameter
that is subclassed into the different types available (StringParameter
, BoolParameter
, DoubleParameter
, ...). These subclasses have their own sets of validation attributes.
A shared 'JobParameterModel' is used for passing the parameters to the view.
For Validation the returned Model is converted to its specific JobParameter.
ParameterTypes:
public enum ParameterType
{
Empty = 0,
Boolean = 1,
Integer = 2,
String = 3,
DateTime = 4,
...
}
JobParameter:
class JobParameter
{
[AValidationAttributeForAllParamters]
public string Name { get; set; }
public virtual string Foo { get; set; }
public int Number { get; set; }
public ParameterType Type {get;set;}
private static readonly IDictionary<ParameterType, Func<object>> ParameterTypeDictionary =
new Dictionary<ParameterType, Func<object>>{
{ParameterType.Empty, () => new EmptyParameter() },
{ParameterType.String, ()=>new StringParameter()},
{ParameterType.Password, ()=>new PasswordParameter()},
...
};
public static ScriptParameter Factory(ParameterType type)
{
return (ScriptParameter)ParameterTypeDictionary[type]();
}
}
BoolParameter:
[ABoolClassLevelValidationAttribute]
class BoolParameter:JobParameter
{
[AValidationAttribute]
public override string Foo {get;set;}
}
....
In our validation framework (which I am told is modeled very closely to MS's) the ViewModel is always converted back to its domain object for validation.
ParameterModel:
class ParameterModel: JobParameter
{
public JobParameter ToDomain()
{
var domainObject = JobParameter.Factory(Type);
Mapper.Map(this, domainObject);
return domainObject;
}
public bool Validate()
{
var dom = ToDomain();
return TryValidate(dom);
}
}
Controller:
class Controller(){
[HttpPost]
public ActionResult SaveParameter(JobParameter model){
if(TryValidateModel(model)){
//persist stuff to db.
//when succesfully saved return to main display page
return RedirectToAction("MainDisplay");
}
return View(main.Prop1);
}
}
For the sake of your specific situation, you don't need to get quite this complicated (Or trust that the specifics of our validation framework will work for you).
Edit/Save Actions for each Prop:
Create a validation model for each prop. Prop1ValidationModel
, Prop2ValidationModel
[HttpGet]
public ActionResult EditProp1()
{
var main = db.retrieveMainModel();
db.Prop1.SubmitUrl = Url.Action("SaveProp1","Controller");
return View(main.Prop1);
}
[HttpPost]
public ActionResult SaveProp1(SomeSubModel model){
var validationModel = new Prop1ValidationModel{
///copy properties
};
if(TryValidateModel(validationModel)){
var main = db.retrieveMainModel();
main.Prop1 = model;
db.Save();
//when succesfully saved return to main display page
return RedirectToAction("MainDisplay");
}
return View(main.Prop1);
}
With this you can use the same strongly typed view for both Prop1 and Prop2.
If SomeSubModel has different validation attributes depending if it is applied either in Prop1 or Prop2...means that actually the two SomeSubModel of prop1 end prop2 are two different classes, because also if they have the same fields the meaning of this fields is different depending if they are attached to prop1 or prop2(that is why they have different validation attributes. Accordingly the best approach is defining two subclasses of SomeSubClass, say SomeSubClass1 and SomeSubClass2 that inherit from the Common SomeSubClass. Once inherited you must not add new properties but just new validation rules either by using fluent validation or by using the MetaDataTypeAttribute to specify Validation attributes out of the class definition. So you will have something like:
[MetaDataType(typeof(ValidationClass1)]
public class SomeSubClass1: SomeSubclass{}
and
[MetaDataType(typeof(ValidationClass2)]
public class SomeSubClass2: SomeSubclass{}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With