Support user overrides of three more framework-provided `ModelState`-related messages

- #3215
- add new accessor properties to `IModelBindingMessageProvider` and plumb them through
  - use in `ModelStateDictionary` when handling a `FormatException` or `OverflowException`
  - use in `ValidationHelpers` when handling a `ModelError` with `null` `ErrorMessage`
- add new `ModelExplorer` parameter to `IHtmlGenerator.GenerateValidationMessage()`
  - plumb through to `ValidationHelpers.GetModelErrorMessageOrDefault()`

Started from work @kichalla did on the `kiran/movemessages-to-messageprovider` branch in #3775.

nits:
- use helper methods more consistently in `HtmlHelper<T>`; slightly improves error checking
- remove unused `Resources` class from `Microsoft.AspNet.Mvc`
- make `ValidationHelpers` class `public`; already in `.Internal` namespace
  - split `GetUserErrorMessageOrDefault()` in two; rename to `GetModelErrorMessageOrDefault()`
- fix some #YOLO wrapping
This commit is contained in:
Doug Bunting 2016-01-19 21:45:47 -08:00
parent 1144fc0e6c
commit 04453a2b4f
25 changed files with 466 additions and 229 deletions

View File

@ -30,5 +30,26 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
/// </summary> /// </summary>
/// <value>Default <see cref="string"/> is "The value '{0}' is invalid.".</value> /// <value>Default <see cref="string"/> is "The value '{0}' is invalid.".</value>
Func<string, string> ValueMustNotBeNullAccessor { get; } Func<string, string> ValueMustNotBeNullAccessor { get; }
/// <summary>
/// Error message the model binding system adds when <see cref="ModelError.Exception"/> is of type
/// <see cref="FormatException"/> or <see cref="OverflowException"/> and value is known.
/// </summary>
/// <value>Default <see cref="string"/> is "The value '{0}' is not valid for {1}.".</value>
Func<string, string, string> AttemptedValueIsInvalidAccessor { get; }
/// <summary>
/// Error message the model binding system adds when <see cref="ModelError.Exception"/> is of type
/// <see cref="FormatException"/> or <see cref="OverflowException"/> and value is unknown.
/// </summary>
/// <value>Default <see cref="string"/> is "The supplied value is invalid for {0}.".</value>
Func<string, string> UnknownValueIsInvalidAccessor { get; }
/// <summary>
/// Fallback error message HTML and tag helpers display when a property is invalid but the
/// <see cref="ModelError"/>s have <c>null</c> <see cref="ModelError.ErrorMessage"/>s.
/// </summary>
/// <value>Default <see cref="string"/> is "The value '{0}' is invalid.".</value>
Func<string, string> ValueIsInvalidAccessor { get; }
} }
} }

View File

@ -267,11 +267,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
string errorMessage; string errorMessage;
if (entry == null) if (entry == null)
{ {
errorMessage = Resources.FormatModelError_InvalidValue_GenericMessage(name); errorMessage = metadata.ModelBindingMessageProvider.UnknownValueIsInvalidAccessor(name);
} }
else else
{ {
errorMessage = Resources.FormatModelError_InvalidValue_MessageWithModelValue( errorMessage = metadata.ModelBindingMessageProvider.AttemptedValueIsInvalidAccessor(
entry.AttemptedValue, entry.AttemptedValue,
name); name);
} }

View File

