Sometimes user input is not strictly invalid but can be considered problematic.
For example:
Name
field. He probably should have used the Description
field instead.Name
that is very similar to that of an existing entity. Perhaps he's inputting the same entity but didn't realize it already exists, or some concurrent user has just entered it. Some of these can easily be checked client-side, some require server-side checks.
What's the best way, perhaps something similar to DataAnnotations
validation, to provide warnings to the user in such cases? The key here is that the user has to be able to override the warning and still submit the form (or re-submit the form, depending on the implementation).
The most viable solution that comes to mind is to create some attribute, similar to a CustomValidationAttribute
, that may make an AJAX call and would display some warning text but doesn't affect the ModelState
. The intended usage is this:
[WarningOnFieldLength(MaxLength = 150)] [WarningOnPossibleDuplicate()] public string Name { get; set; }
In the view:
@Html.EditorFor(model => model.Name) @Html.WarningMessageFor(model => model.Name) @Html.ValidationMessageFor(model => model.Name)
So, any ideas?
Overall Design
To start with, I believe you would have to track somehow if the user choose to ignore the warnings. A simple and transparent way to do that is to have an Ignore Warnings check-box, which user would have to check before submit. Another option is a have them submit the form two times and ignore the warnings on the second submit; then you'd probably need an IgnoreWarnings hidden field. There could be other designs, but for the sake of simplicity I'll go with the first option.
In short, the approach is to create
Please note that the code below just illustrates the approach and I have to assume quite a lot of things without knowing the full context.
View Model
In this scenario it's best to separate a view model from an actual model which is a good idea anyway. One possible approach is to have a base class for all view models which support warnings:
public abstract class BaseViewModel { public bool IgnoreWarnings { get; set; } }
The key reason a model needs to be separate is that there's little sense in storing the IgnoreWarnings
property in your database.
Your derived view model will then look as follows:
public class YourViewModel : BaseViewModel { [Required] [StringLengthWarning(MaximumLength = 5, ErrorMessage = "Your Warning Message")] public string YourProperty { get; set; } }
StringLengthWarning
is a custom data annotation attribute for server and client-side validation. It just supports the maximum length and can easily be extended with any other necessary properties.
Data Annotation Attribute
The core of the attribute is IsValid(value, validationContext
method.
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = true)] public class StringLengthWarningAttribute : ValidationAttribute, IClientValidatable { public int MaximumLength { get; set; } public override bool IsValid(object value) { return true; } protected override ValidationResult IsValid(object value, ValidationContext validationContext) { var model = validationContext.ObjectInstance as BaseViewModel; var str = value as string; if (!model.IgnoreWarnings && (string.IsNullOrWhiteSpace(str) || str.Length > MaximumLength)) return new ValidationResult(ErrorMessage); return base.IsValid(value, validationContext); } public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context) { yield return new StringLengthWarningValidationRule(MaximumLength, ErrorMessage); } }
The attribute implements IClientValidatable
and utilizes a custom client validation rule:
public class StringLengthWarningValidationRule : ModelClientValidationRule { public StringLengthWarningValidationRule(int maximumLength, string errorMessage) { ErrorMessage = errorMessage; ValidationType = "stringlengthwarning"; ValidationParameters.Add("maximumlength", maximumLength); ValidationParameters.Add("ignorewarningsfield", "IgnoreWarnings"); } }
Client-side JavaScript
Finally, to make it work, you'll need the following JavaScript referenced from your view:
$(function () { $.validator.addMethod('stringlengthwarning', function (value, element, params) { var maximumlength = params['maximumlength']; var ignorewarningsfield = params['ignorewarningsfield']; var ctl = $("#" + ignorewarningsfield); if (ctl == null || ctl.is(':checked')) return true; return value.length <= maximumlength; }); $.validator.unobtrusive.adapters.add("stringlengthwarning", ["maximumlength", "ignorewarningsfield"], function (options) { var value = { maximumlength: options.params.maximumlength, ignorewarningsfield: options.params.ignorewarningsfield }; options.rules["stringlengthwarning"] = value; if (options.message) { options.messages["stringlengthwarning"] = options.message; } }); }(jQuery));
The JavaScript makes some assumptions you might want to revisit (the check-box name, etc).
UPDATE: HTML Helpers
To display the validation messages separately for errors and warnings, a couple of helpers will be necessary. The following class provides a sample:
public static class MessageHelpers { public static MvcHtmlString WarningMessageFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression) { if (htmlHelper.ViewData.ModelState["IgnoreWarnings"] != null) return htmlHelper.ValidationMessageFor(expression); return MvcHtmlString.Empty; } public static MvcHtmlString ErrorMessageFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression) { if (htmlHelper.ViewData.ModelState["IgnoreWarnings"] == null) return htmlHelper.ValidationMessageFor(expression); return MvcHtmlString.Empty; } }
In the view they can be used as usual:
@Html.EditorFor(model => model.YourProperty) @Html.ErrorMessageFor(model => model.YourProperty) @Html.WarningMessageFor(model => model.YourProperty)
You could use the depends
function of jquery validation to simplify your life.
Ex.
@Html.LabelFor(m => m.UserName) @Html.TextBoxFor(m => m.UserName) @Html.ValidationMessageFor(m => m.UserName) <label>Ignore Warnings</label> <input id="ignore-warnings" type="checkbox" /> <script> $(function () { $("#UserName").rules("add", { minlength: { param: 6, depends: function (element) { return !$("#ignore-warnings").attr('checked'); } }, // server side remote validation for duplicate check remote: { param: '/account/duplicate', depends: function (element) { return !$("#ignore-warnings").attr('checked'); } } }); }); </script>
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