Reputation: 11375
I have a simple viewmodel with a char
property...
public char Character1 { get; set; }
The default model binding doesn't seem to be converting a space character (" ") into this property, resulting in the following ModelState error...
The Character1 field is required.
The html input element is created in javascript:
var input = $('<input type="password" name="Character' + i + '" id="input-' + i + '" data-val="true" data-val-custom maxlength="1"></input>');
[Required]
attribute on the property.AttemptedValue
property.ModelState.IsValid
returns false due to the above error.\0
after binding.Why is the space character not being bound to the char
property?
Update:
Changing the char
property to a string
binds as expected.
Upvotes: 4
Views: 2116
Reputation: 11
Hit this issue in .NET Core recently because the SimpleTypeModelBinder has the same check, so added the following:
using System; using Microsoft.AspNetCore.Mvc.ModelBinding; public class CharModelBinderProvider : IModelBinderProvider { /// <inheritdoc /> public IModelBinder GetBinder( ModelBinderProviderContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } if (context.Metadata.ModelType == typeof(char)) { return new CharModelBinder(); } return null; } } using System; using System.ComponentModel; using System.Runtime.ExceptionServices; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.ModelBinding; /// <inheritdoc /> /// <summary> /// An <see cref="T:Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinder" /> for char. /// </summary> /// <remarks> /// Difference here is that we allow for a space as a character which the <see cref="T:Microsoft.AspNetCore.Mvc.ModelBinding.SimpleTypeModelBinder" /> does not. /// </remarks> public class CharModelBinder : IModelBinder { private readonly TypeConverter _charConverter; public CharModelBinder() { this._charConverter = new CharConverter(); } /// <inheritdoc /> public Task BindModelAsync( ModelBindingContext bindingContext) { if (bindingContext == null) { throw new ArgumentNullException(nameof(bindingContext)); } var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName); if (valueProviderResult == ValueProviderResult.None) { // no entry return Task.CompletedTask; } bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult); try { var value = valueProviderResult.FirstValue; var model = this._charConverter.ConvertFrom(null, valueProviderResult.Culture, value); this.CheckModel(bindingContext, valueProviderResult, model); return Task.CompletedTask; } catch (Exception exception) { var isFormatException = exception is FormatException; if (!isFormatException && exception.InnerException != null) { // TypeConverter throws System.Exception wrapping the FormatException, so we capture the inner exception. exception = ExceptionDispatchInfo.Capture(exception.InnerException).SourceException; } bindingContext.ModelState.TryAddModelError(bindingContext.ModelName, exception, bindingContext.ModelMetadata); // Were able to find a converter for the type but conversion failed. return Task.CompletedTask; } } protected virtual void CheckModel( ModelBindingContext bindingContext, ValueProviderResult valueProviderResult, object model) { // When converting newModel a null value may indicate a failed conversion for an otherwise required model (can't set a ValueType to null). // This detects if a null model value is acceptable given the current bindingContext. If not, an error is logged. if (model == null && !bindingContext.ModelMetadata.IsReferenceOrNullableType) { bindingContext.ModelState.TryAddModelError( bindingContext.ModelName, bindingContext.ModelMetadata.ModelBindingMessageProvider.ValueMustNotBeNullAccessor(valueProviderResult.ToString())); } else { bindingContext.Result = ModelBindingResult.Success(model); } } }
And within the Startup:
serviceCollection.AddMvc(options => { options.ModelBinderProviders.Insert(0, new CharModelBinderProvider()); })
Upvotes: 1
Reputation: 49095
The reason is simple, char
is defined as a value-type (struct
) while string
is defined as a reference type (class
). This means that char
is non-nullable and must have a value.
That's why the DefaultModelBinder
(that you're probably using) is automatically sets the validation metadata for this property as required
even though you didn't add the [Required]
attribute.
Here's the source for ModelMetaData.cs
(line 58):
_isRequired = !TypeHelpers.TypeAllowsNullValue(modelType);
So you end up with the ModelMetaData.Required
for your Character1
property set to true
.
However, you can explicitly configure the DataAnnotationsModelValidatorProvider
not to automatically set value-types as required
using the following:
DataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes = false;
See MSDN
Upvotes: 2
Reputation: 11375
Ok I have found the offending code in System.Web.Mvc.ValueProviderResult
:
private static object ConvertSimpleType(CultureInfo culture, object value, Type destinationType)
{
if (value == null || destinationType.IsInstanceOfType(value))
return value;
string str = value as string;
if (str != null && string.IsNullOrWhiteSpace(str))
return (object) null;
...
}
I'm unsure if this is a bug or not.
Upvotes: 1
Reputation: 637
I think its a failing of the DefaultModelBinder. If you use FormCollection in your action, the string comes back as a space.
This IModelBinder implementation shows how the default model binder is behaving, and gives a possible solution:
public class CharModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var dmb = new DefaultModelBinder();
var result = dmb.BindModel(controllerContext, bindingContext);
// ^^ result == null
var rawValueAsChar = bindingContext.ValueProvider.GetValue(bindingContext.ModelName).ConvertTo(typeof(char));
// ^^ rawValueAsChar == null
var rawValueAsString = bindingContext.ValueProvider.GetValue(bindingContext.ModelName).AttemptedValue;
if(!string.IsNullOrEmpty(rawValueAsString))
return rawValueAsString.ToCharArray()[0];
return null;
}
}
Register it in your Global.asax with:
ModelBinders.Binders.Add(typeof(char), new CharModelBinder());
Upvotes: 2