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")]
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>
/// The value of the &lt;input&gt; element.
/// </summary>
@ -146,6 +155,11 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
output.CopyHtmlAttribute("type", context);
}
if (Name != null)
{
output.CopyHtmlAttribute(nameof(Name), context);
}
if (Value != null)
{
output.CopyHtmlAttribute(nameof(Value), context);
@ -183,15 +197,27 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
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;
switch (inputType)
{
case "hidden":
tagBuilder = GenerateHidden(modelExplorer);
tagBuilder = GenerateHidden(modelExplorer, htmlAttributes);
break;
case "checkbox":
tagBuilder = GenerateCheckBox(modelExplorer, output);
tagBuilder = GenerateCheckBox(modelExplorer, output, htmlAttributes);
break;
case "password":
@ -200,15 +226,15 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
modelExplorer,
For.Name,
value: null,
htmlAttributes: null);
htmlAttributes: htmlAttributes);
break;
case "radio":
tagBuilder = GenerateRadio(modelExplorer);
tagBuilder = GenerateRadio(modelExplorer, htmlAttributes);
break;
default:
tagBuilder = GenerateTextBox(modelExplorer, inputTypeHint, inputType);
tagBuilder = GenerateTextBox(modelExplorer, inputTypeHint, inputType, htmlAttributes);
break;
}
@ -248,7 +274,10 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
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))
{
@ -282,6 +311,14 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
var renderingMode =
output.TagMode == TagMode.SelfClosing ? TagRenderMode.SelfClosing : TagRenderMode.StartTag;
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)
{
@ -298,10 +335,10 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
modelExplorer,
For.Name,
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.
if (Value == null)
@ -319,10 +356,14 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
For.Name,
Value,
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;
if (string.IsNullOrEmpty(format))
@ -338,12 +379,18 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
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";
}
@ -352,14 +399,14 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
ViewContext,
modelExplorer,
For.Name,
value: modelExplorer.Model,
format: format,
htmlAttributes: htmlAttributes);
modelExplorer.Model,
format,
htmlAttributes);
}
// Imitate Generator.GenerateHidden() using Generator.GenerateTextBox(). This adds support for asp-format that
// is not available in Generator.GenerateHidden().
private TagBuilder GenerateHidden(ModelExplorer modelExplorer)
private TagBuilder GenerateHidden(ModelExplorer modelExplorer, IDictionary<string, object> htmlAttributes)
{
var value = For.Model;
if (value is byte[] byteArrayValue)
@ -367,21 +414,17 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
value = Convert.ToBase64String(byteArrayValue);
}
if (htmlAttributes == null)
{
htmlAttributes = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
}
// In DefaultHtmlGenerator(), GenerateTextBox() calls GenerateInput() _almost_ identically to how
// 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.
var htmlAttributes = new Dictionary<string, object>
{
{ "type", "hidden" }
};
htmlAttributes["type"] = "hidden";
return Generator.GenerateTextBox(
ViewContext,
modelExplorer,
For.Name,
value: value,
format: Format,
htmlAttributes: htmlAttributes);
return Generator.GenerateTextBox(ViewContext, modelExplorer, For.Name, value, Format, htmlAttributes);
}
// 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)]
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 />
public override void Init(TagHelperContext context)
{
@ -89,11 +98,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
var realModelType = For.ModelExplorer.ModelType;
_allowMultiple = typeof(string) != realModelType &&
typeof(IEnumerable).IsAssignableFrom(realModelType);
_currentValues = Generator.GetCurrentValues(
ViewContext,
For.ModelExplorer,
expression: For.Name,
allowMultiple: _allowMultiple);
_currentValues = Generator.GetCurrentValues(ViewContext, For.ModelExplorer, For.Name, _allowMultiple);
// Whether or not (not being highly unlikely) we generate anything, could update contained <option/>
// elements. Provide selected values for <option/> tag helpers.
@ -115,6 +120,13 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
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.
var items = Items ?? Enumerable.Empty<SelectListItem>();
@ -125,6 +137,18 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
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(
ViewContext,
For.ModelExplorer,
@ -133,7 +157,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
selectList: items,
currentValues: _currentValues,
allowMultiple: _allowMultiple,
htmlAttributes: null);
htmlAttributes: htmlAttributes);
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.
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
@ -40,6 +41,15 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
[HtmlAttributeName(ForAttributeName)]
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 />
/// <remarks>Does nothing if <see cref="For"/> is <c>null</c>.</remarks>
public override void Process(TagHelperContext context, TagHelperOutput output)
@ -54,13 +64,32 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
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(
ViewContext,
For.ModelExplorer,
For.Name,
rows: 0,
columns: 0,
htmlAttributes: null);
htmlAttributes: htmlAttributes);
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.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
@ -16,6 +17,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
[HtmlTargetElement("span", Attributes = ValidationForAttributeName)]
public class ValidationMessageTagHelper : TagHelper
{
private const string DataValidationForAttributeName = "data-valmsg-for";
private const string ValidationForAttributeName = "asp-validation-for";
/// <summary>
@ -36,9 +38,6 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
protected IHtmlGenerator Generator { get; }
/// <summary>
/// Name to be validated on the current model.
/// </summary>
[HtmlAttributeName(ValidationForAttributeName)]
public ModelExpression For { get; set; }
@ -58,13 +57,27 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
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(
ViewContext,
For.ModelExplorer,
For.Name,
message: null,
tag: null,
htmlAttributes: null);
htmlAttributes: htmlAttributes);
if (tagBuilder != null)
{

View File

@ -214,8 +214,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
if (modelExplorer.Model != null)
{
bool modelChecked;
if (bool.TryParse(modelExplorer.Model.ToString(), out modelChecked))
if (bool.TryParse(modelExplorer.Model.ToString(), out var modelChecked))
{
isChecked = modelChecked;
}
@ -261,7 +260,10 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
tagBuilder.TagRenderMode = TagRenderMode.SelfClosing;
var fullName = NameAndIdProvider.GetFullHtmlFieldName(viewContext, expression);
tagBuilder.MergeAttribute("name", fullName);
if (!string.IsNullOrEmpty(fullName))
{
tagBuilder.MergeAttribute("name", fullName);
}
return tagBuilder;
}
@ -363,8 +365,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
}
// Special-case opaque values and arbitrary binary data.
var byteArrayValue = value as byte[];
if (byteArrayValue != null)
if (value is byte[] byteArrayValue)
{
value = Convert.ToBase64String(byteArrayValue);
}
@ -596,7 +597,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
}
var fullName = NameAndIdProvider.GetFullHtmlFieldName(viewContext, expression);
if (string.IsNullOrEmpty(fullName))
var htmlAttributeDictionary = GetHtmlAttributeDictionaryOrNull(htmlAttributes);
if (!IsFullNameValid(fullName, htmlAttributeDictionary))
{
throw new ArgumentException(
Resources.FormatHtmlGenerator_FieldNameCannotBeNullOrEmpty(
@ -622,17 +624,20 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
var tagBuilder = new TagBuilder("select");
tagBuilder.InnerHtml.SetHtmlContent(listItemBuilder);
tagBuilder.MergeAttributes(GetHtmlAttributeDictionaryOrNull(htmlAttributes));
tagBuilder.MergeAttribute("name", fullName, true /* replaceExisting */);
tagBuilder.MergeAttributes(htmlAttributeDictionary);
NameAndIdProvider.GenerateId(viewContext, tagBuilder, fullName, IdAttributeDotReplacement);
if (!string.IsNullOrEmpty(fullName))
{
tagBuilder.MergeAttribute("name", fullName, replaceExisting: true);
}
if (allowMultiple)
{
tagBuilder.MergeAttribute("multiple", "multiple");
}
// If there are any errors for a named field, we add the css attribute.
ModelStateEntry entry;
if (viewContext.ViewData.ModelState.TryGetValue(fullName, out entry))
if (viewContext.ViewData.ModelState.TryGetValue(fullName, out var entry))
{
if (entry.Errors.Count > 0)
{
@ -672,7 +677,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
}
var fullName = NameAndIdProvider.GetFullHtmlFieldName(viewContext, expression);
if (string.IsNullOrEmpty(fullName))
var htmlAttributeDictionary = GetHtmlAttributeDictionaryOrNull(htmlAttributes);
if (!IsFullNameValid(fullName, htmlAttributeDictionary))
{
throw new ArgumentException(
Resources.FormatHtmlGenerator_FieldNameCannotBeNullOrEmpty(
@ -684,8 +690,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
nameof(expression));
}
ModelStateEntry entry;
viewContext.ViewData.ModelState.TryGetValue(fullName, out entry);
viewContext.ViewData.ModelState.TryGetValue(fullName, out var entry);
var value = string.Empty;
if (entry != null && entry.AttemptedValue != null)
@ -699,18 +704,24 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
var tagBuilder = new TagBuilder("textarea");
NameAndIdProvider.GenerateId(viewContext, tagBuilder, fullName, IdAttributeDotReplacement);
tagBuilder.MergeAttributes(GetHtmlAttributeDictionaryOrNull(htmlAttributes), true);
tagBuilder.MergeAttributes(htmlAttributeDictionary, replaceExisting: true);
if (rows > 0)
{
tagBuilder.MergeAttribute("rows", rows.ToString(CultureInfo.InvariantCulture), true);
tagBuilder.MergeAttribute("rows", rows.ToString(CultureInfo.InvariantCulture), replaceExisting: true);
}
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);
AddValidationAttributes(viewContext, tagBuilder, modelExplorer, expression);
@ -773,7 +784,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
}
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(
Resources.FormatHtmlGenerator_FieldNameCannotBeNullOrEmpty(
@ -791,8 +803,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
return null;
}
ModelStateEntry entry;
var tryGetModelStateResult = viewContext.ViewData.ModelState.TryGetValue(fullName, out entry);
var tryGetModelStateResult = viewContext.ViewData.ModelState.TryGetValue(fullName, out var entry);
var modelErrors = tryGetModelStateResult ? entry.Errors : null;
ModelError modelError = null;
@ -812,8 +823,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
tag = viewContext.ValidationMessageElement;
}
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.
// Otherwise the content and style is handled by the client-side validation.
@ -838,7 +850,10 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
if (formContext != null)
{
tagBuilder.MergeAttribute("data-valmsg-for", fullName);
if (!string.IsNullOrEmpty(fullName))
{
tagBuilder.MergeAttribute("data-valmsg-for", fullName);
}
var replaceValidationMessageContents = string.IsNullOrEmpty(message);
tagBuilder.MergeAttribute("data-valmsg-replace",
@ -868,9 +883,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
return null;
}
ModelStateEntry entryForModel;
if (excludePropertyErrors &&
(!viewData.ModelState.TryGetValue(viewData.TemplateInfo.HtmlFieldPrefix, out entryForModel) ||
(!viewData.ModelState.TryGetValue(viewData.TemplateInfo.HtmlFieldPrefix, out var entryForModel) ||
entryForModel.Errors.Count == 0))
{
// 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);
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 rawValue = GetModelStateValue(viewContext, fullName, type);
@ -1133,8 +1135,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
internal static object GetModelStateValue(ViewContext viewContext, string key, Type destinationType)
{
ModelStateEntry entry;
if (viewContext.ViewData.ModelState.TryGetValue(key, out entry) && entry.RawValue != null)
if (viewContext.ViewData.ModelState.TryGetValue(key, out var entry) && entry.RawValue != 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
// non-empty.
var fullName = NameAndIdProvider.GetFullHtmlFieldName(viewContext, expression);
if (string.IsNullOrEmpty(fullName))
if (!IsFullNameValid(fullName, htmlAttributes))
{
throw new ArgumentException(
Resources.FormatHtmlGenerator_FieldNameCannotBeNullOrEmpty(
@ -1220,11 +1221,17 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
}
var inputTypeString = GetInputTypeString(inputType);
var tagBuilder = new TagBuilder("input");
tagBuilder.TagRenderMode = TagRenderMode.SelfClosing;
var tagBuilder = new TagBuilder("input")
{
TagRenderMode = TagRenderMode.SelfClosing,
};
tagBuilder.MergeAttributes(htmlAttributes);
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"];
if (_placeholderInputTypes.Contains(suppliedTypeString))
@ -1249,8 +1256,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
case InputType.Radio:
if (!usedModelState)
{
var modelStateValue = GetModelStateValue(viewContext, fullName, typeof(string)) as string;
if (modelStateValue != null)
if (GetModelStateValue(viewContext, fullName, typeof(string)) is string modelStateValue)
{
isChecked = string.Equals(modelStateValue, valueParameter, StringComparison.Ordinal);
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.
ModelStateEntry entry;
if (viewContext.ViewData.ModelState.TryGetValue(fullName, out entry) && entry.Errors.Count > 0)
if (viewContext.ViewData.ModelState.TryGetValue(fullName, out var entry) && entry.Errors.Count > 0)
{
tagBuilder.AddCssClass(HtmlHelper.ValidationInputCssClassName);
}
@ -1410,8 +1415,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
private static object ConvertEnumFromString<TEnum>(string value) where TEnum : struct
{
TEnum enumValue;
if (Enum.TryParse(value, out enumValue))
if (Enum.TryParse(value, out TEnum enumValue))
{
return enumValue;
}
@ -1500,10 +1504,41 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
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 />
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(

View File

@ -160,8 +160,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
/// </remarks>
public static IDictionary<string, object> AnonymousObjectToHtmlAttributes(object htmlAttributes)
{
var dictionary = htmlAttributes as IDictionary<string, object>;
if (dictionary != null)
if (htmlAttributes is IDictionary<string, object> dictionary)
{
return new Dictionary<string, object>(dictionary, StringComparer.OrdinalIgnoreCase);
}
@ -586,7 +585,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
/// <inheritdoc />
public IHtmlContent Raw(object value)
{
return new HtmlString(value == null ? null : value.ToString());
return new HtmlString(value?.ToString());
}
/// <inheritdoc />
@ -685,7 +684,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
/// <inheritdoc />
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);
}
@ -724,6 +727,15 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
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)
{
ViewContext.FormContext.EndOfFormContent.Add(hiddenForCheckbox);
@ -861,7 +873,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
tagBuilder.WriteTo(ViewContext.Writer, _htmlEncoder);
}
var shouldGenerateAntiforgery = antiforgery.HasValue ? antiforgery.Value : method != FormMethod.Get;
var shouldGenerateAntiforgery = antiforgery ?? method != FormMethod.Get;
if (shouldGenerateAntiforgery)
{
ViewContext.FormContext.EndOfFormContent.Add(_htmlGenerator.GenerateAntiforgery(ViewContext));
@ -917,7 +929,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
tagBuilder.WriteTo(ViewContext.Writer, _htmlEncoder);
}
var shouldGenerateAntiforgery = antiforgery.HasValue ? antiforgery.Value : method != FormMethod.Get;
var shouldGenerateAntiforgery = antiforgery ?? method != FormMethod.Get;
if (shouldGenerateAntiforgery)
{
ViewContext.FormContext.EndOfFormContent.Add(_htmlGenerator.GenerateAntiforgery(ViewContext));
@ -969,9 +981,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
var tagBuilder = _htmlGenerator.GenerateLabel(
ViewContext,
modelExplorer,
expression: expression,
labelText: labelText,
htmlAttributes: htmlAttributes);
expression,
labelText,
htmlAttributes);
if (tagBuilder == null)
{
return HtmlString.Empty;

View File

@ -22,7 +22,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
{
public class InputTagHelperTest
{
public static TheoryData MultiAttributeCheckBoxData
public static TheoryData<TagHelperAttributeList, string> MultiAttributeCheckBoxData
{
get
{
@ -252,6 +252,317 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
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),
// model accessor, expression path / id, expected value.
public static TheoryData<object, Type, object, NameAndId, string> TestDataSet
@ -1410,7 +1721,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
};
var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var model = new DateTime(
year: 2000,
month: 1,

View File

@ -6,12 +6,14 @@ using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.TagHelpers.Internal;
using Microsoft.AspNetCore.Mvc.TestCommon;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.AspNetCore.Testing;
using Moq;
using Xunit;
@ -704,6 +706,132 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
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 NameAndId(string name, string id)

View File

@ -6,10 +6,10 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.TestCommon;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.AspNetCore.Testing;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.TagHelpers
@ -160,6 +160,84 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
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 NameAndId(string name, string id)

View File

@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Testing;
using Moq;
using Xunit;
@ -89,6 +90,151 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
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]
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." +
"IHtmlHelper`1.EditorFor with a non-empty htmlFieldName argument value.";
var helper = DefaultTemplatesUtilities.GetHtmlHelper(GetTestModelViewData());
var helper = DefaultTemplatesUtilities.GetHtmlHelper(model: false);
// Act & Assert
ExceptionAssert.ThrowsArgument(
@ -104,6 +104,29 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
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]
public void CheckBoxCheckedWithOnlyName_GeneratesExpectedValue()
{
@ -413,12 +436,17 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
var requiredMessage = ValidationAttributeUtil.GetRequiredErrorMessage("Boolean");
var expected =
$@"<input data-val=""HtmlEncode[[true]]"" data-val-required=""HtmlEncode[[{requiredMessage}]]"" " +
@"id=""HtmlEncode[[MyPrefix]]"" name=""HtmlEncode[[MyPrefix]]"" Property3=""HtmlEncode[[Property3Value]]"" " +
@"type=""HtmlEncode[[checkbox]]"" value=""HtmlEncode[[true]]"" /><input name=""HtmlEncode[[MyPrefix]]"" type=""HtmlEncode[[hidden]]"" " +
@"value=""HtmlEncode[[false]]"" />";
@"id=""HtmlEncode[[MyPrefix]]"" name=""HtmlEncode[[MyPrefix]]"" " +
@"Property3=""HtmlEncode[[Property3Value]]"" type=""HtmlEncode[[checkbox]]"" " +
@"value=""HtmlEncode[[true]]"" /><input name=""HtmlEncode[[MyPrefix]]"" " +
@"type=""HtmlEncode[[hidden]]"" value=""HtmlEncode[[false]]"" />";
var helper = DefaultTemplatesUtilities.GetHtmlHelper(model: false);
var attributes = new Dictionary<string, object> { { "Property3", "Property3Value" } };
helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";
var attributes = new Dictionary<string, object>
{
{ "Property3", "Property3Value" },
{ "name", "-expression-" }, // overridden
};
// Act
var html = helper.CheckBox(string.Empty, isChecked: false, htmlAttributes: attributes);
@ -589,10 +617,15 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
var expected =
$@"<input data-val=""HtmlEncode[[true]]"" data-val-required=""HtmlEncode[[{requiredMessage}]]"" " +
@"id=""HtmlEncode[[Property1]]"" name=""HtmlEncode[[Property1]]"" " +
@"Property3=""HtmlEncode[[Property3Value]]"" type=""HtmlEncode[[checkbox]]"" value=""HtmlEncode[[true]]"" /><input " +
@"name=""HtmlEncode[[Property1]]"" type=""HtmlEncode[[hidden]]"" value=""HtmlEncode[[false]]"" />";
@"Property3=""HtmlEncode[[Property3Value]]"" type=""HtmlEncode[[checkbox]]"" " +
@"value=""HtmlEncode[[true]]"" /><input name=""HtmlEncode[[Property1]]"" " +
@"type=""HtmlEncode[[hidden]]"" value=""HtmlEncode[[false]]"" />";
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
var html = helper.CheckBoxFor(m => m.Property1, attributes);

View File

@ -15,19 +15,36 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
{
public class HtmlHelperHiddenTest
{
public static IEnumerable<object[]> HiddenWithAttributesData
public static TheoryData<object, string> HiddenWithAttributesData
{
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]]"" />";
yield return new object[] { new Dictionary<string, object> { { "baz", "BazValue" } }, expected1 };
yield return new object[] { new { baz = "BazValue" }, expected1 };
var expected2 = @"<input foo-baz=""HtmlEncode[[BazValue]]"" id=""HtmlEncode[[Property1]]"" " +
@"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]]"" " +
@"value=""HtmlEncode[[ModelStateValue]]"" />";
yield return new object[] { new Dictionary<string, object> { { "foo-baz", "BazValue" } }, expected2 };
yield return new object[] { new { foo_baz = "BazValue" }, expected2 };
var data = new TheoryData<object, string>
{
{ htmlAttributes1, expected1 },
{ 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()
{
// Arrange
var helper = DefaultTemplatesUtilities.GetHtmlHelper(GetViewDataWithModelStateAndModelAndViewDataValues());
var helper = DefaultTemplatesUtilities.GetHtmlHelper("model-value");
var attributes = new Dictionary<string, object>
{
{ "class", "some-class"}
@ -419,11 +436,31 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
// Act and Assert
ExceptionAssert.ThrowsArgument(
() => helper.Hidden(string.Empty, string.Empty, attributes),
() => helper.Hidden(expression: string.Empty, value: null, htmlAttributes: attributes),
"expression",
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]
public void HiddenWithViewDataErrors_GeneratesExpectedValue()
{

View File

@ -15,39 +15,58 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
{
public class HtmlHelperPasswordTest
{
public static IEnumerable<object[]> PasswordWithViewDataAndAttributesData
public static TheoryData<object> HtmlAttributeData
{
get
{
var attributes1 = new Dictionary<string, object>
return new TheoryData<object>
{
{ "test-key", "test-value" },
{ "value", "attribute-value" }
new Dictionary<string, object>
{
{ "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();
vdd.Model.Property1 = "does-not-get-used";
yield return new object[] { vdd, attributes1 };
yield return new object[] { vdd, attributes2 };
var data = new TheoryData<ViewDataDictionary<PasswordModel>, object>();
foreach (var items in HtmlAttributeData)
{
data.Add(viewData, items[0]);
data.Add(nullModelViewData, items[0]);
}
var nullModelVdd = GetViewDataWithNullModelAndNonEmptyViewData();
yield return new object[] { nullModelVdd, attributes1 };
yield return new object[] { nullModelVdd, attributes2 };
return data;
}
}
[Theory]
[MemberData(nameof(PasswordWithViewDataAndAttributesData))]
public void Password_UsesAttributeValueWhenValueArgumentIsNull(
ViewDataDictionary<PasswordModel> vdd,
ViewDataDictionary<PasswordModel> viewData,
object attributes)
{
// Arrange
var expected = @"<input id=""HtmlEncode[[Property1]]"" name=""HtmlEncode[[Property1]]"" test-key=""HtmlEncode[[test-value]]"" type=""HtmlEncode[[password]]"" " +
@"value=""HtmlEncode[[attribute-value]]"" />";
var helper = DefaultTemplatesUtilities.GetHtmlHelper(vdd);
var expected = @"<input id=""HtmlEncode[[Property1]]"" name=""HtmlEncode[[Property1]]"" " +
@"test-key=""HtmlEncode[[test-value]]"" type=""HtmlEncode[[password]]"" " +
@"value=""HtmlEncode[[attribute-value]]"" />";
var helper = DefaultTemplatesUtilities.GetHtmlHelper(viewData);
// Act
var result = helper.Password("Property1", value: null, htmlAttributes: attributes);
@ -59,13 +78,14 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
[Theory]
[MemberData(nameof(PasswordWithViewDataAndAttributesData))]
public void Password_UsesExplicitValue_IfSpecified(
ViewDataDictionary<PasswordModel> vdd,
ViewDataDictionary<PasswordModel> viewData,
object attributes)
{
// Arrange
var expected = @"<input id=""HtmlEncode[[Property1]]"" name=""HtmlEncode[[Property1]]"" test-key=""HtmlEncode[[test-value]]"" type=""HtmlEncode[[password]]"" " +
@"value=""HtmlEncode[[explicit-value]]"" />";
var helper = DefaultTemplatesUtilities.GetHtmlHelper(vdd);
var expected = @"<input id=""HtmlEncode[[Property1]]"" name=""HtmlEncode[[Property1]]"" " +
@"test-key=""HtmlEncode[[test-value]]"" type=""HtmlEncode[[password]]"" " +
@"value=""HtmlEncode[[explicit-value]]"" />";
var helper = DefaultTemplatesUtilities.GetHtmlHelper(viewData);
// Act
var result = helper.Password("Property1", "explicit-value", attributes);
@ -128,20 +148,35 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
public void PasswordWithEmptyNameAndPrefixThrows()
{
// Arrange
var helper = DefaultTemplatesUtilities.GetHtmlHelper(GetViewDataWithModelStateAndModelAndViewDataValues());
var name = string.Empty;
var value = string.Empty;
var helper = DefaultTemplatesUtilities.GetHtmlHelper("model-value");
var expression = string.Empty;
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.";
// Act and Assert
ExceptionAssert.ThrowsArgument(
() => helper.Password(name, value, htmlAttributes: null),
() => helper.Password(expression, value: null, htmlAttributes: null),
"expression",
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]
public void Password_UsesModelStateErrors_ButDoesNotUseModelOrViewDataOrModelStateForValueAttribute()
{
@ -218,14 +253,13 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
}
[Theory]
[MemberData(nameof(PasswordWithViewDataAndAttributesData))]
public void PasswordForWithAttributes_GeneratesExpectedValue(
ViewDataDictionary<PasswordModel> vdd,
object htmlAttributes)
[MemberData(nameof(HtmlAttributeData))]
public void PasswordForWithAttributes_GeneratesExpectedValue(object htmlAttributes)
{
// Arrange
var expected = @"<input id=""HtmlEncode[[Property1]]"" name=""HtmlEncode[[Property1]]"" test-key=""HtmlEncode[[test-value]]"" type=""HtmlEncode[[password]]"" " +
@"value=""HtmlEncode[[attribute-value]]"" />";
var expected = @"<input id=""HtmlEncode[[Property1]]"" name=""HtmlEncode[[Property1]]"" " +
@"test-key=""HtmlEncode[[test-value]]"" type=""HtmlEncode[[password]]"" " +
@"value=""HtmlEncode[[attribute-value]]"" />";
var helper = DefaultTemplatesUtilities.GetHtmlHelper(GetViewDataWithModelStateAndModelAndViewDataValues());
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.TestCommon;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Testing;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Core
@ -122,13 +123,19 @@ namespace Microsoft.AspNetCore.Mvc.Core
{
// Arrange
var helper = DefaultTemplatesUtilities.GetHtmlHelper();
var htmlAttributes = new
{
attr = "value",
name = "-expression-", // overridden
};
// Act
var radioButtonResult = helper.RadioButton("Property1", value: "myvalue", htmlAttributes: new { attr = "value" });
var radioButtonResult = helper.RadioButton("Property1", "myvalue", htmlAttributes);
// Assert
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));
}
@ -137,13 +144,61 @@ namespace Microsoft.AspNetCore.Mvc.Core
{
// Arrange
var helper = DefaultTemplatesUtilities.GetHtmlHelper();
var htmlAttributes = new
{
attr = "value",
name = "-expression-", // overridden
};
// 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.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));
}

View File

@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.TestCommon;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Testing;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Core
@ -108,17 +109,27 @@ namespace Microsoft.AspNetCore.Mvc.Core
public void TextBox_UsesSpecifiedHtmlAttributes()
{
// Arrange
var metadataProvider = new EmptyModelMetadataProvider();
var helper = DefaultTemplatesUtilities.GetHtmlHelper(new ViewDataDictionary<TestModel>(metadataProvider));
var htmlAttributes = new
{
attr = "value",
name = "-expression-", // overridden
};
var model = new TestModel
{
Property1 = "propValue"
};
var helper = DefaultTemplatesUtilities.GetHtmlHelper(model);
helper.ViewContext.ClientValidationEnabled = false;
helper.ViewData.Model = new TestModel { Property1 = "propValue" };
// Act
var textBoxResult = helper.TextBox("Property1", value: "myvalue", htmlAttributes: new { attr = "value" });
var textBoxResult = helper.TextBox("Property1", "myvalue", htmlAttributes);
// Assert
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));
}
@ -126,17 +137,73 @@ namespace Microsoft.AspNetCore.Mvc.Core
public void TextBoxFor_UsesSpecifiedHtmlAttributes()
{
// Arrange
var metadataProvider = new EmptyModelMetadataProvider();
var helper = DefaultTemplatesUtilities.GetHtmlHelper(new ViewDataDictionary<TestModel>(metadataProvider));
var htmlAttributes = new
{
attr = "value",
name = "-expression-", // overridden
};
var model = new TestModel
{
Property1 = "propValue"
};
var helper = DefaultTemplatesUtilities.GetHtmlHelper(model);
helper.ViewContext.ClientValidationEnabled = false;
helper.ViewData.Model = new TestModel { Property1 = "propValue" };
// Act
var textBoxForResult = helper.TextBoxFor(m => m.Property1, htmlAttributes: new { attr = "value" });
var textBoxForResult = helper.TextBoxFor(m => m.Property1, htmlAttributes);
// Assert
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));
}

View File

@ -69,7 +69,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
}
[Fact]
public void GetCurrentValues_WithNullExpression_Throws()
public void GetCurrentValues_WithNullExpression_DoesNotThrow()
{
// Arrange
var metadataProvider = new TestModelMetadataProvider();
@ -77,19 +77,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
var viewContext = GetViewContext<Model>(model: null, metadataProvider: metadataProvider);
var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(string), model: null);
var expected = "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.";
// Act and assert
ExceptionAssert.ThrowsArgument(
() => htmlGenerator.GetCurrentValues(
viewContext,
modelExplorer,
expression: null,
allowMultiple: true),
"expression",
expected);
// Act and Assert (does not throw).
htmlGenerator.GetCurrentValues(viewContext, modelExplorer, expression: null, allowMultiple: true);
}
[Fact]
@ -105,20 +94,49 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
"Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper.Editor or Microsoft.AspNetCore.Mvc.Rendering." +
"IHtmlHelper`1.EditorFor with a non-empty htmlFieldName argument value.";
// Act and assert
// Act and Assert
ExceptionAssert.ThrowsArgument(
() => htmlGenerator.GenerateSelect(
viewContext,
modelExplorer,
"label",
null,
new List<SelectListItem>(),
true,
null),
expression: null,
selectList: new List<SelectListItem>(),
allowMultiple: true,
htmlAttributes: null),
"expression",
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]
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." +
"IHtmlHelper`1.EditorFor with a non-empty htmlFieldName argument value.";
// Act and assert
// Act and Assert
ExceptionAssert.ThrowsArgument(
() => htmlGenerator.GenerateTextArea(
viewContext,
modelExplorer,
null,
1,
1,
null),
expression: null,
rows: 1,
columns: 1,
htmlAttributes: null),
"expression",
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]
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." +
"IHtmlHelper`1.EditorFor with a non-empty htmlFieldName argument value.";
// Act and assert
// Act and Assert
ExceptionAssert.ThrowsArgument(
() => htmlGenerator.GenerateValidationMessage(viewContext, null, null, "Message", "tag", null),
() => htmlGenerator.GenerateValidationMessage(
viewContext,
modelExplorer: null,
expression: null,
message: "Message",
tag: "tag",
htmlAttributes: null),
"expression",
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]
[InlineData(false)]
[InlineData(true)]