Why is WebDataBinder RegisterCustomEditor ignoring the Field parameter - spring-mvc

I'm writing an application using Spring Security Reactive and Spring WebFlux, which uses it's own binding implementation called WebExchangeDataBinder. I would like to encode passwords when new users sign up. I wrote a custom property editor for this, but it is only called if I remove the field designation from my InitBinder method--which is no good because then every String field in the User class is encoded.
#InitBinder
public void initBinder (WebDataBinder binder)
{
binder.registerCustomEditor(String.class,"password",encodingPropertyEditor);
}
How can I run this editor ONLY for the "password" field of the User constructor?

Custom field binding at least in Reactive applications is impossible for immutable objects. This can be verified by stepping into the code and seeing that in
org.springframework.web.reactive.result.method.annotation.ModelAttributeMethodArgumentResolver on line 256 when binder.convertIfNecessary(value, paramTypes[i], methodParam) is called, the paramName value is not passed in. Then typeConverterDelegate.convertIfNecessary is called with a null propertyName and when propertyEditorRegistry.findCustomEditor(requiredType, propertyName) is finally called then of course the editor won't be found for the field.

Related

Is it possible to validate a property using data-annotation asynchronously with ASP.NET Core?

I have an asp.net core 3.1 based project. I need to add a custom validation rule that will require a database call in ordered to determine the validity of the value.
For example, when creating a new user, I need to validate that there is no other username in the database with the same username before allowing the user to be created.
If I can create a custom attribute UniqueUsername, then I should be able to do something like this
public class UniqueUsername : ValidationAttribute
{
private readonly UserManager _manager = manager;
public UniqueUsername (UserManager manager)
{
_manager = manager;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
string username = value.ToString();
if(_manager.Exists(username))
{
return new ValidationResult("The username provided belong to a different user.");
}
return ValidationResult.Success;
}
}
But, _manager.Exists(username) is a synchronous call. I want to avoid blocking the main thread, so I want a way to call await _manager.ExistsAsync(username) instead to avoid blocking the main thread.
Is there a way to create an attribute that would validate a single property and write errors "if any" to the ModelState?
If this isn't possible using data-annotation, is there an different way to validate property while writting errors to ModelState so when ModelState.IsValid() or TryValidateModel(model) are called, the attribute is called?
The data-annotations you add in your model are meant to validate the data present within the model (although some can also be translated into your database). These are meant to be reused even if you were to use your models on a client framework.
You can use the ModelState functions you mentioned to make async calls to your database. If you want to remove the logic from your controller, you can create utility functions to do so.
If you are open to third-party libraries, there is a great and popular validations library called Fluent Validation. Here is a documentation on how you can make async calls with it: https://docs.fluentvalidation.net/en/latest/async.html

Replace default client side validation by custom one in ASP.NET MVC

