Fix #1618, Add asp- prefix to custom attributes of MVC tag helpers

- update XML docs to reflect new HTML / custom attribute separation
- update `Exception` messages to use new attribute names
- update MVC tag helper sample to use new custom attribute names
- add missing `<input/>` tag helper `throw`s test

nits:
- reword a few comments and messages for clarity and consistency
- use `<exception/>` sections to describe what's thrown
- add "Reviewers" comments about current throws
 - note `<a/>` and `<form/>` are slightly inconsistent with others: `throw`
   if unable to override specified `href` or `action` attributes; rest
   `throw` only if custom attributes are inconsistent e.g. `<input/>` with
   `asp-format` but no `asp-for`
- create test tag helpers after all property values are available
This commit is contained in:
Doug Bunting 2014-11-30 16:52:44 -08:00
parent 69f63db7bf
commit 27beca7738
18 changed files with 340 additions and 267 deletions

View File

@ -9,48 +9,47 @@
<h2>Create</h2>
@* anti-forgery is on by default *@
@* form will special-case anything that looks like a URI i.e. contains a '/' or doesn't match an action *@
<form anti-forgery="false" action="Create">
@* <form/> tag helper will special-case only elements with an "asp-action" or "asp-anti-forgery" attribute. *@
<form asp-anti-forgery="false" asp-action="Create">
<div class="form-horizontal">
@* validation summary tag helper will target just <div/> elements and append the list of errors *@
@* - i.e. this helper, like <select/> helper has ContentBehavior.Append *@
@* helper does nothing if model is valid and (client-side validation is disabled or validation-summary="ModelOnly") *@
@* - i.e. this helper, like <select/> helper, has ContentBehavior.Append *@
@* helper does nothing if model is valid and (client-side validation is disabled or asp-validation-summary="ModelOnly") *@
@* don't need a bound attribute to match Html.ValidationSummary()'s headerTag parameter; users wrap message as they wish *@
@* initially at least, will not remove the <div/> if list isn't generated *@
@* - should helper remove the <div/> if list isn't generated? *@
@* - (Html.ValidationSummary returns empty string despite non-empty message parameter) *@
@* Acceptable values are: "None", "ModelOnly" and "All" *@
<div validation-summary="All" style="color:blue" id="validation_day" class="form-group">
<div asp-validation-summary="All" style="color:blue" id="validation_day" class="form-group">
<span style="color:red">This is my message</span>
</div>
@* element will have correct name and id attributes for Id property. unusual part is the constant value. *@
@* - the helper will _not_ override the user-specified "value" attribute *@
<input type="hidden" for="Id" value="0" />
<input type="hidden" asp-for="Id" value="0" />
<div class="form-group">
@* no special-case for the "for" attribute; may eventually need to opt out on per-element basis here and in <form/> *@
<label for="Name" class="control-label col-md-2" style="color:blue" />
@* no special-case for the "asp-for" attribute; now distinct from HTML "for" attribute. *@
<label asp-for="Name" class="control-label col-md-2" style="color:blue" />
<div class="col-md-10">
<input type="text" for="Name" style="color:blue" />
<span validation-for="Name" style="color:blue" />
<input type="text" asp-for="Name" style="color:blue" />
<span asp-validation-for="Name" style="color:blue" />
</div>
</div>
<div class="form-group">
<label for="DateOfBirth" class="control-label col-md-2" />
<label asp-for="DateOfBirth" class="control-label col-md-2" />
<div class="col-md-10">
@* will automatically infer type="date" (reused HTML attribute) and format="{0:yyyy-MM-dd}" (optional bound attribute) *@
<input for="DateOfBirth" />
<span validation-for="DateOfBirth">When were you born?</span>
@* will automatically infer type="date" (reused HTML attribute) and asp-format="{0:yyyy-MM-dd}" (optional bound attribute) *@
<input asp-for="DateOfBirth" />
<span asp-validation-for="DateOfBirth">When were you born?</span>
</div>
</div>
<div class="form-group">
<label for="YearsEmployeed" class="control-label col-md-2" />
<label asp-for="YearsEmployeed" class="control-label col-md-2" />
<div class="col-md-10">
@* <select/> tag helper has ContentBehavior.Append -- items render after static options *@
<select for="YearsEmployeed" items="(IEnumerable<SelectListItem>)ViewBag.Items" size="2" class="form-control">
@* schedule-wise option tag helper (which adds "selected" attribute to static <option/>s) comes after helpers *@
@* - static use of "selected" attribute may cause HTML errors if in a single-selection <select/> *@
<select asp-for="YearsEmployeed" asp-items="(IEnumerable<SelectListItem>)ViewBag.Items" size="2" class="form-control">
@* Static use of "selected" attribute may cause HTML errors if in a single-selection <select/> *@
@* - @NTaylorMullen thinks <option/> tag helper could tell <select/> helper not to select anything from "items" *@
@* - wouldn't help if user selected one static <option/> and expression indicated another, especially one earlier in the <select/> *@
@* - may need a "default" bound parameter on the <select/> to avoid these cases and maintain "don't override" *@
@ -68,20 +67,20 @@
@* targets only <span/> in Beta; does not support equivalent of Html.ValidationMessageFor()'s tag parameter *@
@* - may eventually either support additional tags e.g. <p/> and <div/> or all tags /> *@
<span validation-for="YearsEmployeed" />
<span asp-validation-for="YearsEmployeed" />
</div>
</div>
<div class="form-group">
<label for="Blurb" class="control-label col-md-2" />
<label asp-for="Blurb" class="control-label col-md-2" />
<div class="col-md-10">
<textarea rows="4" for="Blurb"></textarea>
<span validation-for="Blurb" />
<textarea rows="4" asp-for="Blurb"></textarea>
<span asp-validation-for="Blurb" />
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
@* this <input/> lacks a "for" attribute and will not be changed by the <input/> tag helper *@
@* this <input/> lacks a "asp-for" attribute and will not be changed by the <input/> tag helper *@
<input type="submit" value="Create" class="btn btn-default" />
</div>
</div>
@ -89,5 +88,5 @@
</form>
<div>
<a action="Index">Back to list</a>
<a asp-action="Index">Back to list</a>
</div>

