- use `IDictionary<string, TValue>` support in `<a/>` and `<form/>` tag helpers
 - make `RouteValues` dictionaries `IDictionary<string, string>` for ease of use
- remove `TagHelperOutputExtensions.FindPrefixedAttributes()`
- set all `GeneratedTagHelperContext` properties
- add error for tag helper dictionary properties where `TValue` is `ModelExpression`
- add new `RazorPage.InvalidTagHelperIndexerAssignment()` method and resource

tests
- use new `isIndexer` argument when creating `TagHelperAttributeDescriptor`
- arrange `AnchorTagHelper` and `FormTagHelper` correctly
 - also expect `routeValues != null` in calls to `IHtmlGenerator`

nits:
- get rid of some `foo` and `bar` gunk in tests
- remove unused variable to cleanup a test compilation warning
This commit is contained in:
Doug Bunting 2015-05-04 11:13:37 -07:00
parent 337bbad51d
commit 517c013882
16 changed files with 225 additions and 149 deletions

View File

@ -72,6 +72,8 @@ namespace Microsoft.AspNet.Mvc.Razor
ExecutionContextAddTagHelperAttributeMethodName =
nameof(TagHelperExecutionContext.AddTagHelperAttribute),
ExecutionContextAddHtmlAttributeMethodName = nameof(TagHelperExecutionContext.AddHtmlAttribute),
ExecutionContextAddMinimizedHtmlAttributeMethodName =
nameof(TagHelperExecutionContext.AddMinimizedHtmlAttribute),
ExecutionContextOutputPropertyName = nameof(TagHelperExecutionContext.Output),
RunnerTypeName = typeof(TagHelperRunner).FullName,
@ -85,6 +87,7 @@ namespace Microsoft.AspNet.Mvc.Razor
// Can't use nameof because RazorPage is not accessible here.
CreateTagHelperMethodName = "CreateTagHelper",
FormatInvalidIndexerAssignmentMethodName = "InvalidTagHelperIndexerAssignment",
StartTagHelperWritingScopeMethodName = "StartTagHelperWritingScope",
EndTagHelperWritingScopeMethodName = "EndTagHelperWritingScope",
@ -211,7 +214,7 @@ namespace Microsoft.AspNet.Mvc.Razor
sourceFileName = _pathNormalizer.NormalizePath(sourceFileName);
var inheritedCodeTrees = ChunkInheritanceUtility.GetInheritedCodeTrees(sourceFileName);
return new MvcRazorParser(razorParser, inheritedCodeTrees, DefaultInheritedChunks);
return new MvcRazorParser(razorParser, inheritedCodeTrees, DefaultInheritedChunks, ModelExpressionType);
}
/// <inheritdoc />
@ -224,7 +227,7 @@ namespace Microsoft.AspNet.Mvc.Razor
public override CodeBuilder DecorateCodeBuilder([NotNull] CodeBuilder incomingBuilder,
[NotNull] CodeBuilderContext context)
{
// Need the normalized path to resolve inherited chunks only. Full paths are needed for generated Razor
// Need the normalized path to resolve inherited chunks only. Full paths are needed for generated Razor
// files checksum and line pragmas to enable DesignTime debugging.
var normalizedPath = _pathNormalizer.NormalizePath(context.SourceFile);
var inheritedChunks = ChunkInheritanceUtility.GetInheritedCodeTrees(normalizedPath);

View File

@ -1,8 +1,10 @@
// Copyright (c) .NET Foundation. 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.Linq;
using Microsoft.AspNet.Mvc.Razor.Host;
using Microsoft.AspNet.Razor;
using Microsoft.AspNet.Razor.Generator.Compiler;
using Microsoft.AspNet.Razor.Parser;
@ -20,6 +22,7 @@ namespace Microsoft.AspNet.Mvc.Razor
public class MvcRazorParser : RazorParser
{
private readonly IEnumerable<TagHelperDirectiveDescriptor> _globalImportDirectiveDescriptors;
private readonly string _modelExpressionTypeName;
/// <summary>
/// Initializes a new instance of <see cref="MvcRazorParser"/>.
@ -32,13 +35,16 @@ namespace Microsoft.AspNet.Mvc.Razor
public MvcRazorParser(
[NotNull] RazorParser parser,
[NotNull] IReadOnlyList<CodeTree> inheritedCodeTrees,
[NotNull] IReadOnlyList<Chunk> defaultInheritedChunks)
[NotNull] IReadOnlyList<Chunk> defaultInheritedChunks,
[NotNull] string modelExpressionTypeName)
: base(parser)
{
// Construct tag helper descriptors from @addTagHelper, @removeTagHelper and @tagHelperPrefix chunks
_globalImportDirectiveDescriptors = GetTagHelperDirectiveDescriptors(
inheritedCodeTrees,
defaultInheritedChunks);
_modelExpressionTypeName = modelExpressionTypeName;
}
/// <inheritdoc />
@ -50,7 +56,27 @@ namespace Microsoft.AspNet.Mvc.Razor
TagHelperDescriptorResolver,
_globalImportDirectiveDescriptors,
errorSink);
return visitor.GetDescriptors(documentRoot);
var descriptors = visitor.GetDescriptors(documentRoot);
foreach (var descriptor in descriptors)
{
foreach (var attributeDescriptor in descriptor.Attributes)
{
if (attributeDescriptor.IsIndexer &&
string.Equals(
attributeDescriptor.TypeName,
_modelExpressionTypeName,
StringComparison.Ordinal))
{
errorSink.OnError(SourceLocation.Undefined, Resources.FormatMvcRazorParser_InvalidPropertyType(
descriptor.TypeName,
attributeDescriptor.Name,
_modelExpressionTypeName));
}
}
}
return descriptors;
}
private static IEnumerable<TagHelperDirectiveDescriptor> GetTagHelperDirectiveDescriptors(
@ -69,7 +95,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var chunksInOrder = defaultInheritedChunks.Concat(chunksFromGlobalImports);
foreach (var chunk in chunksInOrder)
{
// All TagHelperDirectiveDescriptors created here have undefined source locations because the source
// All TagHelperDirectiveDescriptors created here have undefined source locations because the source
// that created them is not in the same file.
var addTagHelperChunk = chunk as AddTagHelperChunk;
if (addTagHelperChunk != null)
@ -154,8 +180,8 @@ namespace Microsoft.AspNet.Mvc.Razor
}
}
// We need to see if the provided descriptors contain a @tagHelperPrefix directive. If so, it
// takes precedence and overrides any provided by the inheritedDescriptors. If not we need to add the
// We need to see if the provided descriptors contain a @tagHelperPrefix directive. If so, it
// takes precedence and overrides any provided by the inheritedDescriptors. If not we need to add the
// inherited @tagHelperPrefix directive back into the merged list.
if (prefixDirectiveDescriptor != null &&
!descriptors.Any(descriptor => descriptor.DirectiveType == TagHelperDirectiveType.TagHelperPrefix))

