How do I access the value to compare when overriding GetClientValidationRules? - asp.net-mvc

I am trying to write a client side validator for angularjs using fluent validation. I used the methods outlined by Darin Dimitrov here. Everything works fine except I can't figure out how to access the greater than value I set up in my validation rule. I need this so I can have my angular directive validate this value for me.
Any ideas? Thanks.
Here is my Rule:
RuleFor(m => m.dropDownListId).GreaterThan(0).WithMessage("Required");
Here is my override code:
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
{
if (!ShouldGenerateClientSideRules()) yield break;
var formatter = new MessageFormatter().AppendPropertyName(Rule.PropertyName);
var message = formatter.BuildMessage(Validator.ErrorMessageSource.GetString());
var rule = new ModelClientValidationRule
{
ValidationType = VALIDATIONTYPE,
ErrorMessage = message
};
//CompareAttribute is deprecated and I can't figure out the new syntax
//also 'MemberToCompare' is always null
rule.ValidationParameters["greaterthan"] = CompareAttribute.FormatPropertyForClientValidation(validator.MemberToCompare.Name);
//what I am trying to do is
rule.ValidationParameters.Add("greaterthan", "the value I setup in my rule");
yield return rule;
}

I hate to answer my own questions, especially when I have missed the obvious but this may help someone.
Because 'GreaterThan' validates a number you need to use Validator.ValueToCompare. Duh.
Here is the correct way.
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
{
if (!ShouldGenerateClientSideRules()) yield break;
var validator = Validator as GreaterThanValidator;
if(validator == null)
throw new ArgumentException("greaterThanValidator");
var valueToCompare = validator.ValueToCompare;
var formatter = new MessageFormatter().AppendPropertyName(Rule.PropertyName);
var message = formatter.BuildMessage(Validator.ErrorMessageSource.GetString());
var rule = new ModelClientValidationRule
{
ValidationType = VALIDATIONTYPE,
ErrorMessage = message
};
rule.ValidationParameters.Add("min", valueToCompare);
yield return rule;
}

Related

Custom ValidationAttribute trigger on different client property change

