diff --git a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs
index ebeb1fbcea..bb9de0d044 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs
@@ -251,7 +251,7 @@ namespace Microsoft.AspNet.Mvc.Core
}
///
- /// The view component name '{0}' matched multiple types: {1}
+ /// The view component name '{0}' matched multiple types:{1}{2}
///
internal static string ViewComponent_AmbiguousTypeMatch
{
@@ -259,11 +259,11 @@ namespace Microsoft.AspNet.Mvc.Core
}
///
- /// The view component name '{0}' matched multiple types: {1}
+ /// The view component name '{0}' matched multiple types:{1}{2}
///
- internal static string FormatViewComponent_AmbiguousTypeMatch(object p0, object p1)
+ internal static string FormatViewComponent_AmbiguousTypeMatch(object p0, object p1, object p2)
{
- return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_AmbiguousTypeMatch"), p0, p1);
+ return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_AmbiguousTypeMatch"), p0, p1, p2);
}
///
@@ -1530,6 +1530,22 @@ namespace Microsoft.AspNet.Mvc.Core
return string.Format(CultureInfo.CurrentCulture, GetString("FileResult_InvalidPath"), p0);
}
+ ///
+ /// Type: '{0}' - Name: '{1}'
+ ///
+ internal static string ViewComponent_AmbiguousTypeMatch_Item
+ {
+ get { return GetString("ViewComponent_AmbiguousTypeMatch_Item"); }
+ }
+
+ ///
+ /// Type: '{0}' - Name: '{1}'
+ ///
+ internal static string FormatViewComponent_AmbiguousTypeMatch_Item(object p0, object p1)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_AmbiguousTypeMatch_Item"), p0, p1);
+ }
+
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);
diff --git a/src/Microsoft.AspNet.Mvc.Core/Resources.resx b/src/Microsoft.AspNet.Mvc.Core/Resources.resx
index 9f9b9be8e3..4714a8d58e 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Resources.resx
+++ b/src/Microsoft.AspNet.Mvc.Core/Resources.resx
@@ -163,7 +163,8 @@
The class ReflectedActionFilterEndPoint only supports ReflectedActionDescriptors.
- The view component name '{0}' matched multiple types: {1}
+ The view component name '{0}' matched multiple types:{1}{2}
+ {1} is the newline character
The async view component method '{0}' should be declared to return Task<T>.
@@ -413,4 +414,7 @@
Could not find file: {0}
{0} is the value for the provided path
+
+ Type: '{0}' - Name: '{1}'
+
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/DefaultViewComponentSelector.cs b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/DefaultViewComponentSelector.cs
index 87338f5bec..c19c868d2d 100644
--- a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/DefaultViewComponentSelector.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/DefaultViewComponentSelector.cs
@@ -2,7 +2,10 @@
// 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.Diagnostics.Contracts;
using System.Linq;
+using System.Reflection;
using Microsoft.AspNet.Mvc.Core;
namespace Microsoft.AspNet.Mvc
@@ -21,30 +24,84 @@ namespace Microsoft.AspNet.Mvc
var assemblies = _assemblyProvider.CandidateAssemblies;
var types = assemblies.SelectMany(a => a.DefinedTypes);
- var components =
+ var candidates =
types
- .Where(ViewComponentConventions.IsComponent)
- .Select(c => new { Name = ViewComponentConventions.GetComponentName(c), Type = c.AsType() });
+ .Where(t => IsViewComponentType(t))
+ .Select(CreateCandidate);
- var matching =
- components
- .Where(c => string.Equals(c.Name, componentName, StringComparison.OrdinalIgnoreCase))
- .ToArray();
+ // ViewComponent names can either be fully-qualified, or refer to the 'short-name'. If the provided
+ // name contains a '.' - then it's a fully-qualified name.
+ var matching = new List();
+ if (componentName.Contains("."))
+ {
+ matching.AddRange(candidates.Where(
+ c => string.Equals(c.FullName, componentName, StringComparison.OrdinalIgnoreCase)));
+ }
+ else
+ {
+ matching.AddRange(candidates.Where(
+ c => string.Equals(c.ShortName, componentName, StringComparison.OrdinalIgnoreCase)));
+ }
- if (matching.Length == 0)
+ if (matching.Count == 0)
{
return null;
}
- else if (matching.Length == 1)
+ else if (matching.Count == 1)
{
return matching[0].Type;
}
else
{
- var typeNames = string.Join(Environment.NewLine, matching.Select(t => t.Type.FullName));
+ var matchedTypes = new List();
+ foreach (var candidate in matching)
+ {
+ matchedTypes.Add(Resources.FormatViewComponent_AmbiguousTypeMatch_Item(
+ candidate.Type.FullName,
+ candidate.FullName));
+ }
+
+ var typeNames = string.Join(Environment.NewLine, matchedTypes);
throw new InvalidOperationException(
- Resources.FormatViewComponent_AmbiguousTypeMatch(componentName, typeNames));
+ Resources.FormatViewComponent_AmbiguousTypeMatch(componentName, Environment.NewLine, typeNames));
}
}
+
+ protected virtual bool IsViewComponentType([NotNull] TypeInfo typeInfo)
+ {
+ return ViewComponentConventions.IsComponent(typeInfo);
+ }
+
+ private static ViewComponentCandidate CreateCandidate(TypeInfo typeInfo)
+ {
+ var candidate = new ViewComponentCandidate()
+ {
+ FullName = ViewComponentConventions.GetComponentFullName(typeInfo),
+ ShortName = ViewComponentConventions.GetComponentName(typeInfo),
+ Type = typeInfo.AsType(),
+ };
+
+ Contract.Assert(!string.IsNullOrEmpty(candidate.FullName));
+ var separatorIndex = candidate.FullName.LastIndexOf(".");
+ if (separatorIndex >= 0)
+ {
+ candidate.ShortName = candidate.FullName.Substring(separatorIndex + 1);
+ }
+ else
+ {
+ candidate.ShortName = candidate.FullName;
+ }
+
+ return candidate;
+ }
+
+ private class ViewComponentCandidate
+ {
+ public string FullName { get; set; }
+
+ public string ShortName { get; set; }
+
+ public Type Type { get; set; }
+ }
}
}
diff --git a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ViewComponentConventions.cs b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ViewComponentConventions.cs
index 1fa23c8cfe..bb913f0b00 100644
--- a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ViewComponentConventions.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ViewComponentConventions.cs
@@ -14,10 +14,44 @@ namespace Microsoft.AspNet.Mvc
{
var attribute = componentType.GetCustomAttribute();
if (attribute != null && !string.IsNullOrEmpty(attribute.Name))
+ {
+ var separatorIndex = attribute.Name.LastIndexOf('.');
+ if (separatorIndex >= 0)
+ {
+ return attribute.Name.Substring(separatorIndex + 1);
+ }
+ else
+ {
+ return attribute.Name;
+ }
+ }
+
+ return GetShortNameByConvention(componentType);
+ }
+
+ public static string GetComponentFullName([NotNull] TypeInfo componentType)
+ {
+ var attribute = componentType.GetCustomAttribute();
+ if (!string.IsNullOrEmpty(attribute?.Name))
{
return attribute.Name;
}
+ // If the view component didn't define a name explicitly then use the namespace + the
+ // 'short name'.
+ var shortName = GetShortNameByConvention(componentType);
+ if (string.IsNullOrEmpty(componentType.Namespace))
+ {
+ return shortName;
+ }
+ else
+ {
+ return componentType.Namespace + "." + shortName;
+ }
+ }
+
+ private static string GetShortNameByConvention(TypeInfo componentType)
+ {
if (componentType.Name.EndsWith(ViewComponentSuffix, StringComparison.OrdinalIgnoreCase))
{
return componentType.Name.Substring(0, componentType.Name.Length - ViewComponentSuffix.Length);
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ViewComponents/DefaultViewComponentSelectorTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ViewComponents/DefaultViewComponentSelectorTest.cs
new file mode 100644
index 0000000000..7a37abf641
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/ViewComponents/DefaultViewComponentSelectorTest.cs
@@ -0,0 +1,193 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Linq;
+using System.Reflection;
+using Xunit;
+
+namespace Microsoft.AspNet.Mvc
+{
+ public class DefaultViewComponentSelectorTest
+ {
+ [Fact]
+ public void SelectComponent_ByShortNameWithSuffix()
+ {
+ // Arrange
+ var selector = CreateSelector();
+
+ // Act
+ var result = selector.SelectComponent("Suffix");
+
+ // Assert
+ Assert.Equal(typeof(SuffixViewComponent), result);
+ }
+
+ [Fact]
+ public void SelectComponent_ByLongNameWithSuffix()
+ {
+ // Arrange
+ var selector = CreateSelector();
+
+ // Act
+ var result = selector.SelectComponent("Microsoft.AspNet.Mvc.Suffix");
+
+ // Assert
+ Assert.Equal(typeof(SuffixViewComponent), result);
+ }
+
+ [Fact]
+ public void SelectComponent_ByShortNameWithoutSuffix()
+ {
+ // Arrange
+ var selector = CreateSelector();
+
+ // Act
+ var result = selector.SelectComponent("WithoutSuffix");
+
+ // Assert
+ Assert.Equal(typeof(WithoutSuffix), result);
+ }
+
+ [Fact]
+ public void SelectComponent_ByLongNameWithoutSuffix()
+ {
+ // Arrange
+ var selector = CreateSelector();
+
+ // Act
+ var result = selector.SelectComponent("Microsoft.AspNet.Mvc.WithoutSuffix");
+
+ // Assert
+ Assert.Equal(typeof(WithoutSuffix), result);
+ }
+
+ [Fact]
+ public void SelectComponent_ByAttribute()
+ {
+ // Arrange
+ var selector = CreateSelector();
+
+ // Act
+ var result = selector.SelectComponent("ByAttribute");
+
+ // Assert
+ Assert.Equal(typeof(ByAttribute), result);
+ }
+
+ [Fact]
+ public void SelectComponent_ByNamingConvention()
+ {
+ // Arrange
+ var selector = CreateSelector();
+
+ // Act
+ var result = selector.SelectComponent("ByNamingConvention");
+
+ // Assert
+ Assert.Equal(typeof(ByNamingConventionViewComponent), result);
+ }
+
+ [Fact]
+ public void SelectComponent_Ambiguity()
+ {
+ // Arrange
+ var selector = CreateSelector();
+
+ var expected =
+ "The view component name 'Ambiguous' matched multiple types:" + Environment.NewLine +
+ "Type: 'Microsoft.AspNet.Mvc.DefaultViewComponentSelectorTest+Ambiguous1' - " +
+ "Name: 'Namespace1.Ambiguous'" + Environment.NewLine +
+ "Type: 'Microsoft.AspNet.Mvc.DefaultViewComponentSelectorTest+Ambiguous2' - " +
+ "Name: 'Namespace2.Ambiguous'";
+
+ // Act
+ var ex = Assert.Throws(() => selector.SelectComponent("Ambiguous"));
+
+ // Assert
+ Assert.Equal(expected, ex.Message);
+ }
+
+ [Fact]
+ public void SelectComponent_FullNameToAvoidAmbiguity()
+ {
+ // Arrange
+ var selector = CreateSelector();
+
+ // Act
+ var result = selector.SelectComponent("Namespace1.Ambiguous");
+
+ // Assert
+ Assert.Equal(typeof(Ambiguous1), result);
+ }
+
+ [Theory]
+ [InlineData("FullNameInAttribute")]
+ [InlineData("CoolNameSpace.FullNameInAttribute")]
+ public void SelectComponent_FullNameInAttribute(string name)
+ {
+ // Arrange
+ var selector = CreateSelector();
+
+ // Act
+ var result = selector.SelectComponent(name);
+
+ // Assert
+ Assert.Equal(typeof(FullNameInAttribute), result);
+ }
+
+ private IViewComponentSelector CreateSelector()
+ {
+ return new FilteredViewComponentSelector();
+ }
+
+ private class SuffixViewComponent : ViewComponent
+ {
+ }
+
+ private class WithoutSuffix : ViewComponent
+ {
+ }
+
+ private class ByNamingConventionViewComponent
+ {
+ }
+
+ [ViewComponent]
+ private class ByAttribute
+ {
+ }
+
+ [ViewComponent(Name = "Namespace1.Ambiguous")]
+ private class Ambiguous1
+ {
+ }
+
+ [ViewComponent(Name = "Namespace2.Ambiguous")]
+ private class Ambiguous2
+ {
+ }
+
+ [ViewComponent(Name = "CoolNameSpace.FullNameInAttribute")]
+ private class FullNameInAttribute
+ {
+ }
+
+ // This will only consider types nested inside this class as ViewComponent classes
+ private class FilteredViewComponentSelector : DefaultViewComponentSelector
+ {
+ public FilteredViewComponentSelector()
+ : base(new StaticAssemblyProvider())
+ {
+ AllowedTypes = typeof(DefaultViewComponentSelectorTest).GetNestedTypes(BindingFlags.NonPublic);
+ }
+
+ public Type[] AllowedTypes { get; private set; }
+
+ protected override bool IsViewComponentType([NotNull] TypeInfo typeInfo)
+ {
+ return AllowedTypes.Contains(typeInfo.AsType());
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewComponentTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewComponentTests.cs
index dcd94a11fe..1080a64b02 100644
--- a/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewComponentTests.cs
+++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewComponentTests.cs
@@ -64,5 +64,35 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
// Assert
Assert.Equal("10", body.Trim());
}
+
+ [Theory]
+ [InlineData("ViewComponentWebSite.Namespace1.SameName")]
+ [InlineData("ViewComponentWebSite.Namespace2.SameName")]
+ public async Task ViewComponents_FullName(string name)
+ {
+ var server = TestServer.Create(_provider, _app);
+ var client = server.CreateClient();
+
+ // Act
+ var body = await client.GetStringAsync("http://localhost/FullName/Invoke?name=" + name);
+
+ // Assert
+ Assert.Equal(name, body.Trim());
+ }
+
+ [Fact]
+ public async Task ViewComponents_ShortNameUsedForViewLookup()
+ {
+ var server = TestServer.Create(_provider, _app);
+ var client = server.CreateClient();
+
+ var name = "ViewComponentWebSite.Integer";
+
+ // Act
+ var body = await client.GetStringAsync("http://localhost/FullName/Invoke?name=" + name);
+
+ // Assert
+ Assert.Equal("17", body.Trim());
+ }
}
}
diff --git a/test/WebSites/ViewComponentWebSite/FullNameController.cs b/test/WebSites/ViewComponentWebSite/FullNameController.cs
new file mode 100644
index 0000000000..b78173b624
--- /dev/null
+++ b/test/WebSites/ViewComponentWebSite/FullNameController.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using Microsoft.AspNet.Mvc;
+
+namespace ViewComponentWebSite
+{
+ public class FullNameController : Controller
+ {
+ public IActionResult Invoke(string name)
+ {
+ ViewBag.Name = name;
+ return View();
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/WebSites/ViewComponentWebSite/IntegerViewComponent.cs b/test/WebSites/ViewComponentWebSite/IntegerViewComponent.cs
index d44ffb4349..b08786884c 100644
--- a/test/WebSites/ViewComponentWebSite/IntegerViewComponent.cs
+++ b/test/WebSites/ViewComponentWebSite/IntegerViewComponent.cs
@@ -7,6 +7,11 @@ namespace ViewComponentWebSite
{
public class IntegerViewComponent : ViewComponent
{
+ public IViewComponentResult Invoke()
+ {
+ return Invoke(17);
+ }
+
public IViewComponentResult Invoke(int valueFromView)
{
return View(valueFromView);
diff --git a/test/WebSites/ViewComponentWebSite/Namespace1/SameNameViewComponent.cs b/test/WebSites/ViewComponentWebSite/Namespace1/SameNameViewComponent.cs
new file mode 100644
index 0000000000..c044fa5981
--- /dev/null
+++ b/test/WebSites/ViewComponentWebSite/Namespace1/SameNameViewComponent.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using Microsoft.AspNet.Mvc;
+
+namespace ViewComponentWebSite.Namespace1
+{
+ // The full name is different here from the other view component with the same short name.
+ public class SameNameViewComponent : ViewComponent
+ {
+ public string Invoke()
+ {
+ return "ViewComponentWebSite.Namespace1.SameName";
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/WebSites/ViewComponentWebSite/Namespace2/SameNameViewComponent.cs b/test/WebSites/ViewComponentWebSite/Namespace2/SameNameViewComponent.cs
new file mode 100644
index 0000000000..1d40437390
--- /dev/null
+++ b/test/WebSites/ViewComponentWebSite/Namespace2/SameNameViewComponent.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using Microsoft.AspNet.Mvc;
+
+namespace ViewComponentWebSite.Namespace2
+{
+ // The full name is different here from the other view component with the same short name.
+ public class SameNameViewComponent : ViewComponent
+ {
+ public string Invoke()
+ {
+ return "ViewComponentWebSite.Namespace2.SameName";
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/WebSites/ViewComponentWebSite/Views/FullName/Invoke.cshtml b/test/WebSites/ViewComponentWebSite/Views/FullName/Invoke.cshtml
new file mode 100644
index 0000000000..f87bef07aa
--- /dev/null
+++ b/test/WebSites/ViewComponentWebSite/Views/FullName/Invoke.cshtml
@@ -0,0 +1 @@
+@Component.Invoke(ViewBag.Name)
\ No newline at end of file