@ -282,38 +282,6 @@ namespace Microsoft.AspNet.Mvc.Abstractions
return string.Format(CultureInfo.CurrentCulture, GetString("BindingSource_MustBeGreedy"), p0, p1); return string.Format(CultureInfo.CurrentCulture, GetString("BindingSource_MustBeGreedy"), p0, p1);
} }
/// <summary>
/// The supplied value is invalid for {0}.
/// </summary>
internal static string ModelError_InvalidValue_GenericMessage
{
get { return GetString("ModelError_InvalidValue_GenericMessage"); }
}
/// <summary>
/// The supplied value is invalid for {0}.
/// </summary>
internal static string FormatModelError_InvalidValue_GenericMessage(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ModelError_InvalidValue_GenericMessage"), p0);
}
/// <summary>
/// The value '{0}' is not valid for {1}.
/// </summary>
internal static string ModelError_InvalidValue_MessageWithModelValue
{
get { return GetString("ModelError_InvalidValue_MessageWithModelValue"); }
}
/// <summary>
/// The value '{0}' is not valid for {1}.
/// </summary>
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) private static string GetString(string name, params string[] formatterNames)
{ {
var value = _resourceManager.GetString(name); var value = _resourceManager.GetString(name);

View File

@ -168,10 +168,4 @@
<data name="BindingSource_MustBeGreedy" xml:space="preserve"> <data name="BindingSource_MustBeGreedy" xml:space="preserve">
<value>The provided binding source '{0}' is not a greedy data source. '{1}' only supports greedy data sources.</value> <value>The provided binding source '{0}' is not a greedy data source. '{1}' only supports greedy data sources.</value>
</data> </data>
<data name="ModelError_InvalidValue_GenericMessage" xml:space="preserve">
<value>The supplied value is invalid for {0}.</value>
</data>
<data name="ModelError_InvalidValue_MessageWithModelValue" xml:space="preserve">
<value>The value '{0}' is not valid for {1}.</value>
</data>
</root> </root>

View File

@ -31,6 +31,9 @@ namespace Microsoft.AspNet.Mvc.Internal
messageProvider.MissingBindRequiredValueAccessor = Resources.FormatModelBinding_MissingBindRequiredMember; messageProvider.MissingBindRequiredValueAccessor = Resources.FormatModelBinding_MissingBindRequiredMember;
messageProvider.MissingKeyOrValueAccessor = Resources.FormatKeyValuePair_BothKeyAndValueMustBePresent; messageProvider.MissingKeyOrValueAccessor = Resources.FormatKeyValuePair_BothKeyAndValueMustBePresent;
messageProvider.ValueMustNotBeNullAccessor = Resources.FormatModelBinding_NullValueNotValid; messageProvider.ValueMustNotBeNullAccessor = Resources.FormatModelBinding_NullValueNotValid;
messageProvider.AttemptedValueIsInvalidAccessor = Resources.FormatModelState_AttemptedValueIsInvalid;
messageProvider.UnknownValueIsInvalidAccessor = Resources.FormatModelState_UnknownValueIsInvalid;
messageProvider.ValueIsInvalidAccessor = Resources.FormatHtmlGeneration_ValueIsInvalid;
// Set up ModelBinding // Set up ModelBinding
options.ModelBinders.Add(new BinderTypeBasedModelBinder()); options.ModelBinders.Add(new BinderTypeBasedModelBinder());

View File

@ -40,6 +40,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
MissingBindRequiredValueAccessor = Resources.FormatModelBinding_MissingBindRequiredMember, MissingBindRequiredValueAccessor = Resources.FormatModelBinding_MissingBindRequiredMember,
MissingKeyOrValueAccessor = Resources.FormatKeyValuePair_BothKeyAndValueMustBePresent, MissingKeyOrValueAccessor = Resources.FormatKeyValuePair_BothKeyAndValueMustBePresent,
ValueMustNotBeNullAccessor = Resources.FormatModelBinding_NullValueNotValid, ValueMustNotBeNullAccessor = Resources.FormatModelBinding_NullValueNotValid,
AttemptedValueIsInvalidAccessor = Resources.FormatModelState_AttemptedValueIsInvalid,
UnknownValueIsInvalidAccessor = Resources.FormatModelState_UnknownValueIsInvalid,
ValueIsInvalidAccessor = Resources.FormatHtmlGeneration_ValueIsInvalid,
}; };
} }
} }

View File

