Reputation: 559
I have a model with a list of child objects. I have created a custom validation attribute, implemented IValidatableObject on the model and i get an error message as expected. The problem is that once the property has an error in the modelstate, i can't get the updated value to post back to the server. They get cleared out some time between hitting the submit button and receiving the model in the controller.
if i call ModelState.Clear() in the controller action, i don't get any messages but the new values post as expected. The model is however picking up on the custom attribute because ModelState.IsValid == false
I'm thinking the best way to handle this is to call ModelState.Clear() on the client somehow after $(ready) so i get the validation messages but can also have the changed values post to the server. Is this possible or is there a better way to do this?
Parent Model
public class PayrollPlanModel : IMapFrom<Data.PayrollPlan>
{
public int? PayrollPlanId { get; set; }
[Required]
public string Name { get; set; }
public List<PlanOptionFormModel> Options { get; set; }
}
Model List property on parent with custom attribute
public class PlanOptionFormModel : IValidatableObject
{
public int PlanOptionValueId { get; set; }
public int PayrollPlanId { get; set; }
public string PlanName { get; set; }
public int PlanOptionId { get; set; }
public string Description { get; set; }
[UIHint("_Money")]
[RequiredIf("Selected", true)]
public decimal? Value { get; set; }
public bool Selected { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (Selected && !Value.HasValue)
{
yield return new ValidationResult("Add a value.");
}
}
}
Custom Attribute (Shamelessly stolen from here)
public class RequiredIfAttribute : ValidationAttribute
{
RequiredAttribute _innerAttribute = new RequiredAttribute();
public string _dependentProperty { get; set; }
public object _targetValue { get; set; }
public RequiredIfAttribute(string dependentProperty, object targetValue)
{
this._dependentProperty = dependentProperty;
this._targetValue = targetValue;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var field = validationContext.ObjectType.GetProperty(_dependentProperty);
if (field != null)
{
var dependentValue = field.GetValue(validationContext.ObjectInstance, null);
if ((dependentValue == null && _targetValue == null) || (dependentValue.Equals(_targetValue)))
{
if (!_innerAttribute.IsValid(value))
{
string name = validationContext.DisplayName;
return new ValidationResult(ErrorMessage = name + " Is required.");
}
}
return ValidationResult.Success;
}
else
{
return new ValidationResult(FormatErrorMessage(_dependentProperty));
}
}
}
Page snippet
for (int i = 0; i < Model.Options.Count; i++)
{
<div class="row">
<div class="col-md-3">
@Html.HiddenFor(m => Model.Options[i].PlanOptionValueId)
@Html.HiddenFor(m => Model.Options[i].PayrollPlanId)
@Html.HiddenFor(m => Model.Options[i].PlanOptionId)
@Html.HiddenFor(m => Model.Options[i].Description)
</div>
<div class="col-md-1 text-right">
@Html.CheckBoxFor(m => Model.Options[i].Selected, new { @data_textbox = "optionValue_" + i.ToString(), @class = "form-control modelOptionSelector" })
</div>
<div class="col-md-2 text-right">
<h4>@Model.Options[i].Description</h4>
</div>
<div class="col-md-1">
@Html.EditorFor(m => Model.Options[i].Value, Model.Options[i].Selected ? new { HtmlAttributes = new { id = "optionValue_" + i.ToString(), @class = "planOptionValueEditor" } } : (object)new { HtmlAttributes = new { disabled = "disabled", id = "optionValue_" + i.ToString(), @class = "planOptionValueEditor" } })
@Html.ValidationMessageFor(m => Model.Options[i].Value)
</div>
</div>
}
<br />
Editor Template
@model decimal?
@{
var defaultHtmlAttributesObject = new { };
var htmlAttributesObject = ViewData["htmlAttributes"] ?? new { };
var htmlAttributes = Html.MergeHtmlAttributes(htmlAttributesObject, defaultHtmlAttributesObject);
string attemptedValue = "";
ModelState modelStateForValue = Html.ViewData.ModelState[Html.IdForModel().ToString()];
if (modelStateForValue != null)
{
attemptedValue = modelStateForValue.Value.AttemptedValue;
}
}
@(Html.Kendo().CurrencyTextBoxFor(m => m)
.HtmlAttributes(htmlAttributes)
.Format("c")
.Spinners(false)
)
Controller
[HttpPost]
public ActionResult EditPlan(PayrollPlanModel model)
{
if(ModelState.IsValid)
{
}
else
{
}
return View(model);
}
Upvotes: 0
Views: 2487
Reputation:
It makes no sense to attempt to attempt to clear ModelState
errors from the client. ModelState
is only set within the controller method (by the DefaultModelBinder
) when you make a request to the method. In any case, your issues are not related to ModelState
being valid or invalid in the controller method.
There are a number of changes you need to make to your code:
You should delete your EditorTemplate
for decimal?
It means that any property of that type is going to use that template. Instead replace your
@Html.EditorFor(m => Model.Options[i].Value, ...)
with
@(Html.Kendo().CurrencyTextBoxFor(m => m.Options[i].Value)....
in the main view.
If you really do want to use a template, then make it a named template (which is called using @Html.EditorFor(m => Model.Options[i].Value, "yourTemplateName")
, but in any case, you need to remove the code relating to attemptedValue
and modelStateForValue
(including the if
block) - the EditorFor()
methods will always correctly use values from ModelState
if they exist.
Next, your RequiredIfAttribute
does not implement IClientValidatable
so you will not get client side validation. You could use the foolproof library, or if you want to write your own, refer this answer for the full implementation of a RequiredIfAttribute
, including the scripts for client side validation.
Next, you need to delete the IValidatableObject
implementation (the Validate()
method) from your model. That is just repeating the validation that the [RequiredIf]
attribute is doing, and you should avoid mixing ValidationAttribute
's with IValidatableObject
(refer The Complete Guide To Validation In ASP.NET MVC 3 - Part 2 for more detailed information).
Finally, the Kendo().CurrencyTextBoxFor()
method hides the input and renders its own html. By default, hidden inputs are not validated, so you need to reconfigure the validator. In the main view, add the following script (after the jquery-{version}.js
, jquery.validate.js
and jquery.validate.unobtrusive.js
scripts
<script>
$.validator.setDefaults({
ignore: []
});
.... // other scripts as required
<script>
Upvotes: 1