Add `InputTagHelper`
- also make `TagHelperOutputExtensions.MergeAttributes()` case-insensitive nits: - deliniate attribute names in all resource strings - update validation messages in TagHelperSample.Web
This commit is contained in:
parent
f8f08f0903
commit
012e03e5d0
|
|
@ -37,7 +37,7 @@
|
|||
<div class="col-md-10">
|
||||
@* will automatically infer type="date" (reused HTML attribute) and format="{0:d}" (optional bound attribute) *@
|
||||
<input for="DateOfBirth" />
|
||||
<span validation-for="DateOfBirth">How old are you?</span>
|
||||
<span validation-for="DateOfBirth">When were you born?</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
<label for="DateOfBirth" class="control-label col-md-2" />
|
||||
<div class="col-md-10">
|
||||
<input type="date" for="DateOfBirth" format="{0:d}" />
|
||||
<span validation-for="DateOfBirth">How old are you?</span>
|
||||
<span validation-for="DateOfBirth" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,415 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNet.Mvc.ModelBinding;
|
||||
using Microsoft.AspNet.Mvc.Rendering;
|
||||
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
|
||||
using Microsoft.AspNet.Razor.TagHelpers;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="ITagHelper"/> implementation targeting <input> elements.
|
||||
/// </summary>
|
||||
[ContentBehavior(ContentBehavior.Replace)]
|
||||
public class InputTagHelper : TagHelper
|
||||
{
|
||||
// Mapping from datatype names and data annotation hints to values for the <input/> element's "type" attribute.
|
||||
private static readonly Dictionary<string, string> _defaultInputTypes =
|
||||
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "HiddenInput", InputType.Hidden.ToString().ToLowerInvariant() },
|
||||
{ "Password", InputType.Password.ToString().ToLowerInvariant() },
|
||||
{ "Text", InputType.Text.ToString() },
|
||||
{ "PhoneNumber", "tel" },
|
||||
{ "Url", "url" },
|
||||
{ "EmailAddress", "email" },
|
||||
{ "Date", "date" },
|
||||
{ "DateTime", "datetime" },
|
||||
{ "DateTime-local", "datetime-local" },
|
||||
{ "Time", "time" },
|
||||
{ nameof(Byte), "number" },
|
||||
{ nameof(SByte), "number" },
|
||||
{ nameof(Int32), "number" },
|
||||
{ nameof(UInt32), "number" },
|
||||
{ nameof(Int64), "number" },
|
||||
{ nameof(UInt64), "number" },
|
||||
{ nameof(Boolean), InputType.CheckBox.ToString().ToLowerInvariant() },
|
||||
{ nameof(Decimal), InputType.Text.ToString().ToLowerInvariant() },
|
||||
{ nameof(String), InputType.Text.ToString().ToLowerInvariant() },
|
||||
};
|
||||
|
||||
// Mapping from <input/> element's type to RFC 3339 date and time formats.
|
||||
private static readonly Dictionary<string, string> _rfc3339Formats =
|
||||
new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
{ "date", "{0:yyyy-MM-dd}" },
|
||||
{ "datetime", "{0:yyyy-MM-ddTHH:mm:ss.fffK}" },
|
||||
{ "datetime-local", "{0:yyyy-MM-ddTHH:mm:ss.fff}" },
|
||||
{ "time", "{0:HH:mm:ss.fff}" },
|
||||
};
|
||||
|
||||
// Protected to ensure subclasses are correctly activated. Internal for ease of use when testing.
|
||||
[Activate]
|
||||
protected internal IHtmlGenerator Generator { get; set; }
|
||||
|
||||
// Protected to ensure subclasses are correctly activated. Internal for ease of use when testing.
|
||||
[Activate]
|
||||
protected internal ViewContext ViewContext { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// An expression to be evaluated against the current model.
|
||||
/// </summary>
|
||||
public ModelExpression For { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The composite format <see cref="string"/> (see http://msdn.microsoft.com/en-us/library/txafckwd.aspx) to
|
||||
/// apply when converting the <see cref="For"/> result to a <see cref="string"/>. Sets the generated "value"
|
||||
/// attribute to that formatted <see cref="string"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Used only the calculated "type" attribute is "text" (the most common value) e.g.
|
||||
/// <see cref="InputTypeName"/> is "String". That is, <see cref="Format"/> is used when calling
|
||||
/// <see cref="IHtmlGenerator.GenerateTextBox"/>.
|
||||
/// </remarks>
|
||||
public string Format { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of the <input> element.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Passed through to the generated HTML in all cases. Also used to determine the <see cref="IHtmlGenerator"/>
|
||||
/// helper to call and the default <see cref="Format"/> value (when calling
|
||||
/// <see cref="IHtmlGenerator.GenerateTextBox"/>).
|
||||
/// </remarks>
|
||||
[HtmlAttributeName("type")]
|
||||
public string InputTypeName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The value of the <input> element.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Passed through to the generated HTML in all cases. Also used to determine the generated "checked" attribute
|
||||
/// if <see cref="InputTypeName"/> is "radio". Must not be <c>null</c> in that case.
|
||||
/// </remarks>
|
||||
public string Value { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>Does nothing if <see cref="For"/> is <c>null</c></remarks>
|
||||
public override void Process(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
// Pass through attributes that are also well-known HTML attributes. Must be done prior to any copying
|
||||
// from a TagBuilder.
|
||||
if (!string.IsNullOrEmpty(InputTypeName))
|
||||
{
|
||||
output.CopyHtmlAttribute("type", context);
|
||||
}
|
||||
|
||||
if (Value != null)
|
||||
{
|
||||
output.CopyHtmlAttribute(nameof(Value), context);
|
||||
}
|
||||
|
||||
if (For == null)
|
||||
{
|
||||
// Regular HTML <input/> element. Just make sure Format wasn't specified.
|
||||
if (Format != null)
|
||||
{
|
||||
throw new InvalidOperationException(Resources.FormatInputTagHelper_UnableToFormat(
|
||||
"<input>",
|
||||
nameof(For).ToLowerInvariant(),
|
||||
nameof(Format).ToLowerInvariant()));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Note null or empty For.Name is allowed because TemplateInfo.HtmlFieldPrefix may be sufficient.
|
||||
// IHtmlGenerator will enforce name requirements.
|
||||
var metadata = For.Metadata;
|
||||
if (metadata == null)
|
||||
{
|
||||
throw new InvalidOperationException(Resources.FormatTagHelpers_NoProvidedMetadata(
|
||||
"<input>",
|
||||
nameof(For).ToLowerInvariant(),
|
||||
nameof(IModelMetadataProvider),
|
||||
For.Name));
|
||||
}
|
||||
|
||||
string inputType;
|
||||
string inputTypeHint;
|
||||
if (string.IsNullOrEmpty(InputTypeName))
|
||||
{
|
||||
inputType = GetInputType(metadata, out inputTypeHint);
|
||||
}
|
||||
else
|
||||
{
|
||||
inputType = InputTypeName.ToLowerInvariant();
|
||||
inputTypeHint = null;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(inputType))
|
||||
{
|
||||
// inputType may be more specific than default the generator chooses below.
|
||||
// TODO: Use Attributes.ContainsKey once aspnet/Razor#186 is fixed.
|
||||
if (!output.Attributes.Any(
|
||||
item => string.Equals("type", item.Key, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
output.Attributes["type"] = inputType;
|
||||
}
|
||||
}
|
||||
|
||||
TagBuilder tagBuilder;
|
||||
switch (inputType)
|
||||
{
|
||||
case "checkbox":
|
||||
GenerateCheckBox(metadata, output);
|
||||
return;
|
||||
|
||||
case "hidden":
|
||||
tagBuilder = Generator.GenerateHidden(
|
||||
ViewContext,
|
||||
metadata,
|
||||
For.Name,
|
||||
value: metadata.Model,
|
||||
useViewData: false,
|
||||
htmlAttributes: null);
|
||||
break;
|
||||
|
||||
case "password":
|
||||
tagBuilder = Generator.GeneratePassword(
|
||||
ViewContext,
|
||||
metadata,
|
||||
For.Name,
|
||||
value: null,
|
||||
htmlAttributes: null);
|
||||
break;
|
||||
|
||||
case "radio":
|
||||
tagBuilder = GenerateRadio(metadata);
|
||||
break;
|
||||
|
||||
default:
|
||||
tagBuilder = GenerateTextBox(metadata, inputTypeHint, inputType);
|
||||
break;
|
||||
}
|
||||
|
||||
if (tagBuilder != null)
|
||||
{
|
||||
// This TagBuilder contains the one <input/> element of interest. Since this is not the "checkbox"
|
||||
// special-case, output is a self-closing element and can merge the TagBuilder in directly.
|
||||
output.SelfClosing = true;
|
||||
output.Merge(tagBuilder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void GenerateCheckBox(ModelMetadata metadata, TagHelperOutput output)
|
||||
{
|
||||
if (typeof(bool) != metadata.RealModelType)
|
||||
{
|
||||
throw new InvalidOperationException(Resources.FormatInputTagHelper_InvalidExpressionResult(
|
||||
"<input>",
|
||||
nameof(For).ToLowerInvariant(),
|
||||
metadata.RealModelType.FullName,
|
||||
typeof(bool).FullName,
|
||||
"type",
|
||||
"checkbox"));
|
||||
}
|
||||
|
||||
// Prepare to move attributes from current element to <input type="checkbox"/> generated just below.
|
||||
var htmlAttributes = output.Attributes.ToDictionary(
|
||||
attribute => attribute.Key,
|
||||
attribute => (object)attribute.Value);
|
||||
|
||||
var tagBuilder = Generator.GenerateCheckBox(
|
||||
ViewContext,
|
||||
metadata,
|
||||
For.Name,
|
||||
isChecked: null,
|
||||
htmlAttributes: htmlAttributes);
|
||||
if (tagBuilder != null)
|
||||
{
|
||||
// Do not generate current element's attributes or tags. Instead put both <input type="checkbox"/> and
|
||||
// <input type="hidden"/> into the output's Content.
|
||||
output.Attributes.Clear();
|
||||
output.SelfClosing = false; // Otherwise Content will be ignored.
|
||||
output.TagName = null;
|
||||
|
||||
output.Content += tagBuilder.ToString(TagRenderMode.SelfClosing);
|
||||
|
||||
tagBuilder = Generator.GenerateHiddenForCheckbox(ViewContext, metadata, For.Name);
|
||||
if (tagBuilder != null)
|
||||
{
|
||||
output.Content += tagBuilder.ToString(TagRenderMode.SelfClosing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private TagBuilder GenerateRadio(ModelMetadata metadata)
|
||||
{
|
||||
// Note empty string is allowed.
|
||||
if (Value == null)
|
||||
{
|
||||
throw new InvalidOperationException(Resources.FormatInputTagHelper_ValueRequired(
|
||||
"<input>",
|
||||
nameof(Value).ToLowerInvariant(),
|
||||
"type",
|
||||
"radio"));
|
||||
}
|
||||
|
||||
return Generator.GenerateRadioButton(
|
||||
ViewContext,
|
||||
metadata,
|
||||
For.Name,
|
||||
Value,
|
||||
isChecked: null,
|
||||
htmlAttributes: null);
|
||||
}
|
||||
|
||||
private TagBuilder GenerateTextBox(ModelMetadata metadata, string inputTypeHint, string inputType)
|
||||
{
|
||||
var format = Format;
|
||||
if (string.IsNullOrEmpty(format))
|
||||
{
|
||||
format = GetFormat(metadata, inputTypeHint, inputType);
|
||||
}
|
||||
|
||||
return Generator.GenerateTextBox(
|
||||
ViewContext,
|
||||
metadata,
|
||||
For.Name,
|
||||
value: metadata.Model,
|
||||
format: Format,
|
||||
htmlAttributes: null);
|
||||
}
|
||||
|
||||
// Get a fall-back format based on the metadata.
|
||||
private string GetFormat(ModelMetadata metadata, string inputTypeHint, string inputType)
|
||||
{
|
||||
string format;
|
||||
string rfc3339Format;
|
||||
if (string.Equals("decimal", inputTypeHint, StringComparison.OrdinalIgnoreCase) &&
|
||||
string.Equals("text", inputType, StringComparison.Ordinal) &&
|
||||
string.IsNullOrEmpty(metadata.EditFormatString))
|
||||
{
|
||||
// Decimal data is edited using an <input type="text"/> element, with a reasonable format.
|
||||
// EditFormatString has precedence over this fall-back format.
|
||||
format = "{0:0.00}";
|
||||
}
|
||||
else if (_rfc3339Formats.TryGetValue(inputType, out rfc3339Format) &&
|
||||
ViewContext.Html5DateRenderingMode == Html5DateRenderingMode.Rfc3339 &&
|
||||
!metadata.HasNonDefaultEditFormat &&
|
||||
(typeof(DateTime) == metadata.RealModelType || typeof(DateTimeOffset) == metadata.RealModelType))
|
||||
{
|
||||
// Rfc3339 mode _may_ override EditFormatString in a limited number of cases e.g. EditFormatString
|
||||
// must be a default format (i.e. came from a built-in [DataType] attribute).
|
||||
format = rfc3339Format;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise use EditFormatString, if any.
|
||||
format = metadata.EditFormatString;
|
||||
}
|
||||
|
||||
return format;
|
||||
}
|
||||
|
||||
private string GetInputType(ModelMetadata metadata, out string inputTypeHint)
|
||||
{
|
||||
foreach (var hint in GetInputTypeHints(metadata))
|
||||
{
|
||||
string inputType;
|
||||
if (_defaultInputTypes.TryGetValue(hint, out inputType))
|
||||
{
|
||||
inputTypeHint = hint;
|
||||
return inputType;
|
||||
}
|
||||
}
|
||||
|
||||
inputTypeHint = InputType.Text.ToString().ToLowerInvariant();
|
||||
return inputTypeHint;
|
||||
}
|
||||
|
||||
// A variant of TemplateRenderer.GetViewNames(). Main change relates to bool? handling.
|
||||
private static IEnumerable<string> GetInputTypeHints(ModelMetadata metadata)
|
||||
{
|
||||
var inputTypeHints = new string[]
|
||||
{
|
||||
metadata.TemplateHint,
|
||||
metadata.DataTypeName,
|
||||
};
|
||||
|
||||
foreach (string inputTypeHint in inputTypeHints.Where(s => !string.IsNullOrEmpty(s)))
|
||||
{
|
||||
yield return inputTypeHint;
|
||||
}
|
||||
|
||||
// In most cases, we don't want to search for Nullable<T>. We want to search for T, which should handle
|
||||
// both T and Nullable<T>. However we special-case bool? to avoid turning an <input/> into a <select/>.
|
||||
var fieldType = metadata.RealModelType;
|
||||
if (typeof(bool?) != fieldType)
|
||||
{
|
||||
var underlyingType = Nullable.GetUnderlyingType(fieldType);
|
||||
if (underlyingType != null)
|
||||
{
|
||||
fieldType = underlyingType;
|
||||
}
|
||||
}
|
||||
|
||||
yield return fieldType.Name;
|
||||
|
||||
if (fieldType == typeof(string))
|
||||
{
|
||||
// Nothing more to provide
|
||||
yield break;
|
||||
}
|
||||
else if (!metadata.IsComplexType)
|
||||
{
|
||||
// IsEnum is false for the Enum class itself
|
||||
if (fieldType.IsEnum())
|
||||
{
|
||||
// Same as fieldType.BaseType.Name in this case
|
||||
yield return "Enum";
|
||||
}
|
||||
else if (fieldType == typeof(DateTimeOffset))
|
||||
{
|
||||
yield return "DateTime";
|
||||
}
|
||||
|
||||
yield return "String";
|
||||
}
|
||||
else if (fieldType.IsInterface())
|
||||
{
|
||||
if (typeof(IEnumerable).IsAssignableFrom(fieldType))
|
||||
{
|
||||
yield return "Collection";
|
||||
}
|
||||
|
||||
yield return "Object";
|
||||
}
|
||||
else
|
||||
{
|
||||
var isEnumerable = typeof(IEnumerable).IsAssignableFrom(fieldType);
|
||||
while (true)
|
||||
{
|
||||
fieldType = fieldType.BaseType();
|
||||
if (fieldType == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (isEnumerable && fieldType == typeof(Object))
|
||||
{
|
||||
yield return "Collection";
|
||||
}
|
||||
|
||||
yield return fieldType.Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
= new ResourceManager("Microsoft.AspNet.Mvc.TagHelpers.Resources", typeof(Resources).GetTypeInfo().Assembly);
|
||||
|
||||
/// <summary>
|
||||
/// Cannot determine an {4} for {0}. An {0} with a specified {1} must not have an {2} or {3} attribute.
|
||||
/// Cannot determine an '{4}' for {0}. An {0} with a specified '{1}' must not have an '{2}' or '{3}' attribute.
|
||||
/// </summary>
|
||||
internal static string AnchorTagHelper_CannotDetermineHrefRouteActionOrControllerSpecified
|
||||
{
|
||||
|
|
@ -19,7 +19,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cannot determine an {4} for {0}. An {0} with a specified {1} must not have an {2} or {3} attribute.
|
||||
/// Cannot determine an '{4}' for {0}. An {0} with a specified '{1}' must not have an '{2}' or '{3}' attribute.
|
||||
/// </summary>
|
||||
internal static string FormatAnchorTagHelper_CannotDetermineHrefRouteActionOrControllerSpecified(object p0, object p1, object p2, object p3, object p4)
|
||||
{
|
||||
|
|
@ -27,7 +27,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cannot determine an {8} for {0}. An {0} with a specified {8} must not have attributes starting with {7} or an {1}, {2}, {3}, {4}, {5} or {6} attribute.
|
||||
/// Cannot determine an '{8}' for {0}. An {0} with a specified '{8}' must not have attributes starting with '{7}' or an '{1}', '{2}', '{3}', '{4}', '{5}', or '{6}' attribute.
|
||||
/// </summary>
|
||||
internal static string AnchorTagHelper_CannotOverrideSpecifiedHref
|
||||
{
|
||||
|
|
@ -35,7 +35,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cannot determine an {8} for {0}. An {0} with a specified {8} must not have attributes starting with {7} or an {1}, {2}, {3}, {4}, {5} or {6} attribute.
|
||||
/// Cannot determine an '{8}' for {0}. An {0} with a specified '{8}' must not have attributes starting with '{7}' or an '{1}', '{2}', '{3}', '{4}', '{5}', or '{6}' attribute.
|
||||
/// </summary>
|
||||
internal static string FormatAnchorTagHelper_CannotOverrideSpecifiedHref(object p0, object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8)
|
||||
{
|
||||
|
|
@ -43,7 +43,55 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cannot determine an {1} for {0}. A {0} with a URL-based {1} must not have attributes starting with {3} or a {2} attribute.
|
||||
/// Unexpected '{1}' expression result type '{2}' for {0}. '{1}' must be of type '{3}' if '{4}' is '{5}'.
|
||||
/// </summary>
|
||||
internal static string InputTagHelper_InvalidExpressionResult
|
||||
{
|
||||
get { return GetString("InputTagHelper_InvalidExpressionResult"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unexpected '{1}' expression result type '{2}' for {0}. '{1}' must be of type '{3}' if '{4}' is '{5}'.
|
||||
/// </summary>
|
||||
internal static string FormatInputTagHelper_InvalidExpressionResult(object p0, object p1, object p2, object p3, object p4, object p5)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("InputTagHelper_InvalidExpressionResult"), p0, p1, p2, p3, p4, p5);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unable to format without a '{1}' expression for {0}. '{2}' must be null if '{1}' is null.
|
||||
/// </summary>
|
||||
internal static string InputTagHelper_UnableToFormat
|
||||
{
|
||||
get { return GetString("InputTagHelper_UnableToFormat"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unable to format without a '{1}' expression for {0}. '{2}' must be null if '{1}' is null.
|
||||
/// </summary>
|
||||
internal static string FormatInputTagHelper_UnableToFormat(object p0, object p1, object p2)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("InputTagHelper_UnableToFormat"), p0, p1, p2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// '{1}' must not be null for {0} if '{2}' is '{3}'.
|
||||
/// </summary>
|
||||
internal static string InputTagHelper_ValueRequired
|
||||
{
|
||||
get { return GetString("InputTagHelper_ValueRequired"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// '{1}' must not be null for {0} if '{2}' is '{3}'.
|
||||
/// </summary>
|
||||
internal static string FormatInputTagHelper_ValueRequired(object p0, object p1, object p2, object p3)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("InputTagHelper_ValueRequired"), p0, p1, p2, p3);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cannot determine an '{1}' for {0}. A {0} with a URL-based '{1}' must not have attributes starting with '{3}' or a '{2}' attribute.
|
||||
/// </summary>
|
||||
internal static string FormTagHelper_CannotDetermineAction
|
||||
{
|
||||
|
|
@ -51,7 +99,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cannot determine an {1} for {0}. A {0} with a URL-based {1} must not have attributes starting with {3} or a {2} attribute.
|
||||
/// Cannot determine an '{1}' for {0}. A {0} with a URL-based '{1}' must not have attributes starting with '{3}' or a '{2}' attribute.
|
||||
/// </summary>
|
||||
internal static string FormatFormTagHelper_CannotDetermineAction(object p0, object p1, object p2, object p3)
|
||||
{
|
||||
|
|
@ -74,6 +122,22 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
return string.Format(CultureInfo.CurrentCulture, GetString("ValidationSummaryTagHelper_InvalidValidationSummaryValue"), p0, p1, p2, p3, p4, p5);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The {2} was unable to provide metadata about '{1}' expression value '{3}' for {0}.
|
||||
/// </summary>
|
||||
internal static string TagHelpers_NoProvidedMetadata
|
||||
{
|
||||
get { return GetString("TagHelpers_NoProvidedMetadata"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The {2} was unable to provide metadata about '{1}' expression value '{3}' for {0}.
|
||||
/// </summary>
|
||||
internal static string FormatTagHelpers_NoProvidedMetadata(object p0, object p1, object p2, object p3)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelpers_NoProvidedMetadata"), p0, p1, p2, p3);
|
||||
}
|
||||
|
||||
private static string GetString(string name, params string[] formatterNames)
|
||||
{
|
||||
var value = _resourceManager.GetString(name);
|
||||
|
|
|
|||
|
|
@ -118,15 +118,27 @@
|
|||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="AnchorTagHelper_CannotDetermineHrefRouteActionOrControllerSpecified" xml:space="preserve">
|
||||
<value>Cannot determine an {4} for {0}. An {0} with a specified {1} must not have an {2} or {3} attribute.</value>
|
||||
<value>Cannot determine an '{4}' for {0}. An {0} with a specified '{1}' must not have an '{2}' or '{3}' attribute.</value>
|
||||
</data>
|
||||
<data name="AnchorTagHelper_CannotOverrideSpecifiedHref" xml:space="preserve">
|
||||
<value>Cannot determine an {8} for {0}. An {0} with a specified {8} must not have attributes starting with {7} or an {1}, {2}, {3}, {4}, {5} or {6} attribute.</value>
|
||||
<value>Cannot determine an '{8}' for {0}. An {0} with a specified '{8}' must not have attributes starting with '{7}' or an '{1}', '{2}', '{3}', '{4}', '{5}', or '{6}' attribute.</value>
|
||||
</data>
|
||||
<data name="InputTagHelper_InvalidExpressionResult" xml:space="preserve">
|
||||
<value>Unexpected '{1}' expression result type '{2}' for {0}. '{1}' must be of type '{3}' if '{4}' is '{5}'.</value>
|
||||
</data>
|
||||
<data name="InputTagHelper_UnableToFormat" xml:space="preserve">
|
||||
<value>Unable to format without a '{1}' expression for {0}. '{2}' must be null if '{1}' is null.</value>
|
||||
</data>
|
||||
<data name="InputTagHelper_ValueRequired" xml:space="preserve">
|
||||
<value>'{1}' must not be null for {0} if '{2}' is '{3}'.</value>
|
||||
</data>
|
||||
<data name="FormTagHelper_CannotDetermineAction" xml:space="preserve">
|
||||
<value>Cannot determine an {1} for {0}. A {0} with a URL-based {1} must not have attributes starting with {3} or a {2} attribute.</value>
|
||||
<value>Cannot determine an '{1}' for {0}. A {0} with a URL-based '{1}' must not have attributes starting with '{3}' or a '{2}' attribute.</value>
|
||||
</data>
|
||||
<data name="ValidationSummaryTagHelper_InvalidValidationSummaryValue" xml:space="preserve">
|
||||
<value>Cannot parse '{1}' value '{2}' for {0}. Acceptable values are '{3}', '{4}' and '{5}'.</value>
|
||||
</data>
|
||||
<data name="TagHelpers_NoProvidedMetadata" xml:space="preserve">
|
||||
<value>The {2} was unable to provide metadata about '{1}' expression value '{3}' for {0}.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -89,7 +89,9 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
{
|
||||
foreach (var attribute in tagBuilder.Attributes)
|
||||
{
|
||||
if (!tagHelperOutput.Attributes.ContainsKey(attribute.Key))
|
||||
// TODO: Use Attributes.ContainsKey once aspnet/Razor#186 is fixed.
|
||||
if (!tagHelperOutput.Attributes.Any(
|
||||
item => string.Equals(attribute.Key, item.Key, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
tagHelperOutput.Attributes.Add(attribute.Key, attribute.Value);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
"warningsAsErrors": true
|
||||
},
|
||||
"dependencies": {
|
||||
"Microsoft.AspNet.Mvc.Common": { "version": "6.0.0-*", "type": "build" },
|
||||
"Microsoft.AspNet.Mvc.Razor": ""
|
||||
},
|
||||
"frameworks": {
|
||||
|
|
|
|||
|
|
@ -176,9 +176,9 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
typeof(AnchorTagHelper).GetProperty(propertyName).SetValue(anchorTagHelper, "Home");
|
||||
}
|
||||
|
||||
var expectedErrorMessage = "Cannot determine an href for <a>. An <a> with a specified href must not " +
|
||||
"have attributes starting with route- or an action, controller, route, " +
|
||||
"protocol, host or fragment attribute.";
|
||||
var expectedErrorMessage = "Cannot determine an 'href' for <a>. An <a> with a specified 'href' must not " +
|
||||
"have attributes starting with 'route-' or an 'action', 'controller', " +
|
||||
"'route', 'protocol', 'host', or 'fragment' attribute.";
|
||||
|
||||
// Act & Assert
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
|
|
@ -202,8 +202,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
"a",
|
||||
attributes: new Dictionary<string, string>(),
|
||||
content: string.Empty);
|
||||
var expectedErrorMessage = "Cannot determine an href for <a>. An <a> with a " +
|
||||
"specified route must not have an action or controller attribute.";
|
||||
var expectedErrorMessage = "Cannot determine an 'href' for <a>. An <a> with a " +
|
||||
"specified 'route' must not have an 'action' or 'controller' attribute.";
|
||||
|
||||
// Act & Assert
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
|
|
|
|||
|
|
@ -301,8 +301,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
Controller = "Home",
|
||||
Method = "POST"
|
||||
};
|
||||
var expectedErrorMessage = "Cannot determine an action for <form>. A <form> with a URL-based action " +
|
||||
"must not have attributes starting with route- or a controller attribute.";
|
||||
var expectedErrorMessage = "Cannot determine an 'action' for <form>. A <form> with a URL-based 'action' " +
|
||||
"must not have attributes starting with 'route-' or a 'controller' attribute.";
|
||||
var tagHelperOutput = new TagHelperOutput(
|
||||
"form",
|
||||
attributes: new Dictionary<string, string>(),
|
||||
|
|
@ -324,8 +324,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
Action = "http://www.contoso.com",
|
||||
Method = "POST"
|
||||
};
|
||||
var expectedErrorMessage = "Cannot determine an action for <form>. A <form> with a URL-based action " +
|
||||
"must not have attributes starting with route- or a controller attribute.";
|
||||
var expectedErrorMessage = "Cannot determine an 'action' for <form>. A <form> with a URL-based 'action' " +
|
||||
"must not have attributes starting with 'route-' or a 'controller' attribute.";
|
||||
var tagHelperOutput = new TagHelperOutput(
|
||||
"form",
|
||||
attributes: new Dictionary<string, string>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,200 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// 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.AspNet.Mvc.ModelBinding;
|
||||
using Microsoft.AspNet.Mvc.Rendering;
|
||||
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||
{
|
||||
public class InputTagHelperTest
|
||||
{
|
||||
// Model (List<Model> or Model instance), container type (Model or NestModel), model accessor,
|
||||
// property path, expected value.
|
||||
public static TheoryData<object, Type, Func<object>, string, string> TestDataSet
|
||||
{
|
||||
get
|
||||
{
|
||||
var modelWithNull = new Model
|
||||
{
|
||||
NestedModel = new NestedModel
|
||||
{
|
||||
Text = null,
|
||||
},
|
||||
Text = null,
|
||||
};
|
||||
var modelWithText = new Model
|
||||
{
|
||||
NestedModel = new NestedModel
|
||||
{
|
||||
Text = "inner text",
|
||||
},
|
||||
Text = "outer text",
|
||||
};
|
||||
var models = new List<Model>
|
||||
{
|
||||
modelWithNull,
|
||||
modelWithText,
|
||||
};
|
||||
|
||||
return new TheoryData<object, Type, Func<object>, string, string>
|
||||
{
|
||||
{ null, typeof(Model), () => null, "Text",
|
||||
string.Empty },
|
||||
|
||||
{ modelWithNull, typeof(Model), () => modelWithNull.Text, "Text",
|
||||
string.Empty },
|
||||
{ modelWithText, typeof(Model), () => modelWithText.Text, "Text",
|
||||
"outer text" },
|
||||
|
||||
{ modelWithNull, typeof(NestedModel), () => modelWithNull.NestedModel.Text, "NestedModel.Text",
|
||||
string.Empty },
|
||||
{ modelWithText, typeof(NestedModel), () => modelWithText.NestedModel.Text, "NestedModel.Text",
|
||||
"inner text" },
|
||||
|
||||
// Top-level indexing does not work end-to-end due to code generation issue #1345.
|
||||
// TODO: Remove above comment when #1345 is fixed.
|
||||
{ models, typeof(Model), () => models[0].Text, "[0].Text",
|
||||
string.Empty },
|
||||
{ models, typeof(Model), () => models[1].Text, "[1].Text",
|
||||
"outer text" },
|
||||
|
||||
{ models, typeof(NestedModel), () => models[0].NestedModel.Text, "[0].NestedModel.Text",
|
||||
string.Empty },
|
||||
{ models, typeof(NestedModel), () => models[1].NestedModel.Text, "[1].NestedModel.Text",
|
||||
"inner text" },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(TestDataSet))]
|
||||
public async Task ProcessAsync_GeneratesExpectedOutput(
|
||||
object model,
|
||||
Type containerType,
|
||||
Func<object> modelAccessor,
|
||||
string propertyPath,
|
||||
string expectedValue)
|
||||
{
|
||||
// Arrange
|
||||
var expectedAttributes = new Dictionary<string, string>
|
||||
{
|
||||
{ "class", "form-control" },
|
||||
{ "type", "text" },
|
||||
{ "id", propertyPath },
|
||||
{ "name", propertyPath },
|
||||
{ "valid", "from validation attributes" },
|
||||
{ "value", expectedValue },
|
||||
};
|
||||
var expectedContent = "original content";
|
||||
var expectedTagName = "input";
|
||||
|
||||
var metadataProvider = new DataAnnotationsModelMetadataProvider();
|
||||
|
||||
// Property name is either nameof(Model.Text) or nameof(NestedModel.Text).
|
||||
var metadata = metadataProvider.GetMetadataForProperty(modelAccessor, containerType, propertyName: "Text");
|
||||
var modelExpression = new ModelExpression(propertyPath, metadata);
|
||||
|
||||
var tagHelperContext = new TagHelperContext(new Dictionary<string, object>());
|
||||
var htmlAttributes = new Dictionary<string, string>
|
||||
{
|
||||
{ "class", "form-control" },
|
||||
};
|
||||
var output = new TagHelperOutput("original tag name", htmlAttributes, expectedContent)
|
||||
{
|
||||
SelfClosing = false,
|
||||
};
|
||||
|
||||
var htmlGenerator = new TestableHtmlGenerator(metadataProvider)
|
||||
{
|
||||
ValidationAttributes =
|
||||
{
|
||||
{ "valid", "from validation attributes" },
|
||||
}
|
||||
};
|
||||
var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);
|
||||
var tagHelper = new InputTagHelper
|
||||
{
|
||||
Generator = htmlGenerator,
|
||||
For = modelExpression,
|
||||
ViewContext = viewContext,
|
||||
};
|
||||
|
||||
// Act
|
||||
await tagHelper.ProcessAsync(tagHelperContext, output);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedAttributes, output.Attributes);
|
||||
Assert.Equal(expectedContent, output.Content);
|
||||
Assert.True(output.SelfClosing);
|
||||
Assert.Equal(expectedTagName, output.TagName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TagHelper_RestoresTypeAndValue_IfForNotBound()
|
||||
{
|
||||
// Arrange
|
||||
var expectedAttributes = new Dictionary<string, string>
|
||||
{
|
||||
{ "class", "form-control" },
|
||||
{ "type", "datetime" },
|
||||
{ "value", "2014-10-15T23:24:19.000-7:00" },
|
||||
};
|
||||
var expectedContent = "original content";
|
||||
var expectedTagName = "original tag name";
|
||||
|
||||
var metadataProvider = new DataAnnotationsModelMetadataProvider();
|
||||
var metadata = metadataProvider.GetMetadataForProperty(
|
||||
modelAccessor: () => null,
|
||||
containerType: typeof(Model),
|
||||
propertyName: nameof(Model.Text));
|
||||
var modelExpression = new ModelExpression(nameof(Model.Text), metadata);
|
||||
|
||||
var tagHelperContext = new TagHelperContext(new Dictionary<string, object>());
|
||||
var output = new TagHelperOutput(expectedTagName, expectedAttributes, expectedContent)
|
||||
{
|
||||
SelfClosing = false,
|
||||
};
|
||||
|
||||
var htmlGenerator = new TestableHtmlGenerator(metadataProvider)
|
||||
{
|
||||
ValidationAttributes =
|
||||
{
|
||||
{ "valid", "from validation attributes" },
|
||||
}
|
||||
};
|
||||
Model model = null;
|
||||
var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);
|
||||
var tagHelper = new InputTagHelper
|
||||
{
|
||||
Generator = htmlGenerator,
|
||||
ViewContext = viewContext,
|
||||
};
|
||||
|
||||
// Act
|
||||
await tagHelper.ProcessAsync(tagHelperContext, output);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedAttributes, output.Attributes);
|
||||
Assert.Equal(expectedContent, output.Content);
|
||||
Assert.False(output.SelfClosing);
|
||||
Assert.Equal(expectedTagName, output.TagName);
|
||||
}
|
||||
|
||||
private class Model
|
||||
{
|
||||
public string Text { get; set; }
|
||||
|
||||
public NestedModel NestedModel { get; set; }
|
||||
}
|
||||
|
||||
private class NestedModel
|
||||
{
|
||||
public string Text { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue