Skip to content
This repository was archived by the owner on Dec 14, 2018. It is now read-only.

Commit 5119d16

Browse files
committed
ViewComponent.Invoke() should be able to invoke views
Fixes #285
1 parent fbaac10 commit 5119d16

File tree

18 files changed

+318
-3
lines changed

18 files changed

+318
-3
lines changed

Mvc.sln

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "WebApiCompatShimWebSite", "
9898
EndProject
9999
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.WebApiCompatShimTest", "test\Microsoft.AspNet.Mvc.WebApiCompatShimTest\Microsoft.AspNet.Mvc.WebApiCompatShimTest.kproj", "{5DE8E4D9-AACD-4B5F-819F-F091383FB996}"
100100
EndProject
101+
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ViewComponentWebSite", "test\WebSites\ViewComponentWebSite\ViewComponentWebSite.kproj", "{24B59501-5F37-4129-96E6-F02EC34C7E2C}"
102+
EndProject
101103
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "TagHelperSample.Web", "samples\TagHelperSample.Web\TagHelperSample.Web.kproj", "{2223120F-D675-40DA-8CD8-11DC14A0B2C7}"
102104
EndProject
103105
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.TagHelpers", "src\Microsoft.AspNet.Mvc.TagHelpers\Microsoft.AspNet.Mvc.TagHelpers.kproj", "{B2347320-308E-4D2B-AEC8-005DFA68B0C9}"
@@ -524,6 +526,16 @@ Global
524526
{5DE8E4D9-AACD-4B5F-819F-F091383FB996}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
525527
{5DE8E4D9-AACD-4B5F-819F-F091383FB996}.Release|Mixed Platforms.Build.0 = Release|Any CPU
526528
{5DE8E4D9-AACD-4B5F-819F-F091383FB996}.Release|x86.ActiveCfg = Release|Any CPU
529+
{24B59501-5F37-4129-96E6-F02EC34C7E2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
530+
{24B59501-5F37-4129-96E6-F02EC34C7E2C}.Debug|Any CPU.Build.0 = Debug|Any CPU
531+
{24B59501-5F37-4129-96E6-F02EC34C7E2C}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
532+
{24B59501-5F37-4129-96E6-F02EC34C7E2C}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
533+
{24B59501-5F37-4129-96E6-F02EC34C7E2C}.Debug|x86.ActiveCfg = Debug|Any CPU
534+
{24B59501-5F37-4129-96E6-F02EC34C7E2C}.Release|Any CPU.ActiveCfg = Release|Any CPU
535+
{24B59501-5F37-4129-96E6-F02EC34C7E2C}.Release|Any CPU.Build.0 = Release|Any CPU
536+
{24B59501-5F37-4129-96E6-F02EC34C7E2C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
537+
{24B59501-5F37-4129-96E6-F02EC34C7E2C}.Release|Mixed Platforms.Build.0 = Release|Any CPU
538+
{24B59501-5F37-4129-96E6-F02EC34C7E2C}.Release|x86.ActiveCfg = Release|Any CPU
527539
{2223120F-D675-40DA-8CD8-11DC14A0B2C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
528540
{2223120F-D675-40DA-8CD8-11DC14A0B2C7}.Debug|Any CPU.Build.0 = Debug|Any CPU
529541
{2223120F-D675-40DA-8CD8-11DC14A0B2C7}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
@@ -601,6 +613,7 @@ Global
601613
{23D30B8C-04B1-4577-A604-ED27EA1E4A0E} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E}
602614
{B2B7BC91-688E-4C1E-A71F-CE948D958DDF} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
603615
{5DE8E4D9-AACD-4B5F-819F-F091383FB996} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1}
616+
{24B59501-5F37-4129-96E6-F02EC34C7E2C} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
604617
{2223120F-D675-40DA-8CD8-11DC14A0B2C7} = {DAAE4C74-D06F-4874-A166-33305D2643CE}
605618
{B2347320-308E-4D2B-AEC8-005DFA68B0C9} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E}
606619
{860119ED-3DB1-424D-8D0A-30132A8A7D96} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1}

src/Microsoft.AspNet.Mvc.Core/ViewComponents/IViewComponentResult.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,23 @@
55