@ -13,6 +13,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
private Func<string, string> _missingBindRequiredValueAccessor; private Func<string, string> _missingBindRequiredValueAccessor;
private Func<string> _missingKeyOrValueAccessor; private Func<string> _missingKeyOrValueAccessor;
private Func<string, string> _valueMustNotBeNullAccessor; private Func<string, string> _valueMustNotBeNullAccessor;
private Func<string, string, string> _attemptedValueIsInvalidAccessor;
private Func<string, string> _unknownValueIsInvalidAccessor;
private Func<string, string> _valueIsInvalidAccessor;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ModelBindingMessageProvider"/> class. /// Initializes a new instance of the <see cref="ModelBindingMessageProvider"/> class.
@ -36,6 +39,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
MissingBindRequiredValueAccessor = originalProvider.MissingBindRequiredValueAccessor; MissingBindRequiredValueAccessor = originalProvider.MissingBindRequiredValueAccessor;
MissingKeyOrValueAccessor = originalProvider.MissingKeyOrValueAccessor; MissingKeyOrValueAccessor = originalProvider.MissingKeyOrValueAccessor;
ValueMustNotBeNullAccessor = originalProvider.ValueMustNotBeNullAccessor; ValueMustNotBeNullAccessor = originalProvider.ValueMustNotBeNullAccessor;
AttemptedValueIsInvalidAccessor = originalProvider.AttemptedValueIsInvalidAccessor;
UnknownValueIsInvalidAccessor = originalProvider.UnknownValueIsInvalidAccessor;
ValueIsInvalidAccessor = originalProvider.ValueIsInvalidAccessor;
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -91,5 +97,59 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
_valueMustNotBeNullAccessor = value; _valueMustNotBeNullAccessor = value;
} }
} }
/// <inheritdoc/>
public Func<string, string, string> AttemptedValueIsInvalidAccessor
{
get
{
return _attemptedValueIsInvalidAccessor;
}
set
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
_attemptedValueIsInvalidAccessor = value;
}
}
/// <inheritdoc/>
public Func<string, string> UnknownValueIsInvalidAccessor
{
get
{
return _unknownValueIsInvalidAccessor;
}
set
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
_unknownValueIsInvalidAccessor = value;
}
}
/// <inheritdoc/>
public Func<string, string> ValueIsInvalidAccessor
{
get
{
return _valueIsInvalidAccessor;
}
set
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
_valueIsInvalidAccessor = value;
}
}
} }
} }

View File

