diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/PageBase.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/PageBase.cs index b23063eeb0..3dda563ebb 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/PageBase.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/PageBase.cs @@ -1214,6 +1214,33 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages public virtual UnauthorizedResult Unauthorized() => new UnauthorizedResult(); + /// + /// Creates a by specifying the name of a partial to render. + /// + /// The partial name. + /// The created object for the response. + public virtual PartialViewResult Partial(string viewName) + { + return Partial(viewName, model: null); + } + + /// + /// Creates a by specifying the name of a partial to render and the model object. + /// + /// The partial name. + /// The model to be passed into the partial. + /// The created object for the response. + public virtual PartialViewResult Partial(string viewName, object model) + { + ViewContext.ViewData.Model = model; + + return new PartialViewResult + { + ViewName = viewName, + ViewData = ViewContext.ViewData + }; + } + #region ViewComponentResult /// /// Creates a by specifying the name of a view component to render. diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/PageModel.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/PageModel.cs index 526d0fad72..17384ca1ce 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/PageModel.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/PageModel.cs @@ -1614,6 +1614,33 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages public virtual UnauthorizedResult Unauthorized() => new UnauthorizedResult(); + /// + /// Creates a by specifying the name of a partial to render. + /// + /// The partial name. + /// The created object for the response. + public virtual PartialViewResult Partial(string viewName) + { + return Partial(viewName, model: null); + } + + /// + /// Creates a by specifying the name of a partial to render and the model object. + /// + /// The partial name. + /// The model to be passed into the partial. + /// The created object for the response. + public virtual PartialViewResult Partial(string viewName, object model) + { + ViewData.Model = model; + + return new PartialViewResult + { + ViewName = viewName, + ViewData = ViewData + }; + } + #region ViewComponentResult /// /// Creates a by specifying the name of a view component to render. diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/AntiforgeryTestHelper.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/AntiforgeryTestHelper.cs index 43bd304d14..dd3c01c170 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/AntiforgeryTestHelper.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/AntiforgeryTestHelper.cs @@ -1,10 +1,8 @@ // 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.Linq; using System.Net.Http; -using AngleSharp.Dom.Html; using AngleSharp.Parser.Html; namespace Microsoft.AspNetCore.Mvc.FunctionalTests @@ -19,27 +17,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests var parser = new HtmlParser(); var htmlDocument = parser.Parse(htmlContent); - return RetrieveAntiforgeryToken(htmlDocument); - } - - public static string RetrieveAntiforgeryToken(IHtmlDocument htmlDocument) - { - var hiddenInputs = htmlDocument.QuerySelectorAll("form input[type=hidden]"); - foreach (var input in hiddenInputs) - { - if (!input.HasAttribute("name")) - { - continue; - } - - var name = input.GetAttribute("name"); - if (name == "__RequestVerificationToken" || name == "HtmlEncode[[__RequestVerificationToken]]") - { - return input.GetAttribute("value"); - } - } - - throw new Exception($"Antiforgery token could not be located in {htmlDocument.Source.Text}."); + return htmlDocument.RetrieveAntiforgeryToken(); } public static CookieMetadata RetrieveAntiforgeryCookie(HttpResponseMessage response) diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/HtmlGenerationTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/HtmlGenerationTest.cs index f865f1bcd4..e0ce243842 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/HtmlGenerationTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/HtmlGenerationTest.cs @@ -566,7 +566,7 @@ Products: Music Systems, Televisions (3)"; var document = await Client.GetHtmlDocumentAsync(url); // Assert - var banner = QuerySelector(document, ".banner"); + var banner = document.RequiredQuerySelector(".banner"); Assert.Equal("Some status message", banner.TextContent); } @@ -581,21 +581,10 @@ Products: Music Systems, Televisions (3)"; var document = await Client.GetHtmlDocumentAsync(url); // Assert - var banner = QuerySelector(document, ".banner"); + var banner = document.RequiredQuerySelector(".banner"); Assert.Empty(banner.TextContent); } - private static IElement QuerySelector(IHtmlDocument document, string selector) - { - var element = document.QuerySelector(selector); - if (element == null) - { - throw new ArgumentException($"Document does not contain element that matches the selector {selector}: " + Environment.NewLine + document.DocumentElement.OuterHtml); - } - - return element; - } - private static HttpRequestMessage RequestWithLocale(string url, string locale) { var request = new HttpRequestMessage(HttpMethod.Get, url); diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Infrastructure/IHtmlDocumentExtensions.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Infrastructure/IHtmlDocumentExtensions.cs new file mode 100644 index 0000000000..b23ccfa657 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Infrastructure/IHtmlDocumentExtensions.cs @@ -0,0 +1,43 @@ +// 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 AngleSharp.Dom; +using AngleSharp.Dom.Html; + +namespace Microsoft.AspNetCore.Mvc.FunctionalTests +{ + public static class IHtmlDocumentExtensions + { + public static IElement RequiredQuerySelector(this IHtmlDocument document, string selector) + { + var element = document.QuerySelector(selector); + if (element == null) + { + throw new ArgumentException($"Document does not contain element that matches the selector {selector}: " + Environment.NewLine + document.DocumentElement.OuterHtml); + } + + return element; + } + + public static string RetrieveAntiforgeryToken(this IHtmlDocument htmlDocument) + { + var hiddenInputs = htmlDocument.QuerySelectorAll("form input[type=hidden]"); + foreach (var input in hiddenInputs) + { + if (!input.HasAttribute("name")) + { + continue; + } + + var name = input.GetAttribute("name"); + if (name == "__RequestVerificationToken" || name == "HtmlEncode[[__RequestVerificationToken]]") + { + return input.GetAttribute("value"); + } + } + + throw new Exception($"Antiforgery token could not be located in {htmlDocument.Source.Text}."); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs index d97e128727..88be27634f 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs @@ -11,7 +11,6 @@ using System.Net.Http.Headers; using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc.Authorization; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Testing; using Newtonsoft.Json.Linq; @@ -144,6 +143,26 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests Assert.Equal("CustomActionResult", content); } + [Fact] + public async Task Page_Handler_ReturnPartialWithoutModel() + { + // Act + var document = await Client.GetHtmlDocumentAsync("RenderPartialWithoutModel"); + + var element = document.RequiredQuerySelector("#content"); + Assert.Equal("Welcome, Guest", element.TextContent); + } + + [Fact] + public async Task Page_Handler_ReturnPartialWithModel() + { + // Act + var document = await Client.GetHtmlDocumentAsync("RenderPartialWithModel"); + + var element = document.RequiredQuerySelector("#content"); + Assert.Equal("Welcome, Admin", element.TextContent); + } + [Fact] public async Task Page_Handler_AsyncReturnTypeImplementsIActionResult() { diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/PageModelTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/PageModelTest.cs index b3443d36c1..3decf16891 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/PageModelTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/PageModelTest.cs @@ -1894,6 +1894,52 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages testPageModel.Verify(); } + [Fact] + public void PartialView_WithName() + { + // Arrange + var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary()); + var pageModel = new TestPageModel + { + PageContext = new PageContext + { + ViewData = viewData + } + }; + + // Act + var result = pageModel.Partial("LoginStatus"); + + // Assert + Assert.NotNull(result); + Assert.Equal("LoginStatus", result.ViewName); + Assert.Same(viewData, result.ViewData); + } + + [Fact] + public void PartialView_WithNameAndModel() + { + // Arrange + var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary()); + var pageModel = new TestPageModel + { + PageContext = new PageContext + { + ViewData = viewData + } + }; + var model = new { Username = "Admin" }; + + // Act + var result = pageModel.Partial("LoginStatus", model); + + // Assert + Assert.NotNull(result); + Assert.Equal("LoginStatus", result.ViewName); + Assert.Equal(model, result.Model); + Assert.Same(viewData, result.ViewData); + } + [Fact] public void ViewComponent_WithName() { diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/PageTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/PageTest.cs index 60d9cc7884..2e4e50cf91 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/PageTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/PageTest.cs @@ -1697,6 +1697,52 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages Assert.Equal(statusCode, result.StatusCode); } + [Fact] + public void PartialView_WithName() + { + // Arrange + var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary()); + var pageModel = new TestPage + { + ViewContext = new ViewContext + { + ViewData = viewData + } + }; + + // Act + var result = pageModel.Partial("LoginStatus"); + + // Assert + Assert.NotNull(result); + Assert.Equal("LoginStatus", result.ViewName); + Assert.Same(viewData, result.ViewData); + } + + [Fact] + public void PartialView_WithNameAndModel() + { + // Arrange + var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary()); + var pageModel = new TestPage + { + ViewContext = new ViewContext + { + ViewData = viewData + } + }; + var model = new { Username = "Admin" }; + + // Act + var result = pageModel.Partial("LoginStatus", model); + + // Assert + Assert.NotNull(result); + Assert.Equal("LoginStatus", result.ViewName); + Assert.Equal(model, result.Model); + Assert.Same(viewData, result.ViewData); + } + [Fact] public void ViewComponent_WithName() { diff --git a/test/WebSites/RazorPagesWebSite/RenderPartialWithModel.cs b/test/WebSites/RazorPagesWebSite/RenderPartialWithModel.cs new file mode 100644 index 0000000000..82711aed2e --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/RenderPartialWithModel.cs @@ -0,0 +1,15 @@ +// 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 Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace RazorPagesWebSite +{ + public class RenderPartialWithModel : PageModel + { + public IActionResult OnGet() => Partial("_PartialWithModel", this); + + public string Username => "Admin"; + } +} diff --git a/test/WebSites/RazorPagesWebSite/RenderPartialWithModel.cshtml b/test/WebSites/RazorPagesWebSite/RenderPartialWithModel.cshtml new file mode 100644 index 0000000000..27d507ed75 --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/RenderPartialWithModel.cshtml @@ -0,0 +1,4 @@ +@page +@model RazorPagesWebSite.RenderPartialWithModel + +

