【问题标题】:ASP.net MVC - Custom attribute error message with nullable propertiesASP.net MVC - 具有可为空属性的自定义属性错误消息
【发布时间】:2012-06-21 08:34:15
【问题描述】:

我的视图模型中有一个可以接受整数和可为空值的属性:

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

当我输入字符串值时,如何显示除默认消息之外的另一条消息:

The field Code Postal must be a number.

谢谢

【问题讨论】:

    标签: asp.net-mvc asp.net-mvc-3 viewmodel


    【解决方案1】:

    您可以编写元数据感知属性:

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

    还有一个使用此属性的自定义模型绑定器,因为它是默认模型绑定器,它添加了您在绑定请求中的这些整数类型时看到的硬编码错误消息:

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

    将在Application_Start注册:

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

    从这一刻开始,事情变得非常标准。

    查看模型:

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

    控制器:

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

    查看:

    @model MyViewModel
    
    @using (Html.BeginForm())
    {
        @Html.EditorFor(x => x.CodePostal)
        @Html.ValidationMessageFor(x => x.CodePostal)
        <button type="submit">OK</button>
    }
    

    【讨论】:

    • 认真的吗?这么多工作只是为了摆脱附加到可空整数的默认错误消息?这样的事情有时让我讨厌 .NET。 ://
    • 它对我不起作用。问题是NullableIntegerModelBinder.BindModel()MustBeAValidIntegerAttribute.OnMetadataCreated() 之前被调用,这导致DefaultModelBinder 的实现总是被使用。
    • 更正:它确实有效,但仅用于服务器端验证。如果您启用了客户端验证,您仍然会收到 DefaultModelBinder 的错误消息。
    • 不支持客户端验证是正常的。我没有实施。如果您需要支持客户端验证,则需要在客户端上实现相同的逻辑。
    • 我的意思是,如果您启用了 ASP.NET MVC 客户端验证/非侵入式验证,则默认实现始终处于启用状态,除非您自己禁用它。这可以解决问题:$(selector).rules('remove', 'number'),其他客户端规则仍然有效。如果您只是添加自定义规则,则默认规则仍将处于活动状态,您仍将收到该错误消息和您的自定义消息。需要明确的是,为了使其充分发挥作用,它需要:1)您的解决方案,2)自定义客户端验证器,3)删除默认客户端验证器。
    【解决方案2】:

    替代方案 - 重写资源字符串

    最简单的方法是替换默认的验证资源字符串。 这个other SO answer 会帮助你。

    但您必须记住,这些字符串将用于您的所有模型,而不仅仅是某个类的特定属性。


    注意:根据 Darin(而且我没有测试代码)的说法,我的回答很引人注目。通过更改资源字符串的简化方法仍然有效。我自己做过,我知道它有效。

    罢工>

    正则表达式属性

    为您的属性添加一个附加属性:

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

    即使您在非字符串属性上设置了正则表达式,这仍然可以工作。如果我们查看验证代码,它是这样的:

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

    正如我们所见,这个验证器自动将值转换为字符串。因此,如果您的值是一个数字,那并不重要,因为它将被转换为字符串并由您的正则表达式验证。

    【讨论】:

    • 太棒了,只是错过了正则表达式前面的@(@。百万感谢罗伯特
    • 除此之外......您是否注意到当模型无效时您在视图中收到的错误消息?这不是I'm now all yours...。您将看到的错误消息将是默认消息。请参阅我的答案以了解原因。 RegularExpression 仅适用于字符串类型。用它来装饰一个int? 类型是没有意义的。它是默认模型绑定器,它在解析请求时添加此默认错误消息,除非您覆盖此模型绑定器,否则您将被卡住。
    • @DarinDimitrov 我编辑了我的答案。检查正则表达式属性这应该仍然有效,因为IsValid 方法代码在验证之前将数据转换为字符串。但这是真的。我没有测试过这个。 other 验证器可能会在正则表达式验证器之前跳入,因此仍显示默认错误。在这种情况下,第二种选择会有所帮助。
    • @RobertKoritnik,你试过了吗?因为我有。你猜怎么着?它不起作用。其实我知道它不会提前工作,但如果你不相信,那么,试试吧。就第二种选择而言,它可以工作,但是您修改了项目中所有整数属性的错误消息,而不会出现根据上下文而有所不同的自定义错误消息。
    • @DarinDimitrov:如前所述。我还没有...而且我相信您在执行其他属性之前会调用 CanBeAssigned 或验证器的方法(类似的方法)。
    【解决方案3】:

    当我们想要的只是默认模型绑定器完成的隐式验证的自定义错误消息时,看到我们必须做的工作量有点令人失望。原因是DefaultModelBinder 隐藏了一些重要的私有方法,尤其是GetValueInvalidResourceGetValueRequiredResource。我希望他们以后会处理好这件事。

    我试图为避免为每种类型创建模型绑定器的问题提供通用解决方案。

    老实说,我没有在所有情况下(例如绑定集合时)测试以下实现,但在基本级别中测试过

    所以这是方法。

    我们有两个自定义属性,可以帮助为我们的自定义模型绑定器传递自定义错误消息。我们可以有一个基类,但这很好。

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

    这是模型绑定器,它是通用的,独立于类型,负责为必需和无效验证自定义错误消息。

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

    我们重写 OnPropertyValidated 方法只是为了重写默认模型绑定器抛出的隐式必需错误消息,并且我们重写 SetProperty 只是为了在类型无效时使用我们自己的消息。

    将我们的自定义活页夹设置为 Global.asax.cs 中的默认值

    ModelBinders.Binders.DefaultBinder = new ExtendedModelBinder();
    

    你可以像这样装饰你的财产..

    [PropertyValueRequired("this field is required")]
    [PropertyValueInvalid("type is invalid")]
    [Display(Name = "Code Postal")]
    public int? CodePostal { get; set; }
    

    【讨论】:

    • 您确实意识到按照惯例所有属性类型都应以 Word 属性为后缀...请提供一个简化的示例,OP 可以直接在其代码中使用。顺便说一句:很好的解决方案。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-10-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-09-01
    • 2013-02-06
    • 1970-01-01
    相关资源
    最近更新 更多