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

View File

@ -1,8 +1,10 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.AspNet.Mvc.Razor.Host;
using Microsoft.AspNet.Razor; using Microsoft.AspNet.Razor;
using Microsoft.AspNet.Razor.Generator.Compiler; using Microsoft.AspNet.Razor.Generator.Compiler;
using Microsoft.AspNet.Razor.Parser; using Microsoft.AspNet.Razor.Parser;
@ -20,6 +22,7 @@ namespace Microsoft.AspNet.Mvc.Razor
public class MvcRazorParser : RazorParser public class MvcRazorParser : RazorParser
{ {
private readonly IEnumerable<TagHelperDirectiveDescriptor> _globalImportDirectiveDescriptors; private readonly IEnumerable<TagHelperDirectiveDescriptor> _globalImportDirectiveDescriptors;
private readonly string _modelExpressionTypeName;
/// <summary> /// <summary>
/// Initializes a new instance of <see cref="MvcRazorParser"/>. /// Initializes a new instance of <see cref="MvcRazorParser"/>.
@ -32,13 +35,16 @@ namespace Microsoft.AspNet.Mvc.Razor
public MvcRazorParser( public MvcRazorParser(
[NotNull] RazorParser parser, [NotNull] RazorParser parser,
[NotNull] IReadOnlyList<CodeTree> inheritedCodeTrees, [NotNull] IReadOnlyList<CodeTree> inheritedCodeTrees,
[NotNull] IReadOnlyList<Chunk> defaultInheritedChunks) [NotNull] IReadOnlyList<Chunk> defaultInheritedChunks,
[NotNull] string modelExpressionTypeName)
: base(parser) : base(parser)
{ {
// Construct tag helper descriptors from @addTagHelper, @removeTagHelper and @tagHelperPrefix chunks // Construct tag helper descriptors from @addTagHelper, @removeTagHelper and @tagHelperPrefix chunks
_globalImportDirectiveDescriptors = GetTagHelperDirectiveDescriptors( _globalImportDirectiveDescriptors = GetTagHelperDirectiveDescriptors(
inheritedCodeTrees, inheritedCodeTrees,
defaultInheritedChunks); defaultInheritedChunks);
_modelExpressionTypeName = modelExpressionTypeName;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -50,7 +56,27 @@ namespace Microsoft.AspNet.Mvc.Razor
TagHelperDescriptorResolver, TagHelperDescriptorResolver,
_globalImportDirectiveDescriptors, _globalImportDirectiveDescriptors,
errorSink); 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( private static IEnumerable<TagHelperDirectiveDescriptor> GetTagHelperDirectiveDescriptors(
@ -69,7 +95,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var chunksInOrder = defaultInheritedChunks.Concat(chunksFromGlobalImports); var chunksInOrder = defaultInheritedChunks.Concat(chunksFromGlobalImports);
foreach (var chunk in chunksInOrder) 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. // that created them is not in the same file.
var addTagHelperChunk = chunk as AddTagHelperChunk; var addTagHelperChunk = chunk as AddTagHelperChunk;
if (addTagHelperChunk != null) 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 // 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 // takes precedence and overrides any provided by the inheritedDescriptors. If not we need to add the
// inherited @tagHelperPrefix directive back into the merged list. // inherited @tagHelperPrefix directive back into the merged list.
if (prefixDirectiveDescriptor != null && if (prefixDirectiveDescriptor != null &&
!descriptors.Any(descriptor => descriptor.DirectiveType == TagHelperDirectiveType.TagHelperPrefix)) !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); 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) private static string GetString(string name, params string[] formatterNames)
{ {
var value = _resourceManager.GetString(name); var value = _resourceManager.GetString(name);

View File

@ -135,4 +135,7 @@
<data name="MvcRazorCodeParser_OnlyOneModelStatementIsAllowed" xml:space="preserve"> <data name="MvcRazorCodeParser_OnlyOneModelStatementIsAllowed" xml:space="preserve">
<value>Only one '{0}' statement is allowed in a file.</value> <value>Only one '{0}' statement is allowed in a file.</value>
</data> </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> </root>

View File

@ -410,6 +410,22 @@ namespace Microsoft.AspNet.Mvc.Razor
return GetString("GeneratedCodeFileName"); 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) private static string GetString(string name, params string[] formatterNames)
{ {
var value = _resourceManager.GetString(name); 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> /// <summary>
/// Creates and activates a <see cref="ITagHelper"/>. /// Creates and activates a <see cref="ITagHelper"/>.
/// </summary> /// </summary>

View File

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

View File

@ -21,7 +21,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
private const string HostAttributeName = "asp-host"; private const string HostAttributeName = "asp-host";
private const string ProtocolAttributeName = "asp-protocol"; private const string ProtocolAttributeName = "asp-protocol";
private const string RouteAttributeName = "asp-route"; 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"; private const string Href = "href";
[Activate] [Activate]
@ -69,6 +70,13 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
[HtmlAttributeName(RouteAttributeName)] [HtmlAttributeName(RouteAttributeName)]
public string Route { get; set; } 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 /> /// <inheritdoc />
/// <remarks>Does nothing if user provides an <c>href</c> attribute.</remarks> /// <remarks>Does nothing if user provides an <c>href</c> attribute.</remarks>
/// <exception cref="InvalidOperationException"> /// <exception cref="InvalidOperationException">
@ -79,8 +87,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
/// </exception> /// </exception>
public override void Process(TagHelperContext context, TagHelperOutput output) 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 "href" is already set, it means the user is attempting to use a normal anchor.
if (output.Attributes.ContainsName(Href)) if (output.Attributes.ContainsName(Href))
{ {
@ -90,7 +96,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
Protocol != null || Protocol != null ||
Host != null || Host != null ||
Fragment != null || Fragment != null ||
routePrefixedAttributes.Any()) RouteValues.Count != 0)
{ {
// User specified an href and one of the bound attributes; can't determine the href attribute. // User specified an href and one of the bound attributes; can't determine the href attribute.
throw new InvalidOperationException( throw new InvalidOperationException(
@ -102,15 +108,19 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
ProtocolAttributeName, ProtocolAttributeName,
HostAttributeName, HostAttributeName,
FragmentAttributeName, FragmentAttributeName,
RouteAttributePrefix, RouteValuesPrefix,
Href)); Href));
} }
} }
else else
{ {
TagBuilder tagBuilder; // Convert from Dictionary<string, string> to Dictionary<string, object>.
var routeValues = GetRouteValues(output, routePrefixedAttributes); var routeValues = RouteValues.ToDictionary(
kvp => kvp.Key,
kvp => (object)kvp.Value,
StringComparer.OrdinalIgnoreCase);
TagBuilder tagBuilder;
if (Route == null) if (Route == null)
{ {
tagBuilder = Generator.GenerateActionLink(linkText: string.Empty, 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 AntiForgeryAttributeName = "asp-anti-forgery";
private const string ControllerAttributeName = "asp-controller"; private const string ControllerAttributeName = "asp-controller";
private const string RouteAttributeName = "asp-route"; 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"; private const string HtmlActionAttributeName = "action";
[Activate] [Activate]
@ -57,6 +58,13 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
[HtmlAttributeName(RouteAttributeName)] [HtmlAttributeName(RouteAttributeName)]
public string Route { get; set; } 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 /> /// <inheritdoc />
/// <remarks> /// <remarks>
/// Does nothing if user provides an <c>action</c> attribute and <see cref="AntiForgery"/> is <c>null</c> or /// 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) public override void Process(TagHelperContext context, TagHelperOutput output)
{ {
var antiForgeryDefault = true; 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 "action" is already set, it means the user is attempting to use a normal <form>.
if (output.Attributes.ContainsName(HtmlActionAttributeName)) 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. // User also specified bound attributes we cannot use.
throw new InvalidOperationException( throw new InvalidOperationException(
@ -84,7 +91,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
ActionAttributeName, ActionAttributeName,
ControllerAttributeName, ControllerAttributeName,
RouteAttributeName, RouteAttributeName,
RouteAttributePrefix)); RouteValuesPrefix));
} }
// User is using the FormTagHelper like a normal <form> tag. Anti-forgery default should be false to // 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 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; TagBuilder tagBuilder;
var routeValues = GetRouteValues(output, routePrefixedAttributes);
if (Route == null) if (Route == null)
{ {
tagBuilder = Generator.GenerateForm( 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> /// <summary>
/// Merges the given <paramref name="tagBuilder"/>'s <see cref="TagBuilder.Attributes"/> into the /// Merges the given <paramref name="tagBuilder"/>'s <see cref="TagBuilder.Attributes"/> into the
/// <paramref name="tagHelperOutput"/>. /// <paramref name="tagHelperOutput"/>.

View File

@ -1071,9 +1071,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties); var dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties);
var testableBinder = new TestableMutableObjectModelBinder(); var testableBinder = new TestableMutableObjectModelBinder();
// The [DefaultValue] on ValueTypeRequiredWithDefaultValue is ignored by model binding.
var expectedValue = 0;
// Make ValueTypeRequired invalid. // Make ValueTypeRequired invalid.
var propertyMetadata = dto.PropertyMetadata.Single(p => p.PropertyName == nameof(Person.ValueTypeRequired)); var propertyMetadata = dto.PropertyMetadata.Single(p => p.PropertyName == nameof(Person.ValueTypeRequired));
dto.Results[propertyMetadata] = new ModelBindingResult( dto.Results[propertyMetadata] = new ModelBindingResult(
@ -1294,7 +1291,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
Assert.Equal(dob, model.DateOfBirth); Assert.Equal(dob, model.DateOfBirth);
Assert.True(bindingContext.ModelState.IsValid); 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). // are bound or not).
Assert.Equal(2, modelValidationNode.ChildNodes.Count()); Assert.Equal(2, modelValidationNode.ChildNodes.Count());
@ -1631,7 +1628,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
bindingContext.ModelState["foo"].Errors[0].Exception.Message); 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. // with a null model for a value type.
[Fact] [Fact]
public void SetProperty_SettingNonNullableValueTypeToNull_CapturesException() public void SetProperty_SettingNonNullableValueTypeToNull_CapturesException()

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Razor; using Microsoft.AspNet.Razor;
using Microsoft.AspNet.Razor.Generator.Compiler; using Microsoft.AspNet.Razor.Generator.Compiler;
using Microsoft.AspNet.Razor.Parser; using Microsoft.AspNet.Razor.Parser;
@ -230,7 +231,7 @@ namespace Microsoft.AspNet.Mvc.Razor
public TestableMvcRazorParser(RazorParser parser, public TestableMvcRazorParser(RazorParser parser,
IReadOnlyList<CodeTree> codeTrees, IReadOnlyList<CodeTree> codeTrees,
IReadOnlyList<Chunk> defaultInheritedChunks) 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, ModelExpressionTypeName = modelExpressionType,
CreateModelExpressionMethodName = "SomeMethod" 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 writer = new CSharpCodeWriter();
var generatorContext = new CodeGeneratorContext(host: null, var generatorContext = new CodeGeneratorContext(host: null,
className: string.Empty, className: string.Empty,

View File

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

View File

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

View File

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