diff --git a/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/Metadata/IModelBindingMessageProvider.cs b/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/Metadata/IModelBindingMessageProvider.cs index 63bae2b9ab..7374a54646 100644 --- a/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/Metadata/IModelBindingMessageProvider.cs +++ b/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/Metadata/IModelBindingMessageProvider.cs @@ -30,5 +30,26 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata /// /// Default is "The value '{0}' is invalid.". Func ValueMustNotBeNullAccessor { get; } + + /// + /// Error message the model binding system adds when is of type + /// or and value is known. + /// + /// Default is "The value '{0}' is not valid for {1}.". + Func AttemptedValueIsInvalidAccessor { get; } + + /// + /// Error message the model binding system adds when is of type + /// or and value is unknown. + /// + /// Default is "The supplied value is invalid for {0}.". + Func UnknownValueIsInvalidAccessor { get; } + + /// + /// Fallback error message HTML and tag helpers display when a property is invalid but the + /// s have null s. + /// + /// Default is "The value '{0}' is invalid.". + Func ValueIsInvalidAccessor { get; } } } diff --git a/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/ModelStateDictionary.cs b/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/ModelStateDictionary.cs index bb27c24dff..7f211ab12c 100644 --- a/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/ModelStateDictionary.cs +++ b/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/ModelStateDictionary.cs @@ -267,11 +267,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding string errorMessage; if (entry == null) { - errorMessage = Resources.FormatModelError_InvalidValue_GenericMessage(name); + errorMessage = metadata.ModelBindingMessageProvider.UnknownValueIsInvalidAccessor(name); } else { - errorMessage = Resources.FormatModelError_InvalidValue_MessageWithModelValue( + errorMessage = metadata.ModelBindingMessageProvider.AttemptedValueIsInvalidAccessor( entry.AttemptedValue, name); } diff --git a/src/Microsoft.AspNet.Mvc.Abstractions/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Abstractions/Properties/Resources.Designer.cs index 3b4ee1a724..ed6b7026cd 100644 --- a/src/Microsoft.AspNet.Mvc.Abstractions/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Abstractions/Properties/Resources.Designer.cs @@ -282,38 +282,6 @@ namespace Microsoft.AspNet.Mvc.Abstractions return string.Format(CultureInfo.CurrentCulture, GetString("BindingSource_MustBeGreedy"), p0, p1); } - /// - /// The supplied value is invalid for {0}. - /// - internal static string ModelError_InvalidValue_GenericMessage - { - get { return GetString("ModelError_InvalidValue_GenericMessage"); } - } - - /// - /// The supplied value is invalid for {0}. - /// - internal static string FormatModelError_InvalidValue_GenericMessage(object p0) - { - return string.Format(CultureInfo.CurrentCulture, GetString("ModelError_InvalidValue_GenericMessage"), p0); - } - - /// - /// The value '{0}' is not valid for {1}. - /// - internal static string ModelError_InvalidValue_MessageWithModelValue - { - get { return GetString("ModelError_InvalidValue_MessageWithModelValue"); } - } - - /// - /// The value '{0}' is not valid for {1}. - /// - internal static string FormatModelError_InvalidValue_MessageWithModelValue(object p0, object p1) - { - return string.Format(CultureInfo.CurrentCulture, GetString("ModelError_InvalidValue_MessageWithModelValue"), p0, p1); - } - private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNet.Mvc.Abstractions/Resources.resx b/src/Microsoft.AspNet.Mvc.Abstractions/Resources.resx index d9ad44cf72..6368aa3629 100644 --- a/src/Microsoft.AspNet.Mvc.Abstractions/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Abstractions/Resources.resx @@ -168,10 +168,4 @@ The provided binding source '{0}' is not a greedy data source. '{1}' only supports greedy data sources. - - The supplied value is invalid for {0}. - - - The value '{0}' is not valid for {1}. - \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Internal/MvcCoreMvcOptionsSetup.cs b/src/Microsoft.AspNet.Mvc.Core/Internal/MvcCoreMvcOptionsSetup.cs index b2c9a58e95..4354ac41b7 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Internal/MvcCoreMvcOptionsSetup.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Internal/MvcCoreMvcOptionsSetup.cs @@ -31,6 +31,9 @@ namespace Microsoft.AspNet.Mvc.Internal messageProvider.MissingBindRequiredValueAccessor = Resources.FormatModelBinding_MissingBindRequiredMember; messageProvider.MissingKeyOrValueAccessor = Resources.FormatKeyValuePair_BothKeyAndValueMustBePresent; messageProvider.ValueMustNotBeNullAccessor = Resources.FormatModelBinding_NullValueNotValid; + messageProvider.AttemptedValueIsInvalidAccessor = Resources.FormatModelState_AttemptedValueIsInvalid; + messageProvider.UnknownValueIsInvalidAccessor = Resources.FormatModelState_UnknownValueIsInvalid; + messageProvider.ValueIsInvalidAccessor = Resources.FormatHtmlGeneration_ValueIsInvalid; // Set up ModelBinding options.ModelBinders.Add(new BinderTypeBasedModelBinder()); diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/EmptyModelMetadataProvider.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/EmptyModelMetadataProvider.cs index 5fb48e82ca..ea8635d795 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/EmptyModelMetadataProvider.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/EmptyModelMetadataProvider.cs @@ -40,6 +40,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding MissingBindRequiredValueAccessor = Resources.FormatModelBinding_MissingBindRequiredMember, MissingKeyOrValueAccessor = Resources.FormatKeyValuePair_BothKeyAndValueMustBePresent, ValueMustNotBeNullAccessor = Resources.FormatModelBinding_NullValueNotValid, + AttemptedValueIsInvalidAccessor = Resources.FormatModelState_AttemptedValueIsInvalid, + UnknownValueIsInvalidAccessor = Resources.FormatModelState_UnknownValueIsInvalid, + ValueIsInvalidAccessor = Resources.FormatHtmlGeneration_ValueIsInvalid, }; } } diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Metadata/ModelBindingMessageProvider.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Metadata/ModelBindingMessageProvider.cs index 33c6dea706..957827dde8 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Metadata/ModelBindingMessageProvider.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Metadata/ModelBindingMessageProvider.cs @@ -13,6 +13,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata private Func _missingBindRequiredValueAccessor; private Func _missingKeyOrValueAccessor; private Func _valueMustNotBeNullAccessor; + private Func _attemptedValueIsInvalidAccessor; + private Func _unknownValueIsInvalidAccessor; + private Func _valueIsInvalidAccessor; /// /// Initializes a new instance of the class. @@ -36,6 +39,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata MissingBindRequiredValueAccessor = originalProvider.MissingBindRequiredValueAccessor; MissingKeyOrValueAccessor = originalProvider.MissingKeyOrValueAccessor; ValueMustNotBeNullAccessor = originalProvider.ValueMustNotBeNullAccessor; + AttemptedValueIsInvalidAccessor = originalProvider.AttemptedValueIsInvalidAccessor; + UnknownValueIsInvalidAccessor = originalProvider.UnknownValueIsInvalidAccessor; + ValueIsInvalidAccessor = originalProvider.ValueIsInvalidAccessor; } /// @@ -91,5 +97,59 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata _valueMustNotBeNullAccessor = value; } } + + /// + public Func AttemptedValueIsInvalidAccessor + { + get + { + return _attemptedValueIsInvalidAccessor; + } + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _attemptedValueIsInvalidAccessor = value; + } + } + + /// + public Func UnknownValueIsInvalidAccessor + { + get + { + return _unknownValueIsInvalidAccessor; + } + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _unknownValueIsInvalidAccessor = value; + } + } + + /// + public Func ValueIsInvalidAccessor + { + get + { + return _valueIsInvalidAccessor; + } + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _valueIsInvalidAccessor = value; + } + } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs index 712a56ea3b..bfab63354e 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs @@ -475,7 +475,7 @@ namespace Microsoft.AspNet.Mvc.Core } /// - /// Unable to find the required services. Please add all the required services by calling '{0}.{1}' inside the call to '{4}' in the application startup code. + /// Unable to find the required services. Please add all the required services by calling '{0}.{1}' inside the call to '{2}' in the application startup code. /// internal static string UnableToFindServices { @@ -483,11 +483,11 @@ namespace Microsoft.AspNet.Mvc.Core } /// - /// Unable to find the required services. Please add all the required services by calling '{0}.{1}' inside the call to '{4}' in the application startup code. + /// Unable to find the required services. Please add all the required services by calling '{0}.{1}' inside the call to '{2}' in the application startup code. /// - internal static string FormatUnableToFindServices(object p0, object p1, object p4) + internal static string FormatUnableToFindServices(object p0, object p1, object p2) { - return string.Format(CultureInfo.CurrentCulture, GetString("UnableToFindServices"), p0, p1, p4); + return string.Format(CultureInfo.CurrentCulture, GetString("UnableToFindServices"), p0, p1, p2); } /// @@ -1066,6 +1066,54 @@ namespace Microsoft.AspNet.Mvc.Core return string.Format(CultureInfo.CurrentCulture, GetString("AcceptHeaderParser_ParseAcceptHeader_InvalidValues"), p0); } + /// + /// The value '{0}' is not valid for {1}. + /// + internal static string ModelState_AttemptedValueIsInvalid + { + get { return GetString("ModelState_AttemptedValueIsInvalid"); } + } + + /// + /// The value '{0}' is not valid for {1}. + /// + internal static string FormatModelState_AttemptedValueIsInvalid(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ModelState_AttemptedValueIsInvalid"), p0, p1); + } + + /// + /// The supplied value is invalid for {0}. + /// + internal static string ModelState_UnknownValueIsInvalid + { + get { return GetString("ModelState_UnknownValueIsInvalid"); } + } + + /// + /// The supplied value is invalid for {0}. + /// + internal static string FormatModelState_UnknownValueIsInvalid(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ModelState_UnknownValueIsInvalid"), p0); + } + + /// + /// The value '{0}' is invalid. + /// + internal static string HtmlGeneration_ValueIsInvalid + { + get { return GetString("HtmlGeneration_ValueIsInvalid"); } + } + + /// + /// The value '{0}' is invalid. + /// + internal static string FormatHtmlGeneration_ValueIsInvalid(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("HtmlGeneration_ValueIsInvalid"), p0); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNet.Mvc.Core/Resources.resx b/src/Microsoft.AspNet.Mvc.Core/Resources.resx index 16d7ca2608..cd6bcce648 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Core/Resources.resx @@ -1,17 +1,17 @@  - @@ -325,4 +325,13 @@ "Invalid values '{0}'." + + The value '{0}' is not valid for {1}. + + + The supplied value is invalid for {0}. + + + The value '{0}' is invalid. + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/ValidationMessageTagHelper.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/ValidationMessageTagHelper.cs index 3204c83405..89fdf1ec37 100644 --- a/src/Microsoft.AspNet.Mvc.TagHelpers/ValidationMessageTagHelper.cs +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/ValidationMessageTagHelper.cs @@ -64,11 +64,13 @@ namespace Microsoft.AspNet.Mvc.TagHelpers if (For != null) { - var tagBuilder = Generator.GenerateValidationMessage(ViewContext, - For.Name, - message: null, - tag: null, - htmlAttributes: null); + var tagBuilder = Generator.GenerateValidationMessage( + ViewContext, + For.ModelExplorer, + For.Name, + message: null, + tag: null, + htmlAttributes: null); if (tagBuilder != null) { diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/Internal/ValidationHelpers.cs b/src/Microsoft.AspNet.Mvc.ViewFeatures/Internal/ValidationHelpers.cs index 1ea1314701..8dfae9cf1c 100644 --- a/src/Microsoft.AspNet.Mvc.ViewFeatures/Internal/ValidationHelpers.cs +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/Internal/ValidationHelpers.cs @@ -3,27 +3,44 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using Microsoft.AspNet.Mvc.ModelBinding; namespace Microsoft.AspNet.Mvc.ViewFeatures.Internal { - internal static class ValidationHelpers + public static class ValidationHelpers { - public static string GetUserErrorMessageOrDefault(ModelError modelError, ModelStateEntry entry) + public static string GetModelErrorMessageOrDefault(ModelError modelError) { + Debug.Assert(modelError != null); + if (!string.IsNullOrEmpty(modelError.ErrorMessage)) { return modelError.ErrorMessage; } - if (entry == null) + // Default in the ValidationSummary case is no error message. + return string.Empty; + } + + public static string GetModelErrorMessageOrDefault( + ModelError modelError, + ModelStateEntry containingEntry, + ModelExplorer modelExplorer) + { + Debug.Assert(modelError != null); + Debug.Assert(containingEntry != null); + Debug.Assert(modelExplorer != null); + + if (!string.IsNullOrEmpty(modelError.ErrorMessage)) { - return string.Empty; + return modelError.ErrorMessage; } - var attemptedValue = entry.AttemptedValue ?? "null"; - return Resources.FormatCommon_ValueNotValidForProperty(attemptedValue); + // Default in the ValidationMessage case is a fallback error message. + var attemptedValue = containingEntry.AttemptedValue ?? "null"; + return modelExplorer.Metadata.ModelBindingMessageProvider.ValueIsInvalidAccessor(attemptedValue); } // Returns non-null list of model states, which caller will render in order provided. diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.ViewFeatures/Properties/Resources.Designer.cs index ffd083d356..5ce77f462a 100644 --- a/src/Microsoft.AspNet.Mvc.ViewFeatures/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/Properties/Resources.Designer.cs @@ -698,22 +698,6 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures return string.Format(CultureInfo.CurrentCulture, GetString("Common_PropertyNotFound"), p0, p1); } - /// - /// The value '{0}' is invalid. - /// - internal static string Common_ValueNotValidForProperty - { - get { return GetString("Common_ValueNotValidForProperty"); } - } - - /// - /// The value '{0}' is invalid. - /// - internal static string FormatCommon_ValueNotValidForProperty(object p0) - { - return string.Format(CultureInfo.CurrentCulture, GetString("Common_ValueNotValidForProperty"), p0); - } - /// /// No URL for remote validation could be found. /// diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/Resources.resx b/src/Microsoft.AspNet.Mvc.ViewFeatures/Resources.resx index e389b62f3a..53e8d0d193 100644 --- a/src/Microsoft.AspNet.Mvc.ViewFeatures/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/Resources.resx @@ -1,17 +1,17 @@  - @@ -247,9 +247,6 @@ The property {0}.{1} could not be found. - - The value '{0}' is invalid. - No URL for remote validation could be found. diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/DefaultHtmlGenerator.cs b/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/DefaultHtmlGenerator.cs index f40f7ad036..96024abdba 100644 --- a/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/DefaultHtmlGenerator.cs +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/DefaultHtmlGenerator.cs @@ -688,6 +688,7 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures /// public virtual TagBuilder GenerateValidationMessage( ViewContext viewContext, + ModelExplorer modelExplorer, string expression, string message, string tag, @@ -754,8 +755,12 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures } else if (modelError != null) { + modelExplorer = modelExplorer ?? ExpressionMetadataProvider.FromStringExpression( + expression, + viewContext.ViewData, + _metadataProvider); tagBuilder.InnerHtml.SetContent( - ValidationHelpers.GetUserErrorMessageOrDefault(modelError, entry)); + ValidationHelpers.GetModelErrorMessageOrDefault(modelError, entry, modelExplorer)); } if (formContext != null) @@ -818,7 +823,7 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures for (var i = 0; i < modelState.Errors.Count; i++) { var modelError = modelState.Errors[i]; - var errorText = ValidationHelpers.GetUserErrorMessageOrDefault(modelError, entry: null); + var errorText = ValidationHelpers.GetModelErrorMessageOrDefault(modelError); if (!string.IsNullOrEmpty(errorText)) { diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/HtmlHelper.cs b/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/HtmlHelper.cs index 1c490a3a44..7d0ce01fa7 100644 --- a/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/HtmlHelper.cs +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/HtmlHelper.cs @@ -676,7 +676,12 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures /// public IHtmlContent ValidationMessage(string expression, string message, object htmlAttributes, string tag) { - return GenerateValidationMessage(expression, message, htmlAttributes, tag); + return GenerateValidationMessage( + modelExplorer: null, + expression: expression, + message: message, + tag: tag, + htmlAttributes: htmlAttributes); } /// @@ -1135,17 +1140,19 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures } protected virtual IHtmlContent GenerateValidationMessage( + ModelExplorer modelExplorer, string expression, string message, - object htmlAttributes, - string tag) + string tag, + object htmlAttributes) { var tagBuilder = _htmlGenerator.GenerateValidationMessage( ViewContext, - expression: expression, - message: message, - tag: tag, - htmlAttributes: htmlAttributes); + modelExplorer, + expression, + message, + tag, + htmlAttributes); if (tagBuilder == null) { return HtmlString.Empty; diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/HtmlHelperOfT.cs b/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/HtmlHelperOfT.cs index 35745e27e2..066f7c490f 100644 --- a/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/HtmlHelperOfT.cs +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/HtmlHelperOfT.cs @@ -80,7 +80,10 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures } var modelExplorer = GetModelExplorer(expression); - return GenerateCheckBox(modelExplorer, GetExpressionName(expression), isChecked: null, + return GenerateCheckBox( + modelExplorer, + GetExpressionName(expression), + isChecked: null, htmlAttributes: htmlAttributes); } @@ -96,10 +99,13 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures throw new ArgumentNullException(nameof(expression)); } - var modelExplorer = ExpressionMetadataProvider.FromLambdaExpression(expression, ViewData, MetadataProvider); - - return GenerateDropDown(modelExplorer, ExpressionHelper.GetExpressionText(expression), selectList, - optionLabel, htmlAttributes); + var modelExplorer = GetModelExplorer(expression); + return GenerateDropDown( + modelExplorer, + GetExpressionName(expression), + selectList, + optionLabel, + htmlAttributes); } /// @@ -114,14 +120,12 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures throw new ArgumentNullException(nameof(expression)); } - var modelExplorer = ExpressionMetadataProvider.FromLambdaExpression(expression, - ViewData, - MetadataProvider); - - return GenerateDisplay(modelExplorer, - htmlFieldName ?? ExpressionHelper.GetExpressionText(expression), - templateName, - additionalViewData); + var modelExplorer = GetModelExplorer(expression); + return GenerateDisplay( + modelExplorer, + htmlFieldName ?? GetExpressionName(expression), + templateName, + additionalViewData); } /// @@ -133,7 +137,7 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures } var modelExplorer = GetModelExplorer(expression); - return GenerateDisplayName(modelExplorer, ExpressionHelper.GetExpressionText(expression)); + return GenerateDisplayName(modelExplorer, GetExpressionName(expression)); } /// @@ -182,11 +186,10 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures throw new ArgumentNullException(nameof(expression)); } - var modelExplorer = ExpressionMetadataProvider.FromLambdaExpression(expression, ViewData, MetadataProvider); - + var modelExplorer = GetModelExplorer(expression); return GenerateEditor( modelExplorer, - htmlFieldName ?? ExpressionHelper.GetExpressionText(expression), + htmlFieldName ?? GetExpressionName(expression), templateName, additionalViewData); } @@ -233,11 +236,7 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures } var modelExplorer = GetModelExplorer(expression); - return GenerateLabel( - modelExplorer, - ExpressionHelper.GetExpressionText(expression), - labelText, - htmlAttributes); + return GenerateLabel(modelExplorer, GetExpressionName(expression), labelText, htmlAttributes); } /// @@ -252,7 +251,7 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures } var modelExplorer = GetModelExplorer(expression); - var name = ExpressionHelper.GetExpressionText(expression); + var name = GetExpressionName(expression); return GenerateListBox(modelExplorer, name, selectList, htmlAttributes); } @@ -365,7 +364,8 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures throw new ArgumentNullException(nameof(expression)); } - var modelExplorer = ExpressionMetadataProvider.FromLambdaExpression(expression, ViewData, MetadataProvider); + var modelExplorer = + ExpressionMetadataProvider.FromLambdaExpression(expression, ViewData, MetadataProvider); if (modelExplorer == null) { var expressionName = GetExpressionName(expression); @@ -387,10 +387,13 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures throw new ArgumentNullException(nameof(expression)); } - return GenerateValidationMessage(ExpressionHelper.GetExpressionText(expression), + var modelExplorer = GetModelExplorer(expression); + return GenerateValidationMessage( + modelExplorer, + GetExpressionName(expression), message, - htmlAttributes, - tag); + tag, + htmlAttributes); } /// @@ -402,11 +405,7 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures } var modelExplorer = GetModelExplorer(expression); - return GenerateValue( - ExpressionHelper.GetExpressionText(expression), - modelExplorer.Model, - format, - useViewData: false); + return GenerateValue(GetExpressionName(expression), modelExplorer.Model, format, useViewData: false); } } } diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/IHtmlGenerator.cs b/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/IHtmlGenerator.cs index 994a36e9a9..396df55eae 100644 --- a/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/IHtmlGenerator.cs +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/IHtmlGenerator.cs @@ -69,8 +69,8 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures /// Generate a <input type="checkbox".../> element. /// /// The instance for the current scope. - /// The for the model. - /// The model expression. + /// The for the . + /// Expression name, relative to the current model. /// The initial state of the checkbox element. /// /// An that contains the HTML attributes for the element. Alternatively, an @@ -323,8 +323,36 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures string format, object htmlAttributes); + /// + /// Generate a element if the 's + /// contains an error for the . + /// + /// A instance for the current scope. + /// The for the . + /// Expression name, relative to the current model. + /// + /// The message to be displayed. If null or empty, method extracts an error string from the + /// object. Message will always be visible but client-side + /// validation may update the associated CSS class. + /// + /// + /// The tag to wrap the in the generated HTML. Its default value is + /// . + /// + /// + /// An that contains the HTML attributes for the element. Alternatively, an + /// instance containing the HTML attributes. + /// + /// + /// A containing a element if the + /// 's contains an error for the + /// or (as a placeholder) if client-side validation is enabled. null if + /// the is valid and client-side validation is disabled. + /// + /// is "span" by default. TagBuilder GenerateValidationMessage( ViewContext viewContext, + ModelExplorer modelExplorer, string expression, string message, string tag, diff --git a/src/Microsoft.AspNet.Mvc/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc/Properties/Resources.Designer.cs deleted file mode 100644 index e1c008b8f1..0000000000 --- a/src/Microsoft.AspNet.Mvc/Properties/Resources.Designer.cs +++ /dev/null @@ -1,45 +0,0 @@ -// -namespace Microsoft.AspNet.Mvc -{ - using System.Reflection; - using System.Resources; - - internal static class Resources - { - private static readonly ResourceManager _resourceManager - = new ResourceManager("Microsoft.AspNet.Mvc.Resources", typeof(Resources).GetTypeInfo().Assembly); - - /// - /// Unable to find the required services. Please add all the required services by calling AddMvc() before calling UseMvc() in the Application Startup. - /// - internal static string UnableToFindServices - { - get { return GetString("UnableToFindServices"); } - } - - /// - /// Unable to find the required services. Please add all the required services by calling AddMvc() before calling UseMvc() in the Application Startup. - /// - internal static string FormatUnableToFindServices() - { - return GetString("UnableToFindServices"); - } - - private static string GetString(string name, params string[] formatterNames) - { - var value = _resourceManager.GetString(name); - - System.Diagnostics.Debug.Assert(value != null); - - if (formatterNames != null) - { - for (var i = 0; i < formatterNames.Length; i++) - { - value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); - } - } - - return value; - } - } -} diff --git a/test/Microsoft.AspNet.Mvc.Abstractions.Test/ModelBinding/ModelStateDictionaryTest.cs b/test/Microsoft.AspNet.Mvc.Abstractions.Test/ModelBinding/ModelStateDictionaryTest.cs index f99c08ff89..03820649c5 100644 --- a/test/Microsoft.AspNet.Mvc.Abstractions.Test/ModelBinding/ModelStateDictionaryTest.cs +++ b/test/Microsoft.AspNet.Mvc.Abstractions.Test/ModelBinding/ModelStateDictionaryTest.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Microsoft.AspNet.Mvc.ModelBinding.Metadata; using Xunit; namespace Microsoft.AspNet.Mvc.ModelBinding @@ -736,6 +737,38 @@ namespace Microsoft.AspNet.Mvc.ModelBinding Assert.Equal(expected, error.ErrorMessage); } + [Fact] + public void ModelStateDictionary_AddsCustomErrorMessage_WhenModelStateNotSet() + { + // Arrange + var expected = "Hmm, the supplied value is not valid for Length."; + var dictionary = new ModelStateDictionary(); + + var messageProvider = new ModelBindingMessageProvider + { + MissingBindRequiredValueAccessor = name => "Unexpected MissingBindRequiredValueAccessor use", + MissingKeyOrValueAccessor = () => "Unexpected MissingKeyOrValueAccessor use", + ValueMustNotBeNullAccessor = value => "Unexpected ValueMustNotBeNullAccessor use", + AttemptedValueIsInvalidAccessor = + (value, name) => "Unexpected InvalidValueWithKnownAttemptedValueAccessor use", + UnknownValueIsInvalidAccessor = name => $"Hmm, the supplied value is not valid for { name }.", + ValueIsInvalidAccessor = value => "Unexpected InvalidValueWithUnknownModelErrorAccessor use", + }; + var bindingMetadataProvider = new DefaultBindingMetadataProvider(messageProvider); + var compositeProvider = new DefaultCompositeMetadataDetailsProvider(new[] { bindingMetadataProvider }); + var provider = new DefaultModelMetadataProvider(compositeProvider); + var metadata = provider.GetMetadataForProperty(typeof(string), nameof(string.Length)); + + // Act + dictionary.TryAddModelError("key", new FormatException(), metadata); + + // Assert + var entry = Assert.Single(dictionary); + Assert.Equal("key", entry.Key); + var error = Assert.Single(entry.Value.Errors); + Assert.Equal(expected, error.ErrorMessage); + } + [Fact] public void ModelStateDictionary_ReturnSpecificErrorMessage_WhenModelStateSet() { @@ -754,6 +787,39 @@ namespace Microsoft.AspNet.Mvc.ModelBinding Assert.Equal(expected, error.ErrorMessage); } + [Fact] + public void ModelStateDictionary_AddsCustomErrorMessage_WhenModelStateSet() + { + // Arrange + var expected = "Hmm, the value 'some value' is not valid for Length."; + var dictionary = new ModelStateDictionary(); + dictionary.SetModelValue("key", new string[] { "some value" }, "some value"); + + var messageProvider = new ModelBindingMessageProvider + { + MissingBindRequiredValueAccessor = name => "Unexpected MissingBindRequiredValueAccessor use", + MissingKeyOrValueAccessor = () => "Unexpected MissingKeyOrValueAccessor use", + ValueMustNotBeNullAccessor = value => "Unexpected ValueMustNotBeNullAccessor use", + AttemptedValueIsInvalidAccessor = + (value, name) => $"Hmm, the value '{ value }' is not valid for { name }.", + UnknownValueIsInvalidAccessor = name => "Unexpected InvalidValueWithUnknownAttemptedValueAccessor use", + ValueIsInvalidAccessor = value => "Unexpected InvalidValueWithUnknownModelErrorAccessor use", + }; + var bindingMetadataProvider = new DefaultBindingMetadataProvider(messageProvider); + var compositeProvider = new DefaultCompositeMetadataDetailsProvider(new[] { bindingMetadataProvider }); + var provider = new DefaultModelMetadataProvider(compositeProvider); + var metadata = provider.GetMetadataForProperty(typeof(string), nameof(string.Length)); + + // Act + dictionary.TryAddModelError("key", new FormatException(), metadata); + + // Assert + var entry = Assert.Single(dictionary); + Assert.Equal("key", entry.Key); + var error = Assert.Single(entry.Value.Errors); + Assert.Equal(expected, error.ErrorMessage); + } + [Fact] public void ModelStateDictionary_NoErrorMessage_ForNonFormatException() { diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/Metadata/DefaultBindingMetadataProviderTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/Metadata/DefaultBindingMetadataProviderTest.cs index 6374136954..14b7963c5d 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/Metadata/DefaultBindingMetadataProviderTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/Metadata/DefaultBindingMetadataProviderTest.cs @@ -522,6 +522,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata MissingBindRequiredValueAccessor = Resources.FormatModelBinding_MissingBindRequiredMember, MissingKeyOrValueAccessor = Resources.FormatKeyValuePair_BothKeyAndValueMustBePresent, ValueMustNotBeNullAccessor = Resources.FormatModelBinding_NullValueNotValid, + AttemptedValueIsInvalidAccessor = Resources.FormatModelState_AttemptedValueIsInvalid, + UnknownValueIsInvalidAccessor = Resources.FormatModelState_UnknownValueIsInvalid, + ValueIsInvalidAccessor = Resources.FormatHtmlGeneration_ValueIsInvalid, }; } diff --git a/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/ModelMetadataProviderTest.cs b/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/ModelMetadataProviderTest.cs index 4b38426f03..82895fca04 100644 --- a/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/ModelMetadataProviderTest.cs +++ b/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/ModelMetadataProviderTest.cs @@ -1037,9 +1037,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata { return new ModelBindingMessageProvider { - MissingBindRequiredValueAccessor = name => $"A value for the '{ name }' property was not provided.", + MissingBindRequiredValueAccessor = + name => $"A value for the '{ name }' property was not provided.", MissingKeyOrValueAccessor = () => $"A value is required.", ValueMustNotBeNullAccessor = value => $"The value '{ value }' is invalid.", + AttemptedValueIsInvalidAccessor = + (value, name) => $"The value '{ value }' is not valid for { name }.", + UnknownValueIsInvalidAccessor = name => $"The supplied value is invalid for { name }.", + ValueIsInvalidAccessor = value => $"The value '{ value }' is invalid.", }; } diff --git a/test/Microsoft.AspNet.Mvc.IntegrationTests/SimpleTypeModelBinderIntegrationTest.cs b/test/Microsoft.AspNet.Mvc.IntegrationTests/SimpleTypeModelBinderIntegrationTest.cs index 760403dbc2..81a077ac8f 100644 --- a/test/Microsoft.AspNet.Mvc.IntegrationTests/SimpleTypeModelBinderIntegrationTest.cs +++ b/test/Microsoft.AspNet.Mvc.IntegrationTests/SimpleTypeModelBinderIntegrationTest.cs @@ -9,7 +9,6 @@ using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Internal; using Microsoft.AspNet.Mvc.Abstractions; -using Microsoft.AspNet.Mvc.Controllers; using Microsoft.AspNet.Mvc.ModelBinding; #if !DNXCORE50 using Microsoft.AspNet.Testing.xunit; @@ -250,6 +249,64 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests Assert.Equal("The value 'abcd' is not valid for Int32.", error.ErrorMessage); } + [Fact] + public async Task BindParameter_NonConvertableValue_GetsCustomErrorMessage() + { + // Arrange + var parameterType = typeof(int); + var metadataProvider = new TestModelMetadataProvider(); + metadataProvider + .ForType(parameterType) + .BindingDetails(binding => + { + // A real details provider could customize message based on BindingMetadataProviderContext. + binding.ModelBindingMessageProvider.AttemptedValueIsInvalidAccessor = + (value, name) => $"Hmm, '{ value }' is not a valid value for '{ name }'."; + }); + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(metadataProvider); + var parameter = new ParameterDescriptor() + { + Name = "Parameter1", + BindingInfo = new BindingInfo(), + ParameterType = parameterType + }; + + var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request => + { + request.QueryString = QueryString.Create("Parameter1", "abcd"); + }); + + var modelState = operationContext.ActionContext.ModelState; + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, operationContext); + + // Assert + + // ModelBindingResult + Assert.False(modelBindingResult.IsModelSet); + + // Model + Assert.Null(modelBindingResult.Model); + + // ModelState + Assert.False(modelState.IsValid); + Assert.Equal(1, modelState.Count); + Assert.Equal(1, modelState.ErrorCount); + + var key = Assert.Single(modelState.Keys); + Assert.Equal("Parameter1", key); + + var entry = modelState[key]; + Assert.Equal("abcd", entry.RawValue); + Assert.Equal("abcd", entry.AttemptedValue); + Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); + + var error = Assert.Single(entry.Errors); + Assert.Null(error.Exception); + Assert.Equal($"Hmm, 'abcd' is not a valid value for 'Int32'.", error.ErrorMessage); + } + #if DNXCORE50 [Theory] #else @@ -306,12 +363,12 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests var metadataProvider = new TestModelMetadataProvider(); metadataProvider .ForType(parameterType) - .BindingDetails((Action)(binding => + .BindingDetails(binding => { // A real details provider could customize message based on BindingMetadataProviderContext. binding.ModelBindingMessageProvider.ValueMustNotBeNullAccessor = value => $"Hurts when '{ value }' is provided."; - })); + }); var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(metadataProvider); var parameter = new ParameterDescriptor diff --git a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/ValidationMessageTagHelperTest.cs b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/ValidationMessageTagHelperTest.cs index 4597209eee..b62f8743ab 100644 --- a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/ValidationMessageTagHelperTest.cs +++ b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/ValidationMessageTagHelperTest.cs @@ -93,16 +93,22 @@ namespace Microsoft.AspNet.Mvc.TagHelpers { // Arrange var expectedViewContext = CreateViewContext(); + var modelExpression = CreateModelExpression("Hello"); var generator = new Mock(); generator - .Setup(mock => - mock.GenerateValidationMessage(expectedViewContext, "Hello", null, null, null)) + .Setup(mock => mock.GenerateValidationMessage( + expectedViewContext, + modelExpression.ModelExplorer, + modelExpression.Name, + null, + null, + null)) .Returns(new TagBuilder("span")) .Verifiable(); var validationMessageTagHelper = new ValidationMessageTagHelper(generator.Object) { - For = CreateModelExpression("Hello") + For = modelExpression, }; var expectedPreContent = "original pre-content"; var expectedContent = "original content"; @@ -155,6 +161,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers var setup = generator .Setup(mock => mock.GenerateValidationMessage( It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), @@ -214,6 +221,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers var setup = generator .Setup(mock => mock.GenerateValidationMessage( It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), diff --git a/test/Microsoft.AspNet.Mvc.TestCommon/TestModelMetadataProvider.cs b/test/Microsoft.AspNet.Mvc.TestCommon/TestModelMetadataProvider.cs index 79301eab25..ddb978c07f 100644 --- a/test/Microsoft.AspNet.Mvc.TestCommon/TestModelMetadataProvider.cs +++ b/test/Microsoft.AspNet.Mvc.TestCommon/TestModelMetadataProvider.cs @@ -111,6 +111,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding MissingBindRequiredValueAccessor = name => $"A value for the '{ name }' property was not provided.", MissingKeyOrValueAccessor = () => $"A value is required.", ValueMustNotBeNullAccessor = value => $"The value '{ value }' is invalid.", + AttemptedValueIsInvalidAccessor = (value, name) => $"The value '{ value }' is not valid for { name }.", + UnknownValueIsInvalidAccessor = name => $"The supplied value is invalid for { name }.", + ValueIsInvalidAccessor = value => $"The value '{ value }' is invalid.", }; } diff --git a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/DefaultHtmlGeneratorTest.cs b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/DefaultHtmlGeneratorTest.cs index 5e1c44833d..0aaba5757b 100644 --- a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/DefaultHtmlGeneratorTest.cs +++ b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/DefaultHtmlGeneratorTest.cs @@ -165,12 +165,7 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures // Act and assert var ex = Assert.Throws( "expression", - () => htmlGenerator.GenerateValidationMessage( - viewContext, - null, - "Message", - "tag", - null)); + () => htmlGenerator.GenerateValidationMessage(viewContext, null, null, "Message", "tag", null)); Assert.Equal(expected, ex.Message); }