View File

@ -106,6 +106,22 @@ namespace Microsoft.AspNet.Mvc.Razor.Host
return string.Format(CultureInfo.CurrentCulture, GetString("MvcRazorCodeParser_OnlyOneModelStatementIsAllowed"), p0);
}
/// <summary>
/// Invalid tag helper property '{0}.{1}'. Dictionary values must not be of type '{2}'.
/// </summary>
internal static string MvcRazorParser_InvalidPropertyType
{
get { return GetString("MvcRazorParser_InvalidPropertyType"); }
}
/// <summary>
/// Invalid tag helper property '{0}.{1}'. Dictionary values must not be of type '{2}'.
/// </summary>
internal static string FormatMvcRazorParser_InvalidPropertyType(object p0, object p1, object p2)
{
return string.Format(CultureInfo.CurrentCulture, GetString("MvcRazorParser_InvalidPropertyType"), p0, p1, p2);
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -135,4 +135,7 @@
<data name="MvcRazorCodeParser_OnlyOneModelStatementIsAllowed" xml:space="preserve">
<value>Only one '{0}' statement is allowed in a file.</value>
</data>
<data name="MvcRazorParser_InvalidPropertyType" xml:space="preserve">
<value>Invalid tag helper property '{0}.{1}'. Dictionary values must not be of type '{2}'.</value>
</data>
</root>

View File

@ -410,6 +410,22 @@ namespace Microsoft.AspNet.Mvc.Razor
return GetString("GeneratedCodeFileName");
}
/// <summary>
/// Unable to perform '{0}' assignment. Tag helper property '{1}.{2}' must not be null.
/// </summary>
internal static string RazorPage_InvalidTagHelperIndexerAssignment
{
get { return GetString("RazorPage_InvalidTagHelperIndexerAssignment"); }
}
/// <summary>
/// Unable to perform '{0}' assignment. Tag helper property '{1}.{2}' must not be null.
/// </summary>
internal static string FormatRazorPage_InvalidTagHelperIndexerAssignment(object p0, object p1, object p2)
{
return string.Format(CultureInfo.CurrentCulture, GetString("RazorPage_InvalidTagHelperIndexerAssignment"), p0, p1, p2);
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -150,6 +150,24 @@ namespace Microsoft.AspNet.Mvc.Razor
}
}
/// <summary>
/// Format an error message about using an indexer when the tag helper property is <c>null</c>.
/// </summary>
/// <param name="attributeName">Name of the HTML attribute associated with the indexer.</param>
/// <param name="tagHelperTypeName">Full name of the tag helper <see cref="Type"/>.</param>
/// <param name="propertyName">Dictionary property in the tag helper.</param>
/// <returns>An error message about using an indexer when the tag helper property is <c>null</c>.</returns>
public static string InvalidTagHelperIndexerAssignment(
string attributeName,
string tagHelperTypeName,
string propertyName)
{
return Resources.FormatRazorPage_InvalidTagHelperIndexerAssignment(
attributeName,
tagHelperTypeName,
propertyName);
}
/// <summary>
/// Creates and activates a <see cref="ITagHelper"/>.
/// </summary>