@ -475,7 +475,7 @@ namespace Microsoft.AspNet.Mvc.Core
} }
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
internal static string UnableToFindServices internal static string UnableToFindServices
{ {
@ -483,11 +483,11 @@ namespace Microsoft.AspNet.Mvc.Core
} }
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
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);
} }
/// <summary> /// <summary>
@ -1066,6 +1066,54 @@ namespace Microsoft.AspNet.Mvc.Core
return string.Format(CultureInfo.CurrentCulture, GetString("AcceptHeaderParser_ParseAcceptHeader_InvalidValues"), p0); return string.Format(CultureInfo.CurrentCulture, GetString("AcceptHeaderParser_ParseAcceptHeader_InvalidValues"), p0);
} }
/// <summary>
/// The value '{0}' is not valid for {1}.
/// </summary>
internal static string ModelState_AttemptedValueIsInvalid
{
get { return GetString("ModelState_AttemptedValueIsInvalid"); }
}
/// <summary>
/// The value '{0}' is not valid for {1}.
/// </summary>
internal static string FormatModelState_AttemptedValueIsInvalid(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ModelState_AttemptedValueIsInvalid"), p0, p1);
}
/// <summary>
/// The supplied value is invalid for {0}.
/// </summary>
internal static string ModelState_UnknownValueIsInvalid
{
get { return GetString("ModelState_UnknownValueIsInvalid"); }
}
/// <summary>
/// The supplied value is invalid for {0}.
/// </summary>
internal static string FormatModelState_UnknownValueIsInvalid(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ModelState_UnknownValueIsInvalid"), p0);
}
/// <summary>
/// The value '{0}' is invalid.
/// </summary>
internal static string HtmlGeneration_ValueIsInvalid
{
get { return GetString("HtmlGeneration_ValueIsInvalid"); }
}
/// <summary>
/// The value '{0}' is invalid.
/// </summary>
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) private static string GetString(string name, params string[] formatterNames)
{ {
var value = _resourceManager.GetString(name); var value = _resourceManager.GetString(name);

View File

@ -325,4 +325,13 @@
<data name="AcceptHeaderParser_ParseAcceptHeader_InvalidValues" xml:space="preserve"> <data name="AcceptHeaderParser_ParseAcceptHeader_InvalidValues" xml:space="preserve">
<value>"Invalid values '{0}'."</value> <value>"Invalid values '{0}'."</value>
</data> </data>
<data name="ModelState_AttemptedValueIsInvalid" xml:space="preserve">
<value>The value '{0}' is not valid for {1}.</value>
</data>
<data name="ModelState_UnknownValueIsInvalid" xml:space="preserve">
<value>The supplied value is invalid for {0}.</value>
</data>
<data name="HtmlGeneration_ValueIsInvalid" xml:space="preserve">
<value>The value '{0}' is invalid.</value>
</data>
</root> </root>

View File

@ -64,11 +64,13 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
if (For != null) if (For != null)
{ {
var tagBuilder = Generator.GenerateValidationMessage(ViewContext, var tagBuilder = Generator.GenerateValidationMessage(
For.Name, ViewContext,
message: null, For.ModelExplorer,
tag: null, For.Name,
htmlAttributes: null); message: null,
tag: null,
htmlAttributes: null);
if (tagBuilder != null) if (tagBuilder != null)
{ {

View File

@ -3,27 +3,44 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.ModelBinding;
namespace Microsoft.AspNet.Mvc.ViewFeatures.Internal 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)) if (!string.IsNullOrEmpty(modelError.ErrorMessage))
{ {
return 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"; // Default in the ValidationMessage case is a fallback error message.
return Resources.FormatCommon_ValueNotValidForProperty(attemptedValue); 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. // Returns non-null list of model states, which caller will render in order provided.

View File

@ -698,22 +698,6 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
return string.Format(CultureInfo.CurrentCulture, GetString("Common_PropertyNotFound"), p0, p1); return string.Format(CultureInfo.CurrentCulture, GetString("Common_PropertyNotFound"), p0, p1);
} }
/// <summary>
/// The value '{0}' is invalid.
/// </summary>
internal static string Common_ValueNotValidForProperty
{
get { return GetString("Common_ValueNotValidForProperty"); }
}
/// <summary>
/// The value '{0}' is invalid.
/// </summary>
internal static string FormatCommon_ValueNotValidForProperty(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("Common_ValueNotValidForProperty"), p0);
}
/// <summary> /// <summary>
/// No URL for remote validation could be found. /// No URL for remote validation could be found.
/// </summary> /// </summary>

View File

@ -247,9 +247,6 @@
<data name="Common_PropertyNotFound" xml:space="preserve"> <data name="Common_PropertyNotFound" xml:space="preserve">
<value>The property {0}.{1} could not be found.</value> <value>The property {0}.{1} could not be found.</value>
</data> </data>
<data name="Common_ValueNotValidForProperty" xml:space="preserve">
<value>The value '{0}' is invalid.</value>
</data>
<data name="RemoteAttribute_NoUrlFound" xml:space="preserve"> <data name="RemoteAttribute_NoUrlFound" xml:space="preserve">
<value>No URL for remote validation could be found.</value> <value>No URL for remote validation could be found.</value>
</data> </data>

View File

@ -688,6 +688,7 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
/// <inheritdoc /> /// <inheritdoc />
public virtual TagBuilder GenerateValidationMessage( public virtual TagBuilder GenerateValidationMessage(
ViewContext viewContext, ViewContext viewContext,
ModelExplorer modelExplorer,
string expression, string expression,
string message, string message,
string tag, string tag,
@ -754,8 +755,12 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
} }
else if (modelError != null) else if (modelError != null)
{ {
modelExplorer = modelExplorer ?? ExpressionMetadataProvider.FromStringExpression(
expression,
viewContext.ViewData,
_metadataProvider);
tagBuilder.InnerHtml.SetContent( tagBuilder.InnerHtml.SetContent(
ValidationHelpers.GetUserErrorMessageOrDefault(modelError, entry)); ValidationHelpers.GetModelErrorMessageOrDefault(modelError, entry, modelExplorer));
} }
if (formContext != null) if (formContext != null)
@ -818,7 +823,7 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
for (var i = 0; i < modelState.Errors.Count; i++) for (var i = 0; i < modelState.Errors.Count; i++)
{ {
var modelError = modelState.Errors[i]; var modelError = modelState.Errors[i];
var errorText = ValidationHelpers.GetUserErrorMessageOrDefault(modelError, entry: null); var errorText = ValidationHelpers.GetModelErrorMessageOrDefault(modelError);
if (!string.IsNullOrEmpty(errorText)) if (!string.IsNullOrEmpty(errorText))
{ {

View File

@ -676,7 +676,12 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
/// <inheritdoc /> /// <inheritdoc />
public IHtmlContent ValidationMessage(string expression, string message, object htmlAttributes, string tag) 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);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -1135,17 +1140,19 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
} }
protected virtual IHtmlContent GenerateValidationMessage( protected virtual IHtmlContent GenerateValidationMessage(
ModelExplorer modelExplorer,
string expression, string expression,
string message, string message,
object htmlAttributes, string tag,
string tag) object htmlAttributes)
{ {
var tagBuilder = _htmlGenerator.GenerateValidationMessage( var tagBuilder = _htmlGenerator.GenerateValidationMessage(
ViewContext, ViewContext,
expression: expression, modelExplorer,
message: message, expression,
tag: tag, message,
htmlAttributes: htmlAttributes); tag,
htmlAttributes);
if (tagBuilder == null) if (tagBuilder == null)
{ {
return HtmlString.Empty; return HtmlString.Empty;

View File

@ -80,7 +80,10 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
} }
var modelExplorer = GetModelExplorer(expression); var modelExplorer = GetModelExplorer(expression);
return GenerateCheckBox(modelExplorer, GetExpressionName(expression), isChecked: null, return GenerateCheckBox(
modelExplorer,
GetExpressionName(expression),
isChecked: null,
htmlAttributes: htmlAttributes); htmlAttributes: htmlAttributes);
} }
@ -96,10 +99,13 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
throw new ArgumentNullException(nameof(expression)); throw new ArgumentNullException(nameof(expression));
} }
var modelExplorer = ExpressionMetadataProvider.FromLambdaExpression(expression, ViewData, MetadataProvider); var modelExplorer = GetModelExplorer(expression);
return GenerateDropDown(
return GenerateDropDown(modelExplorer, ExpressionHelper.GetExpressionText(expression), selectList, modelExplorer,
optionLabel, htmlAttributes); GetExpressionName(expression),
selectList,
optionLabel,
htmlAttributes);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -114,14 +120,12 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
throw new ArgumentNullException(nameof(expression)); throw new ArgumentNullException(nameof(expression));
} }
var modelExplorer = ExpressionMetadataProvider.FromLambdaExpression(expression, var modelExplorer = GetModelExplorer(expression);
ViewData, return GenerateDisplay(
MetadataProvider); modelExplorer,
htmlFieldName ?? GetExpressionName(expression),
return GenerateDisplay(modelExplorer, templateName,
htmlFieldName ?? ExpressionHelper.GetExpressionText(expression), additionalViewData);
templateName,
additionalViewData);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -133,7 +137,7 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
} }
var modelExplorer = GetModelExplorer(expression); var modelExplorer = GetModelExplorer(expression);
return GenerateDisplayName(modelExplorer, ExpressionHelper.GetExpressionText(expression)); return GenerateDisplayName(modelExplorer, GetExpressionName(expression));
} }
/// <inheritdoc /> /// <inheritdoc />
@ -182,11 +186,10 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
throw new ArgumentNullException(nameof(expression)); throw new ArgumentNullException(nameof(expression));
} }
var modelExplorer = ExpressionMetadataProvider.FromLambdaExpression(expression, ViewData, MetadataProvider); var modelExplorer = GetModelExplorer(expression);
return GenerateEditor( return GenerateEditor(
modelExplorer, modelExplorer,
htmlFieldName ?? ExpressionHelper.GetExpressionText(expression), htmlFieldName ?? GetExpressionName(expression),
templateName, templateName,
additionalViewData); additionalViewData);
} }
@ -233,11 +236,7 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
} }
var modelExplorer = GetModelExplorer(expression); var modelExplorer = GetModelExplorer(expression);
return GenerateLabel( return GenerateLabel(modelExplorer, GetExpressionName(expression), labelText, htmlAttributes);
modelExplorer,
ExpressionHelper.GetExpressionText(expression),
labelText,
htmlAttributes);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -252,7 +251,7 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
} }
var modelExplorer = GetModelExplorer(expression); var modelExplorer = GetModelExplorer(expression);
var name = ExpressionHelper.GetExpressionText(expression); var name = GetExpressionName(expression);
return GenerateListBox(modelExplorer, name, selectList, htmlAttributes); return GenerateListBox(modelExplorer, name, selectList, htmlAttributes);
} }
@ -365,7 +364,8 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
throw new ArgumentNullException(nameof(expression)); throw new ArgumentNullException(nameof(expression));
} }
var modelExplorer = ExpressionMetadataProvider.FromLambdaExpression(expression, ViewData, MetadataProvider); var modelExplorer =
ExpressionMetadataProvider.FromLambdaExpression(expression, ViewData, MetadataProvider);
if (modelExplorer == null) if (modelExplorer == null)
{ {
var expressionName = GetExpressionName(expression); var expressionName = GetExpressionName(expression);
@ -387,10 +387,13 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
throw new ArgumentNullException(nameof(expression)); throw new ArgumentNullException(nameof(expression));
} }
return GenerateValidationMessage(ExpressionHelper.GetExpressionText(expression), var modelExplorer = GetModelExplorer(expression);
return GenerateValidationMessage(
modelExplorer,
GetExpressionName(expression),
message, message,
htmlAttributes, tag,
tag); htmlAttributes);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -402,11 +405,7 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
} }
var modelExplorer = GetModelExplorer(expression); var modelExplorer = GetModelExplorer(expression);
return GenerateValue( return GenerateValue(GetExpressionName(expression), modelExplorer.Model, format, useViewData: false);
ExpressionHelper.GetExpressionText(expression),
modelExplorer.Model,
format,
useViewData: false);
} }
} }
} }

