2

I am having a property in my View Model which can accept integer and nullable values:

    [Display(Name = "Code Postal")]
    public int? CodePostal { get; set; }

When I type in string values, how can display another message than the default one:

The field Code Postal must be a number.

Thanks

Pawel Krakowiak
  • 9,940
  • 3
  • 37
  • 55
dtjmsy
  • 2,664
  • 9
  • 42
  • 62

3 Answers3

1

Alternative - rewrite resource strings

The easiest way is to replace default validation resource strings. This other SO answer will help you with that.

But you have to remember that these strings will them be used on all of your models not just particular property of some class.


Note: According to Darin (and me not testing the code) I'm striking part of my answer. The simplified approach by changing resource strings still stands. I've done that myself and I know it works.

Regular expression attribute

Add an additional attribute to your property:

[Display(Name = "Code Postal")]
[RegularExpression("\d+", ErrorMessage = "I'm now all yours...")]
public int? CodePostal { get; set; }

Even though you set regular expression on a non-string property this should still work. If we look at validation code it's like this:

public override bool IsValid(object value)
{
    this.SetupRegex();
    string text = Convert.ToString(value, CultureInfo.CurrentCulture);
    if (string.IsNullOrEmpty(text))
    {
        return true;
    }

    Match match = this.Regex.Match(text);
    return match.Success && match.Index == 0 && match.Length == text.Length;
}

As we can see, this validator automatically converts the value to string. So if your value is a number it doesn't really matter because it will be converted to a string and validated by your regular expression.

Community
  • 1
  • 1
Robert Koritnik
  • 103,639
  • 52
  • 277
  • 404
  • Brilliant, just miss the @ in front of RegularExpression(@. Million thanks Robert – dtjmsy Jun 21 '12 at 08:43
  • 2
    Except that.... Did you notice the error message you got inside the view when the model was invalid? It's not `I'm now all yours...`. The error message you will see will be the default one. See my answer to understand why. The `RegularExpression` works only on string types. It's meaningless to decorate an `int?` type with it. It is the default model binder which adds this default error message when parsing the request and unless you override this model binder you will be stuck. – Darin Dimitrov Jun 21 '12 at 08:51
  • @DarinDimitrov I edited my answer. Checking at regular expression attribute this should still work since `IsValid` method code converts data to a string before validating. But it's true. I haven't tested this. The *other* validator may jump in before regular expression validator hence still showing default error. In that case the second alternative would help. – Robert Koritnik Jun 21 '12 at 09:15
  • @RobertKoritnik, did you try it? Because I have. And guess what? It doesn't work. Actually I know that it doesn't work in advance but if you don't believe, well, try it out. As far as the second alternative is concerned it works but you modify the error message for all integer properties in the project without the possibility to have a custom error message which differs depending on the context. – Darin Dimitrov Jun 21 '12 at 09:16
  • @DarinDimitrov: As said. I haven't... And I believe you that `CanBeAssigned` or however validator's method is called (it is something similar) is called before additional attributes are being executed. – Robert Koritnik Jun 21 '12 at 09:18
  • In this case you could simply strike out the first part of your answer (as you already did previously) as it doesn't provide correct information and might lead people to believe that it works. Thanks. – Darin Dimitrov Jun 21 '12 at 09:20
  • @DarinDimitrov: But resource rewriting part still stands. I've done that myself and I assure you that it works. :) Without custom validation attributes. – Robert Koritnik Jun 21 '12 at 09:21
  • Absolutely. The resource part stands. And it works :-) Except that, ... it's not flexible enough to allow different error messages per model property which in a real world application is almost always a necessity. But you have already mentioned this limitation so it's fine. – Darin Dimitrov Jun 21 '12 at 09:22
  • @Darin: That is true, but if you don't put any validation attributes on your property you still won't be able to customize them. But when you do, you can provide a custom error message anyway. So per-property customization doesn't stand as per your comment. – Robert Koritnik Jun 21 '12 at 09:24
  • But in my solution I presented an attribute that could be used to customize the error message per-property. I don't understand what you mean by `per-property customization doesn't stand as per your comment`. I have illustrated a step by step guide on how to achieve exactly that: per-property error message customization for integral data types. – Darin Dimitrov Jun 21 '12 at 09:30
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/12852/discussion-between-robert-koritnik-and-darin-dimitrov) – Robert Koritnik Jun 21 '12 at 11:45
  • @DarinDimitrov: I was referring to your statement: *it's not flexible enough to allow different error messages per model property*. If you just have a model property without validation attributes (as OP does) then what you get is default validation message. Your great solution requires putting attributes on properties. My solution with resource changes doesn't require that. Per model property error message customization has always been done by adding particular validation attributes. But that's beyond the scope of this question. – Robert Koritnik Jun 21 '12 at 11:51