View File

@ -6,27 +6,27 @@
<form>
<div class="form-horizontal">
<div validation-summary="All"/>
<input type="hidden" for="Id" />
<div asp-validation-summary="All" />
<input type="hidden" asp-for="Id" />
<div class="form-group">
<label for="Name" class="control-label col-md-2" />
<label asp-for="Name" class="control-label col-md-2" />
<div class="col-md-10">
<input type="text" for="Name" />
<input type="text" asp-for="Name" />
<span validation-for="Name" />
</div>
</div>
<div class="form-group">
<label for="DateOfBirth" class="control-label col-md-2" />
<label asp-for="DateOfBirth" class="control-label col-md-2" />
<div class="col-md-10">
<input type="date" for="DateOfBirth" format="{0:yyyy-MM-dd}" />
<span validation-for="DateOfBirth" />
<input type="date" asp-for="DateOfBirth" asp-format="{0:yyyy-MM-dd}" />
<span asp-validation-for="DateOfBirth" />
</div>
</div>
<div class="form-group">
<label for="YearsEmployeed" class="control-label col-md-2" />
<label asp-for="YearsEmployeed" class="control-label col-md-2" />
<div class="col-md-10">
<select for="YearsEmployeed" items="(IEnumerable<SelectListItem>)ViewBag.Items" size="2" class="form-control">
<select asp-for="YearsEmployeed" asp-items="(IEnumerable<SelectListItem>)ViewBag.Items" size="2" class="form-control">
<optgroup label="Newby">
<option value="0">Less than 1</option>
<option value="1">1</option>
@ -37,14 +37,14 @@
<option value="5">5</option>
<option value="6">6</option>
</select>
<span validation-for="YearsEmployeed" />
<span asp-validation-for="YearsEmployeed" />
</div>
</div>
<div class="form-group">
<label for="Blurb" class="control-label col-md-2" />
<label asp-for="Blurb" class="control-label col-md-2" />
<div class="col-md-10">
<textarea rows="4" for="Blurb"></textarea>
<span validation-for="Blurb" />
<textarea rows="4" asp-for="Blurb"></textarea>
<span asp-validation-for="Blurb" />
</div>
</div>
@ -57,5 +57,5 @@
</form>
<div>
<a action="Index">Back to list</a>
<a asp-action="Index">Back to list</a>
</div>

View File

@ -9,36 +9,36 @@
@for (var index = 0; index < Model.Count(); ++index)
{
<div class="form-group">
@Html.LabelFor(m => m[index].Name) @* [index].Name [0].Name *@
@Html.LabelFor(m => m[index].Name)
@Html.TextBoxFor(m => m[index].Name, htmlAttributes: new { disabled = "disabled", @readonly= "readonly" })
@*<label for="[index].Name" />
<input type="text" for="[index].Name" disabled="disabled" readonly="readonly" />*@
@*<label asp-for="[index].Name" />
<input type="text" asp-for="[index].Name" disabled="disabled" readonly="readonly" />*@
</div>
<div class="form-group">
@Html.LabelFor(m => m[index].DateOfBirth)
@Html.TextBoxFor(m => m[index].DateOfBirth, format: "{0:yyyy-MM-dd}", htmlAttributes: new { disabled = "disabled", @readonly = "readonly", type="date" })
@*<label for="[index].DateOfBirth" />
<input type="date" for="[index].DateOfBirth" disabled="disabled" readonly="readonly" />*@
@*<label asp-for="[index].DateOfBirth" />
<input type="date" asp-for="[index].DateOfBirth" disabled="disabled" readonly="readonly" />*@
</div>
<div class="form-group">
@Html.LabelFor(m => m[index].YearsEmployeed)
@Html.TextBoxFor(m => m[index].YearsEmployeed, htmlAttributes: new { disabled = "disabled", @readonly = "readonly", type="number" })
@*<label for="[index].YearsEmployeed" />
<input type="number" for="[index].YearsEmployeed" disabled="disabled" readonly="readonly" />*@
@*<label asp-for="[index].YearsEmployeed" />
<input type="number" asp-for="[index].YearsEmployeed" disabled="disabled" readonly="readonly" />*@
</div>
<div class="form-group">
@Html.LabelFor(m => m[index].Blurb)
@Html.TextAreaFor(m => m[index].Blurb, htmlAttributes: new { disabled = "disabled", @readonly = "readonly" })
@*<label for="[index].Blurb" />
<textarea rows="4" for="[index].Blurb" disabled="disabled" readonly="readonly" />*@
@*<label asp-for="[index].Blurb" />
<textarea rows="4" asp-for="[index].Blurb" disabled="disabled" readonly="readonly" />*@
</div>
<p>
<a action="Edit" route-id="@index">Edit</a>
<a asp-action="Edit" asp-route-id="@index">Edit</a>
</p>
}
</div>
}
<p>
<a action="Create">Create New</a>
<a asp-action="Create">Create New</a>
</p>

View File

