Reputation: 368
(I've made some progress, but still not working, updates below...)
I am trying to implement ye olde start date is not greater than end date validation. This is the first time I've attempted to write a custom validation attribute. Based on what I've been reading out here, this is what I've come up with...
custom validation attribute:
public class DateGreaterThanAttribute : ValidationAttribute
{
private string _startDatePropertyName;
public DateGreaterThanAttribute(string startDatePropertyName)
{
_startDatePropertyName = startDatePropertyName;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var propertyInfo = validationContext.ObjectType.GetProperty(_startDatePropertyName);
if (propertyInfo == null)
{
return new ValidationResult(string.Format("Unknown property {0}", _startDatePropertyName));
}
var propertyValue = propertyInfo.GetValue(validationContext.ObjectInstance, null);
if ((DateTime)value > (DateTime)propertyValue)
{
return ValidationResult.Success;
}
else
{
var startDateDisplayName = propertyInfo
.GetCustomAttributes(typeof(DisplayNameAttribute), true)
.Cast<DisplayNameAttribute>()
.Single()
.DisplayName;
return new ValidationResult(validationContext.DisplayName + " must be later than " + startDateDisplayName + ".");
}
}
}
view model:
public class AddTranscriptViewModel : IValidatableObject
{
...
[DisplayName("Class Start"), Required]
[DataType(DataType.Date)]
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:MM/dd/yyyy}")]
[RegularExpression(@"^(1[012]|0?[1-9])[/]([12][0-9]|3[01]|0?[1-9])[/](19|20)\d\d.*", ErrorMessage = "Date out of range.")]
public DateTime? ClassStart { get; set; }
[DisplayName("Class End"), Required]
[DataType(DataType.Date)]
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:MM/dd/yyyy}")]
[RegularExpression(@"^(1[012]|0?[1-9])[/]([12][0-9]|3[01]|0?[1-9])[/](19|20)\d\d.*", ErrorMessage = "Date out of range.")]
[DateGreaterThan("ClassStart")]
public DateTime? ClassEnd { get; set; }
...
}
Relevant portions of the front-end:
@using (Html.BeginForm("AddManualTranscript", "StudentManagement", FormMethod.Post, new { id = "studentManagementForm", @class = "container form-horizontal" }))
{
...
<div class="col-md-4" id="divUpdateStudent">@Html.Button("Save Transcript Information", "verify()", false, "button")</div>
...
<div class="col-md-2">
<div id="divClassStart">
<div>@Html.LabelFor(d => d.ClassStart, new { @class = "control-label" })</div>
<div>@Html.EditorFor(d => d.ClassStart, new { @class = "form-control" }) </div>
<div>@Html.ValidationMessageFor(d => d.ClassStart)</div>
</div>
</div>
<div class="col-md-2">
<div id="divClassEnd">
<div>@Html.LabelFor(d => d.ClassEnd, new { @class = "control-label" })</div>
<div>@Html.EditorFor(d => d.ClassEnd, new { @class = "form-control" }) </div>
<div>@Html.ValidationMessageFor(d => d.ClassEnd)</div>
</div>
</div>
...
}
<script type="text/javascript">
...
function verify() {
if ($("#StudentGrades").data("tGrid").total == 0) {
alert("Please enter at least one Functional Area for the transcript grades.");
}
else {
$('#studentManagementForm').trigger(jQuery.Event("submit"));
}
}
...
</script>
The behavior I'm seeing is that all other validations on all other fields on the form, which are all standard validations like Required, StringLength, and RegularExpression, etc., are working as expected: when I click the "save" button, the red text appears for those fields that don't pass. I have put a breakpoint in my IsValid code, and it doesn't hit unless all the other validations are passed. And even then, if the validation check fails, it doesn't stop the post.
Further reading led me to add the following to Global.asax.cs:
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(DateGreaterThanAttribute), typeof(DataAnnotationsModelValidator));
But that made no difference. I also tested ModelState.IsValid in the postback function, and it was false. But for the other validators if never gets that far. I even noticed in the markup that it seems like a lot of markup gets created on those fields that have validation attributes when the page is generated. Where does that magic occur and why is my custom validator out of the loop?
There's a lot of variation out there, but what I have here seems to generally line up with what I'm seeing. I've also read some about registering validators on the client side, but that seems to only apply to client-side validation, not model validation at submit/post. I won't be embarrassed if the answer is some silly oversight on my part. After about a day on this, I simply need it to work.
Update:
Rob's answer led me to the link referenced in my comment below, which then led me here client-side validation in custom validation attribute - asp.net mvc 4 which led me here https://thewayofcode.wordpress.com/tag/custom-unobtrusive-validation/
What I read there jived with what I had observed, that something was missing in the markup, and it looked like the author outlined how to get it in there. So I added the following to my validation attribute class:
public class DateGreaterThanAttribute : ValidationAttribute, IClientValidatable // IClientValidatable added here
...
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
//string errorMessage = this.FormatErrorMessage(metadata.DisplayName);
string errorMessage = ErrorMessageString;
// The value we set here are needed by the jQuery adapter
ModelClientValidationRule dateGreaterThanRule = new ModelClientValidationRule
{
ErrorMessage = errorMessage,
ValidationType = "dategreaterthan" // This is the name the jQuery adapter will use, "startdatepropertyname" is the name of the jQuery parameter for the adapter, must be LOWERCASE!
};
dateGreaterThanRule.ValidationParameters.Add("startdatepropertyname", _startDatePropertyName);
yield return dateGreaterThanRule;
}
And created this JavaScript file:
(function ($) {
$.validator.addMethod("dategreaterthan", function (value, element, params) {
console.log("method");
return Date.parse(value) > Date.parse($(params).val());
});
$.validator.unobtrusive.adapters.add("dategreaterthan", ["startdatepropertyname"], function (options) {
console.log("adaptor");
options.rules["dategreaterthan"] = "#" + options.params.startdatepropertyname;
options.messages["dategreaterthan"] = options.message;
});
})(jQuery);
(Notice the console.log hits... I never see those.)
After this, I'm now getting hits when I browse to the page in the DataGreaterThanAttribute constructor and the GetClientValidationRules. As well, the ClassEnd input tag now has the following markup in it:
data-val-dategreaterthan="The field {0} is invalid." data-val-dategreaterthan-startdatepropertyname="ClassStart"
So I'm getting closer. The problem is, the addMethod and adapater.add don't seem to be doing their jobs. When I inspect these objects in the console using the following:
$.validator.methods
$.validator.unobtrusive.adapters
...my added method and adapter are not there. If I run the code from my JavaScript file in the console, they do get added and are there. I also noticed that if I generally inspect the unobtrusive validation object with...
$("#studentManagementForm").data('unobtrusiveValidation')
...there is no evidence of my custom validation.
As I alluded earlier, there are many examples out here, and they all seem to do things just a little differently, so I'm still trying some different things. But I'm really hoping someone who has beaten this into submission before will come along and share that hammer with me.
If I can't get this to work, I'll be putting on the hard-hat and writing some hacky JavaScript to spoof the same functionality.
Upvotes: 2
Views: 2009
Reputation: 920
I think you need IEnumerable<ValidationResult> on your model.
I had to do something similar around 4 years ago and still have the snippet to hand if this helps:
public class ResultsModel : IValidatableObject
{
[Required(ErrorMessage = "Please select the from date")]
public DateTime? FromDate { get; set; }
[Required(ErrorMessage = "Please select the to date")]
public DateTime? ToDate { get; set; }
IEnumerable<ValidationResult> IValidatableObject.Validate(ValidationContext validationContext)
{
var result = new List<ValidationResult>();
if (ToDate < FromDate)
{
var vr = new ValidationResult("The to date cannot be before the from date");
result.Add(vr);
}
return result;
}
}
Upvotes: 2