Allow `null` or empty `fullName` in one special case

- #6662
- users can now provide a `name` or `data-valmsg-for` attribute to avoid `ArgumentException`s
  - affects `<input>`, `<select>`, `<textarea>` elements and validation message `<div>`s
- remove `fullName` check in `DefaultHtmlGenerator.GetCurrentValues(...)` entirely

The new workaround is _not_ identical to changing `ViewData.TemplateInfo.HtmlFieldPrefix`
- does not change where expression values are found in `ModelState` or `ViewData`
- likely needs to be combined with additional workarounds i.e. for advanced use only

nits:
- clean up some excessive argument naming; add a few missing argument names
- take VS suggestions in changed classes e.g. inline a few variable declarations
- clean up some test data
This commit is contained in:
Doug Bunting 2018-01-02 12:31:27 -08:00
parent 75e3ed952b
commit ecedbd5372
16 changed files with 1317 additions and 192 deletions

View File

@ -113,6 +113,15 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
[HtmlAttributeName("type")] [HtmlAttributeName("type")]
public string InputTypeName { get; set; } public string InputTypeName { get; set; }
/// <summary>
/// The name of the &lt;input&gt; element.
/// </summary>
/// <remarks>
/// Passed through to the generated HTML in all cases. Also used to determine whether <see cref="For"/> is
/// valid with an empty <see cref="ModelExpression.Name"/>.
/// </remarks>
public string Name { get; set; }
/// <summary> /// <summary>
/// The value of the &lt;input&gt; element. /// The value of the &lt;input&gt; element.
/// </summary> /// </summary>
@ -146,6 +155,11 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
output.CopyHtmlAttribute("type", context); output.CopyHtmlAttribute("type", context);
} }
if (Name != null)
{
output.CopyHtmlAttribute(nameof(Name), context);
}
if (Value != null) if (Value != null)
{ {
output.CopyHtmlAttribute(nameof(Value), context); output.CopyHtmlAttribute(nameof(Value), context);
@ -183,15 +197,27 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
output.Attributes.SetAttribute("type", inputType); output.Attributes.SetAttribute("type", inputType);
} }
// Ensure Generator does not throw due to empty "fullName" if user provided a name attribute.
IDictionary<string, object> htmlAttributes = null;
if (string.IsNullOrEmpty(For.Name) &&
string.IsNullOrEmpty(ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix) &&
!string.IsNullOrEmpty(Name))
{
htmlAttributes = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
{
{ "name", Name },
};
}
TagBuilder tagBuilder; TagBuilder tagBuilder;
switch (inputType) switch (inputType)
{ {
case "hidden": case "hidden":
tagBuilder = GenerateHidden(modelExplorer); tagBuilder = GenerateHidden(modelExplorer, htmlAttributes);
break; break;
case "checkbox": case "checkbox":
tagBuilder = GenerateCheckBox(modelExplorer, output); tagBuilder = GenerateCheckBox(modelExplorer, output, htmlAttributes);
break; break;
case "password": case "password":
@ -200,15 +226,15 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
modelExplorer, modelExplorer,
For.Name, For.Name,
value: null, value: null,
htmlAttributes: null); htmlAttributes: htmlAttributes);
break; break;
case "radio": case "radio":
tagBuilder = GenerateRadio(modelExplorer); tagBuilder = GenerateRadio(modelExplorer, htmlAttributes);
break; break;
default: default:
tagBuilder = GenerateTextBox(modelExplorer, inputTypeHint, inputType); tagBuilder = GenerateTextBox(modelExplorer, inputTypeHint, inputType, htmlAttributes);
break; break;
} }
@ -248,7 +274,10 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
return inputTypeHint; return inputTypeHint;
} }
private TagBuilder GenerateCheckBox(ModelExplorer modelExplorer, TagHelperOutput output) private TagBuilder GenerateCheckBox(
ModelExplorer modelExplorer,
TagHelperOutput output,
IDictionary<string, object> htmlAttributes)
{ {
if (modelExplorer.ModelType == typeof(string)) if (modelExplorer.ModelType == typeof(string))
{ {
@ -282,6 +311,14 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
var renderingMode = var renderingMode =
output.TagMode == TagMode.SelfClosing ? TagRenderMode.SelfClosing : TagRenderMode.StartTag; output.TagMode == TagMode.SelfClosing ? TagRenderMode.SelfClosing : TagRenderMode.StartTag;
hiddenForCheckboxTag.TagRenderMode = renderingMode; hiddenForCheckboxTag.TagRenderMode = renderingMode;
if (!hiddenForCheckboxTag.Attributes.ContainsKey("name") &&
!string.IsNullOrEmpty(Name))
{
// The checkbox and hidden elements should have the same name attribute value. Attributes will
// match if both are present because both have a generated value. Reach here in the special case
// where user provided a non-empty fallback name.
hiddenForCheckboxTag.MergeAttribute("name", Name);
}
if (ViewContext.FormContext.CanRenderAtEndOfForm) if (ViewContext.FormContext.CanRenderAtEndOfForm)
{ {
@ -298,10 +335,10 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
modelExplorer, modelExplorer,
For.Name, For.Name,
isChecked: null, isChecked: null,
htmlAttributes: null); htmlAttributes: htmlAttributes);
} }
private TagBuilder GenerateRadio(ModelExplorer modelExplorer) private TagBuilder GenerateRadio(ModelExplorer modelExplorer, IDictionary<string, object> htmlAttributes)
{ {
// Note empty string is allowed. // Note empty string is allowed.
if (Value == null) if (Value == null)
@ -319,10 +356,14 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
For.Name, For.Name,
Value, Value,
isChecked: null, isChecked: null,
htmlAttributes: null); htmlAttributes: htmlAttributes);
} }
private TagBuilder GenerateTextBox(ModelExplorer modelExplorer, string inputTypeHint, string inputType) private TagBuilder GenerateTextBox(
ModelExplorer modelExplorer,
string inputTypeHint,
string inputType,
IDictionary<string, object> htmlAttributes)
{ {
var format = Format; var format = Format;
if (string.IsNullOrEmpty(format)) if (string.IsNullOrEmpty(format))
@ -338,12 +379,18 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
format = GetFormat(modelExplorer, inputTypeHint, inputType); format = GetFormat(modelExplorer, inputTypeHint, inputType);
} }
} }
var htmlAttributes = new Dictionary<string, object>
{
{ "type", inputType }
};
if (string.Equals(inputType, "file") && string.Equals(inputTypeHint, TemplateRenderer.IEnumerableOfIFormFileName)) if (htmlAttributes == null)
{
htmlAttributes = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
}
htmlAttributes["type"] = inputType;
if (string.Equals(inputType, "file") &&
string.Equals(
inputTypeHint,
TemplateRenderer.IEnumerableOfIFormFileName,
StringComparison.OrdinalIgnoreCase))
{ {
htmlAttributes["multiple"] = "multiple"; htmlAttributes["multiple"] = "multiple";
} }
@ -352,14 +399,14 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
ViewContext, ViewContext,
modelExplorer, modelExplorer,
For.Name, For.Name,
value: modelExplorer.Model, modelExplorer.Model,
format: format, format,
htmlAttributes: htmlAttributes); htmlAttributes);
} }
// Imitate Generator.GenerateHidden() using Generator.GenerateTextBox(). This adds support for asp-format that // Imitate Generator.GenerateHidden() using Generator.GenerateTextBox(). This adds support for asp-format that
// is not available in Generator.GenerateHidden(). // is not available in Generator.GenerateHidden().
private TagBuilder GenerateHidden(ModelExplorer modelExplorer) private TagBuilder GenerateHidden(ModelExplorer modelExplorer, IDictionary<string, object> htmlAttributes)
{ {
var value = For.Model; var value = For.Model;
if (value is byte[] byteArrayValue) if (value is byte[] byteArrayValue)
@ -367,21 +414,17 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
value = Convert.ToBase64String(byteArrayValue); value = Convert.ToBase64String(byteArrayValue);
} }
if (htmlAttributes == null)
{
htmlAttributes = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
}
// In DefaultHtmlGenerator(), GenerateTextBox() calls GenerateInput() _almost_ identically to how // In DefaultHtmlGenerator(), GenerateTextBox() calls GenerateInput() _almost_ identically to how
// GenerateHidden() does and the main switch inside GenerateInput() handles InputType.Text and // GenerateHidden() does and the main switch inside GenerateInput() handles InputType.Text and
// InputType.Hidden identically. No behavior differences at all when a type HTML attribute already exists. // InputType.Hidden identically. No behavior differences at all when a type HTML attribute already exists.
var htmlAttributes = new Dictionary<string, object> htmlAttributes["type"] = "hidden";
{
{ "type", "hidden" }
};
return Generator.GenerateTextBox( return Generator.GenerateTextBox(ViewContext, modelExplorer, For.Name, value, Format, htmlAttributes);
ViewContext,
modelExplorer,
For.Name,
value: value,
format: Format,
htmlAttributes: htmlAttributes);
} }
// Get a fall-back format based on the metadata. // Get a fall-back format based on the metadata.
@ -462,4 +505,4 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
} }
} }
} }
} }

View File