I have a view model containing a DateTime property, for which I want to provide a text box using using a custom format (only month and year, "MM.YYYY"):
public class MyModel {
public DateTime? DateField {get; set;}
}
Formatting the value for the TextBox is easy (using the format string). I also have implemented a custom model binder to do the conversion and this works fine.
I still have a problem with client side validation: I can implement a custom validator deriving from ValidationAttribute which implements IClientValidatable and set up the corresponding jquery.validate adapters etc.
But MVC still adds the "default" validation attribute data-val-date (in addition to my custom validation attribute data-val-monthyeardate) to the input field, so the default check still applies and the input "MM.YYYY" is rejected.
Is there any way to suppress the default client side validation for a data type and replace it with a custom one (instead of "adding" the custom one)?
Since the framework doesn't let you override the real type with some custom attribute or even override the ModelMetadataProvider for specific types, you'll have to register your own global ModelMetadataProvider that fools the validator to think it's actually a string.
Something like:
public class MyModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
{
if (attributes.OfType<ExcludeCharAttribute>().Any())
modelType = typeof (String);
return base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName); ;
}
}
Register it using:
ModelMetadataProviders.Current = new MyModelMetadataProvider();
Still, since the core problem is a client-side problem, I would deal with it purely with client-side code. In your monthyeardate adapter you can force removal of the date validation (I can provide an example if you'll share your monthyeardate code).
See MSDN
This is hacky but one simple thing you could do is add:
$(function () {
$.validator.methods.date = function () { return true; };
});
So that the default data-val-date always returns true along with firing your custom date validation.
I know that this is a little older, and just in case someone doesn't think about it (and since I cannot leave comments yet), to enhance #AlexC response, you can add validation to to that statement.
For instance, I use moment.js for date validation (moment.js), and this allows you to add your own validation rules.
if (moment(a, "M/YYYY").isValid() || moment(a).isValid())
{
return true;
}
This will check to see if it is a regular date, and also in this case, if the date is in "M/YYYY" format. If one of these are true, it accepts the validation.

Adding additional fields in ApplicationUser class

I have a WebForms applicaiton using the new ASP.NET Identity. I've added a couple of additional fields in the class to allow for Email and a Boolean called IsOnLine.
I use migrations, to explicititly update the tables, and can see that my new fields are there. However, whenever I try to login now i get the following error:
Additional information: The 'IsOnLine' property on 'ApplicationUser' could not be set to a 'null' value. You must set this property to a non-null value of type 'System.Boolean'.
All the exmamples on the net relate to MVC, which i'm not using yet.
How can i fix this?
A bool cannot be null so that is what is set to in the database. If you want it to be null you will need to define it as a nullable bool using the ? operator.
public class ApplicationUser : IdentityUser
{
public bool? IsOnline { get; set; }
}
To update this information in the database take a look at this article on adding email confirmation to ASP.NET Identity. In this example there is a boolean flag IsConfirmed that is updated during the registration process, which will be similar to your IsOnline flag. Here is a snippet of the relevant code to update the database.
user.IsOnline = true;
DbSet<ApplicationUser> dbSet = context.Set<ApplicationUser>();
dbSet.Attach(user);
context.Entry(user).State = EntityState.Modified;
context.SaveChanges();
I prefer to avoid null values whenever possible, to determine if a property can accept nulls values it is better to think what that information represents in the domain problem. In this case i think it not make sense to have an undetermined value for IsOnline because the user is online or offline, not middle terms apply to this fact.
To resolve this problem with the database, taking advantage that you are using Code Migration , you can use the Seed method from Configuration class and assing the value(s) to your new fields that are not nullable.
Pseudo code:
Seed()
for each user
set user IsOnline to false

Fluent binding and conversions

When following the Tutorial for TipCalc's iOS UI, I noticed that the binding method described is obsolete(?) and decided to start using Fluent bindings like described here.
Everything gone fine except for one thing: the iOS slider on the tutorial uses a float value between 0 and 1, and the view model uses a int between 0 and 100. So, obviously, I need a conversion here.
Since it's a two-way-binding, how can bind it to be converted for ViewModel -> View and View -> ViewModel? (ideally with fluent binding)
Also, I'd like to know how can I register a conversion under a "name" to later reuse it. Like it seems to be done on this line.
I tried to search on MvvmCross repos for these named conversions but I didn't find anything like a list of the available conversions, there is such a thing?
Thanks a lot for any help!!
I noticed that the binding method described is obsolete(?)
The message attached to that method is:
[Obsolete("Please use SourceDescribed or FullyDescribed instead")]
So use SourceDescribed if you only want to describe the source, or FullyDescribed if you want to describe the source and target.
set.Bind(label).For(l => l.Text).SourceDescribed("'Hello ' + SourceText");
or:
set.Bind(label).FullyDescribed("Text 'Hello ' + SourceText");
Since it's a two-way-binding, how can bind it to be converted for ViewModel -> View and View -> ViewModel?
Two way converters implement both Convert and ConvertBack.
For example - see:
public class TwoWayConverter : MvxValueConverter<double, string>
{
protected override string Convert(double value, Type targetType, object parameter, CultureInfo culture)
{
return (value*value).ToString();
}
protected override double ConvertBack(string value, Type targetType, object parameter, CultureInfo culture)
{
double doubleValue;
double.TryParse(value, out doubleValue);
return Math.Sqrt(doubleValue);
}
}
from https://github.com/MvvmCross/MvvmCross-Tutorials/blob/master/ValueConversion/ValueConversion.Core/Converters/Converters.cs
I'd like to know how can I register a conversion under a "name" to later reuse it
The names are registered by reflection and convention - see a full description in https://github.com/MvvmCross/MvvmCross/wiki/Value-Converters#referencing-value-converters-in-touch-and-droid
a list of the available conversions
MvvmCross doesn't provide many by default - these are mainly app things.
The only built-in converters that I know of are for:
color
visible
language
command parameter
All of these are discussed in https://github.com/MvvmCross/MvvmCross/wiki/Value-Converters#the-mvx-visibility-valueconverters

How to Extend RemoteAttribute to make Entry to the ModelState?

MODEL
[Remote("ValidateDuplicateUsername", "Account", ErrorMessage = "Username is already taken")]
public string Username { get; set; }
CONTROLLER
ModelState.IsValid
The RequiredAttribute automatically adds entry to ModelState if the field is invalid.
And then it displays the error message in the view.
How can I also do that using the RemoteAttribute? so that when I call the ModelState.IsValid, it also validates the Remote Validation?
The RequiredAttribute automatically adds entry to ModelState if the field is invalid
No, you must have misunderstood something. The Required attribute doesn't add anything to the ModelState. Validation attributes do not have access to ModelState neither to HttpContext. They override the IsValid method and return true or false to indicate whether the model is valid or not. It is the default model binder that executes validation when trying to bind the model from the request that adds errors to ModelState. Data Annotations are designed to be MVC independent. They are in a separate assembly (System.ComponentModel.DataAnnotations) with the idea of using them for example in WPF, Silverlight, ... applications. So you understand that the notion of ModelState doesn't make sense.
So if you look at the IsValid implementation of the RemoteAttribute you will notice the following:
public override bool IsValid(object value)
{
return true;
}
That's the reason why the RemoteAttribute considers your model as valid when you post the form. You could write your custom remote validation attribute and override this method.

Resources