View File

@ -69,8 +69,8 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
/// Generate a &lt;input type="checkbox".../&gt; element. /// Generate a &lt;input type="checkbox".../&gt; element.
/// </summary> /// </summary>
/// <param name="viewContext">The <see cref="ViewContext"/> instance for the current scope.</param> /// <param name="viewContext">The <see cref="ViewContext"/> instance for the current scope.</param>
/// <param name="modelExplorer">The <see cref="ModelExplorer"/> for the model.</param> /// <param name="modelExplorer">The <see cref="ModelExplorer"/> for the <paramref name="expression"/>.</param>
/// <param name="expression">The model expression.</param> /// <param name="expression">Expression name, relative to the current model.</param>
/// <param name="isChecked">The initial state of the checkbox element.</param> /// <param name="isChecked">The initial state of the checkbox element.</param>
/// <param name="htmlAttributes"> /// <param name="htmlAttributes">
/// An <see cref="object"/> that contains the HTML attributes for the element. Alternatively, an /// An <see cref="object"/> that contains the HTML attributes for the element. Alternatively, an
@ -323,8 +323,36 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
string format, string format,
object htmlAttributes); object htmlAttributes);
/// <summary>
/// Generate a <paramref name="tag"/> element if the <paramref name="viewContext"/>'s
/// <see cref="ActionContext.ModelState"/> contains an error for the <paramref name="expression"/>.
/// </summary>
/// <param name="viewContext">A <see cref="ViewContext"/> instance for the current scope.</param>
/// <param name="modelExplorer">The <see cref="ModelExplorer"/> for the <paramref name="expression"/>.</param>
/// <param name="expression">Expression name, relative to the current model.</param>
/// <param name="message">
/// The message to be displayed. If <c>null</c> or empty, method extracts an error string from the
/// <see cref="ModelBinding.ModelStateDictionary"/> object. Message will always be visible but client-side
/// validation may update the associated CSS class.
/// </param>
/// <param name="tag">
/// The tag to wrap the <paramref name="message"/> in the generated HTML. Its default value is
/// <see cref="ViewContext.ValidationMessageElement"/>.
/// </param>
/// <param name="htmlAttributes">
/// An <see cref="object"/> that contains the HTML attributes for the element. Alternatively, an
/// <see cref="IDictionary{string, object}"/> instance containing the HTML attributes.
/// </param>
/// <returns>
/// A <see cref="TagBuilder"/> containing a <paramref name="tag"/> element if the
/// <paramref name="viewContext"/>'s <see cref="ActionContext.ModelState"/> contains an error for the
/// <paramref name="expression"/> or (as a placeholder) if client-side validation is enabled. <c>null</c> if
/// the <paramref name="expression"/> is valid and client-side validation is disabled.
/// </returns>
/// <remarks><see cref="ViewContext.ValidationMessageElement"/> is <c>"span"</c> by default.</remarks>
TagBuilder GenerateValidationMessage( TagBuilder GenerateValidationMessage(
ViewContext viewContext, ViewContext viewContext,
ModelExplorer modelExplorer,
string expression, string expression,
string message, string message,
string tag, string tag,

View File

@ -1,45 +0,0 @@
// <auto-generated />
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);
/// <summary>
/// Unable to find the required services. Please add all the required services by calling AddMvc() before calling UseMvc() in the Application Startup.
/// </summary>
internal static string UnableToFindServices
{
get { return GetString("UnableToFindServices"); }
}
/// <summary>
/// Unable to find the required services. Please add all the required services by calling AddMvc() before calling UseMvc() in the Application Startup.
/// </summary>
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;
}
}
}

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.AspNet.Mvc.ModelBinding.Metadata;
using Xunit; using Xunit;
namespace Microsoft.AspNet.Mvc.ModelBinding namespace Microsoft.AspNet.Mvc.ModelBinding
@ -736,6 +737,38 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
Assert.Equal(expected, error.ErrorMessage); 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] [Fact]
public void ModelStateDictionary_ReturnSpecificErrorMessage_WhenModelStateSet() public void ModelStateDictionary_ReturnSpecificErrorMessage_WhenModelStateSet()
{ {
@ -754,6 +787,39 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
Assert.Equal(expected, error.ErrorMessage); 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] [Fact]
public void ModelStateDictionary_NoErrorMessage_ForNonFormatException() public void ModelStateDictionary_NoErrorMessage_ForNonFormatException()
{ {

View File

@ -522,6 +522,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
MissingBindRequiredValueAccessor = Resources.FormatModelBinding_MissingBindRequiredMember, MissingBindRequiredValueAccessor = Resources.FormatModelBinding_MissingBindRequiredMember,
MissingKeyOrValueAccessor = Resources.FormatKeyValuePair_BothKeyAndValueMustBePresent, MissingKeyOrValueAccessor = Resources.FormatKeyValuePair_BothKeyAndValueMustBePresent,
ValueMustNotBeNullAccessor = Resources.FormatModelBinding_NullValueNotValid, ValueMustNotBeNullAccessor = Resources.FormatModelBinding_NullValueNotValid,
AttemptedValueIsInvalidAccessor = Resources.FormatModelState_AttemptedValueIsInvalid,
UnknownValueIsInvalidAccessor = Resources.FormatModelState_UnknownValueIsInvalid,
ValueIsInvalidAccessor = Resources.FormatHtmlGeneration_ValueIsInvalid,
}; };
} }