View File

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

View File

@ -21,7 +21,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
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 RouteValuesDictionaryName = "asp-all-route-data";
private const string RouteValuesPrefix = "asp-route-";
private const string Href = "href";
[Activate]
@ -69,6 +70,13 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
[HtmlAttributeName(RouteAttributeName)]
public string Route { get; set; }
/// <summary>
/// Additional parameters for the route.
/// </summary>
[HtmlAttributeName(RouteValuesDictionaryName, DictionaryAttributePrefix = RouteValuesPrefix)]
public IDictionary<string, string> RouteValues { get; set; } =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
/// <inheritdoc />
/// <remarks>Does nothing if user provides an <c>href</c> attribute.</remarks>
/// <exception cref="InvalidOperationException">
@ -79,8 +87,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
/// </exception>
public override void Process(TagHelperContext context, TagHelperOutput output)
{
var routePrefixedAttributes = output.FindPrefixedAttributes(RouteAttributePrefix);
// If "href" is already set, it means the user is attempting to use a normal anchor.
if (output.Attributes.ContainsName(Href))
{
@ -90,7 +96,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
Protocol != null ||
Host != null ||
Fragment != null ||
routePrefixedAttributes.Any())
RouteValues.Count != 0)
{
// User specified an href and one of the bound attributes; can't determine the href attribute.
throw new InvalidOperationException(
@ -102,15 +108,19 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
ProtocolAttributeName,
HostAttributeName,
FragmentAttributeName,
RouteAttributePrefix,
RouteValuesPrefix,
Href));
}
}
else
{
TagBuilder tagBuilder;
var routeValues = GetRouteValues(output, routePrefixedAttributes);
// Convert from Dictionary<string, string> to Dictionary<string, object>.
var routeValues = RouteValues.ToDictionary(
kvp => kvp.Key,
kvp => (object)kvp.Value,
StringComparer.OrdinalIgnoreCase);
TagBuilder tagBuilder;
if (Route == null)
{
tagBuilder = Generator.GenerateActionLink(linkText: string.Empty,
@ -150,26 +160,5 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
}
}
}
// TODO: https://github.com/aspnet/Razor/issues/89 - We will not need this method once #89 is completed.
private static Dictionary<string, object> GetRouteValues(
TagHelperOutput output,
IEnumerable<TagHelperAttribute> routePrefixedAttributes)
{
Dictionary<string, object> routeValues = null;
if (routePrefixedAttributes.Any())
{
// Prefixed values should be treated as bound attributes, remove them from the output.
output.RemoveRange(routePrefixedAttributes);
// Remove prefix from keys and convert all values to strings. HtmlString and similar classes are not
// meaningful to routing.
routeValues = routePrefixedAttributes.ToDictionary(
attribute => attribute.Name.Substring(RouteAttributePrefix.Length),
attribute => (object)attribute.Value.ToString());
}
return routeValues;
}
}
}