1

You could write a metadata aware attribute:

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class MustBeAValidIntegerAttribute : Attribute, IMetadataAware
{
    public MustBeAValidIntegerAttribute(string errorMessage)
    {
        ErrorMessage = errorMessage;
    }

    public string ErrorMessage { get; private set; }

    public void OnMetadataCreated(ModelMetadata metadata)
    {
        metadata.AdditionalValues["mustbeavalidinteger"] = ErrorMessage;
    }
}

and a custom model binder that uses this attribute because it is the default model binder that adds the hardcoded error message you are seeing when it binds those integral types from the request:

public class NullableIntegerModelBinder: DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        if (!bindingContext.ModelMetadata.AdditionalValues.ContainsKey("mustbeavalidinteger"))
        {
            return base.BindModel(controllerContext, bindingContext);
        }

        var mustBeAValidIntegerMessage = bindingContext.ModelMetadata.AdditionalValues["mustbeavalidinteger"] as string;
        if (string.IsNullOrEmpty(mustBeAValidIntegerMessage))
        {
            return base.BindModel(controllerContext, bindingContext);
        }

        var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        if (value == null)
        {
            return null;
        }

        try
        {
            return value.ConvertTo(typeof(int?));
        }
        catch (Exception)
        {
            bindingContext.ModelState.AddModelError(bindingContext.ModelName, mustBeAValidIntegerMessage);
            bindingContext.ModelState.SetModelValue(bindingContext.ModelName, value);
        }

        return null;
    }
}

which will be registered in Application_Start:

ModelBinders.Binders.Add(typeof(int?), new NullableIntegerModelBinder());

From this moment on things get pretty standard.

View model:

public class MyViewModel
{
    [Display(Name = "Code Postal")]
    [MustBeAValidInteger("CodePostal must be a valid integer")]
    public int? CodePostal { get; set; }
}

Controller:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        return View(new MyViewModel());
    }

    [HttpPost]
    public ActionResult Index(MyViewModel model)
    {
        return View(model);
    }
}

View:

@model MyViewModel

@using (Html.BeginForm())
{
    @Html.EditorFor(x => x.CodePostal)
    @Html.ValidationMessageFor(x => x.CodePostal)
    <button type="submit">OK</button>
}
Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • 2
    Seriously? So much work just to get rid of the default error message that gets attached to nullable integers? Things like that make me hate .NET at times. :/ – Pawel Krakowiak Jan 30 '13 at 12:07
  • It doesn't work for me. The problem is that `NullableIntegerModelBinder.BindModel()` is called before `MustBeAValidIntegerAttribute.OnMetadataCreated()` which results in the `DefaultModelBinder`'s implementation being always used. – Pawel Krakowiak Jan 30 '13 at 13:00
  • Correction: it does work, but only for server side validation. If you have client side validation enabled you will still get the `DefaultModelBinder`'s error message. – Pawel Krakowiak Jan 30 '13 at 13:06
  • It's normal that it doesn't work with client side validation. I didn't implement it. If you need to support client side validation you will need to implement the same logic on the client. – Darin Dimitrov Jan 30 '13 at 14:09
  • What I mean is that if you have ASP.NET MVC client side validation / unobtrusive validation enabled the default implementation is always on, unless you disable it yourself. This does the trick: `$(selector).rules('remove', 'number')`, other client side rules will still work. If you just add a custom rule the default rule will still be active and you will still get that error message AND your custom message. Just to be clear, in order for this to fully work it needs: 1) your solution, 2) custom client side validator, 3) removal of the default client side validator. – Pawel Krakowiak Jan 30 '13 at 14:45