@ -15,7 +15,13 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
[TagName("a")]
public class AnchorTagHelper : TagHelper
{
private const string RouteAttributePrefix = "route-";
private const string ActionAttributeName = "asp-action";
private const string ControllerAttributeName = "asp-controller";
private const string FragmentAttributeName = "asp-fragment";
private const string HostAttributeName = "asp-host";
private const string ProtocolAttributeName = "asp-protocol";
private const string RouteAttributeName = "asp-route";
private const string RouteAttributePrefix = "asp-route-";
private const string Href = "href";
// Protected to ensure subclasses are correctly activated. Internal for ease of use when testing.
@ -26,27 +32,32 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
/// The name of the action method.
/// </summary>
/// <remarks>Must be <c>null</c> if <see cref="Route"/> is non-<c>null</c>.</remarks>
[HtmlAttributeName(ActionAttributeName)]
public string Action { get; set; }
/// <summary>
/// The name of the controller.
/// </summary>
/// <remarks>Must be <c>null</c> if <see cref="Route"/> is non-<c>null</c>.</remarks>
[HtmlAttributeName(ControllerAttributeName)]
public string Controller { get; set; }
/// <summary>
/// The protocol for the URL, such as &quot;http&quot; or &quot;https&quot;.
/// </summary>
[HtmlAttributeName(ProtocolAttributeName)]
public string Protocol { get; set; }
/// <summary>
/// The host name.
/// </summary>
[HtmlAttributeName(HostAttributeName)]
public string Host { get; set; }
/// <summary>
/// The URL fragment name.
/// </summary>
[HtmlAttributeName(FragmentAttributeName)]
public string Fragment { get; set; }
/// <summary>
@ -55,17 +66,22 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
/// <remarks>
/// Must be <c>null</c> if <see cref="Action"/> or <see cref="Controller"/> is non-<c>null</c>.
/// </remarks>
[HtmlAttributeName(RouteAttributeName)]
public string Route { get; set; }
/// <inheritdoc />
/// <remarks>Does nothing if user provides an "href" attribute. Throws an
/// <see cref="InvalidOperationException"/> if "href" attribute is provided and <see cref="Action"/>,
/// <see cref="Controller"/>, or <see cref="Route"/> are non-<c>null</c>.</remarks>
/// <remarks>Does nothing if user provides an <c>href</c> attribute.</remarks>
/// <exception cref="InvalidOperationException">
/// Thrown if <c>href</c> attribute is provided and <see cref="Action"/>, <see cref="Controller"/>,
/// <see cref="Fragment"/>, <see cref="Host"/>, <see cref="Protocol"/>, or <see cref="Route"/> are
/// non-<c>null</c> or if the user provided <c>asp-route-*</c> attributes. Also thrown if <see cref="Route"/>
/// and one or both of <see cref="Action"/> and <see cref="Controller"/> are non-<c>null</c>.
/// </exception>
public override void Process(TagHelperContext context, TagHelperOutput output)
{
var routePrefixedAttributes = output.FindPrefixedAttributes(RouteAttributePrefix);
// If there's an "href" on the tag it means it's being used as a normal anchor.
// If "href" is already set, it means the user is attempting to use a normal anchor.
if (output.Attributes.ContainsKey(Href))
{
if (Action != null ||
@ -77,15 +93,16 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
routePrefixedAttributes.Any())
{
// User specified an href and one of the bound attributes; can't determine the href attribute.
// Reviewers: Should this instead ignore the helper-specific attributes?
throw new InvalidOperationException(
Resources.FormatAnchorTagHelper_CannotOverrideSpecifiedHref(
Resources.FormatAnchorTagHelper_CannotOverrideHref(
"<a>",
nameof(Action).ToLowerInvariant(),
nameof(Controller).ToLowerInvariant(),
nameof(Route).ToLowerInvariant(),
nameof(Protocol).ToLowerInvariant(),
nameof(Host).ToLowerInvariant(),
nameof(Fragment).ToLowerInvariant(),
ActionAttributeName,
ControllerAttributeName,
RouteAttributeName,
ProtocolAttributeName,
HostAttributeName,
FragmentAttributeName,
RouteAttributePrefix,
Href));
}
@ -112,9 +129,9 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
throw new InvalidOperationException(
Resources.FormatAnchorTagHelper_CannotDetermineHrefRouteActionOrControllerSpecified(
"<a>",
nameof(Route).ToLowerInvariant(),
nameof(Action).ToLowerInvariant(),
nameof(Controller).ToLowerInvariant(),
RouteAttributeName,
ActionAttributeName,
ControllerAttributeName,
Href));
}
else

View File

@ -16,7 +16,11 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
[ContentBehavior(ContentBehavior.Append)]
public class FormTagHelper : TagHelper
{
private const string RouteAttributePrefix = "route-";
private const string ActionAttributeName = "asp-action";
private const string AntiForgeryAttributeName = "asp-anti-forgery";
private const string ControllerAttributeName = "asp-controller";
private const string RouteAttributePrefix = "asp-route-";
private const string HtmlActionAttributeName = "action";
// Protected to ensure subclasses are correctly activated. Internal for ease of use when testing.
[Activate]
@ -29,14 +33,13 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
/// <summary>
/// The name of the action method.
/// </summary>
/// <remarks>
/// If value contains a '/' this <see cref="ITagHelper"/> will do nothing.
/// </remarks>
[HtmlAttributeName(ActionAttributeName)]
public string Action { get; set; }
/// <summary>
/// The name of the controller.
/// </summary>
[HtmlAttributeName(ControllerAttributeName)]
public string Controller { get; set; }
/// <summary>
@ -45,41 +48,45 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
public string Method { get; set; }
/// <summary>
/// Whether the anti-forgery token should be generated. Defaults to <c>true</c> if <see cref="Action"/> is not
/// a URL, <c>false</c> otherwise.
/// Whether the anti-forgery token should be generated.
/// </summary>
[HtmlAttributeName("anti-forgery")]
/// <value>Defaults to <c>false</c> if user provides an <c>action</c> attribute; <c>true</c> otherwise.</value>
[HtmlAttributeName(AntiForgeryAttributeName)]
public bool? AntiForgery { get; set; }
/// <inheritdoc />
/// <remarks>Does nothing if <see cref="Action"/> contains a '/'.</remarks>
/// <remarks>At most adds an anti-forgery token if user provides an <c>action</c> attribute.</remarks>
/// <exception cref="InvalidOperationException">
/// Thrown if <c>action</c> attribute is provided and <see cref="Action"/> or <see cref="Controller"/> are
/// non-<c>null</c> or if the user provided <c>asp-route-*</c> attributes.
/// </exception>
public override void Process(TagHelperContext context, TagHelperOutput output)
{
bool antiForgeryDefault = true;
var routePrefixedAttributes = output.FindPrefixedAttributes(RouteAttributePrefix);
// If Action contains a '/' it means the user is attempting to use the FormTagHelper as a normal form.
if (Action != null && Action.Contains('/'))
// If "action" is already set, it means the user is attempting to use a normal <form>.
if (output.Attributes.ContainsKey(HtmlActionAttributeName))
{
if (Controller != null || routePrefixedAttributes.Any())
if (Action != null || Controller != null || routePrefixedAttributes.Any())
{
// We don't know how to generate a form action since a Controller attribute was also provided.
// User also specified bound attributes we cannot use.
// Reviewers: Should this instead ignore the helper-specific attributes -- only change
// antiForgeryDefault?
throw new InvalidOperationException(
Resources.FormatFormTagHelper_CannotDetermineAction(
Resources.FormatFormTagHelper_CannotOverrideAction(
"<form>",
nameof(Action).ToLowerInvariant(),
nameof(Controller).ToLowerInvariant(),
HtmlActionAttributeName,
ActionAttributeName,
ControllerAttributeName,
RouteAttributePrefix));
}
// User is using the FormTagHelper like a normal <form> tag, anti-forgery default should be false to
// not force the anti-forgery token onto the user.
// User is using the FormTagHelper like a normal <form> tag. Anti-forgery default should be false to
// not force the anti-forgery token on the user.
antiForgeryDefault = false;
// Restore Action, Method and Route HTML attributes if they were provided, user wants non-TagHelper <form>.
output.CopyHtmlAttribute(nameof(Action), context);
// Restore method attribute.
if (Method != null)
{
output.CopyHtmlAttribute(nameof(Method), context);

View File

@ -13,11 +13,14 @@ using Microsoft.AspNet.Razor.TagHelpers;
namespace Microsoft.AspNet.Mvc.TagHelpers
{
/// <summary>
/// <see cref="ITagHelper"/> implementation targeting &lt;input&gt; elements.
/// <see cref="ITagHelper"/> implementation targeting &lt;input&gt; elements with an <c>asp-for</c> attribute.
/// </summary>
[ContentBehavior(ContentBehavior.Replace)]
public class InputTagHelper : TagHelper
{
private const string ForAttributeName = "asp-for";
private const string FormatAttributeName = "asp-format";
// 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)
@ -64,6 +67,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
/// <summary>
/// An expression to be evaluated against the current model.
/// </summary>
[HtmlAttributeName(ForAttributeName)]
public ModelExpression For { get; set; }
/// <summary>
@ -76,6 +80,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
/// <see cref="InputTypeName"/> is "String". That is, <see cref="Format"/> is used when calling
/// <see cref="IHtmlGenerator.GenerateTextBox"/>.
/// </remarks>
[HtmlAttributeName(FormatAttributeName)]
public string Format { get; set; }
/// <summary>
@ -99,7 +104,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
public string Value { get; set; }
/// <inheritdoc />
/// <remarks>Does nothing if <see cref="For"/> is <c>null</c></remarks>
/// <remarks>Does nothing if <see cref="For"/> is <c>null</c>.</remarks>
/// <exception cref="InvalidOperationException">
/// Thrown if <see cref="Format"/> is non-<c>null</c> but <see cref="For"/> is <c>null</c>.
/// </exception>
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
@ -117,12 +125,13 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
if (For == null)
{
// Regular HTML <input/> element. Just make sure Format wasn't specified.
// Reviewers: Should this instead ignore the unused helper-specific attribute?
if (Format != null)
{
throw new InvalidOperationException(Resources.FormatInputTagHelper_UnableToFormat(
"<input>",
nameof(For).ToLowerInvariant(),
nameof(Format).ToLowerInvariant()));
ForAttributeName,
FormatAttributeName));
}
}
else
@ -134,7 +143,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
{
throw new InvalidOperationException(Resources.FormatTagHelpers_NoProvidedMetadata(
"<input>",
nameof(For).ToLowerInvariant(),
ForAttributeName,
nameof(IModelMetadataProvider),
For.Name));
}
@ -210,7 +219,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
{
throw new InvalidOperationException(Resources.FormatInputTagHelper_InvalidExpressionResult(
"<input>",
nameof(For).ToLowerInvariant(),
ForAttributeName,
metadata.RealModelType.FullName,
typeof(bool).FullName,
"type",

View File

@ -8,11 +8,13 @@ using Microsoft.AspNet.Razor.TagHelpers;
namespace Microsoft.AspNet.Mvc.TagHelpers
{
/// <summary>
/// <see cref="ITagHelper"/> implementation targeting &lt;label&gt; elements with <c>for</c> attributes.
/// <see cref="ITagHelper"/> implementation targeting &lt;label&gt; elements with an <c>asp-for</c> attribute.
/// </summary>
[ContentBehavior(ContentBehavior.Modify)]
public class LabelTagHelper : TagHelper
{
private const string ForAttributeName = "asp-for";
// Protected to ensure subclasses are correctly activated. Internal for ease of use when testing.
[Activate]
protected internal ViewContext ViewContext { get; set; }
@ -24,9 +26,11 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
/// <summary>
/// An expression to be evaluated against the current model.
/// </summary>
[HtmlAttributeName(ForAttributeName)]
public ModelExpression For { get; set; }
/// <inheritdoc />
/// <remarks>Does nothing if <see cref="For"/> is <c>null</c>.</remarks>
public override void Process(TagHelperContext context, TagHelperOutput output)
{
if (For != null)

View File

@ -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}' attribute 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}' attribute 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,19 +27,35 @@ 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 override the '{8}' attribute 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
internal static string AnchorTagHelper_CannotOverrideHref
{
get { return GetString("AnchorTagHelper_CannotOverrideSpecifiedHref"); }
get { return GetString("AnchorTagHelper_CannotOverrideHref"); }
}
/// <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 override the '{8}' attribute 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)
internal static string FormatAnchorTagHelper_CannotOverrideHref(object p0, object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8)
{
return string.Format(CultureInfo.CurrentCulture, GetString("AnchorTagHelper_CannotOverrideSpecifiedHref"), p0, p1, p2, p3, p4, p5, p6, p7, p8);
return string.Format(CultureInfo.CurrentCulture, GetString("AnchorTagHelper_CannotOverrideHref"), p0, p1, p2, p3, p4, p5, p6, p7, p8);
}
/// <summary>
/// Cannot override the '{1}' attribute for {0}. A {0} with a specified '{1}' must not have attributes starting with '{4}' or an '{2}' or '{3}' attribute.
/// </summary>
internal static string FormTagHelper_CannotOverrideAction
{
get { return GetString("FormTagHelper_CannotOverrideAction"); }
}
/// <summary>
/// Cannot override the '{1}' attribute for {0}. A {0} with a specified '{1}' must not have attributes starting with '{4}' or an '{2}' or '{3}' attribute.
/// </summary>
internal static string FormatFormTagHelper_CannotOverrideAction(object p0, object p1, object p2, object p3, object p4)
{
return string.Format(CultureInfo.CurrentCulture, GetString("FormTagHelper_CannotOverrideAction"), p0, p1, p2, p3, p4);
}
/// <summary>
@ -90,22 +106,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
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
{
get { return GetString("FormTagHelper_CannotDetermineAction"); }
}
/// <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 FormatFormTagHelper_CannotDetermineAction(object p0, object p1, object p2, object p3)
{
return string.Format(CultureInfo.CurrentCulture, GetString("FormTagHelper_CannotDetermineAction"), p0, p1, p2, p3);
}
/// <summary>
/// Cannot determine body for {0}. '{2}' must be null if '{1}' is null.
/// </summary>

View File

@ -118,10 +118,13 @@
<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}' attribute 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>
<data name="AnchorTagHelper_CannotOverrideHref" xml:space="preserve">
<value>Cannot override the '{8}' attribute 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="FormTagHelper_CannotOverrideAction" xml:space="preserve">
<value>Cannot override the '{1}' attribute for {0}. A {0} with a specified '{1}' must not have attributes starting with '{4}' or an '{2}' or '{3}' 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>
@ -132,9 +135,6 @@
<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>
</data>
<data name="SelectTagHelper_CannotDetermineContentWhenOnlyItemsSpecified" xml:space="preserve">
<value>Cannot determine body for {0}. '{2}' must be null if '{1}' is null.</value>
</data>

View File

@ -13,11 +13,14 @@ using Microsoft.AspNet.Razor.TagHelpers;
namespace Microsoft.AspNet.Mvc.TagHelpers
{
/// <summary>
/// <see cref="ITagHelper"/> implementation targeting &lt;select&gt; elements.
/// <see cref="ITagHelper"/> implementation targeting &lt;select&gt; elements with an <c>asp-for</c> attribute.
/// </summary>
[ContentBehavior(ContentBehavior.Append)]
public class SelectTagHelper : TagHelper
{
private const string ForAttributeName = "asp-for";
private const string ItemsAttributeName = "asp-items";
/// <summary>
/// Key used for selected values in <see cref="FormContext.FormData"/>.
/// </summary>
@ -39,6 +42,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
/// <summary>
/// An expression to be evaluated against the current model.
/// </summary>
[HtmlAttributeName(ForAttributeName)]
public ModelExpression For { get; set; }
/// <summary>
@ -55,21 +59,26 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
/// A collection of <see cref="SelectListItem"/> objects used to populate the &lt;select&gt; element with
/// &lt;optgroup&gt; and &lt;option&gt; elements.
/// </summary>
[HtmlAttributeName(ItemsAttributeName)]
public IEnumerable<SelectListItem> Items { get; set; }
/// <inheritdoc />
/// <remarks>Does nothing if <see cref="For"/> is <c>null</c>.</remarks>
/// <exception cref="InvalidOperationException">
/// Thrown if <see cref="Items"/> is non-<c>null</c> but <see cref="For"/> is <c>null</c>.
/// </exception>
public override void Process(TagHelperContext context, TagHelperOutput output)
{
if (For == null)
{
// Regular HTML <select/> element. Just make sure Items wasn't specified.
// Reviewers: Should this instead ignore the unused helper-specific attribute?
if (Items != null)
{
var message = Resources.FormatSelectTagHelper_CannotDetermineContentWhenOnlyItemsSpecified(
"<select>",
nameof(For).ToLowerInvariant(),
nameof(Items).ToLowerInvariant());
ForAttributeName,
ItemsAttributeName);
throw new InvalidOperationException(message);
}
@ -88,7 +97,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
{
throw new InvalidOperationException(Resources.FormatTagHelpers_NoProvidedMetadata(
"<select>",
nameof(For).ToLowerInvariant(),
ForAttributeName,
nameof(IModelMetadataProvider),
For.Name));
}

View File

@ -8,11 +8,13 @@ using Microsoft.AspNet.Razor.TagHelpers;
namespace Microsoft.AspNet.Mvc.TagHelpers
{
/// <summary>
/// <see cref="ITagHelper"/> implementation targeting &lt;textarea&gt; elements.
/// <see cref="ITagHelper"/> implementation targeting &lt;textarea&gt; elements with an <c>asp-for</c> attribute.
/// </summary>
[ContentBehavior(ContentBehavior.Replace)]
public class TextAreaTagHelper : TagHelper
{
private const string ForAttributeName = "asp-for";
// Protected to ensure subclasses are correctly activated. Internal for ease of use when testing.
[Activate]
protected internal IHtmlGenerator Generator { get; set; }
@ -24,10 +26,11 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
/// <summary>
/// An expression to be evaluated against the current model.
/// </summary>
[HtmlAttributeName(ForAttributeName)]
public ModelExpression For { get; set; }
/// <inheritdoc />
/// <remarks>Does nothing unless user binds "for" attribute in Razor source.</remarks>
/// <remarks>Does nothing if <see cref="For"/> is <c>null</c>.</remarks>
public override void Process(TagHelperContext context, TagHelperOutput output)
{
if (For != null)

View File

@ -8,12 +8,15 @@ using Microsoft.AspNet.Razor.TagHelpers;
namespace Microsoft.AspNet.Mvc.TagHelpers
{
/// <summary>
/// <see cref="ITagHelper"/> implementation targeting &lt;span&gt; elements with <c>validation-for</c> attributes.
/// <see cref="ITagHelper"/> implementation targeting &lt;span&gt; elements with an <c>asp-validation-for</c>
/// attribute.
/// </summary>
[TagName("span")]
[ContentBehavior(ContentBehavior.Modify)]
public class ValidationMessageTagHelper : TagHelper
{
private const string ValidationForAttributeName = "asp-validation-for";
// Protected to ensure subclasses are correctly activated. Internal for ease of use when testing.
[Activate]
protected internal ViewContext ViewContext { get; set; }
@ -25,10 +28,11 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
/// <summary>
/// Name to be validated on the current model.
/// </summary>
[HtmlAttributeName("validation-for")]
[HtmlAttributeName(ValidationForAttributeName)]
public ModelExpression For { get; set; }
/// <inheritdoc />
/// <remarks>Does nothing if <see cref="For"/> is <c>null</c>.</remarks>
public override void Process(TagHelperContext context, TagHelperOutput output)
{
if (For != null)

View File

@ -9,13 +9,15 @@ using Microsoft.AspNet.Razor.TagHelpers;
namespace Microsoft.AspNet.Mvc.TagHelpers
{
/// <summary>
/// <see cref="ITagHelper"/> implementation targeting &lt;div&gt; elements with a <c>validation-summary</c>
/// <see cref="ITagHelper"/> implementation targeting &lt;div&gt; elements with an <c>asp-validation-summary</c>
/// attribute.
/// </summary>
[TagName("div")]
[ContentBehavior(ContentBehavior.Append)]
public class ValidationSummaryTagHelper : TagHelper
{
private const string ValidationSummaryAttributeName = "asp-validation-summary";
// Protected to ensure subclasses are correctly activated. Internal for ease of use when testing.
[Activate]
protected internal ViewContext ViewContext { get; set; }
@ -29,11 +31,14 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
/// If <c>All</c> or <c>ModelOnly</c>, appends a validation summary. Acceptable values are defined by the
/// <see cref="ValidationSummary"/> enum.
/// </summary>
[HtmlAttributeName("validation-summary")]
[HtmlAttributeName(ValidationSummaryAttributeName)]
public string ValidationSummaryValue { get; set; }
/// <inheritdoc />
/// Does nothing if <see cref="ValidationSummaryValue"/> is <c>null</c>, empty or "None".
/// <remarks>Does nothing if <see cref="ValidationSummaryValue"/> is <c>null</c>, empty or "None".</remarks>
/// <exception cref="InvalidOperationException">
/// Thrown if <see cref="ValidationSummaryValue"/> is not a valid <see cref="ValidationSummary"/> value.
/// </exception>
public override void Process(TagHelperContext context, TagHelperOutput output)
{
if (!string.IsNullOrEmpty(ValidationSummaryValue))
@ -44,7 +49,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
throw new InvalidOperationException(
Resources.FormatTagHelpers_InvalidValue_ThreeAcceptableValues(
"<div>",
"validation-summary",
ValidationSummaryAttributeName,
ValidationSummaryValue,
ValidationSummary.All,
ValidationSummary.ModelOnly,

View File

@ -20,32 +20,24 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
// Arrange
var expectedTagName = "not-a";
var metadataProvider = new DataAnnotationsModelMetadataProvider();
var anchorTagHelper = new AnchorTagHelper
{
Action = "index",
Controller = "home",
Fragment = "hello=world",
Host = "contoso.com",
Protocol = "http"
};
var tagHelperContext = new TagHelperContext(
allAttributes: new Dictionary<string, object>
{
{ "id", "myanchor" },
{ "route-foo", "bar" },
{ "action", "index" },
{ "controller", "home" },
{ "fragment", "hello=world" },
{ "host", "contoso.com" },
{ "protocol", "http" }
{ "asp-route-foo", "bar" },
{ "asp-action", "index" },
{ "asp-controller", "home" },
{ "asp-fragment", "hello=world" },
{ "asp-host", "contoso.com" },
{ "asp-protocol", "http" }
});
var output = new TagHelperOutput(
expectedTagName,
attributes: new Dictionary<string, string>
{
{ "id", "myanchor" },
{ "route-foo", "bar" },
{ "asp-route-foo", "bar" },
},
content: "Something");
@ -64,7 +56,15 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
var viewContext = TestableHtmlGenerator.GetViewContext(model: null,
htmlGenerator: htmlGenerator,
metadataProvider: metadataProvider);
anchorTagHelper.Generator = htmlGenerator;
var anchorTagHelper = new AnchorTagHelper
{
Action = "index",
Controller = "home",
Fragment = "hello=world",
Generator = htmlGenerator,
Host = "contoso.com",
Protocol = "http",
};
// Act
await anchorTagHelper.ProcessAsync(tagHelperContext, output);
@ -83,13 +83,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
public async Task ProcessAsync_CallsIntoRouteLinkWithExpectedParameters()
{
// Arrange
var anchorTagHelper = new AnchorTagHelper
{
Route = "Default",
Protocol = "http",
Host = "contoso.com",
Fragment = "hello=world"
};
var context = new TagHelperContext(
allAttributes: new Dictionary<string, object>());
var output = new TagHelperOutput(
@ -103,7 +96,14 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
string.Empty, "Default", "http", "contoso.com", "hello=world", null, null))
.Returns(new TagBuilder("a"))
.Verifiable();
anchorTagHelper.Generator = generator.Object;
var anchorTagHelper = new AnchorTagHelper
{
Fragment = "hello=world",
Generator = generator.Object,
Host = "contoso.com",
Protocol = "http",
Route = "Default",
};
// Act & Assert
await anchorTagHelper.ProcessAsync(context, output);
@ -117,14 +117,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
public async Task ProcessAsync_CallsIntoActionLinkWithExpectedParameters()
{
// Arrange
var anchorTagHelper = new AnchorTagHelper
{
Action = "Index",
Controller = "Home",
Protocol = "http",
Host = "contoso.com",
Fragment = "hello=world"
};
var context = new TagHelperContext(
allAttributes: new Dictionary<string, object>());
var output = new TagHelperOutput(
@ -138,7 +130,15 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
string.Empty, "Index", "Home", "http", "contoso.com", "hello=world", null, null))
.Returns(new TagBuilder("a"))
.Verifiable();
anchorTagHelper.Generator = generator.Object;
var anchorTagHelper = new AnchorTagHelper
{
Action = "Index",
Controller = "Home",
Fragment = "hello=world",
Generator = generator.Object,
Host = "contoso.com",
Protocol = "http",
};
// Act & Assert
await anchorTagHelper.ProcessAsync(context, output);
@ -155,7 +155,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
[InlineData("Protocol")]
[InlineData("Host")]
[InlineData("Fragment")]
[InlineData("route-")]
[InlineData("asp-route-")]
public async Task ProcessAsync_ThrowsIfHrefConflictsWithBoundAttributes(string propertyName)
{
// Arrange
@ -167,19 +167,19 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
{ "href", "http://www.contoso.com" }
},
content: string.Empty);
if (propertyName == "route-")
if (propertyName == "asp-route-")
{
output.Attributes.Add("route-foo", "bar");
output.Attributes.Add("asp-route-foo", "bar");
}
else
{
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 override the 'href' attribute for <a>. An <a> with a specified " +
"'href' must not have attributes starting with 'asp-route-' or an " +
"'asp-action', 'asp-controller', 'asp-route', 'asp-protocol', 'asp-host', or " +
"'asp-fragment' attribute.";
// Act & Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
@ -196,15 +196,15 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
// Arrange
var anchorTagHelper = new AnchorTagHelper
{
Route = "Default"
Route = "Default",
};
typeof(AnchorTagHelper).GetProperty(propertyName).SetValue(anchorTagHelper, "Home");
var output = new TagHelperOutput(
"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' attribute for <a>. An <a> with a specified " +
"'asp-route' must not have an 'asp-action' or 'asp-controller' attribute.";
// Act & Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(

View File

@ -23,29 +23,22 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
// Arrange
var expectedTagName = "not-form";
var metadataProvider = new DataAnnotationsModelMetadataProvider();
var formTagHelper = new FormTagHelper
{
Action = "index",
Controller = "home",
Method = "post",
AntiForgery = true
};
var tagHelperContext = new TagHelperContext(
allAttributes: new Dictionary<string, object>
{
{ "id", "myform" },
{ "route-foo", "bar" },
{ "action", "index" },
{ "controller", "home" },
{ "asp-route-foo", "bar" },
{ "asp-action", "index" },
{ "asp-controller", "home" },
{ "method", "post" },
{ "anti-forgery", true }
{ "asp-anti-forgery", true }
});
var output = new TagHelperOutput(
expectedTagName,
attributes: new Dictionary<string, string>
{
{ "id", "myform" },
{ "route-foo", "bar" },
{ "asp-route-foo", "bar" },
},
content: "Something");
var urlHelper = new Mock<IUrlHelper>();
@ -64,8 +57,15 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
metadataProvider: metadataProvider);
var expectedContent = "Something" + htmlGenerator.GenerateAntiForgery(viewContext)
.ToString(TagRenderMode.SelfClosing);
formTagHelper.ViewContext = viewContext;
formTagHelper.Generator = htmlGenerator;
var formTagHelper = new FormTagHelper
{
Action = "index",
AntiForgery = true,
Controller = "home",
Generator = htmlGenerator,
Method = "post",
ViewContext = viewContext,
};
// Act
await formTagHelper.ProcessAsync(tagHelperContext, output);
@ -90,11 +90,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
{
// Arrange
var viewContext = CreateViewContext();
var formTagHelper = new FormTagHelper
{
Action = "Index",
AntiForgery = antiForgery
};
var context = new TagHelperContext(
allAttributes: new Dictionary<string, object>());
var output = new TagHelperOutput(
@ -114,8 +109,13 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
generator.Setup(mock => mock.GenerateAntiForgery(viewContext))
.Returns(new TagBuilder("input"));
formTagHelper.ViewContext = viewContext;
formTagHelper.Generator = generator.Object;
var formTagHelper = new FormTagHelper
{
Action = "Index",
AntiForgery = antiForgery,
Generator = generator.Object,
ViewContext = viewContext,
};
// Act
await formTagHelper.ProcessAsync(context, output);
@ -131,20 +131,15 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
{
// Arrange
var testViewContext = CreateViewContext();
var formTagHelper = new FormTagHelper
{
Action = "Index",
AntiForgery = false
};
var context = new TagHelperContext(
allAttributes: new Dictionary<string, object>());
var expectedAttribute = new KeyValuePair<string, string>("ROUTEE-NotRoute", "something");
var expectedAttribute = new KeyValuePair<string, string>("asp-ROUTEE-NotRoute", "something");
var output = new TagHelperOutput(
"form",
attributes: new Dictionary<string, string>()
{
{ "route-val", "hello" },
{ "roUte--Foo", "bar" }
{ "asp-route-val", "hello" },
{ "asp-roUte--Foo", "bar" }
},
content: string.Empty);
output.Attributes.Add(expectedAttribute);
@ -174,8 +169,13 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
})
.Returns(new TagBuilder("form"))
.Verifiable();
formTagHelper.ViewContext = testViewContext;
formTagHelper.Generator = generator.Object;
var formTagHelper = new FormTagHelper
{
Action = "Index",
AntiForgery = false,
Generator = generator.Object,
ViewContext = testViewContext,
};
// Act & Assert
await formTagHelper.ProcessAsync(context, output);
@ -192,13 +192,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
{
// Arrange
var viewContext = CreateViewContext();
var formTagHelper = new FormTagHelper
{
Action = "Index",
Controller = "Home",
Method = "POST",
AntiForgery = false
};
var context = new TagHelperContext(
allAttributes: new Dictionary<string, object>());
var output = new TagHelperOutput(
@ -210,8 +203,15 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
.Setup(mock => mock.GenerateForm(viewContext, "Index", "Home", null, "POST", null))
.Returns(new TagBuilder("form"))
.Verifiable();
formTagHelper.ViewContext = viewContext;
formTagHelper.Generator = generator.Object;
var formTagHelper = new FormTagHelper
{
Action = "Index",
AntiForgery = false,
Controller = "Home",
Generator = generator.Object,
Method = "POST",
ViewContext = viewContext,
};
// Act & Assert
await formTagHelper.ProcessAsync(context, output);
@ -222,22 +222,26 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
Assert.Empty(output.Content);
}
[Fact]
public async Task ProcessAsync_RestoresBoundAttributesIfActionIsURL()
[Theory]
[InlineData("my-action")]
[InlineData("http://www.contoso.com")]
[InlineData("my/action")]
public async Task ProcessAsync_RestoresBoundAttributesIfActionIsSpecified(string htmlAction)
{
// Arrange
var formTagHelper = new FormTagHelper
{
Action = "http://www.contoso.com",
Method = "POST"
};
var output = new TagHelperOutput("form",
attributes: new Dictionary<string, string>(),
attributes: new Dictionary<string, string>
{
{ "aCTiON", htmlAction },
},
content: string.Empty);
var context = new TagHelperContext(
allAttributes: new Dictionary<string, object>()
{
{ "aCTiON", "http://www.contoso.com" },
{ "METhod", "POST" }
});
@ -248,7 +252,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
Assert.Equal("form", output.TagName);
Assert.Equal(2, output.Attributes.Count);
var attribute = Assert.Single(output.Attributes, kvp => kvp.Key.Equals("aCTiON"));
Assert.Equal("http://www.contoso.com", attribute.Value);
Assert.Equal(htmlAction, attribute.Value);
attribute = Assert.Single(output.Attributes, kvp => kvp.Key.Equals("METhod"));
Assert.Equal("POST", attribute.Value);
Assert.Empty(output.Content);
@ -258,7 +262,9 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
[InlineData(true, "<input />")]
[InlineData(false, "")]
[InlineData(null, "")]
public async Task ProcessAsync_SupportsAntiForgeryIfActionIsURL(bool? antiForgery, string expectedContent)
public async Task ProcessAsync_SupportsAntiForgeryIfActionIsSpecified(
bool? antiForgery,
string expectedContent)
{
// Arrange
var viewContext = CreateViewContext();
@ -267,20 +273,18 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
.Returns(new TagBuilder("input"));
var formTagHelper = new FormTagHelper
{
Action = "http://www.contoso.com",
AntiForgery = antiForgery,
Generator = generator.Object,
ViewContext = viewContext,
};
formTagHelper.ViewContext = viewContext;
formTagHelper.Generator = generator.Object;
var output = new TagHelperOutput("form",
attributes: new Dictionary<string, string>(),
attributes: new Dictionary<string, string>
{
{ "aCTiON", "my-action" },
},
content: string.Empty);
var context = new TagHelperContext(
allAttributes: new Dictionary<string, object>()
{
{ "aCTiON", "http://www.contoso.com" }
});
var context = new TagHelperContext(allAttributes: new Dictionary<string, object>());
// Act
await formTagHelper.ProcessAsync(context, output);
@ -288,52 +292,40 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
// Assert
Assert.Equal("form", output.TagName);
var attribute = Assert.Single(output.Attributes);
Assert.Equal(new KeyValuePair<string, string>("aCTiON", "http://www.contoso.com"), attribute);
Assert.Equal(new KeyValuePair<string, string>("aCTiON", "my-action"), attribute);
Assert.Equal(expectedContent, output.Content);
}
[Fact]
public async Task ProcessAsync_ThrowsIfActionIsUrlWithSpecifiedController()
[Theory]
[InlineData("Action")]
[InlineData("Controller")]
[InlineData("asp-route-")]
public async Task ProcessAsync_ThrowsIfActionConflictsWithBoundAttributes(string propertyName)
{
// Arrange
var formTagHelper = new FormTagHelper
{
Action = "http://www.contoso.com",
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 tagHelperOutput = new TagHelperOutput(
"form",
attributes: new Dictionary<string, string>(),
content: string.Empty);
// Act & Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
() => formTagHelper.ProcessAsync(context: null, output: tagHelperOutput));
Assert.Equal(expectedErrorMessage, ex.Message);
}
[Fact]
public async Task ProcessAsync_ThrowsIfActionIsUrlWithSpecifiedRoutes()
{
// Arrange
var formTagHelper = new FormTagHelper
{
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 tagHelperOutput = new TagHelperOutput(
"form",
attributes: new Dictionary<string, string>
{
{ "route-foo", "bar" }
{ "action", "my-action" },
},
content: string.Empty);
if (propertyName == "asp-route-")
{
tagHelperOutput.Attributes.Add("asp-route-foo", "bar");
}
else
{
typeof(FormTagHelper).GetProperty(propertyName).SetValue(formTagHelper, "Home");
}
var expectedErrorMessage = "Cannot override the 'action' attribute for <form>. A <form> with a specified " +
"'action' must not have attributes starting with 'asp-route-' or an " +
"'asp-action' or 'asp-controller' attribute.";
// Act & Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(

View File

@ -552,6 +552,30 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
expressionName: propertyName);
}
[Fact]
public async Task ProcessAsync_Throws_IfForNotBoundButFormatIs()
{
// Arrange
var contextAttributes = new Dictionary<string, object>();
var originalAttributes = new Dictionary<string, string>();
var content = "original content";
var expectedTagName = "select";
var expectedMessage = "Unable to format without a 'asp-for' expression for <input>. 'asp-format' must " +
"be null if 'asp-for' is null.";
var tagHelperContext = new TagHelperContext(contextAttributes);
var output = new TagHelperOutput(expectedTagName, originalAttributes, content);
var tagHelper = new InputTagHelper
{
Format = "{0}",
};
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
() => tagHelper.ProcessAsync(tagHelperContext, output));
Assert.Equal(expectedMessage, exception.Message);
}
private static InputTagHelper GetTagHelper(
IHtmlGenerator htmlGenerator,
object container,

View File

@ -469,7 +469,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
var originalAttributes = new Dictionary<string, string>();
var content = "original content";
var expectedTagName = "select";
var expectedMessage = "Cannot determine body for <select>. 'items' must be null if 'for' is null.";
var expectedMessage = "Cannot determine body for <select>. 'asp-items' must be null if 'asp-for' is null.";
var tagHelperContext = new TagHelperContext(contextAttributes);
var output = new TagHelperOutput(expectedTagName, originalAttributes, content);

View File

@ -253,8 +253,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
attributes: new Dictionary<string, string>(),
content: "Content of validation message");
var expectedViewContext = CreateViewContext();
var expectedMessage = "Cannot parse 'validation-summary' value 'Hello World' for <div>. Acceptable values " +
"are 'All', 'ModelOnly' and 'None'.";
var expectedMessage = "Cannot parse 'asp-validation-summary' value 'Hello World' for <div>. Acceptable " +
"values are 'All', 'ModelOnly' and 'None'.";
// Act
var ex = await Assert.ThrowsAsync<InvalidOperationException>(