View File

@ -18,7 +18,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
private const string AntiForgeryAttributeName = "asp-anti-forgery";
private const string ControllerAttributeName = "asp-controller";
private const string RouteAttributeName = "asp-route";
private const string RouteAttributePrefix = "asp-route-";
private const string RouteValuesDictionaryName = "asp-all-route-data";
private const string RouteValuesPrefix = "asp-route-";
private const string HtmlActionAttributeName = "action";
[Activate]
@ -57,6 +58,13 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
[HtmlAttributeName(RouteAttributeName)]
public string Route { get; set; }
/// <summary>
/// Additional parameters for the route.
/// </summary>
[HtmlAttributeName(RouteValuesDictionaryName, DictionaryAttributePrefix = RouteValuesPrefix)]
public IDictionary<string, string> RouteValues { get; set; } =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
/// <inheritdoc />
/// <remarks>
/// Does nothing if user provides an <c>action</c> attribute and <see cref="AntiForgery"/> is <c>null</c> or
@ -69,12 +77,11 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
public override void Process(TagHelperContext context, TagHelperOutput output)
{
var antiForgeryDefault = true;
var routePrefixedAttributes = output.FindPrefixedAttributes(RouteAttributePrefix);
// If "action" is already set, it means the user is attempting to use a normal <form>.
if (output.Attributes.ContainsName(HtmlActionAttributeName))
{
if (Action != null || Controller != null || Route != null || routePrefixedAttributes.Any())
if (Action != null || Controller != null || Route != null || RouteValues.Count != 0)
{
// User also specified bound attributes we cannot use.
throw new InvalidOperationException(
@ -84,7 +91,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
ActionAttributeName,
ControllerAttributeName,
RouteAttributeName,
RouteAttributePrefix));
RouteValuesPrefix));
}
// User is using the FormTagHelper like a normal <form> tag. Anti-forgery default should be false to
@ -93,8 +100,13 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
}
else
{
// Convert from Dictionary<string, string> to Dictionary<string, object>.
var routeValues = RouteValues.ToDictionary(
kvp => kvp.Key,
kvp => (object)kvp.Value,
StringComparer.OrdinalIgnoreCase);
TagBuilder tagBuilder;
var routeValues = GetRouteValues(output, routePrefixedAttributes);
if (Route == null)
{
tagBuilder = Generator.GenerateForm(
@ -142,26 +154,5 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
}
}
}
// TODO: https://github.com/aspnet/Razor/issues/89 - We will not need this method once #89 is completed.
private static Dictionary<string, object> GetRouteValues(
TagHelperOutput output,
IEnumerable<TagHelperAttribute> routePrefixedAttributes)
{
Dictionary<string, object> routeValues = null;
if (routePrefixedAttributes.Any())
{
// Prefixed values should be treated as bound attributes, remove them from the output.
output.RemoveRange(routePrefixedAttributes);
// Remove prefix from keys and convert all values to strings. HtmlString and similar classes are not
// meaningful to routing.
routeValues = routePrefixedAttributes.ToDictionary(
attribute => attribute.Name.Substring(RouteAttributePrefix.Length),
attribute => (object)attribute.Value.ToString());
}
return routeValues;
}
}
}