1

It's little disappointing to see about the amount of work we have to do when all we want is a custom error message for the implicit validations done by the default model binder. The reason is the DefaultModelBinder hides some important methods as private especially the GetValueInvalidResource and GetValueRequiredResource. I hope they will take care of this in future.

I was trying to give a generic solution for the problem avoiding to create model binders for every type.

Honestly I haven't tested the below implementation in all the cases(ex. when binding collections) but did in basic levels.

So here is the approach.

We have two custom attributes that helps to pass custom error message for our custom model binder. We could have a base class but that's fine.

public class PropertyValueInvalidAttribute: Attribute
{
    public string ErrorMessage { get; set; }

    public PropertyValueInvalid(string errorMessage)
    {
        ErrorMessage = errorMessage;
    }
}

public class PropertyValueRequiredAttribute: Attribute
{
    public string ErrorMessage { get; set; }

    public PropertyValueRequired(string errorMessage)
    {
        ErrorMessage = errorMessage;
    }
}

Here is the model binder this is generic and independent of types and takes care of customizing error messages for both required and invalid validations.

public class ExtendedModelBinder : DefaultModelBinder
{
    protected override void SetProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value)
    {
        base.SetProperty(controllerContext, bindingContext, propertyDescriptor, value);

        if (propertyDescriptor.Attributes.OfType<PropertyValueInvalidAttribute>().Any())
        {
            var attr = propertyDescriptor.Attributes.OfType<PropertyValueInvalidAttribute>().First();

            foreach (ModelError error in bindingContext.ModelState[propertyDescriptor.Name]
            .Errors.Where(err => String.IsNullOrEmpty(err.ErrorMessage) && err.Exception != null)
            .ToList())
            {
                for (Exception exception = error.Exception; exception != null; exception = exception.InnerException)
                {
                    if (exception is FormatException)
                    {
                        bindingContext.ModelState[propertyDescriptor.Name].Errors.Remove(error);
                        bindingContext.ModelState[propertyDescriptor.Name].Errors.Add(attr.ErrorMessage);
                        break;
                    }
                }
            }
        }
    }

    protected override void OnPropertyValidated(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value)
    {
        if (propertyDescriptor.Attributes.OfType<PropertyValueRequiredAttribute>().Any())
        {
            var attr = propertyDescriptor.Attributes.OfType<PropertyValueRequiredAttribute>().First();

            var isTypeAllowsNullValue = (!propertyDescriptor.PropertyType.IsValueType || Nullable.GetUnderlyingType(propertyDescriptor.PropertyType) != null);

            if (value == null && !isTypeAllowsNullValue)
            {
                bindingContext.ModelState[propertyDescriptor.Name].Errors.Clear();
                bindingContext.ModelState.AddModelError(propertyDescriptor.Name, attr.ErrorMessage);
            }
        }

        base.OnPropertyValidated(controllerContext, bindingContext, propertyDescriptor, value);
    }
}

We are overriding the OnPropertyValidated method just to override the implicit required error message thrown by the default model binder, and we are overriding the SetProperty just to use our own message when the type is not valid.

Set our custom binder as the default in Global.asax.cs

ModelBinders.Binders.DefaultBinder = new ExtendedModelBinder();

And you can decorate your properties like this..

[PropertyValueRequired("this field is required")]
[PropertyValueInvalid("type is invalid")]
[Display(Name = "Code Postal")]
public int? CodePostal { get; set; }
VJAI
  • 32,167
  • 23
  • 102
  • 164
  • You do realise that **by convention** all attribute types should be suffixed by the Word Attribute... And please provide a simplified example that OP can use directly in their code. BTW: nice solution. – Robert Koritnik Jun 21 '12 at 11:53