Custom ValidationAttribute trigger on different client property change - asp.net-mvc

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.

Related

BreezeJs: Error: Unable to convert this endpoint to an IQueryable

I have asp.net core 1.1.0 project and trying the model of CodeCamp sample. In this we a controller which return Lookup data as below :
[BreezeController]
public class BreezeController : ApiController
{
[HttpGet]
public object Lookups()
{
var rooms = _repository.Rooms;
var tracks = _repository.Tracks;
var timeslots = _repository.TimeSlots;
return new { rooms, tracks, timeslots };
}
And the above Lookups is called in dataContext.js as below:
function getLookups() {
return EntityQuery.from('Lookups')
.using(manager).execute()
.to$q(querySucceeded, _queryFailed);
function querySucceeded(data) {
log('Retrieved [Lookups]', data, true);
return true;
}
}
Now, I am trying to follow same as above in my project its giving me error as below :
Get http://Localhost:12345//breeze/demo/Lookups 500(Internal server error)
Uncaught (in promise)
Error: Unable to convert this endpoint to an IQueryable
Any solution to above issue...its working fine in John Papa's Code camper project. My web api lookups code is working fine if I run it in browser but not with breezejs.
Breeze's .NET Core implementation expects a hidden first parameter. It uses this to perform the IQueryable filtering of a REST operation. For example, if you have an operation that looks like this:
[HttpGet]
public IQueryable<Order> Orders()
And you wanted to get all Orders with the Status of 123 that Cost less than $10 then the first parameter would be something like this:
{
"where":{
"Status":123,
"Cost":{
"lt":10
}
},
"select":[
"OrderId"
]
}
This is a significant departure from the previous version. The client can be changed to pass parameters compatable with this by adding:
breeze.config.initializeAdapterInstance("uriBuilder", "json");
I added this to my fetchMetadata call.
However, this causes a lot of problems if you have specific get methods with parameters and you want to call it from Swagger or another application.
Something Like this:
[HttpGet]
public IQueryable<Order> GetOrdersByStatusAndLessThanCost(int status, int cost)
Will generate a url like this:
GetOrdersByStatusAndLessThanCost?status=123&cost=10
Breeze assumes that the first parameter (status=123) is its JSON. So it tries to parse it out.
This gives the first most common error with migrating Breeze to .NET Core:
This EntityQuery ctor requires a valid json string. The following is not json: status=123
If you happened to pass in Json, but the result is not an IQueryable, then you will get this error:
Unable to convert this endpoint to an IQueryable
The key to all of this is to give breeze what it is looking for. For the example above the following URL would work:
GetOrdersByStatusAndLessThanCost?{}&status=123&cost=10
Note the added {}& as the first parameter. This tells breeze that there is not anything expected as far as filtering goes.
To get this working for Swashbuckle (and by extension Swagger\Open API) add this to your Startup.cs ConfigureServices method inside the call to services.AddSwaggerGen(c =>:
c.OperationFilter<AddBreezeParameter>();
And then create the file that is needed for that:
public class AddBreezeParameter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
if (context.MethodInfo.ReturnType.Name.StartsWith("IQueryable"))
{
if (operation.Parameters == null)
{
operation.Parameters = new List<OpenApiParameter>();
}
var exampleString = "<br>\":{}," +
"<br> \"where\":{" +
"<br> \"Status\":123," +
"<br> \"Cost\":{" +
"<br> \"lt\":10" +
"<br> }" +
"<br> }," +
"<br> \"select\":[" +
"<br> \"OrderId\"," +
"<br> \"OrderDateTime\"" +
"<br> ]" +
"<br>}";
var breezeJsonParam = new OpenApiParameter
{
Name = "{\"breezeJson",
In = ParameterLocation.Query,
AllowEmptyValue = true,
Description =
"Json used to query a REST resource. <br>Due to Breeze's nonstandardness and Swashbuckle's not able to customize to allow for it, this MUST start with \":{} and end with } In between those you can put your query if it is appropriate. If you do you must add a comma after the starting value and before you value. Here is an example: " +
exampleString,
AllowReserved = true
};
var schema = new OpenApiSchema {Type = "json", Default = new OpenApiString("\":{}}")};
breezeJsonParam.Schema = schema;
operation.Parameters.Insert(0, breezeJsonParam);
}
else
{
if (operation.Parameters == null)
{
operation.Parameters = new List<OpenApiParameter>();
}
var breezeJsonParam = new OpenApiParameter();
// Breeze looks for the first parameter so it can do an IQueryable Filter on it.
// We want it to not have anything for that parameter if it is not an IQueryable.
breezeJsonParam.Name = "{}&";
breezeJsonParam.In = ParameterLocation.Query;
breezeJsonParam.Description = "Do NOT modify this parameter. (It is here for Breeze compatibility.)";
var schema = new OpenApiSchema {Example = new OpenApiString(" ")};
//var schema = new OpenApiSchema {Type = "string", Default = new OpenApiString("\":{}}")};
breezeJsonParam.Schema = schema;
operation.Parameters.Insert(0, breezeJsonParam);
}
}
}