66
namespace Microsoft.AspNet.Mvc
77
{
8+
/// <summary>
9+
/// Result type of a <see cref="ViewComponent"/>.
10+
/// </summary>
811
public interface IViewComponentResult
912
{
13+
/// <summary>
14+
/// Executes the result of a <see cref="ViewComponent"/> using the specified <paramref name="context"/>.
15+
/// </summary>
16+
/// <param name="context">The <see cref="ViewComponentContext"/> for the current component execution.</param>
1017
void Execute([NotNull] ViewComponentContext context);
1118

19+
/// <summary>
20+
/// Asynchronously executes the result of a <see cref="ViewComponent"/> using the specified
21+
/// <paramref name="context"/>.
22+
/// </summary>
23+
/// <param name="context">The <see cref="ViewComponentContext"/> for the current component execution.</param>
24+
/// <returns>A <see cref="Task"/> that represents the asynchronous execution.</returns>
1225
Task ExecuteAsync([NotNull] ViewComponentContext context);
1326
}
1427
}

src/Microsoft.AspNet.Mvc.Core/ViewComponents/ViewViewComponentResult.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99

1010
namespace Microsoft.AspNet.Mvc
1111
{
12+
/// <summary>
13+
/// A <see cref="IViewComponentResult"/> that renders a partial view when executed.
14+
/// </summary>
1215
public class ViewViewComponentResult : IViewComponentResult
1316
{
1417
// {0} is the component name, {1} is the view name.
@@ -27,11 +30,20 @@ public ViewViewComponentResult([NotNull] IViewEngine viewEngine, string viewName
2730

2831
public ViewDataDictionary ViewData { get; private set; }
2932

33+
/// <summary>
34+
/// Locates and renders a view specified by <paramref name="context"/>.
35+
/// </summary>
36+
/// <param name="context">The <see cref="ViewComponentContext"/> for the current component execution.</param>
37+
/// <remarks>
38+
/// This method synchronously calls and blocks on <see cref="ExecuteAsync(ViewComponentContext)"/>.
39+
/// </remarks>
3040
public void Execute([NotNull] ViewComponentContext context)
3141
{
32-
throw new NotImplementedException("There's no support for syncronous views right now.");
42+
var task = ExecuteAsync(context);
43+
TaskHelper.WaitAndThrowIfFaulted(task);
3344
}
3445

46+
/// <inheritdoc />
3547
public async Task ExecuteAsync([NotNull] ViewComponentContext context)
3648
{
3749
string qualifiedViewName;

test/Microsoft.AspNet.Mvc.Core.Test/ViewComponents/ViewViewComponentResultTest.cs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,77 @@ namespace Microsoft.AspNet.Mvc
1616
{
1717
public class ViewViewComponentResultTest
1818
{
19+
[Fact]
20+
public void Execute_RendersPartialViews()
21+
{
22+
// Arrange
23+
var view = new Mock<IView>();
24+
view.Setup(v => v.RenderAsync(It.IsAny<ViewContext>()))
25+
.Returns(Task.FromResult(result: true))
26+
.Verifiable();
27+
var viewEngine = new Mock<IViewEngine>(MockBehavior.Strict);
28+
viewEngine.Setup(e => e.FindPartialView(It.IsAny<ActionContext>(), It.IsAny<string>()))
29+
.Returns(ViewEngineResult.Found("some-view", view.Object))
30+
.Verifiable();
31+
var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider());
32+
var result = new ViewViewComponentResult(viewEngine.Object, "some-view", viewData);
33+
var viewComponentContext = GetViewComponentContext(view.Object, viewData);
34+
35+
// Act
36+
result.Execute(viewComponentContext);
37+
38+
// Assert
39+
viewEngine.Verify();
40+
view.Verify();
41+
}
42+
43+
[Fact]
44+
public void Execute_ThrowsIfPartialViewCannotBeFound()
45+
{
46+
// Arrange
47+
var expected = string.Join(Environment.NewLine,
48+
"The view 'Components/Object/some-view' was not found. The following locations were searched:",
49+
"location1",
50+
"location2.");
51+
var view = Mock.Of<IView>();
52+
var viewEngine = new Mock<IViewEngine>(MockBehavior.Strict);
53+
viewEngine.Setup(e => e.FindPartialView(It.IsAny<ActionContext>(), It.IsAny<string>()))
54+
.Returns(ViewEngineResult.NotFound("some-view", new[] { "location1", "location2" }))
55+
.Verifiable();
56+
var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider());
57+
var result = new ViewViewComponentResult(viewEngine.Object, "some-view", viewData);
58+
var viewComponentContext = GetViewComponentContext(view, viewData);
59+
60+
// Act and Assert
61+
var ex = Assert.Throws<InvalidOperationException>(() => result.Execute(viewComponentContext));
62+
Assert.Equal(expected, ex.Message);
63+
}
64+
65+
[Fact]
66+
public void Execute_DoesNotWrapThrownExceptionsInAggregateExceptions()
67+
{
68+
// Arrange
69+
var expected = new IndexOutOfRangeException();
70+
var view = new Mock<IView>();
71+
view.Setup(v => v.RenderAsync(It.IsAny<ViewContext>()))
72+
.Throws(expected)
73+
.Verifiable();
74+
var viewEngine = new Mock<IViewEngine>(MockBehavior.Strict);
75+
viewEngine.Setup(e => e.FindPartialView(It.IsAny<ActionContext>(), It.IsAny<string>()))
76+
.Returns(ViewEngineResult.Found("some-view", view.Object))
77+
.Verifiable();
78+
var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider());
79+
var result = new ViewViewComponentResult(viewEngine.Object, "some-view", viewData);
80+
var viewComponentContext = GetViewComponentContext(view.Object, viewData);
81+
82+
// Act
83+
var actual = Record.Exception(() => result.Execute(viewComponentContext));
84+
85+
// Assert
86+
Assert.Same(expected, actual);
87+
view.Verify();
88+
}
89+
1990
[Fact]
2091
public async Task ExecuteAsync_RendersPartialViews()
2192
{
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Threading.Tasks;
7+
using Microsoft.AspNet.Builder;
8+
using Microsoft.AspNet.TestHost;
9+
using ViewComponentWebSite;
10+
using Xunit;
11+
12+
namespace Microsoft.AspNet.Mvc.FunctionalTests
13+
{
14+
public class ViewComponentTests
15+
{
16+
private readonly IServiceProvider _provider = TestHelper.CreateServices("ViewComponentWebSite");
17+
private readonly Action<IApplicationBuilder> _app = new Startup().Configure;
18+
19+
public static IEnumerable<object[]> ViewViewComponents_AreRenderedCorrectlyData
20+
{
21+
get
22+
{
23+
yield return new[]
24+
{
25+
"ViewWithAsyncComponents",
26+
string.Join(Environment.NewLine,
27+
"<test-component>value-from-component value-from-view</test-component>",
28+
"ViewWithAsyncComponents InvokeAsync: hello from viewdatacomponent")
29+
};
30+
31+
yield return new[]
32+
{
33+
"ViewWithSyncComponents",
34+
string.Join(Environment.NewLine,
35+
"<test-component>value-from-component value-from-view</test-component>",
36+
"ViewWithSyncComponents Invoke: hello from viewdatacomponent")
37+
};
38+
}
39+
}
40+
41+
[Theory]
42+
[MemberData(nameof(ViewViewComponents_AreRenderedCorrectlyData))]
43+
public async Task ViewViewComponents_AreRenderedCorrectly(string actionName, string expected)
44+
{
45+
var server = TestServer.Create(_provider, _app);
46+
var client = server.CreateClient();
47+
48+
// Act
49+
var body = await client.GetStringAsync("http://localhost/Home/" + actionName);
50+
51+
// Assert
52+
Assert.Equal(expected, body.Trim());
53+
}
54+
}
55+
}

test/Microsoft.AspNet.Mvc.FunctionalTests/project.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@
2121
"RoutingWebSite": "1.0.0",
2222
"RazorWebSite": "1.0.0",
2323
"RazorInstrumentationWebsite": "1.0.0",
24+
"TagHelpersWebSite": "1.0.0",
25+
"UrlHelperWebSite": "1.0.0",
2426
"ValueProvidersSite": "1.0.0",
27+
"ViewComponentWebSite": "1.0.0",
2528
"XmlSerializerWebSite": "1.0.0",
26-
"UrlHelperWebSite": "1.0.0",
2729
"WebApiCompatShimWebSite": "1.0.0",
2830

2931
"Microsoft.AspNet.TestHost": "1.0.0-*",
@@ -34,7 +36,6 @@
3436
"Microsoft.Framework.DependencyInjection": "1.0.0-*",
3537
"Microsoft.Framework.Logging": "1.0.0-*",
3638
"Microsoft.Framework.Runtime.Interfaces": "1.0.0-*",
37-
"TagHelpersWebSite": "1.0.0",
3839
"Xunit.KRunner": "1.0.0-*"
3940
},
4041
"commands": {
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.AspNet.Mvc;
5+
6+
namespace ViewComponentWebSite
7+
{
8+
public class HomeController
9+
{
10+
public ViewResult ViewWithAsyncComponents()
11+
{
12+
return new ViewResult();
13+
}
14+
15+
public ViewResult ViewWithSyncComponents()
16+
{
17+
return new ViewResult();
18+
}
19+
}
20+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Threading.Tasks;
5+
6+
namespace ViewComponentWebSite
7+
{
8+
public class SampleModel
9+
{
10+
public string Prop1 { get; set; }
11+
12+
public string Prop2 { get; set; }
13+
14+
public Task<string> GetValueAsync()
15+
{
16+
return Task.FromResult(Prop1 + " " + Prop2);
17+
}
18+
}
19+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.AspNet.Builder;
5+
using Microsoft.Framework.DependencyInjection;
6+
7+
namespace ViewComponentWebSite
8+
{
9+
public class Startup
10+
{
11+
public void Configure(IApplicationBuilder app)
12+
{
13+
var configuration = app.GetTestConfiguration();
14+
app.UseServices(services =>
15+
{
16+
services.AddMvc(configuration);
17+
});
18+
app.UseMvc();
19+
}
20+
}
21+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.AspNet.Mvc;
5+
6+
namespace ViewComponentWebSite
7+
{
8+
public class TestViewComponent : ViewComponent
9+
{
10+
public IViewComponentResult Invoke(string valueFromView)
11+
{
12+
var model = new SampleModel
13+
{
14+
Prop1 = "value-from-component",
15+
Prop2 = valueFromView
16+
};
17+
return View(model);
18+
}
19+
}
20+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<Project ToolsVersion="__ToolsVersion__" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
3+
<PropertyGroup>
4+
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
5+
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
6+
</PropertyGroup>
7+
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.Props" Condition="'$(VSToolsPath)' != ''" />
8+
<PropertyGroup Label="Globals">
9+
<ProjectGuid>24b59501-5f37-4129-96e6-f02ec34c7e2c</ProjectGuid>
10+
</PropertyGroup>
11+
12+
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x86'" Label="Configuration">
13+
</PropertyGroup>
14+
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x86'" Label="Configuration">
15+
</PropertyGroup>
16+
<PropertyGroup>
17+
<SchemaVersion>2.0</SchemaVersion>
18+
</PropertyGroup>
19+
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
20+
</Project>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Threading.Tasks;
5+
using Microsoft.AspNet.Mvc;
6+
7+
namespace ViewComponentWebSite
8+
{
9+
[ViewComponent(Name = "ViewData")]
10+
public class ViewDataComponent : ViewComponent
11+
{
12+
public ViewViewComponentResult Invoke()
13+
{
14+
ViewData["value-from-component"] = nameof(Invoke) + ": hello from viewdatacomponent";
15+
return View("ComponentThatReadsViewData");
16+
}
17+
18+
public Task<ViewViewComponentResult> InvokeAsync()
19+
{
20+
ViewData["value-from-component"] = nameof(InvokeAsync) + ": hello from viewdatacomponent";
21+
var result = View("ComponentThatReadsViewData");
22+
return Task.FromResult(result);
23+
}
24+
}
25+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@model SampleModel
2+
<test-component>@await Model.GetValueAsync()</test-component>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@ViewData["value-from-view"] @ViewData["value-from-component"]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
@{
2+
ViewData["value-from-view"] = ViewContext.ActionDescriptor.Name;
3+
}
4+
@await Component.InvokeAsync("Test", "value-from-view")
5+
@await Component.InvokeAsync("ViewData")

0 commit comments

Comments
 (0)