I have built a custom validation attribute - LessThanDifference. Basically I give it two properties, and it checks to see if the value of the validated field is less than the difference of the two property names. Basically "is FieldC < (FieldA - FieldB)". That part works.
The IsValid function works fine, here is my client validation rules. (Bonus question - Is there any way to get the display name for firstoperand and secondoperand? It has the property name by attribute parameter.)
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var rule = new ModelClientValidationRule
{
ValidationType = "lessthandifference",
ErrorMessage = FormatErrorMessage(metadata.GetDisplayName())
};
rule.ValidationParameters.Add("firstoperand", FirstOperand);
rule.ValidationParameters.Add("secondoperand", SecondOperand);
yield return rule;
}
Where I'm totally lost is how to trigger the validation If A(firstoperand) or B(secondoperand) changes.
$.validator.addMethod(
'lessthandifference',
function (value, element, params) {
var firstVal = $('#' + params.firstoperand).val();
var secondVal = $('#' + params.secondoperand).val();
return (value <= (firstVal - secondVal));
});
$.validator.unobtrusive.adapters.add(
'lessthandifference', ['firstoperand', 'secondoperand'], function (options) {
var params = {
firstoperand: options.params.firstoperand,
secondoperand: options.params.secondoperand
};
options.rules['lessthandifference'] = params;
options.messages['lessthandifference'] = options.message;
//Set up Trigger?
});
I've tried to pass something like (#' + options.params.secondoperand) into another method, but have been unable to get the prop name for the base attribute (FieldC).
$().change seems like it would be the way to go if I could get it set right.
Thoughts?
I solved the main issue:
function addSecondaryValidatorCheck(mainElement, secondaryElement) {
$(secondaryElement).change(function () {
if ($(mainElement).val() > 0)
$(mainElement).valid();
});
And Implementation from the $.validator.unobtrusive.adapters.add function
addSecondaryValidatorCheck('#' + options.element.id, '#' + options.params.compareAttribute);
Still looking for a good way to pass the display name.

Show only first error message

I am using ASP.NET MVC3 for a form that has both server and client validations. I'm showing error messages as balloons above the inputs. Due to the presentation of the errors, I need to only show one error at a time, otherwise the balloons tend to obscure other fields that may also be in error.
How can I customize the validation behavior to only render the first error message?
Edit: Please notice that the form has both server and client validations, and that I only want to show the first error message for the entire form (not per field).
In case anyone needs it, the solution I came up with is to add the following script towards the bottom of the page. This hooks into the existing javascript validation to dynamically hide all but the first error in the form.
<script>
$(function() {
var form = $('form')[0];
var settings = $.data(form, 'validator').settings;
var errorPlacementFunction = settings.errorPlacement;
var successFunction = settings.success;
settings.errorPlacement = function(error, inputElement) {
errorPlacementFunction(error, inputElement);
showOneError();
}
settings.success = function (error) {
successFunction(error);
showOneError();
}
function showOneError() {
var $errors = $(form).find(".field-validation-error");
$errors.slice(1).hide();
$errors.filter(":first:not(:visible)").show();
}
});
</script>
Could give this a shot on your controller action
var goodErrors = ModelState.GroupBy(MS => MS.Key).Select(ms => ms.First()).ToDictionary(ms => ms.Key, ms => ms.Value);
ModelState.Clear();
foreach (var item in goodErrors)
{
ModelState.Add(item.Key, item.Value);
}
I'm just selecting only one of each property error, clearing all errors then adding the individual ones back.
this is completely untested but should work.
You could create a custom validation summary which would display only the first error. This could be done either by creating an extension for the HtmlHelper class, or by writing your own HtmlHelper. The former is the more straightforward.
public static class HtmlHelperExtensions
{
static string SingleMessageValidationSummary(this HtmlHelper helper, string validationMessage="")
{
string retVal = "";
if (helper.ViewData.ModelState.IsValid)
return "";
retVal += #"<div class=""notification-warnings""><span>";
if (!String.IsNullOrEmpty(validationMessage))
retVal += validationMessage;
retVal += "</span>";
retVal += #"<div class=""text"">";
foreach (var key in helper.ViewData.ModelState.Keys)
{
foreach(var err in helper.ViewData.ModelState[key].Errors)
retVal += "<p>" + err.ErrorMessage + "</p>";
break;
}
retVal += "</div></div>";
return retVal.ToString();
}
}
This is for the ValidationSummary, but the same can be done for ValidationMessageFor.
See: Custom ValidationSummary template Asp.net MVC 3
Edit: Client Side...
Update jquery.validate.unobstrusive.js. In particular the onError function, where it says error.removeClass("input-validation-error").appendTo(container);
Untested, but change that line to: error.removeClass("input-validation-error").eq(0).appendTo(container);
Create a html helper extension that renders only one message.
public static MvcHtmlString ValidationError(this HtmlHelper helper)
{
var result = new StringBuilder();
var tag = new TagBuilder("div");
tag.AddCssClass("validation-summary-errors");
var firstError = helper.ViewData.ModelState.SelectMany(k => k.Value.Errors).FirstOrDefault();
if (firstError != null)
{
tag.InnerHtml = firstError.ErrorMessage;
}
result.Append(tag.ToString());
return MvcHtmlString.Create(result.ToString());
}
Update the jquery.validate.unobtrusive.js OnErrors function as below,
function onErrors(form, validator) { // 'this' is the form element
// newly added condition
if ($(form.currentTarget).hasClass("one-error")) {
var container = $(this).find(".validation-summary-errors");
var firstError = validator.errorList[0];
$(container).html(firstError.message);
}
else {
var container = $(this).find("[data-valmsg-summary=true]"),
list = container.find("ul");
if (list && list.length && validator.errorList.length) {
list.empty();
container.addClass("validation-summary-errors").removeClass("validation-summary-valid");
$.each(validator.errorList, function () {
$("<li />").html(this.message).appendTo(list);
});
}
}
}
Basically we have added a condition in the OnError to check whether the form contains a css-class named one-error and if yes then displays a single error else display all.

MongoDB custom serializer implementation

I am new to MongoDB, and am trying to get the C# driver to work serializing F# classes. I have it working with the class automapper using mutable F# fields & a parameterless constructor, but really I need to retain immutability, so I started looking at implementing an IBsonSerializer to perform custom serialization. I haven't found any documentation for writing one of these so have just tried to infer from the driver source code.
I have run into a problem whereby when the Deserialize method is called on the serializer, the CurrentBsonType is set to EndOfDocument rather than the start as I am expecting. I wrote the equivalent in C# just to make sure it wasn't some F# weirdness, but the problem persists. The serialization part seems to work fine and is queryable from the shell. Here is the sample code:
class Calendar {
public string Id { get; private set; }
public DateTime[] Holidays { get; private set; }
public Calendar(string id, DateTime[] holidays) {
Id = id;
Holidays = holidays;
}
}
class CalendarSerializer : BsonBaseSerializer {
public override void Serialize(BsonWriter bsonWriter, Type nominalType, object value, IBsonSerializationOptions options) {
var calendar = (Calendar) value;
bsonWriter.WriteStartDocument();
bsonWriter.WriteString("_id", calendar.Id);
bsonWriter.WriteName("holidays");
var ser = new ArraySerializer<DateTime>();
ser.Serialize(bsonWriter, typeof(DateTime[]), calendar.Holidays, null);
bsonWriter.WriteEndDocument();
}
public override object Deserialize(BsonReader bsonReader, Type nominalType, Type actualType, IBsonSerializationOptions options) {
if (nominalType != typeof(Calendar) || actualType != typeof(Calendar))
throw new BsonSerializationException();
if (bsonReader.CurrentBsonType != BsonType.Document)
throw new FileFormatException();
bsonReader.ReadStartDocument();
var id = bsonReader.ReadString("_id");
var ser = new ArraySerializer<DateTime>();
var holidays = (DateTime[])ser.Deserialize(bsonReader, typeof(DateTime[]), null);
bsonReader.ReadEndDocument();
return new Calendar(id, holidays);
}
public override bool GetDocumentId(object document, out object id, out Type idNominalType, out IIdGenerator idGenerator) {
var calendar = (Calendar) document;
id = calendar.Id;
idNominalType = typeof (string);
idGenerator = new StringObjectIdGenerator();
return true;
}
public override void SetDocumentId(object document, object id) {
throw new NotImplementedException("SetDocumentId is not implemented");
}
}
This blows up with FileFormatException in Deserialize when the CurrentBsonType is not Document. I am using the latest version 1.4 of the driver source.
I figured this out in the end. I should have used bsonReader.GetCurrentBsonType() instead of bsonReader.CurrentBsonType. This reads the BsonType in from the buffer rather than just looking at the last thing there. I also fixed a subsequent bug derserializing. The updated method looks like this:
public override object Deserialize(BsonReader bsonReader, Type nominalType, Type actualType, IBsonSerializationOptions options) {
if (nominalType != typeof(Calendar) || actualType != typeof(Calendar))
throw new BsonSerializationException();
if (bsonReader.GetCurrentBsonType() != BsonType.Document)
throw new FileFormatException();
bsonReader.ReadStartDocument();
var id = bsonReader.ReadString("_id");
bsonReader.ReadName();
var ser = new ArraySerializer<DateTime>();
var holidays = (DateTime[])ser.Deserialize(bsonReader, typeof(DateTime[]), null);
bsonReader.ReadEndDocument();
return new Calendar(id, holidays);
}

Get another property's value from ModelMetaData

I'm trying to get to another property's value from within the GetClientValidationRules method of a custom validation attribute.
Here is my attempt (based on Darin's response on another question):
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(
ModelMetadata metadata, ControllerContext context)
{
var parentType = metadata.ContainerType;
var parentMetaData = ModelMetadataProviders.Current
.GetMetadataForType(null, parentType);
var parentMetaData = ModelMetadataProviders.Current
.GetMetadataForProperties(context.Controller.ViewData.Model, parentType);
var otherProperty = parentMetaData.FirstOrDefault(p =>
p.PropertyName == "SomeProperty");
var otherValue = otherProperty.Model;
var rule = new ModelClientValidationRule
{
ValidationType = "customvalidatorattribute",
ErrorMessage = this.FormatErrorMessage(metadata.GetDisplayName()),
};
yield return rule;
}
However, when trying to set otherValue, I get:
System.Reflection.TargetException: Object does not match target type.
The problem is that you are not passing in the bound model. Change the following two lines:
var parentMetaData = ModelMetadataProviders.Current
.GetMetadataForProperties(context.Controller.ViewData.Model, parentType);
var otherValue = (string)parentMetaData.FirstOrDefault(p =>
p.PropertyName == "SomeProperty").Model;
This will get the full metadata (including the bound values) from the current model.
#JeradRose, the problem with your TargetException is because of this line:
var parentMetaData = ModelMetadataProviders.Current
.GetMetadataForProperties(context.Controller.ViewData.Model, parentType);
parentType needs to be context.Controller.ViewData.Model.GetType().
Probably you already fixed it, but I just got it today.

Asking for CAPTCHA only once with MvcReCaptcha

I'm using MvcRecaptcha to prevent bot posts for a complex unauthenticated client form on an ASP.NET MVC 2.0 site.
I only want to require one correct CAPTCHA entry from an unauthenticated client, even if some of the form's inputs are incorrect.
I have tried using a Session["CaptchaSuccess"] = true; variable to suppress Html.GenerateCaptcha() in my view following a successful entry, but the presence of the [CaptchaValidator] attribute on my [HttpPost] view causes an error because it naturally requires some ReCaptcha form inputs.
What is the simplest way to achieve this reliably, including on mobile browsers?
Solved by modifying the [CaptchaValidatorAttribute] OnActionExecuting method, where CaptchaSuccessFieldKey refers to the constant string value "CaptchaSuccess":
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
bool? bCaptchaSuccess = filterContext.HttpContext.Session[CaptchaSuccessFieldKey] as bool?;
if (bCaptchaSuccess.HasValue && bCaptchaSuccess.Value)
{
filterContext.ActionParameters["captchaValid"] = true;
}
else
{
var captchaChallengeValue = filterContext.HttpContext.Request.Form[ChallengeFieldKey];
var captchaResponseValue = filterContext.HttpContext.Request.Form[ResponseFieldKey];
var captchaValidtor = new Recaptcha.RecaptchaValidator
{
PrivateKey = ConfigurationManager.AppSettings["ReCaptchaPrivateKey"],
RemoteIP = filterContext.HttpContext.Request.UserHostAddress,
Challenge = captchaChallengeValue,
Response = captchaResponseValue
};
var recaptchaResponse = captchaValidtor.Validate();
// this will push the result value into a parameter in our Action
filterContext.ActionParameters["captchaValid"] = recaptchaResponse.IsValid;
}
base.OnActionExecuting(filterContext);
// Add string to Trace for testing
//filterContext.HttpContext.Trace.Write("Log: OnActionExecuting", String.Format("Calling {0}", filterContext.ActionDescriptor.ActionName));
}

Resources