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>
/// <value>Default <see cref="string"/> is "The value '{0}' is invalid.".</value>
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;
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);
}

View File

@ -282,38 +282,6 @@ namespace Microsoft.AspNet.Mvc.Abstractions
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)
{
var value = _resourceManager.GetString(name);

View File

@ -168,10 +168,4 @@
<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>
</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>

View File

@ -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());

View File

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

View File

@ -13,6 +13,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
private Func<string, string> _missingBindRequiredValueAccessor;
private Func<string> _missingKeyOrValueAccessor;
private Func<string, string> _valueMustNotBeNullAccessor;
private Func<string, string, string> _attemptedValueIsInvalidAccessor;
private Func<string, string> _unknownValueIsInvalidAccessor;
private Func<string, string> _valueIsInvalidAccessor;
/// <summary>
/// Initializes a new instance of the <see cref="ModelBindingMessageProvider"/> 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;
}
/// <inheritdoc/>
@ -91,5 +97,59 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
_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>
/// 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>
internal static string UnableToFindServices
{
@ -483,11 +483,11 @@ namespace Microsoft.AspNet.Mvc.Core
}
/// <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>
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>
@ -1066,6 +1066,54 @@ namespace Microsoft.AspNet.Mvc.Core
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)
{
var value = _resourceManager.GetString(name);

View File

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
@ -325,4 +325,13 @@
<data name="AcceptHeaderParser_ParseAcceptHeader_InvalidValues" xml:space="preserve">
<value>"Invalid values '{0}'."</value>
</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>

View File

@ -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)
{

View File

@ -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.

View File

@ -698,22 +698,6 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
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>
/// No URL for remote validation could be found.
/// </summary>

View File

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
@ -247,9 +247,6 @@
<data name="Common_PropertyNotFound" xml:space="preserve">
<value>The property {0}.{1} could not be found.</value>
</data>
<data name="Common_ValueNotValidForProperty" xml:space="preserve">
<value>The value '{0}' is invalid.</value>
</data>
<data name="RemoteAttribute_NoUrlFound" xml:space="preserve">
<value>No URL for remote validation could be found.</value>
</data>

View File

@ -688,6 +688,7 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
/// <inheritdoc />
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))
{

View File

@ -676,7 +676,12 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
/// <inheritdoc />
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 />
@ -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;

View File

@ -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);
}
/// <inheritdoc />
@ -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);
}
/// <inheritdoc />
@ -133,7 +137,7 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
}
var modelExplorer = GetModelExplorer(expression);
return GenerateDisplayName(modelExplorer, ExpressionHelper.GetExpressionText(expression));
return GenerateDisplayName(modelExplorer, GetExpressionName(expression));
}
/// <inheritdoc />
@ -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);
}
/// <inheritdoc />
@ -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);
}
/// <inheritdoc />
@ -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);
}
}
}

View File

@ -69,8 +69,8 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
/// Generate a &lt;input type="checkbox".../&gt; element.
/// </summary>
/// <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="expression">The model expression.</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="isChecked">The initial state of the checkbox element.</param>
/// <param name="htmlAttributes">
/// 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,
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(
ViewContext viewContext,
ModelExplorer modelExplorer,
string expression,
string message,
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.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()
{

View File

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

View File

@ -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.",
};
}

View File

@ -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<ModelBinding.Metadata.BindingMetadata>)(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

View File

@ -93,16 +93,22 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
{
// Arrange
var expectedViewContext = CreateViewContext();
var modelExpression = CreateModelExpression("Hello");
var generator = new Mock<IHtmlGenerator>();
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<ViewContext>(),
It.IsAny<ModelExplorer>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
@ -214,6 +221,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
var setup = generator
.Setup(mock => mock.GenerateValidationMessage(
It.IsAny<ViewContext>(),
It.IsAny<ModelExplorer>(),
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.",
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.",
};
}

View File

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