From cf30bb730fdcbd8df51dad26a920ba8f4ecc757a Mon Sep 17 00:00:00 2001 From: Doug Bunting Date: Sun, 15 Nov 2015 22:42:07 -0800 Subject: [PATCH] PR comments and some smallish cleanup - `IRazorViewEngine.MakePathAbsolute()` -> `GetAbsolutePath()` - set `IsPartial` on all `IRazorPage` instances - improve consistency of methods in `HtmlHelperPartialExtensions` - a couple unnecessarily passed `htmlHelper.ViewData` - add missing tests of these extension methods - restore parameter checks in `CompositeViewEngine` - reduce `List` and remove enumerator allocations in `CompositeViewEngine` nits: - correct a few comments - use `` --- .../IRazorViewEngine.cs | 6 +- src/Microsoft.AspNet.Mvc.Razor/RazorView.cs | 2 +- .../RazorViewEngine.cs | 7 +- .../Rendering/HtmlHelperPartialExtensions.cs | 9 +- .../Rendering/ViewContext.cs | 2 +- .../ViewEngines/CompositeViewEngine.cs | 59 +++++-- .../InlineConstraintTests.cs | 2 +- .../RazorViewEngineTest.cs | 18 +-- .../RazorViewTest.cs | 20 +-- .../HtmlHelperPartialExtensionsTest.cs | 151 +++++++++++++++++- 10 files changed, 224 insertions(+), 52 deletions(-) diff --git a/src/Microsoft.AspNet.Mvc.Razor/IRazorViewEngine.cs b/src/Microsoft.AspNet.Mvc.Razor/IRazorViewEngine.cs index f2bcbf800d..0c200f95f8 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/IRazorViewEngine.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/IRazorViewEngine.cs @@ -18,7 +18,7 @@ namespace Microsoft.AspNet.Mvc.Razor /// The name of the page. /// Determines if the page being found is a partial. /// The of locating the page. - /// Page search semantics match . + /// . RazorPageResult FindPage(ActionContext context, string pageName, bool isPartial); /// @@ -29,7 +29,7 @@ namespace Microsoft.AspNet.Mvc.Razor /// The path to the page. /// Determines if the page being found is a partial. /// The of locating the page. - /// See also . + /// . RazorPageResult GetPage(string executingFilePath, string pagePath, bool isPartial); /// @@ -43,6 +43,6 @@ namespace Microsoft.AspNet.Mvc.Razor /// is a relative path. The value (unchanged) /// otherwise. /// - string MakePathAbsolute(string executingFilePath, string pagePath); + string GetAbsolutePath(string executingFilePath, string pagePath); } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs index 3923e0ea2f..c1f461e8a1 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs @@ -174,7 +174,7 @@ namespace Microsoft.AspNet.Mvc.Razor // Pass correct absolute path to next layout or the entry page if this view start set Layout to a // relative path. - layout = _viewEngine.MakePathAbsolute(viewStart.Path, viewStart.Layout); + layout = _viewEngine.GetAbsolutePath(viewStart.Path, viewStart.Layout); } } finally diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs index 11cea4997e..c3613ee667 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs @@ -276,7 +276,7 @@ namespace Microsoft.AspNet.Mvc.Razor private ViewLocationCacheResult LocatePageFromPath(string executingFilePath, string pagePath, bool isPartial) { - var applicationRelativePath = MakePathAbsolute(executingFilePath, pagePath); + var applicationRelativePath = GetAbsolutePath(executingFilePath, pagePath); var cacheKey = new ViewLocationCacheKey(applicationRelativePath, isPartial); ViewLocationCacheResult cacheResult; if (!ViewLookupCache.TryGetValue(cacheKey, out cacheResult)) @@ -350,7 +350,7 @@ namespace Microsoft.AspNet.Mvc.Razor } /// - public string MakePathAbsolute(string executingFilePath, string pagePath) + public string GetAbsolutePath(string executingFilePath, string pagePath) { if (string.IsNullOrEmpty(pagePath)) { @@ -505,11 +505,14 @@ namespace Microsoft.AspNet.Mvc.Razor } var page = result.ViewEntry.PageFactory(); + page.IsPartial = isPartial; + var viewStarts = new IRazorPage[result.ViewStartEntries.Count]; for (var i = 0; i < viewStarts.Length; i++) { var viewStartItem = result.ViewStartEntries[i]; viewStarts[i] = viewStartItem.PageFactory(); + viewStarts[i].IsPartial = true; } var view = new RazorView( diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/Rendering/HtmlHelperPartialExtensions.cs b/src/Microsoft.AspNet.Mvc.ViewFeatures/Rendering/HtmlHelperPartialExtensions.cs index a53893bd34..340ea3dc75 100644 --- a/src/Microsoft.AspNet.Mvc.ViewFeatures/Rendering/HtmlHelperPartialExtensions.cs +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/Rendering/HtmlHelperPartialExtensions.cs @@ -68,7 +68,7 @@ namespace Microsoft.AspNet.Mvc.Rendering throw new ArgumentNullException(nameof(partialViewName)); } - return htmlHelper.PartialAsync(partialViewName, htmlHelper.ViewData.Model, viewData: viewData); + return htmlHelper.PartialAsync(partialViewName, htmlHelper.ViewData.Model, viewData); } /// @@ -259,8 +259,7 @@ namespace Microsoft.AspNet.Mvc.Rendering throw new ArgumentNullException(nameof(partialViewName)); } - return htmlHelper.RenderPartialAsync(partialViewName, htmlHelper.ViewData.Model, - viewData: htmlHelper.ViewData); + return htmlHelper.RenderPartialAsync(partialViewName, htmlHelper.ViewData.Model, viewData: null); } /// @@ -290,7 +289,7 @@ namespace Microsoft.AspNet.Mvc.Rendering throw new ArgumentNullException(nameof(partialViewName)); } - return htmlHelper.RenderPartialAsync(partialViewName, htmlHelper.ViewData.Model, viewData: viewData); + return htmlHelper.RenderPartialAsync(partialViewName, htmlHelper.ViewData.Model, viewData); } /// @@ -320,7 +319,7 @@ namespace Microsoft.AspNet.Mvc.Rendering throw new ArgumentNullException(nameof(partialViewName)); } - return htmlHelper.RenderPartialAsync(partialViewName, model, htmlHelper.ViewData); + return htmlHelper.RenderPartialAsync(partialViewName, model, viewData: null); } } } diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/Rendering/ViewContext.cs b/src/Microsoft.AspNet.Mvc.ViewFeatures/Rendering/ViewContext.cs index ec3569eed4..a1f16306d8 100644 --- a/src/Microsoft.AspNet.Mvc.ViewFeatures/Rendering/ViewContext.cs +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/Rendering/ViewContext.cs @@ -14,7 +14,7 @@ namespace Microsoft.AspNet.Mvc.Rendering /// public class ViewContext : ActionContext { - // We need a default FormContext if the user uses html
instead of an MvcForm + // We need a default FormContext if the user uses HTML instead of an MvcForm private readonly FormContext _defaultFormContext = new FormContext(); private FormContext _formContext; diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewEngines/CompositeViewEngine.cs b/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewEngines/CompositeViewEngine.cs index 28de971413..2e1b0ed122 100644 --- a/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewEngines/CompositeViewEngine.cs +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewEngines/CompositeViewEngine.cs @@ -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.ViewFeatures; using Microsoft.Extensions.OptionsModel; namespace Microsoft.AspNet.Mvc.ViewEngines @@ -10,8 +12,6 @@ namespace Microsoft.AspNet.Mvc.ViewEngines /// public class CompositeViewEngine : ICompositeViewEngine { - private const string ViewExtension = ".cshtml"; - /// /// Initializes a new instance of . /// @@ -27,10 +27,22 @@ namespace Microsoft.AspNet.Mvc.ViewEngines /// public ViewEngineResult FindView(ActionContext context, string viewName, bool isPartial) { - List searchedLocations = null; - foreach (var engine in ViewEngines) + if (context == null) { - var result = engine.FindView(context, viewName, isPartial); + throw new ArgumentNullException(nameof(context)); + } + + if (string.IsNullOrEmpty(viewName)) + { + throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(viewName)); + } + + // Do not allocate in the common cases: ViewEngines contains one entry or initial attempt is successful. + IEnumerable searchedLocations = null; + List searchedList = null; + for (var index = 0; index < ViewEngines.Count; index++) + { + var result = ViewEngines[index].FindView(context, viewName, isPartial); if (result.Success) { return result; @@ -38,11 +50,19 @@ namespace Microsoft.AspNet.Mvc.ViewEngines if (searchedLocations == null) { - searchedLocations = new List(result.SearchedLocations); + // First failure. + searchedLocations = result.SearchedLocations; } else { - searchedLocations.AddRange(result.SearchedLocations); + if (searchedList == null) + { + // Second failure. + searchedList = new List(searchedLocations); + searchedLocations = searchedList; + } + + searchedList.AddRange(result.SearchedLocations); } } @@ -52,10 +72,17 @@ namespace Microsoft.AspNet.Mvc.ViewEngines /// public ViewEngineResult GetView(string executingFilePath, string viewPath, bool isPartial) { - List searchedLocations = null; - foreach (var engine in ViewEngines) + if (string.IsNullOrEmpty(viewPath)) { - var result = engine.GetView(executingFilePath, viewPath, isPartial); + throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(viewPath)); + } + + // Do not allocate in the common cases: ViewEngines contains one entry or initial attempt is successful. + IEnumerable searchedLocations = null; + List searchedList = null; + for (var index = 0; index < ViewEngines.Count; index++) + { + var result = ViewEngines[index].GetView(executingFilePath, viewPath, isPartial); if (result.Success) { return result; @@ -63,11 +90,19 @@ namespace Microsoft.AspNet.Mvc.ViewEngines if (searchedLocations == null) { - searchedLocations = new List(result.SearchedLocations); + // First failure. + searchedLocations = result.SearchedLocations; } else { - searchedLocations.AddRange(result.SearchedLocations); + if (searchedList == null) + { + // Second failure. + searchedList = new List(searchedLocations); + searchedLocations = searchedList; + } + + searchedList.AddRange(result.SearchedLocations); } } diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/InlineConstraintTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/InlineConstraintTests.cs index 94037b6897..233ef31339 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/InlineConstraintTests.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/InlineConstraintTests.cs @@ -46,7 +46,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests " The following locations were searched:" + PlatformNormalizer.GetNewLinesAsUnderscores(1) + "/Areas/Users/Views/Home/Index.cshtml" + PlatformNormalizer.GetNewLinesAsUnderscores(1) + "/Areas/Users/Views/Shared/Index.cshtml" + PlatformNormalizer.GetNewLinesAsUnderscores(1) + - "/Views/Shared/Index.cshtml.", + "/Views/Shared/Index.cshtml", exception.ExceptionMessage); } diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineTest.cs index 453a11628d..a33fbdca79 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineTest.cs @@ -1306,13 +1306,13 @@ namespace Microsoft.AspNet.Mvc.Razor.Test [InlineData("/Home/Index.cshtml", "Page")] [InlineData("/Home/Index.cshtml", "Folder/Page")] [InlineData("/Home/Index.cshtml", "Folder1/Folder2/Page")] - public void MakePathAbsolute_ReturnsPagePathUnchanged_IfNotAPath(string executingFilePath, string pagePath) + public void GetAbsolutePath_ReturnsPagePathUnchanged_IfNotAPath(string executingFilePath, string pagePath) { // Arrange var viewEngine = CreateViewEngine(); // Act - var result = viewEngine.MakePathAbsolute(executingFilePath, pagePath); + var result = viewEngine.GetAbsolutePath(executingFilePath, pagePath); // Assert Assert.Same(pagePath, result); @@ -1325,13 +1325,13 @@ namespace Microsoft.AspNet.Mvc.Razor.Test [InlineData("/Home/Index.cshtml", "~/Page")] [InlineData("/Home/Index.cshtml", "/Folder/Page.cshtml")] [InlineData("/Home/Index.cshtml", "~/Folder1/Folder2/Page.rzr")] - public void MakePathAbsolute_ReturnsPageUnchanged_IfAppRelative(string executingFilePath, string pagePath) + public void GetAbsolutePath_ReturnsPageUnchanged_IfAppRelative(string executingFilePath, string pagePath) { // Arrange var viewEngine = CreateViewEngine(); // Act - var result = viewEngine.MakePathAbsolute(executingFilePath, pagePath); + var result = viewEngine.GetAbsolutePath(executingFilePath, pagePath); // Assert Assert.Same(pagePath, result); @@ -1341,14 +1341,14 @@ namespace Microsoft.AspNet.Mvc.Razor.Test [InlineData("Page.cshtml")] [InlineData("Folder/Page.cshtml")] [InlineData("../../Folder1/Folder2/Page.cshtml")] - public void MakePathAbsolute_ResolvesRelativeToExecutingPage(string pagePath) + public void GetAbsolutePath_ResolvesRelativeToExecutingPage(string pagePath) { // Arrange var expectedPagePath = "/Home/" + pagePath; var viewEngine = CreateViewEngine(); // Act - var result = viewEngine.MakePathAbsolute("/Home/Page.cshtml", pagePath); + var result = viewEngine.GetAbsolutePath("/Home/Page.cshtml", pagePath); // Assert Assert.Equal(expectedPagePath, result); @@ -1358,14 +1358,14 @@ namespace Microsoft.AspNet.Mvc.Razor.Test [InlineData("Page.cshtml")] [InlineData("Folder/Page.cshtml")] [InlineData("../../Folder1/Folder2/Page.cshtml")] - public void MakePathAbsolute_ResolvesRelativeToAppRoot_IfNoPageExecuting(string pagePath) + public void GetAbsolutePath_ResolvesRelativeToAppRoot_IfNoPageExecuting(string pagePath) { // Arrange var expectedPagePath = "/" + pagePath; var viewEngine = CreateViewEngine(); // Act - var result = viewEngine.MakePathAbsolute(executingFilePath: null, pagePath: pagePath); + var result = viewEngine.GetAbsolutePath(executingFilePath: null, pagePath: pagePath); // Assert Assert.Equal(expectedPagePath, result); @@ -1649,7 +1649,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test } } - // Return RazorViewEngine with factories that always successfully create instances. + // Return RazorViewEngine with a page factory provider that is always successful. private RazorViewEngine CreateSuccessfulViewEngine() { var pageFactory = new Mock(MockBehavior.Strict); diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs index 6bee8e5b34..e301d71e8e 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs @@ -135,7 +135,7 @@ namespace Microsoft.AspNet.Mvc.Razor var activator = Mock.Of(); var viewEngine = new Mock(); viewEngine - .Setup(p => p.MakePathAbsolute("_ViewStart", LayoutPath)) + .Setup(p => p.GetAbsolutePath("_ViewStart", LayoutPath)) .Returns(LayoutPath); viewEngine .Setup(v => v.GetPage(pagePath, LayoutPath, /*isPartial*/ true)) @@ -345,10 +345,10 @@ namespace Microsoft.AspNet.Mvc.Razor var viewEngine = new Mock(MockBehavior.Strict); viewEngine - .Setup(engine => engine.MakePathAbsolute(/*executingFilePath*/ null, "/fake-layout-path")) + .Setup(engine => engine.GetAbsolutePath(/*executingFilePath*/ null, "/fake-layout-path")) .Returns("/fake-layout-path"); viewEngine - .Setup(engine => engine.MakePathAbsolute(/*executingFilePath*/ null, layoutPath)) + .Setup(engine => engine.GetAbsolutePath(/*executingFilePath*/ null, layoutPath)) .Returns(layoutPath); var view = new RazorView( @@ -1588,10 +1588,10 @@ namespace Microsoft.AspNet.Mvc.Razor }); var viewEngine = new Mock(MockBehavior.Strict); viewEngine - .Setup(engine => engine.MakePathAbsolute(/*executingFilePath*/ null, expectedViewStart)) + .Setup(engine => engine.GetAbsolutePath(/*executingFilePath*/ null, expectedViewStart)) .Returns(expectedViewStart); viewEngine - .Setup(engine => engine.MakePathAbsolute(/*executingFilePath*/ null, expectedPage)) + .Setup(engine => engine.GetAbsolutePath(/*executingFilePath*/ null, expectedPage)) .Returns(expectedPage); var view = new RazorView( @@ -1646,10 +1646,10 @@ namespace Microsoft.AspNet.Mvc.Razor var viewEngine = new Mock(MockBehavior.Strict); viewEngine - .Setup(engine => engine.MakePathAbsolute("~/_ViewStart.cshtml", "_Layout.cshtml")) + .Setup(engine => engine.GetAbsolutePath("~/_ViewStart.cshtml", "_Layout.cshtml")) .Returns("~/_Layout.cshtml"); viewEngine - .Setup(engine => engine.MakePathAbsolute("~/Home/_ViewStart.cshtml", "_Layout.cshtml")) + .Setup(engine => engine.GetAbsolutePath("~/Home/_ViewStart.cshtml", "_Layout.cshtml")) .Returns("~/Home/_Layout.cshtml"); var view = new RazorView( @@ -1691,10 +1691,10 @@ namespace Microsoft.AspNet.Mvc.Razor }); var viewEngine = new Mock(MockBehavior.Strict); viewEngine - .Setup(engine => engine.MakePathAbsolute(/*executingFilePath*/ null, "Layout")) + .Setup(engine => engine.GetAbsolutePath(/*executingFilePath*/ null, "Layout")) .Returns("Layout"); viewEngine - .Setup(engine => engine.MakePathAbsolute(/*executingFilePath*/ null, /*pagePath*/ null)) + .Setup(engine => engine.GetAbsolutePath(/*executingFilePath*/ null, /*pagePath*/ null)) .Returns(null); var view = new RazorView( @@ -1737,7 +1737,7 @@ namespace Microsoft.AspNet.Mvc.Razor }); var viewEngine = new Mock(MockBehavior.Strict); viewEngine - .Setup(p => p.MakePathAbsolute(/*executingFilePath*/ null, "/Layout.cshtml")) + .Setup(p => p.GetAbsolutePath(/*executingFilePath*/ null, "/Layout.cshtml")) .Returns("/Layout.cshtml"); viewEngine .Setup(p => p.GetPage(/*executingFilePath*/ null, "/Layout.cshtml", /*isPartial*/ true)) diff --git a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/Rendering/HtmlHelperPartialExtensionsTest.cs b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/Rendering/HtmlHelperPartialExtensionsTest.cs index 49566f10cd..a008de9be8 100644 --- a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/Rendering/HtmlHelperPartialExtensionsTest.cs +++ b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/Rendering/HtmlHelperPartialExtensionsTest.cs @@ -17,24 +17,62 @@ namespace Microsoft.AspNet.Mvc.Rendering { public class HtmlHelperPartialExtensionsTest { - public static TheoryData> PartialExtensionMethods + // Func, expected Model, expected ViewDataDictionary + public static TheoryData, object, ViewDataDictionary> PartialExtensionMethods { get { - var vdd = new ViewDataDictionary(new EmptyModelMetadataProvider()); - return new TheoryData> + var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider()); + var model = new object(); + return new TheoryData, object, ViewDataDictionary> { - helper => helper.Partial("test"), - helper => helper.Partial("test", new object()), - helper => helper.Partial("test", vdd), - helper => helper.Partial("test", new object(), vdd) + { helper => helper.Partial("test"), null, null }, + { helper => helper.Partial("test", model), model, null }, + { helper => helper.Partial("test", viewData), null, viewData }, + { helper => helper.Partial("test", model, viewData), model, viewData }, }; } } [Theory] [MemberData(nameof(PartialExtensionMethods))] - public void PartialMethods_DoesNotWrapThrownException(Func partialMethod) + public void PartialMethods_CallHtmlHelperWithExpectedArguments( + Func partialMethod, + object expectedModel, + ViewDataDictionary expectedViewData) + { + // Arrange + var htmlContent = Mock.Of(); + var helper = new Mock(MockBehavior.Strict); + if (expectedModel == null) + { + // Extension methods without model parameter use ViewData.Model to get Model. + var viewData = expectedViewData ?? new ViewDataDictionary(new EmptyModelMetadataProvider()); + helper + .SetupGet(h => h.ViewData) + .Returns(viewData) + .Verifiable(); + } + + helper + .Setup(h => h.PartialAsync("test", expectedModel, expectedViewData)) + .Returns(Task.FromResult(htmlContent)) + .Verifiable(); + + // Act + var result = partialMethod(helper.Object); + + // Assert + Assert.Same(htmlContent, result); + helper.VerifyAll(); + } + + [Theory] + [MemberData(nameof(PartialExtensionMethods))] + public void PartialMethods_DoesNotWrapThrownException( + Func partialMethod, + object unusedModel, + ViewDataDictionary unusedViewData) { // Arrange var expected = new InvalidOperationException(); @@ -54,6 +92,103 @@ namespace Microsoft.AspNet.Mvc.Rendering Assert.Same(expected, actual); } + // Func, expected Model, expected ViewDataDictionary + public static TheoryData>, object, ViewDataDictionary> PartialAsyncExtensionMethods + { + get + { + var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider()); + var model = new object(); + return new TheoryData>, object, ViewDataDictionary> + { + { helper => helper.PartialAsync("test"), null, null }, + { helper => helper.PartialAsync("test", model), model, null }, + { helper => helper.PartialAsync("test", viewData), null, viewData }, + }; + } + } + + [Theory] + [MemberData(nameof(PartialAsyncExtensionMethods))] + public async Task PartialAsyncMethods_CallHtmlHelperWithExpectedArguments( + Func> partialAsyncMethod, + object expectedModel, + ViewDataDictionary expectedViewData) + { + // Arrange + var htmlContent = Mock.Of(); + var helper = new Mock(MockBehavior.Strict); + if (expectedModel == null) + { + // Extension methods without model parameter use ViewData.Model to get Model. + var viewData = expectedViewData ?? new ViewDataDictionary(new EmptyModelMetadataProvider()); + helper + .SetupGet(h => h.ViewData) + .Returns(viewData) + .Verifiable(); + } + + helper + .Setup(h => h.PartialAsync("test", expectedModel, expectedViewData)) + .Returns(Task.FromResult(htmlContent)) + .Verifiable(); + + // Act + var result = await partialAsyncMethod(helper.Object); + + // Assert + Assert.Same(htmlContent, result); + helper.VerifyAll(); + } + + // Func, expected Model, expected ViewDataDictionary + public static TheoryData, object, ViewDataDictionary> RenderPartialAsyncExtensionMethods + { + get + { + var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider()); + var model = new object(); + return new TheoryData, object, ViewDataDictionary> + { + { helper => helper.RenderPartialAsync("test"), null, null }, + { helper => helper.RenderPartialAsync("test", model), model, null }, + { helper => helper.RenderPartialAsync("test", viewData), null, viewData }, + }; + } + } + + [Theory] + [MemberData(nameof(RenderPartialAsyncExtensionMethods))] + public async Task RenderPartialAsyncMethods_CallHtmlHelperWithExpectedArguments( + Func renderPartialAsyncMethod, + object expectedModel, + ViewDataDictionary expectedViewData) + { + // Arrange + var htmlContent = Mock.Of(); + var helper = new Mock(MockBehavior.Strict); + if (expectedModel == null) + { + // Extension methods without model parameter use ViewData.Model to get Model. + var viewData = expectedViewData ?? new ViewDataDictionary(new EmptyModelMetadataProvider()); + helper + .SetupGet(h => h.ViewData) + .Returns(viewData) + .Verifiable(); + } + + helper + .Setup(h => h.RenderPartialAsync("test", expectedModel, expectedViewData)) + .Returns(Task.FromResult(true)) + .Verifiable(); + + // Act + await renderPartialAsyncMethod(helper.Object); + + // Assert + helper.VerifyAll(); + } + [Fact] public void Partial_InvokesPartialAsyncWithCurrentModel() {