The partial will be loaded here ...

\ No newline at end of file diff --git a/test/WebSites/RazorPagesWebSite/RenderPartialWithoutModel.cshtml b/test/WebSites/RazorPagesWebSite/RenderPartialWithoutModel.cshtml new file mode 100644 index 0000000000..b817a76e67 --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/RenderPartialWithoutModel.cshtml @@ -0,0 +1,5 @@ +@page + +@functions { + public IActionResult OnGet() => Partial("_PartialWithoutModel"); +} diff --git a/test/WebSites/RazorPagesWebSite/Views/Shared/_PartialWithModel.cshtml b/test/WebSites/RazorPagesWebSite/Views/Shared/_PartialWithModel.cshtml new file mode 100644 index 0000000000..5c8194f423 --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Views/Shared/_PartialWithModel.cshtml @@ -0,0 +1,3 @@ +@model RazorPagesWebSite.RenderPartialWithModel + +Welcome, @Model.Username \ No newline at end of file diff --git a/test/WebSites/RazorPagesWebSite/Views/Shared/_PartialWithoutModel.cshtml b/test/WebSites/RazorPagesWebSite/Views/Shared/_PartialWithoutModel.cshtml new file mode 100644 index 0000000000..40b6bbaf56 --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Views/Shared/_PartialWithoutModel.cshtml @@ -0,0 +1 @@ +Welcome, Guest \ No newline at end of file