View File

@ -51,28 +51,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
}
}
/// <summary>
/// Returns all attributes from <paramref name="tagHelperOutput"/>'s
/// <see cref="TagHelperOutput.Attributes"/> that have the given <paramref name="prefix"/>.
/// </summary>
/// <param name="tagHelperOutput">The <see cref="TagHelperOutput"/> this method extends.</param>
/// <param name="prefix">A prefix to look for.</param>
/// <returns><see cref="KeyValuePair{string, string}"/>s with <see cref="KeyValuePair{string, string}.Key"/>
/// starting with the given <paramref name="prefix"/>.</returns>
public static IEnumerable<TagHelperAttribute> FindPrefixedAttributes(
[NotNull] this TagHelperOutput tagHelperOutput,
[NotNull] string prefix)
{
// TODO: https://github.com/aspnet/Razor/issues/89 - We will not need this method once #89 is completed.
// We're only interested in HTML attributes that have the desired prefix.
var prefixedAttributes = tagHelperOutput.Attributes
.Where(attribute => attribute.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
.ToArray();
return prefixedAttributes;
}
/// <summary>
/// Merges the given <paramref name="tagBuilder"/>'s <see cref="TagBuilder.Attributes"/> into the
/// <paramref name="tagHelperOutput"/>.

View File

@ -1071,9 +1071,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties);
var testableBinder = new TestableMutableObjectModelBinder();
// The [DefaultValue] on ValueTypeRequiredWithDefaultValue is ignored by model binding.
var expectedValue = 0;
// Make ValueTypeRequired invalid.
var propertyMetadata = dto.PropertyMetadata.Single(p => p.PropertyName == nameof(Person.ValueTypeRequired));
dto.Results[propertyMetadata] = new ModelBindingResult(
@ -1294,7 +1291,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
Assert.Equal(dob, model.DateOfBirth);
Assert.True(bindingContext.ModelState.IsValid);
// Ensure that we add child nodes for all the nodes which have a result (irrespective of if they
// Ensure that we add child nodes for all the nodes which have a result (irrespective of if they
// are bound or not).
Assert.Equal(2, modelValidationNode.ChildNodes.Count());
@ -1631,7 +1628,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
bindingContext.ModelState["foo"].Errors[0].Exception.Message);
}
// This can only really be done by writing an invalid model binder and returning 'isModelSet: true'
// This can only really be done by writing an invalid model binder and returning 'isModelSet: true'
// with a null model for a value type.
[Fact]
public void SetProperty_SettingNonNullableValueTypeToNull_CapturesException()

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Razor;
using Microsoft.AspNet.Razor.Generator.Compiler;
using Microsoft.AspNet.Razor.Parser;
@ -230,7 +231,7 @@ namespace Microsoft.AspNet.Mvc.Razor
public TestableMvcRazorParser(RazorParser parser,
IReadOnlyList<CodeTree> codeTrees,
IReadOnlyList<Chunk> defaultInheritedChunks)
: base(parser, codeTrees, defaultInheritedChunks)
: base(parser, codeTrees, defaultInheritedChunks, typeof(ModelExpression).FullName)
{
}

View File