View File

@ -1037,9 +1037,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
{ {
return new ModelBindingMessageProvider 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.", MissingKeyOrValueAccessor = () => $"A value is required.",
ValueMustNotBeNullAccessor = value => $"The value '{ value }' is invalid.", 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.",
}; };
} }

View File

@ -9,7 +9,6 @@ using System.Threading.Tasks;
using Microsoft.AspNet.Http; using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Internal; using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNet.Mvc.Abstractions; using Microsoft.AspNet.Mvc.Abstractions;
using Microsoft.AspNet.Mvc.Controllers;
using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.ModelBinding;
#if !DNXCORE50 #if !DNXCORE50
using Microsoft.AspNet.Testing.xunit; 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); 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 #if DNXCORE50
[Theory] [Theory]
#else #else
@ -306,12 +363,12 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
var metadataProvider = new TestModelMetadataProvider(); var metadataProvider = new TestModelMetadataProvider();
metadataProvider metadataProvider
.ForType(parameterType) .ForType(parameterType)
.BindingDetails((Action<ModelBinding.Metadata.BindingMetadata>)(binding => .BindingDetails(binding =>
{ {
// A real details provider could customize message based on BindingMetadataProviderContext. // A real details provider could customize message based on BindingMetadataProviderContext.
binding.ModelBindingMessageProvider.ValueMustNotBeNullAccessor = binding.ModelBindingMessageProvider.ValueMustNotBeNullAccessor =
value => $"Hurts when '{ value }' is provided."; value => $"Hurts when '{ value }' is provided.";
})); });
var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(metadataProvider); var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(metadataProvider);
var parameter = new ParameterDescriptor var parameter = new ParameterDescriptor