@ -57,6 +57,15 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
[HtmlAttributeName(ItemsAttributeName)] [HtmlAttributeName(ItemsAttributeName)]
public IEnumerable<SelectListItem> Items { get; set; } public IEnumerable<SelectListItem> Items { get; set; }
/// <summary>
/// The name of the &lt;input&gt; element.
/// </summary>
/// <remarks>
/// Passed through to the generated HTML in all cases. Also used to determine whether <see cref="For"/> is
/// valid with an empty <see cref="ModelExpression.Name"/>.
/// </remarks>
public string Name { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public override void Init(TagHelperContext context) public override void Init(TagHelperContext context)
{ {
@ -89,11 +98,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
var realModelType = For.ModelExplorer.ModelType; var realModelType = For.ModelExplorer.ModelType;
_allowMultiple = typeof(string) != realModelType && _allowMultiple = typeof(string) != realModelType &&
typeof(IEnumerable).IsAssignableFrom(realModelType); typeof(IEnumerable).IsAssignableFrom(realModelType);
_currentValues = Generator.GetCurrentValues( _currentValues = Generator.GetCurrentValues(ViewContext, For.ModelExplorer, For.Name, _allowMultiple);
ViewContext,
For.ModelExplorer,
expression: For.Name,
allowMultiple: _allowMultiple);
// Whether or not (not being highly unlikely) we generate anything, could update contained <option/> // Whether or not (not being highly unlikely) we generate anything, could update contained <option/>
// elements. Provide selected values for <option/> tag helpers. // elements. Provide selected values for <option/> tag helpers.
@ -115,6 +120,13 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
throw new ArgumentNullException(nameof(output)); throw new ArgumentNullException(nameof(output));
} }
// Pass through attribute that is also a well-known HTML attribute. Must be done prior to any copying
// from a TagBuilder.
if (Name != null)
{
output.CopyHtmlAttribute(nameof(Name), context);
}
// Ensure GenerateSelect() _never_ looks anything up in ViewData. // Ensure GenerateSelect() _never_ looks anything up in ViewData.
var items = Items ?? Enumerable.Empty<SelectListItem>(); var items = Items ?? Enumerable.Empty<SelectListItem>();
@ -125,6 +137,18 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
return; return;
} }
// Ensure Generator does not throw due to empty "fullName" if user provided a name attribute.
IDictionary<string, object> htmlAttributes = null;
if (string.IsNullOrEmpty(For.Name) &&
string.IsNullOrEmpty(ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix) &&
!string.IsNullOrEmpty(Name))
{
htmlAttributes = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
{
{ "name", Name },
};
}
var tagBuilder = Generator.GenerateSelect( var tagBuilder = Generator.GenerateSelect(
ViewContext, ViewContext,
For.ModelExplorer, For.ModelExplorer,
@ -133,7 +157,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
selectList: items, selectList: items,
currentValues: _currentValues, currentValues: _currentValues,
allowMultiple: _allowMultiple, allowMultiple: _allowMultiple,
htmlAttributes: null); htmlAttributes: htmlAttributes);
if (tagBuilder != null) if (tagBuilder != null)
{ {
@ -145,4 +169,4 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
} }
} }
} }
} }

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.AspNetCore.Razor.TagHelpers;
@ -40,6 +41,15 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
[HtmlAttributeName(ForAttributeName)] [HtmlAttributeName(ForAttributeName)]
public ModelExpression For { get; set; } public ModelExpression For { get; set; }
/// <summary>
/// The name of the &lt;input&gt; element.
/// </summary>
/// <remarks>
/// Passed through to the generated HTML in all cases. Also used to determine whether <see cref="For"/> is
/// valid with an empty <see cref="ModelExpression.Name"/>.
/// </remarks>
public string Name { get; set; }
/// <inheritdoc /> /// <inheritdoc />
/// <remarks>Does nothing if <see cref="For"/> is <c>null</c>.</remarks> /// <remarks>Does nothing if <see cref="For"/> is <c>null</c>.</remarks>
public override void Process(TagHelperContext context, TagHelperOutput output) public override void Process(TagHelperContext context, TagHelperOutput output)
@ -54,13 +64,32 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
throw new ArgumentNullException(nameof(output)); throw new ArgumentNullException(nameof(output));
} }
// Pass through attribute that is also a well-known HTML attribute. Must be done prior to any copying
// from a TagBuilder.
if (Name != null)
{
output.CopyHtmlAttribute(nameof(Name), context);
}
// Ensure Generator does not throw due to empty "fullName" if user provided a name attribute.
IDictionary<string, object> htmlAttributes = null;
if (string.IsNullOrEmpty(For.Name) &&
string.IsNullOrEmpty(ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix) &&
!string.IsNullOrEmpty(Name))
{
htmlAttributes = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
{
{ "name", Name },
};
}
var tagBuilder = Generator.GenerateTextArea( var tagBuilder = Generator.GenerateTextArea(
ViewContext, ViewContext,
For.ModelExplorer, For.ModelExplorer,
For.Name, For.Name,
rows: 0, rows: 0,
columns: 0, columns: 0,
htmlAttributes: null); htmlAttributes: htmlAttributes);
if (tagBuilder != null) if (tagBuilder != null)
{ {

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Mvc.ViewFeatures;
@ -16,6 +17,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
[HtmlTargetElement("span", Attributes = ValidationForAttributeName)] [HtmlTargetElement("span", Attributes = ValidationForAttributeName)]
public class ValidationMessageTagHelper : TagHelper public class ValidationMessageTagHelper : TagHelper
{ {
private const string DataValidationForAttributeName = "data-valmsg-for";
private const string ValidationForAttributeName = "asp-validation-for"; private const string ValidationForAttributeName = "asp-validation-for";
/// <summary> /// <summary>
@ -36,9 +38,6 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
protected IHtmlGenerator Generator { get; } protected IHtmlGenerator Generator { get; }
/// <summary>
/// Name to be validated on the current model.
/// </summary>
[HtmlAttributeName(ValidationForAttributeName)] [HtmlAttributeName(ValidationForAttributeName)]
public ModelExpression For { get; set; } public ModelExpression For { get; set; }
@ -58,13 +57,27 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
if (For != null) if (For != null)
{ {
// Ensure Generator does not throw due to empty "fullName" if user provided data-valmsg-for attribute.
// Assume data-valmsg-for value is non-empty if attribute is present at all. Should align with name of
// another tag helper e.g. an <input/> and those tag helpers bind Name.
IDictionary<string, object> htmlAttributes = null;
if (string.IsNullOrEmpty(For.Name) &&
string.IsNullOrEmpty(ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix) &&
output.Attributes.ContainsName(DataValidationForAttributeName))
{
htmlAttributes = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
{
{ DataValidationForAttributeName, "-non-empty-value-" },
};
}
var tagBuilder = Generator.GenerateValidationMessage( var tagBuilder = Generator.GenerateValidationMessage(
ViewContext, ViewContext,
For.ModelExplorer, For.ModelExplorer,
For.Name, For.Name,
message: null, message: null,
tag: null, tag: null,
htmlAttributes: null); htmlAttributes: htmlAttributes);
if (tagBuilder != null) if (tagBuilder != null)
{ {

View File

@ -214,8 +214,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
if (modelExplorer.Model != null) if (modelExplorer.Model != null)
{ {
bool modelChecked; if (bool.TryParse(modelExplorer.Model.ToString(), out var modelChecked))
if (bool.TryParse(modelExplorer.Model.ToString(), out modelChecked))
{ {
isChecked = modelChecked; isChecked = modelChecked;
} }
@ -261,7 +260,10 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
tagBuilder.TagRenderMode = TagRenderMode.SelfClosing; tagBuilder.TagRenderMode = TagRenderMode.SelfClosing;
var fullName = NameAndIdProvider.GetFullHtmlFieldName(viewContext, expression); var fullName = NameAndIdProvider.GetFullHtmlFieldName(viewContext, expression);
tagBuilder.MergeAttribute("name", fullName); if (!string.IsNullOrEmpty(fullName))
{
tagBuilder.MergeAttribute("name", fullName);
}
return tagBuilder; return tagBuilder;
} }
@ -363,8 +365,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
} }
// Special-case opaque values and arbitrary binary data. // Special-case opaque values and arbitrary binary data.
var byteArrayValue = value as byte[]; if (value is byte[] byteArrayValue)
if (byteArrayValue != null)
{ {
value = Convert.ToBase64String(byteArrayValue); value = Convert.ToBase64String(byteArrayValue);
} }
@ -596,7 +597,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
} }
var fullName = NameAndIdProvider.GetFullHtmlFieldName(viewContext, expression); var fullName = NameAndIdProvider.GetFullHtmlFieldName(viewContext, expression);
if (string.IsNullOrEmpty(fullName)) var htmlAttributeDictionary = GetHtmlAttributeDictionaryOrNull(htmlAttributes);
if (!IsFullNameValid(fullName, htmlAttributeDictionary))
{ {
throw new ArgumentException( throw new ArgumentException(
Resources.FormatHtmlGenerator_FieldNameCannotBeNullOrEmpty( Resources.FormatHtmlGenerator_FieldNameCannotBeNullOrEmpty(
@ -622,17 +624,20 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
var tagBuilder = new TagBuilder("select"); var tagBuilder = new TagBuilder("select");
tagBuilder.InnerHtml.SetHtmlContent(listItemBuilder); tagBuilder.InnerHtml.SetHtmlContent(listItemBuilder);
tagBuilder.MergeAttributes(GetHtmlAttributeDictionaryOrNull(htmlAttributes)); tagBuilder.MergeAttributes(htmlAttributeDictionary);
tagBuilder.MergeAttribute("name", fullName, true /* replaceExisting */);
NameAndIdProvider.GenerateId(viewContext, tagBuilder, fullName, IdAttributeDotReplacement); NameAndIdProvider.GenerateId(viewContext, tagBuilder, fullName, IdAttributeDotReplacement);
if (!string.IsNullOrEmpty(fullName))
{
tagBuilder.MergeAttribute("name", fullName, replaceExisting: true);
}
if (allowMultiple) if (allowMultiple)
{ {
tagBuilder.MergeAttribute("multiple", "multiple"); tagBuilder.MergeAttribute("multiple", "multiple");
} }
// If there are any errors for a named field, we add the css attribute. // If there are any errors for a named field, we add the css attribute.
ModelStateEntry entry; if (viewContext.ViewData.ModelState.TryGetValue(fullName, out var entry))
if (viewContext.ViewData.ModelState.TryGetValue(fullName, out entry))
{ {
if (entry.Errors.Count > 0) if (entry.Errors.Count > 0)
{ {
@ -672,7 +677,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
} }
var fullName = NameAndIdProvider.GetFullHtmlFieldName(viewContext, expression); var fullName = NameAndIdProvider.GetFullHtmlFieldName(viewContext, expression);
if (string.IsNullOrEmpty(fullName)) var htmlAttributeDictionary = GetHtmlAttributeDictionaryOrNull(htmlAttributes);
if (!IsFullNameValid(fullName, htmlAttributeDictionary))
{ {
throw new ArgumentException( throw new ArgumentException(
Resources.FormatHtmlGenerator_FieldNameCannotBeNullOrEmpty( Resources.FormatHtmlGenerator_FieldNameCannotBeNullOrEmpty(
@ -684,8 +690,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
nameof(expression)); nameof(expression));
} }
ModelStateEntry entry; viewContext.ViewData.ModelState.TryGetValue(fullName, out var entry);
viewContext.ViewData.ModelState.TryGetValue(fullName, out entry);
var value = string.Empty; var value = string.Empty;
if (entry != null && entry.AttemptedValue != null) if (entry != null && entry.AttemptedValue != null)
@ -699,18 +704,24 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
var tagBuilder = new TagBuilder("textarea"); var tagBuilder = new TagBuilder("textarea");
NameAndIdProvider.GenerateId(viewContext, tagBuilder, fullName, IdAttributeDotReplacement); NameAndIdProvider.GenerateId(viewContext, tagBuilder, fullName, IdAttributeDotReplacement);
tagBuilder.MergeAttributes(GetHtmlAttributeDictionaryOrNull(htmlAttributes), true); tagBuilder.MergeAttributes(htmlAttributeDictionary, replaceExisting: true);
if (rows > 0) if (rows > 0)
{ {
tagBuilder.MergeAttribute("rows", rows.ToString(CultureInfo.InvariantCulture), true); tagBuilder.MergeAttribute("rows", rows.ToString(CultureInfo.InvariantCulture), replaceExisting: true);
} }
if (columns > 0) if (columns > 0)
{ {
tagBuilder.MergeAttribute("cols", columns.ToString(CultureInfo.InvariantCulture), true); tagBuilder.MergeAttribute(
"cols",
columns.ToString(CultureInfo.InvariantCulture),
replaceExisting: true);
} }
tagBuilder.MergeAttribute("name", fullName, true); if (!string.IsNullOrEmpty(fullName))
{
tagBuilder.MergeAttribute("name", fullName, replaceExisting: true);
}
AddPlaceholderAttribute(viewContext.ViewData, tagBuilder, modelExplorer, expression); AddPlaceholderAttribute(viewContext.ViewData, tagBuilder, modelExplorer, expression);
AddValidationAttributes(viewContext, tagBuilder, modelExplorer, expression); AddValidationAttributes(viewContext, tagBuilder, modelExplorer, expression);
@ -773,7 +784,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
} }
var fullName = NameAndIdProvider.GetFullHtmlFieldName(viewContext, expression); var fullName = NameAndIdProvider.GetFullHtmlFieldName(viewContext, expression);
if (string.IsNullOrEmpty(fullName)) var htmlAttributeDictionary = GetHtmlAttributeDictionaryOrNull(htmlAttributes);
if (!IsFullNameValid(fullName, htmlAttributeDictionary, fallbackAttributeName: "data-valmsg-for"))
{ {
throw new ArgumentException( throw new ArgumentException(
Resources.FormatHtmlGenerator_FieldNameCannotBeNullOrEmpty( Resources.FormatHtmlGenerator_FieldNameCannotBeNullOrEmpty(
@ -791,8 +803,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
return null; return null;
} }
ModelStateEntry entry; var tryGetModelStateResult = viewContext.ViewData.ModelState.TryGetValue(fullName, out var entry);
var tryGetModelStateResult = viewContext.ViewData.ModelState.TryGetValue(fullName, out entry);
var modelErrors = tryGetModelStateResult ? entry.Errors : null; var modelErrors = tryGetModelStateResult ? entry.Errors : null;
ModelError modelError = null; ModelError modelError = null;
@ -812,8 +823,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{ {
tag = viewContext.ValidationMessageElement; tag = viewContext.ValidationMessageElement;
} }
var tagBuilder = new TagBuilder(tag); var tagBuilder = new TagBuilder(tag);
tagBuilder.MergeAttributes(GetHtmlAttributeDictionaryOrNull(htmlAttributes)); tagBuilder.MergeAttributes(htmlAttributeDictionary);
// Only the style of the span is changed according to the errors if message is null or empty. // Only the style of the span is changed according to the errors if message is null or empty.
// Otherwise the content and style is handled by the client-side validation. // Otherwise the content and style is handled by the client-side validation.
@ -838,7 +850,10 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
if (formContext != null) if (formContext != null)
{ {
tagBuilder.MergeAttribute("data-valmsg-for", fullName); if (!string.IsNullOrEmpty(fullName))
{
tagBuilder.MergeAttribute("data-valmsg-for", fullName);
}
var replaceValidationMessageContents = string.IsNullOrEmpty(message); var replaceValidationMessageContents = string.IsNullOrEmpty(message);
tagBuilder.MergeAttribute("data-valmsg-replace", tagBuilder.MergeAttribute("data-valmsg-replace",
@ -868,9 +883,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
return null; return null;
} }
ModelStateEntry entryForModel;
if (excludePropertyErrors && if (excludePropertyErrors &&
(!viewData.ModelState.TryGetValue(viewData.TemplateInfo.HtmlFieldPrefix, out entryForModel) || (!viewData.ModelState.TryGetValue(viewData.TemplateInfo.HtmlFieldPrefix, out var entryForModel) ||
entryForModel.Errors.Count == 0)) entryForModel.Errors.Count == 0))
{ {
// Client-side validation (if enabled) will not affect the generated element and element will be empty. // Client-side validation (if enabled) will not affect the generated element and element will be empty.
@ -964,18 +978,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
} }
var fullName = NameAndIdProvider.GetFullHtmlFieldName(viewContext, expression); var fullName = NameAndIdProvider.GetFullHtmlFieldName(viewContext, expression);
if (string.IsNullOrEmpty(fullName))
{
throw new ArgumentException(
Resources.FormatHtmlGenerator_FieldNameCannotBeNullOrEmpty(
typeof(IHtmlHelper).FullName,
nameof(IHtmlHelper.Editor),
typeof(IHtmlHelper<>).FullName,
nameof(IHtmlHelper<object>.EditorFor),
"htmlFieldName"),
nameof(expression));
}
var type = allowMultiple ? typeof(string[]) : typeof(string); var type = allowMultiple ? typeof(string[]) : typeof(string);
var rawValue = GetModelStateValue(viewContext, fullName, type); var rawValue = GetModelStateValue(viewContext, fullName, type);
@ -1133,8 +1135,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
internal static object GetModelStateValue(ViewContext viewContext, string key, Type destinationType) internal static object GetModelStateValue(ViewContext viewContext, string key, Type destinationType)
{ {
ModelStateEntry entry; if (viewContext.ViewData.ModelState.TryGetValue(key, out var entry) && entry.RawValue != null)
if (viewContext.ViewData.ModelState.TryGetValue(key, out entry) && entry.RawValue != null)
{ {
return ModelBindingHelper.ConvertTo(entry.RawValue, destinationType, culture: null); return ModelBindingHelper.ConvertTo(entry.RawValue, destinationType, culture: null);
} }
@ -1207,7 +1208,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
// elements. But we support the *ForModel() methods in any lower-level template, once HtmlFieldPrefix is // elements. But we support the *ForModel() methods in any lower-level template, once HtmlFieldPrefix is
// non-empty. // non-empty.
var fullName = NameAndIdProvider.GetFullHtmlFieldName(viewContext, expression); var fullName = NameAndIdProvider.GetFullHtmlFieldName(viewContext, expression);
if (string.IsNullOrEmpty(fullName)) if (!IsFullNameValid(fullName, htmlAttributes))
{ {
throw new ArgumentException( throw new ArgumentException(
Resources.FormatHtmlGenerator_FieldNameCannotBeNullOrEmpty( Resources.FormatHtmlGenerator_FieldNameCannotBeNullOrEmpty(
@ -1220,11 +1221,17 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
} }
var inputTypeString = GetInputTypeString(inputType); var inputTypeString = GetInputTypeString(inputType);
var tagBuilder = new TagBuilder("input"); var tagBuilder = new TagBuilder("input")
tagBuilder.TagRenderMode = TagRenderMode.SelfClosing; {
TagRenderMode = TagRenderMode.SelfClosing,
};
tagBuilder.MergeAttributes(htmlAttributes); tagBuilder.MergeAttributes(htmlAttributes);
tagBuilder.MergeAttribute("type", inputTypeString); tagBuilder.MergeAttribute("type", inputTypeString);
tagBuilder.MergeAttribute("name", fullName, replaceExisting: true); if (!string.IsNullOrEmpty(fullName))
{
tagBuilder.MergeAttribute("name", fullName, replaceExisting: true);
}
var suppliedTypeString = tagBuilder.Attributes["type"]; var suppliedTypeString = tagBuilder.Attributes["type"];
if (_placeholderInputTypes.Contains(suppliedTypeString)) if (_placeholderInputTypes.Contains(suppliedTypeString))
@ -1249,8 +1256,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
case InputType.Radio: case InputType.Radio:
if (!usedModelState) if (!usedModelState)
{ {
var modelStateValue = GetModelStateValue(viewContext, fullName, typeof(string)) as string; if (GetModelStateValue(viewContext, fullName, typeof(string)) is string modelStateValue)
if (modelStateValue != null)
{ {
isChecked = string.Equals(modelStateValue, valueParameter, StringComparison.Ordinal); isChecked = string.Equals(modelStateValue, valueParameter, StringComparison.Ordinal);
usedModelState = true; usedModelState = true;
@ -1313,8 +1319,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
} }
// If there are any errors for a named field, we add the CSS attribute. // If there are any errors for a named field, we add the CSS attribute.
ModelStateEntry entry; if (viewContext.ViewData.ModelState.TryGetValue(fullName, out var entry) && entry.Errors.Count > 0)
if (viewContext.ViewData.ModelState.TryGetValue(fullName, out entry) && entry.Errors.Count > 0)
{ {
tagBuilder.AddCssClass(HtmlHelper.ValidationInputCssClassName); tagBuilder.AddCssClass(HtmlHelper.ValidationInputCssClassName);
} }
@ -1410,8 +1415,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
private static object ConvertEnumFromString<TEnum>(string value) where TEnum : struct private static object ConvertEnumFromString<TEnum>(string value) where TEnum : struct
{ {
TEnum enumValue; if (Enum.TryParse(value, out TEnum enumValue))
if (Enum.TryParse(value, out enumValue))
{ {
return enumValue; return enumValue;
} }
@ -1500,10 +1504,41 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
return selectList; return selectList;
} }
private static bool IsFullNameValid(string fullName, IDictionary<string, object> htmlAttributeDictionary)
{
return IsFullNameValid(fullName, htmlAttributeDictionary, fallbackAttributeName: "name");
}
private static bool IsFullNameValid(
string fullName,
IDictionary<string, object> htmlAttributeDictionary,
string fallbackAttributeName)
{
if (string.IsNullOrEmpty(fullName))
{
// fullName==null is normally an error because name="" is not valid in HTML 5.
if (htmlAttributeDictionary == null)
{
return false;
}
// Check if user has provided an explicit name attribute.
// Generalized a bit because other attributes e.g. data-valmsg-for refer to element names.
htmlAttributeDictionary.TryGetValue(fallbackAttributeName, out var attributeObject);
var attributeString = Convert.ToString(attributeObject, CultureInfo.InvariantCulture);
if (string.IsNullOrEmpty(attributeString))
{
return false;
}
}
return true;
}
/// <inheritdoc /> /// <inheritdoc />
public IHtmlContent GenerateGroupsAndOptions(string optionLabel, IEnumerable<SelectListItem> selectList) public IHtmlContent GenerateGroupsAndOptions(string optionLabel, IEnumerable<SelectListItem> selectList)
{ {
return GenerateGroupsAndOptions(optionLabel: optionLabel, selectList: selectList, currentValues: null); return GenerateGroupsAndOptions(optionLabel, selectList, currentValues: null);
} }
private IHtmlContent GenerateGroupsAndOptions( private IHtmlContent GenerateGroupsAndOptions(

View File

@ -160,8 +160,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
/// </remarks> /// </remarks>
public static IDictionary<string, object> AnonymousObjectToHtmlAttributes(object htmlAttributes) public static IDictionary<string, object> AnonymousObjectToHtmlAttributes(object htmlAttributes)
{ {
var dictionary = htmlAttributes as IDictionary<string, object>; if (htmlAttributes is IDictionary<string, object> dictionary)
if (dictionary != null)
{ {
return new Dictionary<string, object>(dictionary, StringComparer.OrdinalIgnoreCase); return new Dictionary<string, object>(dictionary, StringComparer.OrdinalIgnoreCase);
} }
@ -586,7 +585,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
/// <inheritdoc /> /// <inheritdoc />
public IHtmlContent Raw(object value) public IHtmlContent Raw(object value)
{ {
return new HtmlString(value == null ? null : value.ToString()); return new HtmlString(value?.ToString());
} }
/// <inheritdoc /> /// <inheritdoc />
@ -685,7 +684,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
/// <inheritdoc /> /// <inheritdoc />
public IHtmlContent TextBox(string expression, object value, string format, object htmlAttributes) public IHtmlContent TextBox(string expression, object value, string format, object htmlAttributes)
{ {
return GenerateTextBox(modelExplorer: null, expression: expression, value: value, format: format, return GenerateTextBox(
modelExplorer: null,
expression: expression,
value: value,
format: format,
htmlAttributes: htmlAttributes); htmlAttributes: htmlAttributes);
} }
@ -724,6 +727,15 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
return HtmlString.Empty; return HtmlString.Empty;
} }
if (!hiddenForCheckbox.Attributes.ContainsKey("name") &&
checkbox.Attributes.TryGetValue("name", out var name))
{
// The checkbox and hidden elements should have the same name attribute value. Attributes will match
// if both are present because both have a generated value. Reach here in the special case where user
// provided a non-empty fallback name.
hiddenForCheckbox.MergeAttribute("name", name);
}
if (ViewContext.FormContext.CanRenderAtEndOfForm) if (ViewContext.FormContext.CanRenderAtEndOfForm)
{ {
ViewContext.FormContext.EndOfFormContent.Add(hiddenForCheckbox); ViewContext.FormContext.EndOfFormContent.Add(hiddenForCheckbox);
@ -861,7 +873,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
tagBuilder.WriteTo(ViewContext.Writer, _htmlEncoder); tagBuilder.WriteTo(ViewContext.Writer, _htmlEncoder);
} }
var shouldGenerateAntiforgery = antiforgery.HasValue ? antiforgery.Value : method != FormMethod.Get; var shouldGenerateAntiforgery = antiforgery ?? method != FormMethod.Get;
if (shouldGenerateAntiforgery) if (shouldGenerateAntiforgery)
{ {
ViewContext.FormContext.EndOfFormContent.Add(_htmlGenerator.GenerateAntiforgery(ViewContext)); ViewContext.FormContext.EndOfFormContent.Add(_htmlGenerator.GenerateAntiforgery(ViewContext));
@ -917,7 +929,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
tagBuilder.WriteTo(ViewContext.Writer, _htmlEncoder); tagBuilder.WriteTo(ViewContext.Writer, _htmlEncoder);
} }
var shouldGenerateAntiforgery = antiforgery.HasValue ? antiforgery.Value : method != FormMethod.Get; var shouldGenerateAntiforgery = antiforgery ?? method != FormMethod.Get;
if (shouldGenerateAntiforgery) if (shouldGenerateAntiforgery)
{ {
ViewContext.FormContext.EndOfFormContent.Add(_htmlGenerator.GenerateAntiforgery(ViewContext)); ViewContext.FormContext.EndOfFormContent.Add(_htmlGenerator.GenerateAntiforgery(ViewContext));
@ -969,9 +981,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
var tagBuilder = _htmlGenerator.GenerateLabel( var tagBuilder = _htmlGenerator.GenerateLabel(
ViewContext, ViewContext,
modelExplorer, modelExplorer,
expression: expression, expression,
labelText: labelText, labelText,
htmlAttributes: htmlAttributes); htmlAttributes);
if (tagBuilder == null) if (tagBuilder == null)
{ {
return HtmlString.Empty; return HtmlString.Empty;

View File

@ -22,7 +22,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
{ {
public class InputTagHelperTest public class InputTagHelperTest
{ {
public static TheoryData MultiAttributeCheckBoxData public static TheoryData<TagHelperAttributeList, string> MultiAttributeCheckBoxData
{ {
get get
{ {
@ -252,6 +252,317 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
Assert.Equal(expectedPostElement, output.PostElement.GetContent()); Assert.Equal(expectedPostElement, output.PostElement.GetContent());
} }
[Theory]
[InlineData("checkbox")]
[InlineData("hidden")]
[InlineData("number")]
[InlineData("password")]
[InlineData("text")]
public void Process_WithEmptyForName_Throws(string inputTypeName)
{
// Arrange
var expectedMessage = "The name of an HTML field cannot be null or empty. Instead use methods " +
"Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper.Editor or Microsoft.AspNetCore.Mvc.Rendering." +
"IHtmlHelper`1.EditorFor with a non-empty htmlFieldName argument value.";
var metadataProvider = new EmptyModelMetadataProvider();
var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
var model = false;
var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(bool), model);
var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer);
var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);
var tagHelper = new InputTagHelper(htmlGenerator)
{
For = modelExpression,
InputTypeName = inputTypeName,
ViewContext = viewContext,
};
var attributes = new TagHelperAttributeList
{
{ "type", inputTypeName },
};
var context = new TagHelperContext(attributes, new Dictionary<object, object>(), "test");
var output = new TagHelperOutput(
"input",
new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null))
{
TagMode = TagMode.SelfClosing,
};
// Act & Assert
ExceptionAssert.ThrowsArgument(
() => tagHelper.Process(context, output),
paramName: "expression",
exceptionMessage: expectedMessage);
}
[Fact]
public void Process_Radio_WithEmptyForName_Throws()
{
// Arrange
var expectedMessage = "The name of an HTML field cannot be null or empty. Instead use methods " +
"Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper.Editor or Microsoft.AspNetCore.Mvc.Rendering." +
"IHtmlHelper`1.EditorFor with a non-empty htmlFieldName argument value.";
var inputTypeName = "radio";
var metadataProvider = new EmptyModelMetadataProvider();
var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
var model = 23;
var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(int), model);
var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer);
var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);
var tagHelper = new InputTagHelper(htmlGenerator)
{
For = modelExpression,
InputTypeName = inputTypeName,
Value = "24",
ViewContext = viewContext,
};
var attributes = new TagHelperAttributeList
{
{ "type", inputTypeName },
{ "value", "24" },
};
var context = new TagHelperContext(attributes, new Dictionary<object, object>(), "test");
var output = new TagHelperOutput(
"input",
new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null))
{
TagMode = TagMode.SelfClosing,
};
// Act & Assert
ExceptionAssert.ThrowsArgument(
() => tagHelper.Process(context, output),
paramName: "expression",
exceptionMessage: expectedMessage);
}
[Theory]
[InlineData("hidden")]
[InlineData("number")]
[InlineData("text")]
public void Process_WithEmptyForName_DoesNotThrow_WithName(string inputTypeName)
{
// Arrange
var expectedAttributeValue = "-expression-";
var expectedTagName = "input";
var expectedAttributes = new TagHelperAttributeList
{
{ "name", expectedAttributeValue },
{ "type", inputTypeName },
{ "value", "False" },
};
var metadataProvider = new EmptyModelMetadataProvider();
var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
var model = false;
var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(bool), model);
var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer);
var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);
viewContext.ClientValidationEnabled = false;
var tagHelper = new InputTagHelper(htmlGenerator)
{
For = modelExpression,
InputTypeName = inputTypeName,
Name = expectedAttributeValue,
ViewContext = viewContext,
};
var attributes = new TagHelperAttributeList
{
{ "name", expectedAttributeValue },
{ "type", inputTypeName },
};
var context = new TagHelperContext(attributes, new Dictionary<object, object>(), "test");
var output = new TagHelperOutput(
expectedTagName,
new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null))
{
TagMode = TagMode.SelfClosing,
};
// Act
tagHelper.Process(context, output);
// Assert
Assert.Equal(expectedAttributes, output.Attributes);
Assert.False(output.IsContentModified);
Assert.Equal(expectedTagName, output.TagName);
}
[Fact]
public void Process_Checkbox_WithEmptyForName_DoesNotThrow_WithName()
{
// Arrange
var expectedAttributeValue = "-expression-";
var expectedPostElementContent = $"<input name=\"HtmlEncode[[{expectedAttributeValue}]]\" " +
"type=\"HtmlEncode[[hidden]]\" value=\"HtmlEncode[[false]]\" />";
var expectedTagName = "input";
var inputTypeName = "checkbox";
var expectedAttributes = new TagHelperAttributeList
{
{ "name", expectedAttributeValue },
{ "type", inputTypeName },
{ "value", "true" },
};
var metadataProvider = new EmptyModelMetadataProvider();
var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
var model = false;
var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(bool), model);
var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer);
var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);
viewContext.ClientValidationEnabled = false;
var tagHelper = new InputTagHelper(htmlGenerator)
{
For = modelExpression,
InputTypeName = inputTypeName,
Name = expectedAttributeValue,
ViewContext = viewContext,
};
var attributes = new TagHelperAttributeList
{
{ "name", expectedAttributeValue },
{ "type", inputTypeName },
};
var context = new TagHelperContext(attributes, new Dictionary<object, object>(), "test");
var output = new TagHelperOutput(
expectedTagName,
new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null))
{
TagMode = TagMode.SelfClosing,
};
// Act
tagHelper.Process(context, output);
// Assert
Assert.Equal(expectedAttributes, output.Attributes);
Assert.False(output.IsContentModified);
Assert.Equal(expectedTagName, output.TagName);
Assert.False(viewContext.FormContext.HasEndOfFormContent);
Assert.Equal(expectedPostElementContent, HtmlContentUtilities.HtmlContentToString(output.PostElement));
}
[Fact]
public void Process_Password_WithEmptyForName_DoesNotThrow_WithName()
{
// Arrange
var expectedAttributeValue = "-expression-";
var expectedTagName = "input";
var inputTypeName = "password";
var expectedAttributes = new TagHelperAttributeList
{
{ "name", expectedAttributeValue },
{ "type", inputTypeName },
};
var metadataProvider = new EmptyModelMetadataProvider();
var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
var model = "password";
var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(string), model);
var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer);
var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);
viewContext.ClientValidationEnabled = false;
var tagHelper = new InputTagHelper(htmlGenerator)
{
For = modelExpression,
InputTypeName = inputTypeName,
Name = expectedAttributeValue,
ViewContext = viewContext,
};
// Expect attributes to just pass through. Tag helper binds all input attributes and doesn't add any.
var context = new TagHelperContext(expectedAttributes, new Dictionary<object, object>(), "test");
var output = new TagHelperOutput(
expectedTagName,
new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null))
{
TagMode = TagMode.SelfClosing,
};
// Act
tagHelper.Process(context, output);
// Assert
Assert.Equal(expectedAttributes, output.Attributes);
Assert.False(output.IsContentModified);
Assert.Equal(expectedTagName, output.TagName);
}
[Fact]
public void Process_Radio_WithEmptyForName_DoesNotThrow_WithName()
{
// Arrange
var expectedAttributeValue = "-expression-";
var expectedTagName = "input";
var inputTypeName = "radio";
var expectedAttributes = new TagHelperAttributeList
{
{ "name", expectedAttributeValue },
{ "type", inputTypeName },
{ "value", "24" },
};
var metadataProvider = new EmptyModelMetadataProvider();
var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
var model = 23;
var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(int), model);
var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer);
var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);
viewContext.ClientValidationEnabled = false;
var tagHelper = new InputTagHelper(htmlGenerator)
{
For = modelExpression,
InputTypeName = inputTypeName,
Name = expectedAttributeValue,
Value = "24",
ViewContext = viewContext,
};
var attributes = new TagHelperAttributeList
{
{ "name", expectedAttributeValue },
{ "type", inputTypeName },
{ "value", "24" },
};
var context = new TagHelperContext(attributes, new Dictionary<object, object>(), "test");
var output = new TagHelperOutput(
expectedTagName,
new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null))
{
TagMode = TagMode.SelfClosing,
};
// Act
tagHelper.Process(context, output);
// Assert
Assert.Equal(expectedAttributes, output.Attributes);
Assert.False(output.IsContentModified);
Assert.Equal(expectedTagName, output.TagName);
}
// Top-level container (List<Model> or Model instance), immediate container type (Model or NestModel), // Top-level container (List<Model> or Model instance), immediate container type (Model or NestModel),
// model accessor, expression path / id, expected value. // model accessor, expression path / id, expected value.
public static TheoryData<object, Type, object, NameAndId, string> TestDataSet public static TheoryData<object, Type, object, NameAndId, string> TestDataSet
@ -1410,7 +1721,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
}; };
var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var model = new DateTime( var model = new DateTime(
year: 2000, year: 2000,
month: 1, month: 1,

View File

@ -6,12 +6,14 @@ using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.TagHelpers.Internal; using Microsoft.AspNetCore.Mvc.TagHelpers.Internal;
using Microsoft.AspNetCore.Mvc.TestCommon; using Microsoft.AspNetCore.Mvc.TestCommon;
using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.AspNetCore.Testing;
using Moq; using Moq;
using Xunit; using Xunit;
@ -704,6 +706,132 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
Assert.Same(currentValues, actualCurrentValues.Values); Assert.Same(currentValues, actualCurrentValues.Values);
} }
[Fact]
public void Process_WithEmptyForName_Throws()
{
// Arrange
var expectedMessage = "The name of an HTML field cannot be null or empty. Instead use methods " +
"Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper.Editor or Microsoft.AspNetCore.Mvc.Rendering." +
"IHtmlHelper`1.EditorFor with a non-empty htmlFieldName argument value.";
var expectedTagName = "select";
var metadataProvider = new EmptyModelMetadataProvider();
var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
var model = "model-value";
var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(string), model);
var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer);
var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);
var tagHelper = new SelectTagHelper(htmlGenerator)
{
For = modelExpression,
ViewContext = viewContext,
};
var context = new TagHelperContext(new TagHelperAttributeList(), new Dictionary<object, object>(), "test");
var output = new TagHelperOutput(
expectedTagName,
new TagHelperAttributeList(),
(_, __) => Task.FromResult<TagHelperContent>(new DefaultTagHelperContent()));
// Act & Assert
ExceptionAssert.ThrowsArgument(
() => tagHelper.Process(context, output),
paramName: "expression",
exceptionMessage: expectedMessage);
}
[Fact]
public void Process_WithEmptyForName_DoesNotThrow_WithName()
{
// Arrange
var expectedAttributeValue = "-expression-";
var expectedTagName = "select";
var metadataProvider = new EmptyModelMetadataProvider();
var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
var model = "model-value";
var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(string), model);
var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer);
var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);
var tagHelper = new SelectTagHelper(htmlGenerator)
{
For = modelExpression,
Name = expectedAttributeValue,
ViewContext = viewContext,
};
var attributes = new TagHelperAttributeList
{
{ "name", expectedAttributeValue },
};
var context = new TagHelperContext(attributes, new Dictionary<object, object>(), "test");
var output = new TagHelperOutput(
expectedTagName,
new TagHelperAttributeList(),
(_, __) => Task.FromResult<TagHelperContent>(new DefaultTagHelperContent()));
// Act
tagHelper.Process(context, output);
// Assert
Assert.Equal(expectedTagName, output.TagName);
Assert.False(output.IsContentModified);
var attribute = Assert.Single(output.Attributes);
Assert.Equal("name", attribute.Name);
Assert.Equal(expectedAttributeValue, attribute.Value);
}
[Fact]
public void Process_PassesNameThrough_EvenIfNullFor()
{
// Arrange
var expectedAttributeValue = "-expression-";
var expectedTagName = "span";
var selectList = Array.Empty<SelectListItem>();
var generator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
generator
.Setup(gen => gen.GenerateGroupsAndOptions(/* optionLabel: */ null, selectList))
.Returns(HtmlString.Empty)
.Verifiable();
var metadataProvider = new EmptyModelMetadataProvider();
var viewContext = TestableHtmlGenerator.GetViewContext(
model: null,
htmlGenerator: generator.Object,
metadataProvider: metadataProvider);
var tagHelper = new SelectTagHelper(generator.Object)
{
Items = selectList,
Name = expectedAttributeValue,
ViewContext = viewContext,
};
var attributes = new TagHelperAttributeList
{
{ "name", expectedAttributeValue },
};
var tagHelperContext = new TagHelperContext(attributes, new Dictionary<object, object>(), "test");
var output = new TagHelperOutput(
expectedTagName,
new TagHelperAttributeList(),
(_, __) => Task.FromResult<TagHelperContent>(new DefaultTagHelperContent()));
// Act
tagHelper.Process(tagHelperContext, output);
// Assert
generator.VerifyAll();
Assert.Equal(expectedTagName, output.TagName);
var attribute = Assert.Single(output.Attributes);
Assert.Equal("name", attribute.Name);
Assert.Equal(expectedAttributeValue, attribute.Value);
}
public class NameAndId public class NameAndId
{ {
public NameAndId(string name, string id) public NameAndId(string name, string id)

View File

@ -6,10 +6,10 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.TestCommon; using Microsoft.AspNetCore.Mvc.TestCommon;
using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.AspNetCore.Testing;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.TagHelpers namespace Microsoft.AspNetCore.Mvc.TagHelpers
@ -160,6 +160,84 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
Assert.Equal(expectedTagName, output.TagName); Assert.Equal(expectedTagName, output.TagName);
} }
[Fact]
public void Process_WithEmptyForName_Throws()
{
// Arrange
var expectedMessage = "The name of an HTML field cannot be null or empty. Instead use methods " +
"Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper.Editor or Microsoft.AspNetCore.Mvc.Rendering." +
"IHtmlHelper`1.EditorFor with a non-empty htmlFieldName argument value.";
var expectedTagName = "textarea";
var metadataProvider = new EmptyModelMetadataProvider();
var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
var model = "model-value";
var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(string), model);
var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer);
var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);
var tagHelper = new TextAreaTagHelper(htmlGenerator)
{
For = modelExpression,
ViewContext = viewContext,
};
var context = new TagHelperContext(new TagHelperAttributeList(), new Dictionary<object, object>(), "test");
var output = new TagHelperOutput(
expectedTagName,
new TagHelperAttributeList(),
(_, __) => Task.FromResult<TagHelperContent>(new DefaultTagHelperContent()));
// Act & Assert
ExceptionAssert.ThrowsArgument(
() => tagHelper.Process(context, output),
paramName: "expression",
exceptionMessage: expectedMessage);
}
[Fact]
public void Process_WithEmptyForName_DoesNotThrow_WithName()
{
// Arrange
var expectedAttributeValue = "-expression-";
var expectedContent = Environment.NewLine + "HtmlEncode[[model-value]]";
var expectedTagName = "textarea";
var metadataProvider = new EmptyModelMetadataProvider();
var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
var model = "model-value";
var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(string), model);
var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer);
var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);
var tagHelper = new TextAreaTagHelper(htmlGenerator)
{
For = modelExpression,
Name = expectedAttributeValue,
ViewContext = viewContext,
};
var attributes = new TagHelperAttributeList
{
{ "name", expectedAttributeValue },
};
var context = new TagHelperContext(attributes, new Dictionary<object, object>(), "test");
var output = new TagHelperOutput(
expectedTagName,
new TagHelperAttributeList(),
(_, __) => Task.FromResult<TagHelperContent>(new DefaultTagHelperContent()));
// Act
tagHelper.Process(context, output);
// Assert
Assert.Equal(expectedTagName, output.TagName);
Assert.Equal(expectedContent, HtmlContentUtilities.HtmlContentToString(output.Content));
var attribute = Assert.Single(output.Attributes);
Assert.Equal("name", attribute.Name);
Assert.Equal(expectedAttributeValue, attribute.Value);
}
public class NameAndId public class NameAndId
{ {
public NameAndId(string name, string id) public NameAndId(string name, string id)

View File

@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Testing;
using Moq; using Moq;
using Xunit; using Xunit;
@ -89,6 +90,151 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
Assert.Equal(expectedTagName, output.TagName); Assert.Equal(expectedTagName, output.TagName);
} }
[Fact]
public async Task ProcessAsync_WithEmptyNameFor_Throws()
{
// Arrange
var expectedTagName = "span";
var expectedMessage = "The name of an HTML field cannot be null or empty. Instead use methods " +
"Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper.Editor or Microsoft.AspNetCore.Mvc.Rendering." +
"IHtmlHelper`1.EditorFor with a non-empty htmlFieldName argument value.";
var metadataProvider = new EmptyModelMetadataProvider();
var modelExpression = CreateModelExpression(string.Empty);
var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
var viewContext = TestableHtmlGenerator.GetViewContext(
model: null,
htmlGenerator: htmlGenerator,
metadataProvider: metadataProvider);
var validationMessageTagHelper = new ValidationMessageTagHelper(htmlGenerator)
{
For = modelExpression,
ViewContext = viewContext,
};
var tagHelperContext = new TagHelperContext(
expectedTagName,
new TagHelperAttributeList
{
{ "for", modelExpression },
},
new Dictionary<object, object>(),
"test");
var output = new TagHelperOutput(
expectedTagName,
new TagHelperAttributeList(),
(_, __) => Task.FromResult<TagHelperContent>(new DefaultTagHelperContent()));
// Act & Assert
await ExceptionAssert.ThrowsArgumentAsync(
() => validationMessageTagHelper.ProcessAsync(tagHelperContext, output),
paramName: "expression",
exceptionMessage: expectedMessage);
}
[Fact]
public async Task ProcessAsync_GeneratesExpectedOutput_WithEmptyNameFor_WithValidationFor()
{
// Arrange
var expectedAttributeValue = "-expression-";
var expectedTagName = "span";
var metadataProvider = new EmptyModelMetadataProvider();
var modelExpression = CreateModelExpression(string.Empty);
var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
var viewContext = TestableHtmlGenerator.GetViewContext(
model: null,
htmlGenerator: htmlGenerator,
metadataProvider: metadataProvider);
var validationMessageTagHelper = new ValidationMessageTagHelper(htmlGenerator)
{
For = modelExpression,
ViewContext = viewContext,
};
var tagHelperContext = new TagHelperContext(
expectedTagName,
new TagHelperAttributeList
{
{ "for", modelExpression },
},
new Dictionary<object, object>(),
"test");
var output = new TagHelperOutput(
expectedTagName,
new TagHelperAttributeList
{
{ "data-valmsg-for", expectedAttributeValue },
},
(_, __) => Task.FromResult<TagHelperContent>(new DefaultTagHelperContent()));
validationMessageTagHelper.ViewContext = viewContext;
// Act
await validationMessageTagHelper.ProcessAsync(tagHelperContext, output);
// Assert
Assert.Equal(expectedTagName, output.TagName);
Assert.Collection(output.Attributes,
attribute =>
{
Assert.Equal("data-valmsg-for", attribute.Name);
Assert.Equal(expectedAttributeValue, attribute.Value);
},
attribute =>
{
Assert.Equal("class", attribute.Name);
Assert.Equal("field-validation-valid", attribute.Value);
},
attribute =>
{
Assert.Equal("data-valmsg-replace", attribute.Name);
Assert.Equal("true", attribute.Value);
});
}
[Fact]
public async Task ProcessAsync_PassesValidationForThrough_EvenIfNullFor()
{
// Arrange
var expectedAttributeValue = "-expression-";
var expectedTagName = "span";
// Generator is not used in this scenario.
var generator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
var validationMessageTagHelper = new ValidationMessageTagHelper(generator.Object)
{
ViewContext = CreateViewContext(),
};
var tagHelperContext = new TagHelperContext(
expectedTagName,
new TagHelperAttributeList(),
new Dictionary<object, object>(),
"test");
var output = new TagHelperOutput(
expectedTagName,
new TagHelperAttributeList
{
{ "data-valmsg-for", expectedAttributeValue },
},
(_, __) => Task.FromResult<TagHelperContent>(new DefaultTagHelperContent()));
// Act
await validationMessageTagHelper.ProcessAsync(tagHelperContext, output);
// Assert
Assert.Equal(expectedTagName, output.TagName);
var attribute = Assert.Single(output.Attributes);
Assert.Equal("data-valmsg-for", attribute.Name);
Assert.Equal(expectedAttributeValue, attribute.Value);
}
[Fact] [Fact]
public async Task ProcessAsync_CallsIntoGenerateValidationMessageWithExpectedParameters() public async Task ProcessAsync_CallsIntoGenerateValidationMessageWithExpectedParameters()
{ {

View File

@ -95,7 +95,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
"Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper.Editor or Microsoft.AspNetCore.Mvc.Rendering." + "Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper.Editor or Microsoft.AspNetCore.Mvc.Rendering." +
"IHtmlHelper`1.EditorFor with a non-empty htmlFieldName argument value."; "IHtmlHelper`1.EditorFor with a non-empty htmlFieldName argument value.";
var helper = DefaultTemplatesUtilities.GetHtmlHelper(GetTestModelViewData()); var helper = DefaultTemplatesUtilities.GetHtmlHelper(model: false);
// Act & Assert // Act & Assert
ExceptionAssert.ThrowsArgument( ExceptionAssert.ThrowsArgument(
@ -104,6 +104,29 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
expected); expected);
} }
[Fact]
public void CheckBoxWithNullExpression_DoesNotThrow_WithNameAttribute()
{
// Arrange
var expected = @"<input class=""HtmlEncode[[some-class]]"" name=""HtmlEncode[[-expression-]]"" " +
@"type=""HtmlEncode[[checkbox]]"" value=""HtmlEncode[[true]]"" /><input " +
@"name=""HtmlEncode[[-expression-]]"" type=""HtmlEncode[[hidden]]"" value=""HtmlEncode[[false]]"" />";
var helper = DefaultTemplatesUtilities.GetHtmlHelper(model: false);
helper.ViewContext.ClientValidationEnabled = false;
var attributes = new Dictionary<string, object>
{
{ "class", "some-class"},
{ "name", "-expression-" },
};
// Act
var html = helper.CheckBox(null, isChecked: false, htmlAttributes: attributes);
// Assert
Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(html));
}
[Fact] [Fact]
public void CheckBoxCheckedWithOnlyName_GeneratesExpectedValue() public void CheckBoxCheckedWithOnlyName_GeneratesExpectedValue()
{ {
@ -413,12 +436,17 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
var requiredMessage = ValidationAttributeUtil.GetRequiredErrorMessage("Boolean"); var requiredMessage = ValidationAttributeUtil.GetRequiredErrorMessage("Boolean");
var expected = var expected =
$@"<input data-val=""HtmlEncode[[true]]"" data-val-required=""HtmlEncode[[{requiredMessage}]]"" " + $@"<input data-val=""HtmlEncode[[true]]"" data-val-required=""HtmlEncode[[{requiredMessage}]]"" " +
@"id=""HtmlEncode[[MyPrefix]]"" name=""HtmlEncode[[MyPrefix]]"" Property3=""HtmlEncode[[Property3Value]]"" " + @"id=""HtmlEncode[[MyPrefix]]"" name=""HtmlEncode[[MyPrefix]]"" " +
@"type=""HtmlEncode[[checkbox]]"" value=""HtmlEncode[[true]]"" /><input name=""HtmlEncode[[MyPrefix]]"" type=""HtmlEncode[[hidden]]"" " + @"Property3=""HtmlEncode[[Property3Value]]"" type=""HtmlEncode[[checkbox]]"" " +
@"value=""HtmlEncode[[false]]"" />"; @"value=""HtmlEncode[[true]]"" /><input name=""HtmlEncode[[MyPrefix]]"" " +
@"type=""HtmlEncode[[hidden]]"" value=""HtmlEncode[[false]]"" />";
var helper = DefaultTemplatesUtilities.GetHtmlHelper(model: false); var helper = DefaultTemplatesUtilities.GetHtmlHelper(model: false);
var attributes = new Dictionary<string, object> { { "Property3", "Property3Value" } };
helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix"; helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";
var attributes = new Dictionary<string, object>
{
{ "Property3", "Property3Value" },
{ "name", "-expression-" }, // overridden
};
// Act // Act
var html = helper.CheckBox(string.Empty, isChecked: false, htmlAttributes: attributes); var html = helper.CheckBox(string.Empty, isChecked: false, htmlAttributes: attributes);
@ -589,10 +617,15 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
var expected = var expected =
$@"<input data-val=""HtmlEncode[[true]]"" data-val-required=""HtmlEncode[[{requiredMessage}]]"" " + $@"<input data-val=""HtmlEncode[[true]]"" data-val-required=""HtmlEncode[[{requiredMessage}]]"" " +
@"id=""HtmlEncode[[Property1]]"" name=""HtmlEncode[[Property1]]"" " + @"id=""HtmlEncode[[Property1]]"" name=""HtmlEncode[[Property1]]"" " +
@"Property3=""HtmlEncode[[Property3Value]]"" type=""HtmlEncode[[checkbox]]"" value=""HtmlEncode[[true]]"" /><input " + @"Property3=""HtmlEncode[[Property3Value]]"" type=""HtmlEncode[[checkbox]]"" " +
@"name=""HtmlEncode[[Property1]]"" type=""HtmlEncode[[hidden]]"" value=""HtmlEncode[[false]]"" />"; @"value=""HtmlEncode[[true]]"" /><input name=""HtmlEncode[[Property1]]"" " +
@"type=""HtmlEncode[[hidden]]"" value=""HtmlEncode[[false]]"" />";
var helper = DefaultTemplatesUtilities.GetHtmlHelper(GetTestModelViewData()); var helper = DefaultTemplatesUtilities.GetHtmlHelper(GetTestModelViewData());
var attributes = new Dictionary<string, object> { { "Property3", "Property3Value" } }; var attributes = new Dictionary<string, object>
{
{ "Property3", "Property3Value" },
{ "name", "-expression-" }, // overridden
};
// Act // Act
var html = helper.CheckBoxFor(m => m.Property1, attributes); var html = helper.CheckBoxFor(m => m.Property1, attributes);

View File

@ -15,19 +15,36 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
{ {
public class HtmlHelperHiddenTest public class HtmlHelperHiddenTest
{ {
public static IEnumerable<object[]> HiddenWithAttributesData public static TheoryData<object, string> HiddenWithAttributesData
{ {
get get
{ {
var expected1 = @"<input baz=""HtmlEncode[[BazValue]]"" id=""HtmlEncode[[Property1]]"" name=""HtmlEncode[[Property1]]"" type=""HtmlEncode[[hidden]]"" " + var expected1 = @"<input baz=""HtmlEncode[[BazValue]]"" id=""HtmlEncode[[Property1]]"" " +
@"name=""HtmlEncode[[Property1]]"" type=""HtmlEncode[[hidden]]"" " +
@"value=""HtmlEncode[[ModelStateValue]]"" />"; @"value=""HtmlEncode[[ModelStateValue]]"" />";
yield return new object[] { new Dictionary<string, object> { { "baz", "BazValue" } }, expected1 }; var expected2 = @"<input foo-baz=""HtmlEncode[[BazValue]]"" id=""HtmlEncode[[Property1]]"" " +
yield return new object[] { new { baz = "BazValue" }, expected1 }; @"name=""HtmlEncode[[Property1]]"" type=""HtmlEncode[[hidden]]"" " +
@"value=""HtmlEncode[[ModelStateValue]]"" />";
var htmlAttributes1 = new Dictionary<string, object>
{
{ "baz", "BazValue" },
{ "name", "-expression-" }, // overridden
};
var htmlAttributes2 = new
{
baz = "BazValue",
name = "-expression-", // overridden
};
var expected2 = @"<input foo-baz=""HtmlEncode[[BazValue]]"" id=""HtmlEncode[[Property1]]"" name=""HtmlEncode[[Property1]]"" type=""HtmlEncode[[hidden]]"" " + var data = new TheoryData<object, string>
@"value=""HtmlEncode[[ModelStateValue]]"" />"; {
yield return new object[] { new Dictionary<string, object> { { "foo-baz", "BazValue" } }, expected2 }; { htmlAttributes1, expected1 },
yield return new object[] { new { foo_baz = "BazValue" }, expected2 }; { htmlAttributes2, expected1 },
{ new Dictionary<string, object> { { "foo-baz", "BazValue" } }, expected2 },
{ new { foo_baz = "BazValue" }, expected2 }
};
return data;
} }
} }
@ -408,7 +425,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
public void HiddenWithEmptyNameAndPrefixThrows() public void HiddenWithEmptyNameAndPrefixThrows()
{ {
// Arrange // Arrange
var helper = DefaultTemplatesUtilities.GetHtmlHelper(GetViewDataWithModelStateAndModelAndViewDataValues()); var helper = DefaultTemplatesUtilities.GetHtmlHelper("model-value");
var attributes = new Dictionary<string, object> var attributes = new Dictionary<string, object>
{ {
{ "class", "some-class"} { "class", "some-class"}
@ -419,11 +436,31 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
// Act and Assert // Act and Assert
ExceptionAssert.ThrowsArgument( ExceptionAssert.ThrowsArgument(
() => helper.Hidden(string.Empty, string.Empty, attributes), () => helper.Hidden(expression: string.Empty, value: null, htmlAttributes: attributes),
"expression", "expression",
expected); expected);
} }
[Fact]
public void HiddenWithEmptyNameAndPrefix_DoesNotThrow_WithNameAttribute()
{
// Arrange
var expected = @"<input class=""HtmlEncode[[some-class]]"" name=""HtmlEncode[[-expression-]]"" " +
@"type=""HtmlEncode[[hidden]]"" value=""HtmlEncode[[model-value]]"" />";
var helper = DefaultTemplatesUtilities.GetHtmlHelper("model-value");
var attributes = new Dictionary<string, object>
{
{ "class", "some-class"},
{ "name", "-expression-" },
};
// Act
var result = helper.Hidden(expression: string.Empty, value: null, htmlAttributes: attributes);
// Assert
Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(result));
}
[Fact] [Fact]
public void HiddenWithViewDataErrors_GeneratesExpectedValue() public void HiddenWithViewDataErrors_GeneratesExpectedValue()
{ {

View File

@ -15,39 +15,58 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
{ {
public class HtmlHelperPasswordTest public class HtmlHelperPasswordTest
{ {
public static IEnumerable<object[]> PasswordWithViewDataAndAttributesData public static TheoryData<object> HtmlAttributeData
{ {
get get
{ {
var attributes1 = new Dictionary<string, object> return new TheoryData<object>
{ {
{ "test-key", "test-value" }, new Dictionary<string, object>
{ "value", "attribute-value" } {
{ "name", "-expression-" }, // overridden
{ "test-key", "test-value" },
{ "value", "attribute-value" },
},
new
{
name = "-expression-", // overridden
test_key = "test-value",
value = "attribute-value",
},
}; };
}
}
var attributes2 = new { test_key = "test-value", value = "attribute-value" }; public static TheoryData<ViewDataDictionary<PasswordModel>, object> PasswordWithViewDataAndAttributesData
{
get
{
var nullModelViewData = GetViewDataWithNullModelAndNonEmptyViewData();
var viewData = GetViewDataWithModelStateAndModelAndViewDataValues();
viewData.Model.Property1 = "does-not-get-used";
var vdd = GetViewDataWithModelStateAndModelAndViewDataValues(); var data = new TheoryData<ViewDataDictionary<PasswordModel>, object>();
vdd.Model.Property1 = "does-not-get-used"; foreach (var items in HtmlAttributeData)
yield return new object[] { vdd, attributes1 }; {
yield return new object[] { vdd, attributes2 }; data.Add(viewData, items[0]);
data.Add(nullModelViewData, items[0]);
}
var nullModelVdd = GetViewDataWithNullModelAndNonEmptyViewData(); return data;
yield return new object[] { nullModelVdd, attributes1 };
yield return new object[] { nullModelVdd, attributes2 };
} }
} }
[Theory] [Theory]
[MemberData(nameof(PasswordWithViewDataAndAttributesData))] [MemberData(nameof(PasswordWithViewDataAndAttributesData))]
public void Password_UsesAttributeValueWhenValueArgumentIsNull( public void Password_UsesAttributeValueWhenValueArgumentIsNull(
ViewDataDictionary<PasswordModel> vdd, ViewDataDictionary<PasswordModel> viewData,
object attributes) object attributes)
{ {
// Arrange // Arrange
var expected = @"<input id=""HtmlEncode[[Property1]]"" name=""HtmlEncode[[Property1]]"" test-key=""HtmlEncode[[test-value]]"" type=""HtmlEncode[[password]]"" " + var expected = @"<input id=""HtmlEncode[[Property1]]"" name=""HtmlEncode[[Property1]]"" " +
@"value=""HtmlEncode[[attribute-value]]"" />"; @"test-key=""HtmlEncode[[test-value]]"" type=""HtmlEncode[[password]]"" " +
var helper = DefaultTemplatesUtilities.GetHtmlHelper(vdd); @"value=""HtmlEncode[[attribute-value]]"" />";
var helper = DefaultTemplatesUtilities.GetHtmlHelper(viewData);
// Act // Act
var result = helper.Password("Property1", value: null, htmlAttributes: attributes); var result = helper.Password("Property1", value: null, htmlAttributes: attributes);
@ -59,13 +78,14 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
[Theory] [Theory]
[MemberData(nameof(PasswordWithViewDataAndAttributesData))] [MemberData(nameof(PasswordWithViewDataAndAttributesData))]
public void Password_UsesExplicitValue_IfSpecified( public void Password_UsesExplicitValue_IfSpecified(
ViewDataDictionary<PasswordModel> vdd, ViewDataDictionary<PasswordModel> viewData,
object attributes) object attributes)
{ {
// Arrange // Arrange
var expected = @"<input id=""HtmlEncode[[Property1]]"" name=""HtmlEncode[[Property1]]"" test-key=""HtmlEncode[[test-value]]"" type=""HtmlEncode[[password]]"" " + var expected = @"<input id=""HtmlEncode[[Property1]]"" name=""HtmlEncode[[Property1]]"" " +
@"value=""HtmlEncode[[explicit-value]]"" />"; @"test-key=""HtmlEncode[[test-value]]"" type=""HtmlEncode[[password]]"" " +
var helper = DefaultTemplatesUtilities.GetHtmlHelper(vdd); @"value=""HtmlEncode[[explicit-value]]"" />";
var helper = DefaultTemplatesUtilities.GetHtmlHelper(viewData);
// Act // Act
var result = helper.Password("Property1", "explicit-value", attributes); var result = helper.Password("Property1", "explicit-value", attributes);
@ -128,20 +148,35 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
public void PasswordWithEmptyNameAndPrefixThrows() public void PasswordWithEmptyNameAndPrefixThrows()
{ {
// Arrange // Arrange
var helper = DefaultTemplatesUtilities.GetHtmlHelper(GetViewDataWithModelStateAndModelAndViewDataValues()); var helper = DefaultTemplatesUtilities.GetHtmlHelper("model-value");
var name = string.Empty; var expression = string.Empty;
var value = string.Empty;
var expectedMessage = "The name of an HTML field cannot be null or empty. Instead use methods " + var expectedMessage = "The name of an HTML field cannot be null or empty. Instead use methods " +
"Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper.Editor or Microsoft.AspNetCore.Mvc.Rendering." + "Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper.Editor or Microsoft.AspNetCore.Mvc.Rendering." +
"IHtmlHelper`1.EditorFor with a non-empty htmlFieldName argument value."; "IHtmlHelper`1.EditorFor with a non-empty htmlFieldName argument value.";
// Act and Assert // Act and Assert
ExceptionAssert.ThrowsArgument( ExceptionAssert.ThrowsArgument(
() => helper.Password(name, value, htmlAttributes: null), () => helper.Password(expression, value: null, htmlAttributes: null),
"expression", "expression",
expectedMessage); expectedMessage);
} }
[Fact]
public void PasswordWithEmptyNameAndPrefix_DoesNotThrow_WithNameAttribute()
{
// Arrange
var expected = @"<input name=""HtmlEncode[[-expression-]]"" type=""HtmlEncode[[password]]"" />";
var helper = DefaultTemplatesUtilities.GetHtmlHelper("model-value");
var expression = string.Empty;
var htmlAttributes = new { name = "-expression-" };
// Act
var result = helper.Password(expression, value: null, htmlAttributes: htmlAttributes);
// Assert
Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(result));
}
[Fact] [Fact]
public void Password_UsesModelStateErrors_ButDoesNotUseModelOrViewDataOrModelStateForValueAttribute() public void Password_UsesModelStateErrors_ButDoesNotUseModelOrViewDataOrModelStateForValueAttribute()
{ {
@ -218,14 +253,13 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
} }
[Theory] [Theory]
[MemberData(nameof(PasswordWithViewDataAndAttributesData))] [MemberData(nameof(HtmlAttributeData))]
public void PasswordForWithAttributes_GeneratesExpectedValue( public void PasswordForWithAttributes_GeneratesExpectedValue(object htmlAttributes)
ViewDataDictionary<PasswordModel> vdd,
object htmlAttributes)
{ {
// Arrange // Arrange
var expected = @"<input id=""HtmlEncode[[Property1]]"" name=""HtmlEncode[[Property1]]"" test-key=""HtmlEncode[[test-value]]"" type=""HtmlEncode[[password]]"" " + var expected = @"<input id=""HtmlEncode[[Property1]]"" name=""HtmlEncode[[Property1]]"" " +
@"value=""HtmlEncode[[attribute-value]]"" />"; @"test-key=""HtmlEncode[[test-value]]"" type=""HtmlEncode[[password]]"" " +
@"value=""HtmlEncode[[attribute-value]]"" />";
var helper = DefaultTemplatesUtilities.GetHtmlHelper(GetViewDataWithModelStateAndModelAndViewDataValues()); var helper = DefaultTemplatesUtilities.GetHtmlHelper(GetViewDataWithModelStateAndModelAndViewDataValues());
helper.ViewData.Model.Property1 = "test"; helper.ViewData.Model.Property1 = "test";

View File

@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.TestCommon; using Microsoft.AspNetCore.Mvc.TestCommon;
using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Testing;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.Core namespace Microsoft.AspNetCore.Mvc.Core
@ -122,13 +123,19 @@ namespace Microsoft.AspNetCore.Mvc.Core
{ {
// Arrange // Arrange
var helper = DefaultTemplatesUtilities.GetHtmlHelper(); var helper = DefaultTemplatesUtilities.GetHtmlHelper();
var htmlAttributes = new
{
attr = "value",
name = "-expression-", // overridden
};
// Act // Act
var radioButtonResult = helper.RadioButton("Property1", value: "myvalue", htmlAttributes: new { attr = "value" }); var radioButtonResult = helper.RadioButton("Property1", "myvalue", htmlAttributes);
// Assert // Assert
Assert.Equal( Assert.Equal(
"<input attr=\"HtmlEncode[[value]]\" id=\"HtmlEncode[[Property1]]\" name=\"HtmlEncode[[Property1]]\" type=\"HtmlEncode[[radio]]\" value=\"HtmlEncode[[myvalue]]\" />", "<input attr=\"HtmlEncode[[value]]\" id=\"HtmlEncode[[Property1]]\" " +
"name=\"HtmlEncode[[Property1]]\" type=\"HtmlEncode[[radio]]\" value=\"HtmlEncode[[myvalue]]\" />",
HtmlContentUtilities.HtmlContentToString(radioButtonResult)); HtmlContentUtilities.HtmlContentToString(radioButtonResult));
} }
@ -137,13 +144,61 @@ namespace Microsoft.AspNetCore.Mvc.Core
{ {
// Arrange // Arrange
var helper = DefaultTemplatesUtilities.GetHtmlHelper(); var helper = DefaultTemplatesUtilities.GetHtmlHelper();
var htmlAttributes = new
{
attr = "value",
name = "-expression-", // overridden
};
// Act // Act
var radioButtonForResult = helper.RadioButtonFor(m => m.Property1, value: "myvalue", htmlAttributes: new { attr = "value" }); var radioButtonForResult = helper.RadioButtonFor(m => m.Property1, "myvalue", htmlAttributes);
// Assert // Assert
Assert.Equal( Assert.Equal(
"<input attr=\"HtmlEncode[[value]]\" id=\"HtmlEncode[[Property1]]\" name=\"HtmlEncode[[Property1]]\" type=\"HtmlEncode[[radio]]\" value=\"HtmlEncode[[myvalue]]\" />", "<input attr=\"HtmlEncode[[value]]\" id=\"HtmlEncode[[Property1]]\" " +
"name=\"HtmlEncode[[Property1]]\" type=\"HtmlEncode[[radio]]\" value=\"HtmlEncode[[myvalue]]\" />",
HtmlContentUtilities.HtmlContentToString(radioButtonForResult));
}
[Fact]
public void RadioButtonFor_Throws_IfFullNameEmpty()
{
// Arrange
var expectedMessage = "The name of an HTML field cannot be null or empty. Instead use methods " +
"Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper.Editor or Microsoft.AspNetCore.Mvc.Rendering." +
"IHtmlHelper`1.EditorFor with a non-empty htmlFieldName argument value.";
var helper = DefaultTemplatesUtilities.GetHtmlHelper("anotherValue");
var htmlAttributes = new
{
attr = "value",
};
// Act & Assert
ExceptionAssert.ThrowsArgument(
() => helper.RadioButtonFor(m => m, "myvalue", htmlAttributes),
paramName: "expression",
exceptionMessage: expectedMessage);
}
[Fact]
public void RadioButtonFor_DoesNotThrow_IfFullNameEmpty_WithNameAttribute()
{
// Arrange
var helper = DefaultTemplatesUtilities.GetHtmlHelper("anotherValue");
var htmlAttributes = new
{
attr = "value",
name = "-expression-",
};
// Act
var radioButtonForResult = helper.RadioButtonFor(m => m, "myvalue", htmlAttributes);
// Assert
Assert.Equal(
"<input attr=\"HtmlEncode[[value]]\" " +
"name=\"HtmlEncode[[-expression-]]\" type=\"HtmlEncode[[radio]]\" value=\"HtmlEncode[[myvalue]]\" />",
HtmlContentUtilities.HtmlContentToString(radioButtonForResult)); HtmlContentUtilities.HtmlContentToString(radioButtonForResult));
} }

View File

@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.TestCommon; using Microsoft.AspNetCore.Mvc.TestCommon;
using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Testing;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.Core namespace Microsoft.AspNetCore.Mvc.Core
@ -108,17 +109,27 @@ namespace Microsoft.AspNetCore.Mvc.Core
public void TextBox_UsesSpecifiedHtmlAttributes() public void TextBox_UsesSpecifiedHtmlAttributes()
{ {
// Arrange // Arrange
var metadataProvider = new EmptyModelMetadataProvider(); var htmlAttributes = new
var helper = DefaultTemplatesUtilities.GetHtmlHelper(new ViewDataDictionary<TestModel>(metadataProvider)); {
attr = "value",
name = "-expression-", // overridden
};
var model = new TestModel
{
Property1 = "propValue"
};
var helper = DefaultTemplatesUtilities.GetHtmlHelper(model);
helper.ViewContext.ClientValidationEnabled = false; helper.ViewContext.ClientValidationEnabled = false;
helper.ViewData.Model = new TestModel { Property1 = "propValue" };
// Act // Act
var textBoxResult = helper.TextBox("Property1", value: "myvalue", htmlAttributes: new { attr = "value" }); var textBoxResult = helper.TextBox("Property1", "myvalue", htmlAttributes);
// Assert // Assert
Assert.Equal( Assert.Equal(
"<input attr=\"HtmlEncode[[value]]\" id=\"HtmlEncode[[Property1]]\" name=\"HtmlEncode[[Property1]]\" type=\"HtmlEncode[[text]]\" value=\"HtmlEncode[[myvalue]]\" />", "<input attr=\"HtmlEncode[[value]]\" id=\"HtmlEncode[[Property1]]\" " +
"name=\"HtmlEncode[[Property1]]\" type=\"HtmlEncode[[text]]\" value=\"HtmlEncode[[myvalue]]\" />",
HtmlContentUtilities.HtmlContentToString(textBoxResult)); HtmlContentUtilities.HtmlContentToString(textBoxResult));
} }
@ -126,17 +137,73 @@ namespace Microsoft.AspNetCore.Mvc.Core
public void TextBoxFor_UsesSpecifiedHtmlAttributes() public void TextBoxFor_UsesSpecifiedHtmlAttributes()
{ {
// Arrange // Arrange
var metadataProvider = new EmptyModelMetadataProvider(); var htmlAttributes = new
var helper = DefaultTemplatesUtilities.GetHtmlHelper(new ViewDataDictionary<TestModel>(metadataProvider)); {
attr = "value",
name = "-expression-", // overridden
};
var model = new TestModel
{
Property1 = "propValue"
};
var helper = DefaultTemplatesUtilities.GetHtmlHelper(model);
helper.ViewContext.ClientValidationEnabled = false; helper.ViewContext.ClientValidationEnabled = false;
helper.ViewData.Model = new TestModel { Property1 = "propValue" };
// Act // Act
var textBoxForResult = helper.TextBoxFor(m => m.Property1, htmlAttributes: new { attr = "value" }); var textBoxForResult = helper.TextBoxFor(m => m.Property1, htmlAttributes);
// Assert // Assert
Assert.Equal( Assert.Equal(
"<input attr=\"HtmlEncode[[value]]\" id=\"HtmlEncode[[Property1]]\" name=\"HtmlEncode[[Property1]]\" type=\"HtmlEncode[[text]]\" value=\"HtmlEncode[[propValue]]\" />", "<input attr=\"HtmlEncode[[value]]\" id=\"HtmlEncode[[Property1]]\" " +
"name=\"HtmlEncode[[Property1]]\" type=\"HtmlEncode[[text]]\" value=\"HtmlEncode[[propValue]]\" />",
HtmlContentUtilities.HtmlContentToString(textBoxForResult));
}
[Fact]
public void TextBoxFor_Throws_IfFullNameEmpty()
{
// Arrange
var expectedMessage = "The name of an HTML field cannot be null or empty. Instead use methods " +
"Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper.Editor or Microsoft.AspNetCore.Mvc.Rendering." +
"IHtmlHelper`1.EditorFor with a non-empty htmlFieldName argument value.";
var htmlAttributes = new
{
attr = "value",
};
var helper = DefaultTemplatesUtilities.GetHtmlHelper("propValue");
helper.ViewContext.ClientValidationEnabled = false;
// Act & Assert
ExceptionAssert.ThrowsArgument(
() => helper.TextBoxFor(m => m, htmlAttributes),
paramName: "expression",
exceptionMessage: expectedMessage);
}
[Fact]
public void TextBoxFor_DoesNotThrow_IfFullNameEmpty_WithNameAttribute()
{
// Arrange
var htmlAttributes = new
{
attr = "value",
name = "-expression-",
};
var helper = DefaultTemplatesUtilities.GetHtmlHelper("propValue");
helper.ViewContext.ClientValidationEnabled = false;
// Act
var textBoxForResult = helper.TextBoxFor(m => m, htmlAttributes);
// Assert
Assert.Equal(
"<input attr=\"HtmlEncode[[value]]\" " +
"name=\"HtmlEncode[[-expression-]]\" type=\"HtmlEncode[[text]]\" value=\"HtmlEncode[[propValue]]\" />",
HtmlContentUtilities.HtmlContentToString(textBoxForResult)); HtmlContentUtilities.HtmlContentToString(textBoxForResult));
} }

View File

@ -69,7 +69,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
} }
[Fact] [Fact]
public void GetCurrentValues_WithNullExpression_Throws() public void GetCurrentValues_WithNullExpression_DoesNotThrow()
{ {
// Arrange // Arrange
var metadataProvider = new TestModelMetadataProvider(); var metadataProvider = new TestModelMetadataProvider();
@ -77,19 +77,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
var viewContext = GetViewContext<Model>(model: null, metadataProvider: metadataProvider); var viewContext = GetViewContext<Model>(model: null, metadataProvider: metadataProvider);
var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(string), model: null); var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(string), model: null);
var expected = "The name of an HTML field cannot be null or empty. Instead use " + // Act and Assert (does not throw).
"methods Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper.Editor or Microsoft.AspNetCore.Mvc.Rendering." + htmlGenerator.GetCurrentValues(viewContext, modelExplorer, expression: null, allowMultiple: true);
"IHtmlHelper`1.EditorFor with a non-empty htmlFieldName argument value.";
// Act and assert
ExceptionAssert.ThrowsArgument(
() => htmlGenerator.GetCurrentValues(
viewContext,
modelExplorer,
expression: null,
allowMultiple: true),
"expression",
expected);
} }
[Fact] [Fact]
@ -105,20 +94,49 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
"Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper.Editor or Microsoft.AspNetCore.Mvc.Rendering." + "Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper.Editor or Microsoft.AspNetCore.Mvc.Rendering." +
"IHtmlHelper`1.EditorFor with a non-empty htmlFieldName argument value."; "IHtmlHelper`1.EditorFor with a non-empty htmlFieldName argument value.";
// Act and assert // Act and Assert
ExceptionAssert.ThrowsArgument( ExceptionAssert.ThrowsArgument(
() => htmlGenerator.GenerateSelect( () => htmlGenerator.GenerateSelect(
viewContext, viewContext,
modelExplorer, modelExplorer,
"label", "label",
null, expression: null,
new List<SelectListItem>(), selectList: new List<SelectListItem>(),
true, allowMultiple: true,
null), htmlAttributes: null),
"expression", "expression",
expected); expected);
} }
[Fact]
public void GenerateSelect_WithNullExpression_WithNameAttribute_DoesNotThrow()
{
// Arrange
var expected = "-expression-";
var metadataProvider = new TestModelMetadataProvider();
var htmlGenerator = GetGenerator(metadataProvider);
var viewContext = GetViewContext<Model>(model: null, metadataProvider: metadataProvider);
var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(string), model: null);
var htmlAttributes = new Dictionary<string, object>
{
{ "name", expected },
};
// Act
var tagBuilder = htmlGenerator.GenerateSelect(
viewContext,
modelExplorer,
"label",
expression: null,
selectList: new List<SelectListItem>(),
allowMultiple: true,
htmlAttributes: htmlAttributes);
// Assert
var attribute = Assert.Single(tagBuilder.Attributes, a => a.Key == "name");
Assert.Equal(expected, attribute.Value);
}
[Fact] [Fact]
public void GenerateTextArea_WithNullExpression_Throws() public void GenerateTextArea_WithNullExpression_Throws()
{ {
@ -132,19 +150,47 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
"Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper.Editor or Microsoft.AspNetCore.Mvc.Rendering." + "Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper.Editor or Microsoft.AspNetCore.Mvc.Rendering." +
"IHtmlHelper`1.EditorFor with a non-empty htmlFieldName argument value."; "IHtmlHelper`1.EditorFor with a non-empty htmlFieldName argument value.";
// Act and assert // Act and Assert
ExceptionAssert.ThrowsArgument( ExceptionAssert.ThrowsArgument(
() => htmlGenerator.GenerateTextArea( () => htmlGenerator.GenerateTextArea(
viewContext, viewContext,
modelExplorer, modelExplorer,
null, expression: null,
1, rows: 1,
1, columns: 1,
null), htmlAttributes: null),
"expression", "expression",
expected); expected);
} }
[Fact]
public void GenerateTextArea_WithNullExpression_WithNameAttribute_DoesNotThrow()
{
// Arrange
var expected = "-expression-";
var metadataProvider = new TestModelMetadataProvider();
var htmlGenerator = GetGenerator(metadataProvider);
var viewContext = GetViewContext<Model>(model: null, metadataProvider: metadataProvider);
var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(string), model: null);
var htmlAttributes = new Dictionary<string, object>
{
{ "name", expected },
};
// Act
var tagBuilder = htmlGenerator.GenerateTextArea(
viewContext,
modelExplorer,
expression: null,
rows: 1,
columns: 1,
htmlAttributes: htmlAttributes);
// Assert
var attribute = Assert.Single(tagBuilder.Attributes, a => a.Key == "name");
Assert.Equal(expected, attribute.Value);
}
[Fact] [Fact]
public void GenerateValidationMessage_WithNullExpression_Throws() public void GenerateValidationMessage_WithNullExpression_Throws()
{ {
@ -158,13 +204,47 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
"Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper.Editor or Microsoft.AspNetCore.Mvc.Rendering." + "Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper.Editor or Microsoft.AspNetCore.Mvc.Rendering." +
"IHtmlHelper`1.EditorFor with a non-empty htmlFieldName argument value."; "IHtmlHelper`1.EditorFor with a non-empty htmlFieldName argument value.";
// Act and assert // Act and Assert
ExceptionAssert.ThrowsArgument( ExceptionAssert.ThrowsArgument(
() => htmlGenerator.GenerateValidationMessage(viewContext, null, null, "Message", "tag", null), () => htmlGenerator.GenerateValidationMessage(
viewContext,
modelExplorer: null,
expression: null,
message: "Message",
tag: "tag",
htmlAttributes: null),
"expression", "expression",
expected); expected);
} }
[Fact]
public void GenerateValidationMessage_WithNullExpression_WithValidationForAttribute_DoesNotThrow()
{
// Arrange
var expected = "-expression-";
var metadataProvider = new TestModelMetadataProvider();
var htmlGenerator = GetGenerator(metadataProvider);
var viewContext = GetViewContext<Model>(model: null, metadataProvider: metadataProvider);
var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(string), model: null);
var htmlAttributes = new Dictionary<string, object>
{
{ "data-valmsg-for", expected },
};
// Act
var tagBuilder = htmlGenerator.GenerateValidationMessage(
viewContext,
modelExplorer: null,
expression: null,
message: "Message",
tag: "tag",
htmlAttributes: htmlAttributes);
// Assert
var attribute = Assert.Single(tagBuilder.Attributes, a => a.Key == "data-valmsg-for");
Assert.Equal(expected, attribute.Value);
}
[Theory] [Theory]
[InlineData(false)] [InlineData(false)]
[InlineData(true)] [InlineData(true)]