@ -25,7 +25,11 @@ namespace Microsoft.AspNet.Mvc.Razor
ModelExpressionTypeName = modelExpressionType,
CreateModelExpressionMethodName = "SomeMethod"
});
var attributeDescriptor = new TagHelperAttributeDescriptor("MyAttribute", "SomeProperty", propertyType);
var attributeDescriptor = new TagHelperAttributeDescriptor(
name: "MyAttribute",
propertyName: "SomeProperty",
typeName: propertyType,
isIndexer: false);
var writer = new CSharpCodeWriter();
var generatorContext = new CodeGeneratorContext(host: null,
className: string.Empty,

View File

@ -8,7 +8,6 @@ using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
using Microsoft.Framework.WebEncoders;
using Microsoft.Framework.WebEncoders.Testing;
using Moq;
using Xunit;
@ -28,7 +27,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
allAttributes: new TagHelperAttributeList
{
{ "id", "myanchor" },
{ "asp-route-foo", "bar" },
{ "asp-route-name", "value" },
{ "asp-action", "index" },
{ "asp-controller", "home" },
{ "asp-fragment", "hello=world" },
@ -48,7 +47,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
attributes: new TagHelperAttributeList
{
{ "id", "myanchor" },
{ "asp-route-foo", "bar" },
});
output.Content.SetContent("Something");
@ -68,6 +66,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
Generator = htmlGenerator,
Host = "contoso.com",
Protocol = "http",
RouteValues =
{
{ "name", "value" },
},
};
// Act
@ -106,7 +108,13 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
var generator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
generator
.Setup(mock => mock.GenerateRouteLink(
string.Empty, "Default", "http", "contoso.com", "hello=world", null, null))
string.Empty,
"Default",
"http",
"contoso.com",
"hello=world",
It.IsAny<IDictionary<string, object>>(),
null))
.Returns(new TagBuilder("a", new CommonTestEncoder()))
.Verifiable();
var anchorTagHelper = new AnchorTagHelper
@ -149,7 +157,14 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
var generator = new Mock<IHtmlGenerator>();
generator
.Setup(mock => mock.GenerateActionLink(
string.Empty, "Index", "Home", "http", "contoso.com", "hello=world", null, null))
string.Empty,
"Index",
"Home",
"http",
"contoso.com",
"hello=world",
It.IsAny<IDictionary<string, object>>(),
null))
.Returns(new TagBuilder("a", new CommonTestEncoder()))
.Verifiable();
var anchorTagHelper = new AnchorTagHelper
@ -190,7 +205,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
});
if (propertyName == "asp-route-")
{
output.Attributes.Add("asp-route-foo", "bar");
anchorTagHelper.RouteValues.Add("name", "value");
}
else
{

View File

@ -29,7 +29,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
allAttributes: new TagHelperAttributeList
{
{ "id", "myform" },
{ "asp-route-foo", "bar" },
{ "asp-route-name", "value" },
{ "asp-action", "index" },
{ "asp-controller", "home" },
{ "method", "post" },
@ -48,7 +48,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
attributes: new TagHelperAttributeList
{
{ "id", "myform" },
{ "asp-route-foo", "bar" },
});
output.PostContent.SetContent("Something");
var urlHelper = new Mock<IUrlHelper>();
@ -68,6 +67,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
Controller = "home",
Generator = htmlGenerator,
ViewContext = viewContext,
RouteValues =
{
{ "name", "value" },
},
};
// Act
@ -145,7 +148,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
}
[Fact]
public async Task ProcessAsync_BindsRouteValuesFromTagHelperOutput()
public async Task ProcessAsync_BindsRouteValues()
{
// Arrange
var testViewContext = CreateViewContext();
@ -163,11 +166,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
var expectedAttribute = new TagHelperAttribute("asp-ROUTEE-NotRoute", "something");
var output = new TagHelperOutput(
"form",
attributes: new TagHelperAttributeList()
{
{ "asp-route-val", "hello" },
{ "asp-roUte--Foo", "bar" }
});
attributes: new TagHelperAttributeList());
output.Attributes.Add(expectedAttribute);
var generator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
@ -190,8 +189,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
Assert.Equal(2, routeValueDictionary.Count);
var routeValue = Assert.Single(routeValueDictionary, attr => attr.Key.Equals("val"));
Assert.Equal("hello", routeValue.Value);
routeValue = Assert.Single(routeValueDictionary, attr => attr.Key.Equals("-Foo"));
Assert.Equal("bar", routeValue.Value);
routeValue = Assert.Single(routeValueDictionary, attr => attr.Key.Equals("-Name"));
Assert.Equal("Value", routeValue.Value);
})
.Returns(new TagBuilder("form", new CommonTestEncoder()))
.Verifiable();
@ -201,6 +200,11 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
AntiForgery = false,
Generator = generator.Object,
ViewContext = testViewContext,
RouteValues =
{
{ "val", "hello" },
{ "-Name", "Value" },
},
};
// Act & Assert
@ -237,7 +241,13 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
attributes: new TagHelperAttributeList());
var generator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
generator
.Setup(mock => mock.GenerateForm(viewContext, "Index", "Home", null, null, null))
.Setup(mock => mock.GenerateForm(
viewContext,
"Index",
"Home",
It.IsAny<IDictionary<string, object>>(),
null,
null))
.Returns(new TagBuilder("form", new CommonTestEncoder()))
.Verifiable();
var formTagHelper = new FormTagHelper
@ -280,16 +290,13 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
});
var output = new TagHelperOutput(
"form",
attributes: new TagHelperAttributeList
{
{ "asp-route-foo", "bar" }
});
attributes: new TagHelperAttributeList());
var generator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
generator
.Setup(mock => mock.GenerateRouteForm(
viewContext,
"Default",
It.Is<Dictionary<string, object>>(m => string.Equals(m["foo"], "bar")),
It.Is<Dictionary<string, object>>(m => string.Equals(m["name"], "value")),
null,
null))
.Returns(new TagBuilder("form", new CommonTestEncoder()))
@ -300,6 +307,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
Route = "Default",
Generator = generator.Object,
ViewContext = viewContext,
RouteValues =
{
{ "name", "value" },
},
};
// Act & Assert
@ -384,7 +395,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
});
if (propertyName == "asp-route-")
{
tagHelperOutput.Attributes.Add("asp-route-foo", "bar");
formTagHelper.RouteValues.Add("name", "value");
}
else
{

View File

@ -222,7 +222,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
});
var expectedAttribute = new TagHelperAttribute("type", "btn");
tagHelperOutput.Attributes.Add(expectedAttribute);
var attributes = tagHelperOutput.FindPrefixedAttributes("route-");
var attributes = tagHelperOutput.Attributes
.Where(item => item.Name.StartsWith("route-", StringComparison.OrdinalIgnoreCase))
.ToList();
// Act
tagHelperOutput.RemoveRange(attributes);
@ -264,19 +267,21 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
"p",
attributes: new TagHelperAttributeList()
{
{ "routeHello", "World" },
{ "Routee-I", "Am" }
{ "route-Hello", "World" },
{ "Route-I", "Am" }
});
var expectedAttribute = new TagHelperAttribute("type", "btn");
tagHelperOutput.Attributes.Add(expectedAttribute);
var attributes = tagHelperOutput.Attributes
.Where(item => item.Name.StartsWith("route-", StringComparison.OrdinalIgnoreCase));
// Act
var attributes = tagHelperOutput.FindPrefixedAttributes("route-");
tagHelperOutput.RemoveRange(attributes);
// Assert
Assert.Empty(attributes);
var attribute = Assert.Single(tagHelperOutput.Attributes, attr => attr.Name.Equals("routeHello"));
Assert.Equal(attribute.Value, "World");
attribute = Assert.Single(tagHelperOutput.Attributes, attr => attr.Name.Equals("Routee-I"));
Assert.Equal(attribute.Value, "Am");
var attribute = Assert.Single(tagHelperOutput.Attributes);
Assert.Equal(expectedAttribute, attribute);
}
public static TheoryData MultipleAttributeSameNameData