View File

@ -93,16 +93,22 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
{ {
// Arrange // Arrange
var expectedViewContext = CreateViewContext(); var expectedViewContext = CreateViewContext();
var modelExpression = CreateModelExpression("Hello");
var generator = new Mock<IHtmlGenerator>(); var generator = new Mock<IHtmlGenerator>();
generator generator
.Setup(mock => .Setup(mock => mock.GenerateValidationMessage(
mock.GenerateValidationMessage(expectedViewContext, "Hello", null, null, null)) expectedViewContext,
modelExpression.ModelExplorer,
modelExpression.Name,
null,
null,
null))
.Returns(new TagBuilder("span")) .Returns(new TagBuilder("span"))
.Verifiable(); .Verifiable();
var validationMessageTagHelper = new ValidationMessageTagHelper(generator.Object) var validationMessageTagHelper = new ValidationMessageTagHelper(generator.Object)
{ {
For = CreateModelExpression("Hello") For = modelExpression,
}; };
var expectedPreContent = "original pre-content"; var expectedPreContent = "original pre-content";
var expectedContent = "original content"; var expectedContent = "original content";
@ -155,6 +161,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
var setup = generator var setup = generator
.Setup(mock => mock.GenerateValidationMessage( .Setup(mock => mock.GenerateValidationMessage(
It.IsAny<ViewContext>(), It.IsAny<ViewContext>(),
It.IsAny<ModelExplorer>(),
It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<string>(), It.IsAny<string>(),
@ -214,6 +221,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
var setup = generator var setup = generator
.Setup(mock => mock.GenerateValidationMessage( .Setup(mock => mock.GenerateValidationMessage(
It.IsAny<ViewContext>(), It.IsAny<ViewContext>(),
It.IsAny<ModelExplorer>(),
It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<string>(), It.IsAny<string>(),

View File

@ -111,6 +111,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
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.", MissingKeyOrValueAccessor = () => $"A value is required.",
ValueMustNotBeNullAccessor = value => $"The value '{ value }' is invalid.", 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.",
}; };
} }

View File

@ -165,12 +165,7 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
// Act and assert // Act and assert
var ex = Assert.Throws<ArgumentException>( var ex = Assert.Throws<ArgumentException>(
"expression", "expression",
() => htmlGenerator.GenerateValidationMessage( () => htmlGenerator.GenerateValidationMessage(viewContext, null, null, "Message", "tag", null));
viewContext,
null,
"Message",
"tag",
null));
Assert.Equal(expected, ex.Message); Assert.Equal(expected, ex.Message);
} }