How to assign default value for dropdown list in Umbraco?

I have created custom data type based on built-in dropdown list, but cannot figure out how to specify default value for the list. The default value is always blank:
The default dropdown does not support default value
There is two way of achieving what you want
create your own dropdown datatype (or use a plugin someone else has made - I am not sure which one support it, but maybe have a look at nuPickers )
since it is your custom made you can control it. More about how to create one checkout doc Tutorial - Creating a property editor
use a web api handler to intercept the call of getting the content value - and set a default value to your property if it is empty (null)
below is some un-tested code:
first create the web api handler
public class SetDropdownDefaultHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync
(HttpRequestMessage request, CancellationToken cancellationToken)
{
var response = await base.SendAsync(request, cancellationToken);
var url = request?.RequestUri?.AbsolutePath.ToLower;
// only process when a create (getempty) or editing a specific content (getbyid)
if (url == "/umbraco/backoffice/umbracoapi/content/getempty"
|| url == "/umbraco/backoffice/umbracoapi/content/getbyid")
{
var content = (ObjectContent)response.Content;
var data = content?.Value as PagedResult<ContentItemBasic<ContentPropertyBasic, IContent>>;
if (data?.Items != null)
{
var tempResult = data?.Items?.ToList();
foreach (var item in tempResult)
{
foreach (var prop in item?.Properties?.Where(p => p?.Editor == "Umbraco.DropDown"))
{
var propStr = prop.Value?.ToString();
if (!propStr.IsNullOrWhiteSpace())
{
// set your default value if it is empty
prop.Value = "your default option prevalue id";
}
}
}
data.Items = tempResult;
}
}
return response;
}
}
then register it at started event
public class UmbracoEvent : ApplicationEventHandler
{
protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
{
GlobalConfiguration.Configuration.MessageHandlers.Add(new SetDropdownDefaultHandler());
}
}
your problem maybe you don't know your prevalueid - you can look it up in db or you could use datatype service to get the datatype prevalues then decide which to put as default
Look at: FieldType.DropDownList in the fieldTypes folder.
Replace:<option value=""></option>
With:
var settings = Model.AdditionalSettings;
<option value="">#settings["DefaultValue"]</option>
Then ensure you set the default value property in your dropdown list in the Umbraco Forms backoffice for the given form

$watch not invoked on input change when input doesn't match the pattern

I'm using ngVal to decorate my inputs with AngularJS directives based on the ASP.NET MVC Data Annotations.
My model has the following annotations:
[Display(Name="Test Number")]
[Required]
[RegularExpression(#"^[\d]{6}$", ErrorMessage = "The value must be six numbers.")]
public string TestNumber { get; set; }
ngVal is then used to display the error messages associated with the input field.
From what I can determine (with a debug breakpoint in the $watch function) is that the function that should be called on modification of the value in scope is not being called unless the pattern is matched. Here's how the ngVal directive sets up the scope.$watch:
var ngval = angular.module('ngval', []);
ngval.directive('ngval', ['$parse', function ($parse) {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, iElm, iAttrs, ngModel) {
var messages = angular.fromJson(iAttrs.ngval);
var getErrors = function() {
var errors = [];
for (var prop in messages) {
if (ngModel.$error[prop])
errors.push({ validator: prop, message: messages[prop] });
}
return errors;
};
scope.$watch(function() {
return ngModel.$modelValue;
}, function () {
ngModel.ngval = {
hasError: ngModel.$dirty && ngModel.$invalid,
errors: getErrors()
};
});
}
};
}]);
The result is that initially I see that the field is required but typing something invalid doesn't change the error to the one specified for the pattern - it still says the field is required. This is because the function given to the $watch isn't being invoked. If I enter 6 numbers, the field is valid and modifying the value after that shows the pattern mismatch message. The only exception to this is if I select the contents of the input box and erase it with one key press (del/backspace).
Is this normal behaviour; that scope.$watch isn't invoked if the value doesn't match the pattern?
EDIT: I've added a ng-change directive on the input and it behaves the same - it's not invoked if the input doesn't match the pattern. A demonstration of the behaviour can be seen in this plunker: http://plnkr.co/edit/krmQVk0giwCYGxdY5YNS?p=preview
The problem is that the watch is set up against the model, and the value of the property on the model is not set for input changes that do not match the ngPattern. The property on the model is only changed to the input value when it matches the pattern; otherwise the property is emptied (null, undefined or blank - I'm not sure.
The workaround I have in place is to watch the $error properties of the DOM element, which are updated by AngularJS. I added a formItem property to allow the name of the form and input element to be passed to the HTML Helper method so that it can add it to the markup. The watch is now set up as follows:
for (prop in messages) {
scope.$watch(iAttrs.formitem + ".$error."+ prop,
function() {
ngModel.ngval = {
hasError: ngModel.$dirty && ngModel.$invalid,
errors: getErrors()
};
});
}
I hope that helps someone out there with the same issue.

How do I access the value to compare when overriding GetClientValidationRules?

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;
}

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.

Resources