diff --git a/build/dependencies.props b/build/dependencies.props index 8bb74f83e6..e65587c712 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,6 +3,7 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + 0.9.9 0.10.13 2.1.0-preview2-15749 2.1.0-preview2-30478 diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs index 8d6e48bb0e..35a6e75a45 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs @@ -101,6 +101,8 @@ namespace Microsoft.Extensions.DependencyInjection ServiceDescriptor.Singleton()); services.TryAddEnumerable( ServiceDescriptor.Singleton()); + services.TryAddEnumerable( + ServiceDescriptor.Singleton()); services.TryAddEnumerable( ServiceDescriptor.Singleton()); diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageResultExecutor.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageResultExecutor.cs index 4ebf07eab2..10539c0b2a 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageResultExecutor.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageResultExecutor.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure { @@ -66,6 +67,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure pageContext.ViewData.Model = result.Model; } + OnExecuting(pageContext); + var viewStarts = new IRazorPage[pageContext.ViewStartFactories.Count]; for (var i = 0; i < pageContext.ViewStartFactories.Count; i++) { @@ -83,5 +86,14 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure return ExecuteAsync(viewContext, result.ContentType, result.StatusCode); } + + private void OnExecuting(PageContext pageContext) + { + var viewDataValuesProvider = pageContext.HttpContext.Features.Get(); + if (viewDataValuesProvider != null) + { + viewDataValuesProvider.ProvideViewDataValues(pageContext.ViewData); + } + } } } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageSaveTempDataPropertyFilterFactory.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageSaveTempDataPropertyFilterFactory.cs index e33e9ff537..b50ce6b2a9 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageSaveTempDataPropertyFilterFactory.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageSaveTempDataPropertyFilterFactory.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Mvc.RazorPages.Internal; using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; using Microsoft.Extensions.DependencyInjection; diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageViewDataAttributeFilter.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageViewDataAttributeFilter.cs new file mode 100644 index 0000000000..1199f1a91e --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageViewDataAttributeFilter.cs @@ -0,0 +1,50 @@ +// 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.Collections.Generic; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; + +namespace Microsoft.AspNetCore.Mvc.RazorPages +{ + internal class PageViewDataAttributeFilter : IPageFilter, IViewDataValuesProviderFeature + { + public PageViewDataAttributeFilter(IReadOnlyList properties) + { + Properties = properties; + } + + public IReadOnlyList Properties { get; } + + public object Subject { get; set; } + + public void OnPageHandlerExecuted(PageHandlerExecutedContext context) + { + } + + public void OnPageHandlerExecuting(PageHandlerExecutingContext context) + { + Subject = context.HandlerInstance; + context.HttpContext.Features.Set(this); + } + + public void OnPageHandlerSelected(PageHandlerSelectedContext context) + { + } + + public void ProvideViewDataValues(ViewDataDictionary viewData) + { + for (var i = 0; i < Properties.Count; i++) + { + var property = Properties[i]; + var value = property.GetValue(Subject); + + if (value != null) + { + viewData[property.Key] = value; + } + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageViewDataAttributeFilterFactory.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageViewDataAttributeFilterFactory.cs new file mode 100644 index 0000000000..e75f518aec --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageViewDataAttributeFilterFactory.cs @@ -0,0 +1,28 @@ +// 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 Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; + +namespace Microsoft.AspNetCore.Mvc.RazorPages +{ + internal class PageViewDataAttributeFilterFactory : IFilterFactory + { + public PageViewDataAttributeFilterFactory(IReadOnlyList properties) + { + Properties = properties; + } + + public IReadOnlyList Properties { get; } + + // PageViewDataAttributeFilter is stateful and cannot be reused. + public bool IsReusable => false; + + public IFilterMetadata CreateInstance(IServiceProvider serviceProvider) + { + return new PageViewDataAttributeFilter(Properties); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/ViewDataAttributePageApplicationModelProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/ViewDataAttributePageApplicationModelProvider.cs new file mode 100644 index 0000000000..cdfbe11f0c --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/ViewDataAttributePageApplicationModelProvider.cs @@ -0,0 +1,42 @@ +// 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 Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.RazorPages.Internal; +using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; + +namespace Microsoft.AspNetCore.Mvc.RazorPages +{ + internal class ViewDataAttributePageApplicationModelProvider : IPageApplicationModelProvider + { + /// + /// This order ensures that runs after the . + public int Order => -1000 + 10; + + /// + public void OnProvidersExecuted(PageApplicationModelProviderContext context) + { + } + + /// + public void OnProvidersExecuting(PageApplicationModelProviderContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var handlerType = context.PageApplicationModel.HandlerType.AsType(); + + var viewDataProperties = ViewDataAttributePropertyProvider.GetViewDataProperties(handlerType); + if (viewDataProperties == null) + { + return; + } + + var filter = new PageViewDataAttributeFilterFactory(viewDataProperties); + context.PageApplicationModel.Filters.Add(filter); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs index 8cbcd02974..ebedcbc56c 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs @@ -203,6 +203,8 @@ namespace Microsoft.Extensions.DependencyInjection // services.TryAddEnumerable( ServiceDescriptor.Transient()); + services.TryAddEnumerable( + ServiceDescriptor.Transient()); services.TryAddSingleton(); diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ControllerViewDataAttributeFilter.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ControllerViewDataAttributeFilter.cs new file mode 100644 index 0000000000..f08508736d --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ControllerViewDataAttributeFilter.cs @@ -0,0 +1,45 @@ +// 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.Collections.Generic; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; + +namespace Microsoft.AspNetCore.Mvc.ViewFeatures +{ + internal class ControllerViewDataAttributeFilter : IActionFilter, IViewDataValuesProviderFeature + { + public ControllerViewDataAttributeFilter(IReadOnlyList properties) + { + Properties = properties; + } + + public object Subject { get; set; } + + public IReadOnlyList Properties { get; } + + public void OnActionExecuted(ActionExecutedContext context) + { + } + + public void OnActionExecuting(ActionExecutingContext context) + { + Subject = context.Controller; + context.HttpContext.Features.Set(this); + } + + public void ProvideViewDataValues(ViewDataDictionary viewData) + { + for (var i = 0; i < Properties.Count; i++) + { + var property = Properties[i]; + var value = property.GetValue(Subject); + + if (value != null) + { + viewData[property.Key] = value; + } + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ControllerViewDataAttributeFilterFactory.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ControllerViewDataAttributeFilterFactory.cs new file mode 100644 index 0000000000..61e9e172df --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ControllerViewDataAttributeFilterFactory.cs @@ -0,0 +1,28 @@ +// 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 Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; + +namespace Microsoft.AspNetCore.Mvc.ViewFeatures +{ + internal class ControllerViewDataAttributeFilterFactory : IFilterFactory + { + public ControllerViewDataAttributeFilterFactory(IReadOnlyList properties) + { + Properties = properties; + } + + public IReadOnlyList Properties { get; } + + // ControllerViewDataAttributeFilter is stateful and cannot be reused. + public bool IsReusable => false; + + public IFilterMetadata CreateInstance(IServiceProvider serviceProvider) + { + return new ControllerViewDataAttributeFilter(Properties); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/IViewDataValuesProviderFeature.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/IViewDataValuesProviderFeature.cs new file mode 100644 index 0000000000..50648d6ecf --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/IViewDataValuesProviderFeature.cs @@ -0,0 +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. + +namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal +{ + public interface IViewDataValuesProviderFeature + { + void ProvideViewDataValues(ViewDataDictionary viewData); + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ViewDataAttributeApplicationModelProvider.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ViewDataAttributeApplicationModelProvider.cs new file mode 100644 index 0000000000..3237558537 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ViewDataAttributeApplicationModelProvider.cs @@ -0,0 +1,45 @@ +// 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 Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.Internal; +using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; + +namespace Microsoft.AspNetCore.Mvc.ViewFeatures +{ + internal class ViewDataAttributeApplicationModelProvider : IApplicationModelProvider + { + /// + /// This order ensures that runs after the . + public int Order => -1000 + 10; + + /// + public void OnProvidersExecuted(ApplicationModelProviderContext context) + { + } + + /// + public void OnProvidersExecuting(ApplicationModelProviderContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + foreach (var controllerModel in context.Result.Controllers) + { + var controllerType = controllerModel.ControllerType.AsType(); + + var viewDataProperties = ViewDataAttributePropertyProvider.GetViewDataProperties(controllerType); + if (viewDataProperties == null) + { + continue; + } + + var filter = new ControllerViewDataAttributeFilterFactory(viewDataProperties); + controllerModel.Filters.Add(filter); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ViewDataAttributePropertyProvider.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ViewDataAttributePropertyProvider.cs new file mode 100644 index 0000000000..494a4b6c2d --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ViewDataAttributePropertyProvider.cs @@ -0,0 +1,44 @@ +// 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.Reflection; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal +{ + public static class ViewDataAttributePropertyProvider + { + public static IReadOnlyList GetViewDataProperties(Type type) + { + List results = null; + + var propertyHelpers = PropertyHelper.GetVisibleProperties(type: type); + + for (var i = 0; i < propertyHelpers.Length; i++) + { + var propertyHelper = propertyHelpers[i]; + var property = propertyHelper.Property; + var tempDataAttribute = property.GetCustomAttribute(); + if (tempDataAttribute != null) + { + if (results == null) + { + results = new List(); + } + + var key = tempDataAttribute.Key; + if (string.IsNullOrEmpty(key)) + { + key = property.Name; + } + + results.Add(new LifecycleProperty(property, key)); + } + } + + return results; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewDataAttribute.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewDataAttribute.cs new file mode 100644 index 0000000000..ca0cee6ea0 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewDataAttribute.cs @@ -0,0 +1,25 @@ +// 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 Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; + +namespace Microsoft.AspNetCore.Mvc +{ + /// + /// Properties decorated with will have their values stored in + /// and loaded from the . + /// is supported on properties of Controllers, and Razor Page handlers. + /// + [AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)] + public sealed class ViewDataAttribute : Attribute + { + /// + /// Gets or sets the key used to get or add the property from value from . + /// When unspecified, the key is the property name. + /// + public string Key { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewComponentResultExecutor.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewComponentResultExecutor.cs index 1ae46c44b0..164de35ba1 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewComponentResultExecutor.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewComponentResultExecutor.cs @@ -115,6 +115,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures writer, _htmlHelperOptions); + OnExecuting(viewContext); + // IViewComponentHelper is stateful, we want to make sure to retrieve it every time we need it. var viewComponentHelper = context.HttpContext.RequestServices.GetRequiredService(); (viewComponentHelper as IViewContextAware)?.Contextualize(viewContext); @@ -124,6 +126,15 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures } } + private void OnExecuting(ViewContext viewContext) + { + var viewDataValuesProvider = viewContext.HttpContext.Features.Get(); + if (viewDataValuesProvider != null) + { + viewDataValuesProvider.ProvideViewDataValues(viewContext.ViewData); + } + } + private Task GetViewComponentResult(IViewComponentHelper viewComponentHelper, ILogger logger, ViewComponentResult result) { if (result.ViewComponentType == null && result.ViewComponentName == null) diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewExecutor.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewExecutor.cs index cae30b038e..748a9c94b7 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewExecutor.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewExecutor.cs @@ -231,6 +231,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures response.StatusCode = statusCode.Value; } + OnExecuting(viewContext); + using (var writer = WriterFactory.CreateWriter(response.Body, resolvedContentTypeEncoding)) { var view = viewContext.View; @@ -257,5 +259,14 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures await writer.FlushAsync(); } } + + private void OnExecuting(ViewContext viewContext) + { + var viewDataValuesProvider = viewContext.HttpContext.Features.Get(); + if (viewDataValuesProvider != null) + { + viewDataValuesProvider.ProvideViewDataValues(viewContext.ViewData); + } + } } } diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewResultExecutor.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewResultExecutor.cs index 7d6b1ebc7c..af284f0908 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewResultExecutor.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewResultExecutor.cs @@ -167,7 +167,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures var view = viewEngineResult.View; using (view as IDisposable) { - await ExecuteAsync( context, view, diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicTests.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicTests.cs index 6df040c055..488cb6633a 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicTests.cs @@ -489,5 +489,36 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests // Assert Assert.Equal(expected, assemblyParts); } + + [Fact] + public async Task ViewDataProperties_AreTransferredToViews() + { + // Act + var document = await Client.GetHtmlDocumentAsync("ViewDataProperty/ViewDataPropertyToView"); + + // Assert + var message = document.QuerySelector("#message").TextContent; + Assert.Equal("Message set in action", message); + + var filterMessage = document.QuerySelector("#filter-message").TextContent; + Assert.Equal("Value set in OnActionExecuting", filterMessage); + + var title = document.QuerySelector("title").TextContent; + Assert.Equal("View Data Property Sample", title); + } + + [Fact] + public async Task ViewDataProperties_AreTransferredToViewComponents() + { + // Act + var document = await Client.GetHtmlDocumentAsync("ViewDataProperty/ViewDataPropertyToViewComponent"); + + // Assert + var message = document.QuerySelector("#message").TextContent; + Assert.Equal("Message set in action", message); + + var title = document.QuerySelector("title").TextContent; + Assert.Equal("View Data Property Sample", title); + } } } diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Infrastructure/HttpClientExtensions.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Infrastructure/HttpClientExtensions.cs new file mode 100644 index 0000000000..590e1d02a9 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Infrastructure/HttpClientExtensions.cs @@ -0,0 +1,75 @@ +// 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.Net; +using System.Net.Http; +using System.Threading.Tasks; +using AngleSharp.Dom.Html; +using AngleSharp.Parser.Html; +using Xunit; +using Xunit.Sdk; + +namespace Microsoft.AspNetCore.Mvc.FunctionalTests +{ + public static class HttpClientExtensions + { + public static async Task GetHtmlDocumentAsync(this HttpClient client, string requestUri) + { + var response = await client.GetAsync(requestUri); + await AssertStatusCodeAsync(response, HttpStatusCode.OK); + + var content = await response.Content.ReadAsStringAsync(); + var parser = new HtmlParser(); + var document = parser.Parse(content); + if (document == null) + { + throw new InvalidOperationException("Response content could not be parsed as HTML: " + Environment.NewLine + content); + } + + return document; + } + + public static async Task AssertStatusCodeAsync(this HttpResponseMessage response, HttpStatusCode expectedStatusCode) + { + if (response.StatusCode == HttpStatusCode.OK) + { + return response; + } + + string responseContent = null; + try + { + responseContent = await response.Content.ReadAsStringAsync(); + } + catch + { + // No-op + } + + throw new StatusCodeMismatchException + { + ExpectedStatusCode = HttpStatusCode.OK, + ActualStatusCode = response.StatusCode, + ResponseContent = responseContent, + }; + } + + private class StatusCodeMismatchException : XunitException + { + public HttpStatusCode ExpectedStatusCode { get; set; } + + public HttpStatusCode ActualStatusCode { get; set; } + + public string ResponseContent { get; set; } + + public override string Message + { + get + { + return $"Excepted status code 200. Actual {ActualStatusCode}. Response Content:" + Environment.NewLine + ResponseContent; + } + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj index 54e7694ffa..954ef92727 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj @@ -18,20 +18,10 @@ - + - + @@ -59,6 +49,7 @@ + diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs index 18ba7a5df6..0f896568d5 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs @@ -845,7 +845,6 @@ Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary`1[AspNetCore.InjectedPa Assert.Equal(expected, response.Headers.Location.ToString()); } - [Fact] public async Task RedirectToSelfWorks() { diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs index d465d58b02..680af29004 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs @@ -504,5 +504,53 @@ Hello from /Pages/Shared/"; // Assert Assert.Contains("This page is overriden by RazorPagesWebSite", response); } + + [Fact] + public async Task ViewDataAttributes_SetInPageModel_AreTransferedToLayout() + { + // Arrange + var document = await Client.GetHtmlDocumentAsync("/ViewData/ViewDataInPage"); + + // Assert + var description = document.QuerySelector("meta[name='description']").Attributes["content"]; + Assert.Equal("Description set in handler", description.Value); + + var keywords = document.QuerySelector("meta[name='keywords']").Attributes["content"]; + Assert.Equal("Value set in filter", keywords.Value); + + var author = document.QuerySelector("meta[name='author']").Attributes["content"]; + Assert.Equal("Property with key", author.Value); + + var title = document.QuerySelector("title").TextContent; + Assert.Equal("Title with default value", title); + } + + [Fact] + public async Task ViewDataAttributes_SetInPageWithoutModel_AreTransferedToLayout() + { + // Arrange + var document = await Client.GetHtmlDocumentAsync("/ViewData/ViewDataInPageWithoutModel"); + + // Assert + var description = document.QuerySelector("meta[name='description']").Attributes["content"]; + Assert.Equal("Description set in page handler", description.Value); + + var title = document.QuerySelector("title").TextContent; + Assert.Equal("Default value", title); + } + + [Fact] + public async Task ViewDataProperties_SetInPageModel_AreTransferredToViewComponents() + { + // Act + var document = await Client.GetHtmlDocumentAsync("ViewData/ViewDataToViewComponentPage"); + + // Assert + var message = document.QuerySelector("#message").TextContent; + Assert.Equal("Message set in handler", message); + + var title = document.QuerySelector("title").TextContent; + Assert.Equal("View Data in Pages", title); + } } } diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageViewDataAttributeFilterFactoryTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageViewDataAttributeFilterFactoryTest.cs new file mode 100644 index 0000000000..8b032e59f9 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageViewDataAttributeFilterFactoryTest.cs @@ -0,0 +1,32 @@ +// 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 Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.RazorPages +{ + public class PageViewDataAttributeFilterFactoryTest + { + [Fact] + public void CreateInstance_CreatesFilter() + { + // Arrange + var properties = new LifecycleProperty[] + { + new LifecycleProperty(), + new LifecycleProperty(), + }; + var filterFactory = new PageViewDataAttributeFilterFactory(properties); + + // Act + var result = filterFactory.CreateInstance(Mock.Of()); + + // Assert + var filter = Assert.IsType(result); + Assert.Same(properties, filter.Properties); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageViewDataAttributeFilterTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageViewDataAttributeFilterTest.cs new file mode 100644 index 0000000000..1978424d5a --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageViewDataAttributeFilterTest.cs @@ -0,0 +1,105 @@ +// 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.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; +using Microsoft.AspNetCore.Routing; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.RazorPages +{ + public class PageViewDataAttributeFilterTest + { + [Fact] + public void OnPageHandlerExecuting_AddsFeature() + { + // Arrange + var filter = new PageViewDataAttributeFilter(Array.Empty()); + var handler = new object(); + var httpContext = new DefaultHttpContext(); + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + var pageContext = new PageContext(actionContext); + var context = new PageHandlerExecutingContext(pageContext, new IFilterMetadata[0], new HandlerMethodDescriptor(), new Dictionary(), handler); + + // Act + filter.OnPageHandlerExecuting(context); + + // Assert + var feature = Assert.Single(httpContext.Features, f => f.Key == typeof(IViewDataValuesProviderFeature)); + Assert.Same(filter, feature.Value); + } + + [Fact] + public void OnPageHandlerExecuting_SetsSubject() + { + // Arrange + var filter = new PageViewDataAttributeFilter(Array.Empty()); + var handler = new object(); + var httpContext = new DefaultHttpContext(); + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + var pageContext = new PageContext(actionContext); + var context = new PageHandlerExecutingContext(pageContext, new IFilterMetadata[0], new HandlerMethodDescriptor(), new Dictionary(), handler); + + // Act + filter.OnPageHandlerExecuting(context); + + // Assert + Assert.Same(handler, filter.Subject); + } + + [Fact] + public void ProvideValues_AddsNonNullPropertyValuesToViewData() + { + // Arrange + var type = typeof(TestModel); + var properties = new[] + { + new LifecycleProperty(type.GetProperty(nameof(TestModel.Prop1)), "Prop1"), + new LifecycleProperty(type.GetProperty(nameof(TestModel.Prop2)), "Prop2"), + new LifecycleProperty(type.GetProperty(nameof(TestModel.Prop3)), "Prop3"), + }; + + var controller = new TestModel(); + var filter = new PageViewDataAttributeFilter(properties) + { + Subject = controller, + }; + var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary()); + + // Act + controller.Prop1 = "New-Value"; + filter.ProvideViewDataValues(viewData); + + // Assert + Assert.Collection( + viewData.OrderBy(kvp => kvp.Key), + kvp => + { + Assert.Equal("Prop1", kvp.Key); + Assert.Equal("New-Value", kvp.Value); + }, + kvp => + { + Assert.Equal("Prop2", kvp.Key); + Assert.Equal("Test", kvp.Value); + }); + } + + public class TestModel + { + public string Prop1 { get; set; } + + public string Prop2 => "Test"; + + public string Prop3 { get; set; } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/ViewDataAttributePageApplicationModelProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/ViewDataAttributePageApplicationModelProviderTest.cs new file mode 100644 index 0000000000..7caf17e358 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/ViewDataAttributePageApplicationModelProviderTest.cs @@ -0,0 +1,77 @@ +// 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.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +{ + public class ViewDataAttributePageApplicationModelProviderTest + { + [Fact] + public void OnProvidersExecuting_DoesNotAddFilter_IfTypeHasNoViewDataProperties() + { + // Arrange + var type = typeof(TestPageModel_NoViewDataProperties); + var provider = new ViewDataAttributePageApplicationModelProvider(); + var context = CreateProviderContext(type); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + Assert.Empty(context.PageApplicationModel.Filters); + } + + [Fact] + public void AddsViewDataPropertyFilter_ForViewDataAttributeProperties() + { + // Arrange + var type = typeof(TestPageModel_ViewDataProperties); + var provider = new ViewDataAttributePageApplicationModelProvider(); + var context = CreateProviderContext(type); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + var filter = Assert.Single(context.PageApplicationModel.Filters); + var viewDataFilter = Assert.IsType(filter); + Assert.Collection( + viewDataFilter.Properties, + property => Assert.Equal(nameof(TestPageModel_ViewDataProperties.DateTime), property.PropertyInfo.Name)); + } + + private static PageApplicationModelProviderContext CreateProviderContext(Type handlerType) + { + var descriptor = new CompiledPageActionDescriptor(); + var context = new PageApplicationModelProviderContext(descriptor, typeof(TestPage).GetTypeInfo()) + { + PageApplicationModel = new PageApplicationModel(descriptor, handlerType.GetTypeInfo(), Array.Empty()), + }; + + return context; + } + + private class TestPage : Page + { + public object Model => null; + + public override Task ExecuteAsync() => null; + } + + public class TestPageModel_NoViewDataProperties + { + public DateTime? DateTime { get; set; } + } + + public class TestPageModel_ViewDataProperties + { + [ViewData] + public DateTime? DateTime { get; set; } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs index d17a0c701f..a9fa6f7d9c 100644 --- a/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs @@ -442,6 +442,7 @@ namespace Microsoft.AspNetCore.Mvc typeof(CorsApplicationModelProvider), typeof(AuthorizationApplicationModelProvider), typeof(TempDataApplicationModelProvider), + typeof(ViewDataAttributeApplicationModelProvider), typeof(ApiBehaviorApplicationModelProvider), } }, @@ -469,6 +470,7 @@ namespace Microsoft.AspNetCore.Mvc typeof(AuthorizationPageApplicationModelProvider), typeof(DefaultPageApplicationModelProvider), typeof(TempDataFilterPageApplicationModelProvider), + typeof(ViewDataAttributePageApplicationModelProvider), typeof(ResponseCacheFilterApplicationModelProvider), } }, diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ControllerViewDataAttributeFilterFactoryTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ControllerViewDataAttributeFilterFactoryTest.cs new file mode 100644 index 0000000000..75638684ee --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ControllerViewDataAttributeFilterFactoryTest.cs @@ -0,0 +1,32 @@ +// 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 Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.ViewFeatures +{ + public class ControllerViewDataAttributeFilterFactoryTest + { + [Fact] + public void CreateInstance_CreatesFilter() + { + // Arrange + var properties = new LifecycleProperty[] + { + new LifecycleProperty(), + new LifecycleProperty(), + }; + var filterFactory = new ControllerViewDataAttributeFilterFactory(properties); + + // Act + var result = filterFactory.CreateInstance(Mock.Of()); + + // Assert + var filter = Assert.IsType(result); + Assert.Same(properties, filter.Properties); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ControllerViewDataAttributeFilterTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ControllerViewDataAttributeFilterTest.cs new file mode 100644 index 0000000000..c90f220557 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ControllerViewDataAttributeFilterTest.cs @@ -0,0 +1,101 @@ +// 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.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; +using Microsoft.AspNetCore.Routing; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.ViewFeatures +{ + public class ControllerViewDataAttributeFilterTest + { + [Fact] + public void OnActionExecuting_AddsFeature() + { + // Arrange + var filter = new ControllerViewDataAttributeFilter(Array.Empty()); + var controller = new object(); + var httpContext = new DefaultHttpContext(); + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + var context = new ActionExecutingContext(actionContext, new IFilterMetadata[0], new Dictionary(), controller); + + // Act + filter.OnActionExecuting(context); + + // Assert + var feature = Assert.Single(httpContext.Features, f => f.Key == typeof(IViewDataValuesProviderFeature)); + Assert.Same(filter, feature.Value); + } + + [Fact] + public void OnActionExecuting_SetsSubject() + { + // Arrange + var filter = new ControllerViewDataAttributeFilter(Array.Empty()); + var controller = new object(); + var httpContext = new DefaultHttpContext(); + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + var context = new ActionExecutingContext(actionContext, new IFilterMetadata[0], new Dictionary(), controller); + + // Act + filter.OnActionExecuting(context); + + // Assert + Assert.Same(controller, filter.Subject); + } + + [Fact] + public void ProvideValues_AddsNonNullPropertyValuesToViewData() + { + // Arrange + var type = typeof(TestController); + var properties = new[] + { + new LifecycleProperty(type.GetProperty(nameof(TestController.Prop1)), "Prop1"), + new LifecycleProperty(type.GetProperty(nameof(TestController.Prop2)), "Prop2"), + new LifecycleProperty(type.GetProperty(nameof(TestController.Prop3)), "Prop3"), + }; + + var controller = new TestController(); + var filter = new ControllerViewDataAttributeFilter(properties) + { + Subject = controller, + }; + var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary()); + + // Act + controller.Prop1 = "New-Value"; + filter.ProvideViewDataValues(viewData); + + // Assert + Assert.Collection( + viewData.OrderBy(kvp => kvp.Key), + kvp => + { + Assert.Equal("Prop1", kvp.Key); + Assert.Equal("New-Value", kvp.Value); + }, + kvp => + { + Assert.Equal("Prop2", kvp.Key); + Assert.Equal("Test", kvp.Value); + }); + } + + public class TestController + { + public string Prop1 { get; set; } + + public string Prop2 => "Test"; + + public string Prop3 { get; set; } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ViewDataAttributeApplicationModelProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ViewDataAttributeApplicationModelProviderTest.cs new file mode 100644 index 0000000000..0d8bf53d6d --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ViewDataAttributeApplicationModelProviderTest.cs @@ -0,0 +1,99 @@ +// 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.Reflection; +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.Internal; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal +{ + public class ViewDataAttributeApplicationModelProviderTest + { + [Fact] + public void OnProvidersExecuting_DoesNotAddFilter_IfTypeHasNoViewDataProperties() + { + // Arrange + var type = typeof(TestController_NoViewDataProperties); + var provider = new ViewDataAttributeApplicationModelProvider(); + var context = GetContext(type); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + var controller = Assert.Single(context.Result.Controllers); + Assert.Empty(controller.Filters); + } + + [Fact] + public void AddsViewDataPropertyFilter_ForViewDataAttributeProperties() + { + // Arrange + var type = typeof(TestController_NullableNonPrimitiveViewDataProperty); + var provider = new ViewDataAttributeApplicationModelProvider(); + var context = GetContext(type); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + var controller = Assert.Single(context.Result.Controllers); + Assert.IsType(Assert.Single(controller.Filters)); + } + + [Fact] + public void InitializeFilterFactory_WithExpectedPropertyHelpers_ForViewDataAttributeProperties() + { + // Arrange + var expected = typeof(TestController_OneViewDataProperty).GetProperty(nameof(TestController_OneViewDataProperty.Test2)); + var provider = new ViewDataAttributeApplicationModelProvider(); + var context = GetContext(typeof(TestController_OneViewDataProperty)); + + // Act + provider.OnProvidersExecuting(context); + var controller = context.Result.Controllers.SingleOrDefault(); + var filter = Assert.IsType(Assert.Single(controller.Filters)); + + // Assert + Assert.NotNull(filter); + var property = Assert.Single(filter.Properties); + Assert.Same(expected, property.PropertyInfo); + Assert.Equal("Test2", property.Key); + } + + private static ApplicationModelProviderContext GetContext(Type type) + { + var defaultProvider = new DefaultApplicationModelProvider( + Options.Create(new MvcOptions()), + new EmptyModelMetadataProvider()); + + var context = new ApplicationModelProviderContext(new[] { type.GetTypeInfo() }); + defaultProvider.OnProvidersExecuting(context); + return context; + } + + public class TestController_NoViewDataProperties + { + public DateTime? DateTime { get; set; } + } + + public class TestController_NullableNonPrimitiveViewDataProperty + { + [ViewData] + public DateTime? DateTime { get; set; } + } + + public class TestController_OneViewDataProperty + { + public string Test { get; set; } + + [ViewData] + public string Test2 { get; set; } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ViewDataAttributePropertyProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ViewDataAttributePropertyProviderTest.cs new file mode 100644 index 0000000000..48f0726884 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ViewDataAttributePropertyProviderTest.cs @@ -0,0 +1,102 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal +{ + public class ViewDataAttributePropertyProviderTest + { + [Fact] + public void GetViewDataProperties_ReturnsNull_IfTypeDoesNotHaveAnyViewDataProperties() + { + // Arrange + var type = typeof(TestController_NoViewDataProperties); + + // Act + var result = ViewDataAttributePropertyProvider.GetViewDataProperties(type); + + // Assert + Assert.Null(result); + } + + [Fact] + public void GetViewDataProperties_ReturnsViewDataProperties() + { + // Arrange + var type = typeof(BaseController); + + // Act + var result = ViewDataAttributePropertyProvider.GetViewDataProperties(type); + + // Assert + Assert.Collection( + result.OrderBy(p => p.Key), + property => + { + Assert.Equal(nameof(BaseController.BaseProperty), property.PropertyInfo.Name); + Assert.Equal(nameof(BaseController.BaseProperty), property.Key); + }); + } + + [Fact] + public void GetViewDataProperties_ReturnsViewDataProperties_FromBaseTypes() + { + // Arrange + var type = typeof(DerivedController); + + // Act + var result = ViewDataAttributePropertyProvider.GetViewDataProperties(type); + + // Assert + Assert.Collection( + result.OrderBy(p => p.Key), + property => Assert.Equal(nameof(BaseController.BaseProperty), property.PropertyInfo.Name), + property => Assert.Equal(nameof(DerivedController.DeriviedProperty), property.PropertyInfo.Name)); + } + + [Fact] + public void GetViewDataProperties_UsesKeyFromViewDataAttribute() + { + // Arrange + var type = typeof(PropertyWithKeyController); + + // Act + var result = ViewDataAttributePropertyProvider.GetViewDataProperties(type); + + // Assert + Assert.Collection( + result.OrderBy(p => p.Key), + property => + { + Assert.Equal(nameof(PropertyWithKeyController.Different), property.PropertyInfo.Name); + Assert.Equal("Test", property.Key); + }); + } + + public class TestController_NoViewDataProperties + { + public DateTime? DateTime { get; set; } + } + + public class BaseController + { + [ViewData] + public string BaseProperty { get; } + } + + public class DerivedController : BaseController + { + [ViewData] + public string DeriviedProperty { get; set; } + } + + public class PropertyWithKeyController + { + [ViewData(Key = "Test")] + public string Different { get; set; } + } + } +} diff --git a/test/WebSites/BasicWebSite/Components/PassThroughViewComponent.cs b/test/WebSites/BasicWebSite/Components/PassThroughViewComponent.cs index 7af257e126..e49606578f 100644 --- a/test/WebSites/BasicWebSite/Components/PassThroughViewComponent.cs +++ b/test/WebSites/BasicWebSite/Components/PassThroughViewComponent.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Mvc; -namespace HtmlGenerationWebSite.Components +namespace BasicWebSite.Components { public class PassThroughViewComponent : ViewComponent { diff --git a/test/WebSites/BasicWebSite/Components/ViewDataViewComponent.cs b/test/WebSites/BasicWebSite/Components/ViewDataViewComponent.cs new file mode 100644 index 0000000000..4e586ee1c9 --- /dev/null +++ b/test/WebSites/BasicWebSite/Components/ViewDataViewComponent.cs @@ -0,0 +1,12 @@ +// 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; + +namespace BasicWebSite.Components +{ + public class ViewDataViewComponent : ViewComponent + { + public IViewComponentResult Invoke() => View(); + } +} diff --git a/test/WebSites/BasicWebSite/Controllers/PassThroughController.cs b/test/WebSites/BasicWebSite/Controllers/PassThroughController.cs index a1359ce5a2..d1101b2f76 100644 --- a/test/WebSites/BasicWebSite/Controllers/PassThroughController.cs +++ b/test/WebSites/BasicWebSite/Controllers/PassThroughController.cs @@ -1,7 +1,7 @@ // 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 HtmlGenerationWebSite.Components; +using BasicWebSite.Components; using Microsoft.AspNetCore.Mvc; namespace BasicWebSite.Controllers diff --git a/test/WebSites/BasicWebSite/Controllers/ViewDataPropertyController.cs b/test/WebSites/BasicWebSite/Controllers/ViewDataPropertyController.cs new file mode 100644 index 0000000000..4041c8ffdb --- /dev/null +++ b/test/WebSites/BasicWebSite/Controllers/ViewDataPropertyController.cs @@ -0,0 +1,40 @@ +// 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 BasicWebSite.Components; +using BasicWebSite.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.ViewFeatures; + +namespace BasicWebSite.Controllers +{ + public class ViewDataPropertyController : Controller + { + [ViewData] + public string Title => "View Data Property Sample"; + + [ViewData] + public string Message { get; private set; } + + [ViewData] + public string FilterMessage { get; set; } + + public IActionResult ViewDataPropertyToView() + { + Message = "Message set in action"; + return View(); + } + + public IActionResult ViewDataPropertyToViewComponent() + { + Message = "Message set in action"; + return ViewComponent(typeof(ViewDataViewComponent)); + } + + public override void OnActionExecuting(ActionExecutingContext context) + { + FilterMessage = "Value set in OnActionExecuting"; + } + } +} diff --git a/test/WebSites/BasicWebSite/Views/Shared/Components/ViewData/Default.cshtml b/test/WebSites/BasicWebSite/Views/Shared/Components/ViewData/Default.cshtml new file mode 100644 index 0000000000..5144ba57cd --- /dev/null +++ b/test/WebSites/BasicWebSite/Views/Shared/Components/ViewData/Default.cshtml @@ -0,0 +1,8 @@ + + + @ViewData["Title"] + + + @ViewData["Message"] + + diff --git a/test/WebSites/BasicWebSite/Views/ViewDataProperty/ViewDataInViewComponent.cshtml b/test/WebSites/BasicWebSite/Views/ViewDataProperty/ViewDataInViewComponent.cshtml new file mode 100644 index 0000000000..7a56ec1d82 --- /dev/null +++ b/test/WebSites/BasicWebSite/Views/ViewDataProperty/ViewDataInViewComponent.cshtml @@ -0,0 +1,2 @@ +Sample that shows value of ViewDataAttribute being passed to a ViewComponent +@ViewData["Message"] diff --git a/test/WebSites/BasicWebSite/Views/ViewDataProperty/ViewDataPropertyToView.cshtml b/test/WebSites/BasicWebSite/Views/ViewDataProperty/ViewDataPropertyToView.cshtml new file mode 100644 index 0000000000..ac08b926f2 --- /dev/null +++ b/test/WebSites/BasicWebSite/Views/ViewDataProperty/ViewDataPropertyToView.cshtml @@ -0,0 +1,2 @@ +@{ Layout = "_Layout"; } +Sample that shows ViewDataAttribute being applied to a controller diff --git a/test/WebSites/BasicWebSite/Views/ViewDataProperty/_Layout.cshtml b/test/WebSites/BasicWebSite/Views/ViewDataProperty/_Layout.cshtml new file mode 100644 index 0000000000..64fd714d0f --- /dev/null +++ b/test/WebSites/BasicWebSite/Views/ViewDataProperty/_Layout.cshtml @@ -0,0 +1,10 @@ + + + @ViewData["Title"] + + + @ViewData["Message"] + @ViewData["FilterMessage"] + @RenderBody() + + diff --git a/test/WebSites/RazorPagesWebSite/Components/ViewDataViewComponent.cs b/test/WebSites/RazorPagesWebSite/Components/ViewDataViewComponent.cs new file mode 100644 index 0000000000..ad499c9e0f --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Components/ViewDataViewComponent.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; + +namespace RazorPagesWebSite.Components +{ + public class ViewDataViewComponent : ViewComponent + { + public IViewComponentResult Invoke() + { + return View(); + } + } +} diff --git a/test/WebSites/RazorPagesWebSite/Pages/Shared/Components/ViewData/Default.cshtml b/test/WebSites/RazorPagesWebSite/Pages/Shared/Components/ViewData/Default.cshtml new file mode 100644 index 0000000000..5144ba57cd --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/Shared/Components/ViewData/Default.cshtml @@ -0,0 +1,8 @@ + + + @ViewData["Title"] + + + @ViewData["Message"] + + diff --git a/test/WebSites/RazorPagesWebSite/Pages/ViewData/ViewDataInPage.cs b/test/WebSites/RazorPagesWebSite/Pages/ViewData/ViewDataInPage.cs new file mode 100644 index 0000000000..fd6eed7a37 --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/ViewData/ViewDataInPage.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace RazorPagesWebSite +{ + public class ViewDataInPage : PageModel + { + [ViewData] + public string Title => "Title with default value"; + + [ViewData] + public string Keywords { get; set; } + + [ViewData] + public string Description { get; set;} + + [ViewData(Key = "Author")] + public string AuthorName { get; set; } + + public void OnGet() + { + Description = "Description set in handler"; + AuthorName = "Property with key"; + } + + public override void OnPageHandlerExecuting(PageHandlerExecutingContext context) + { + Keywords = "Value set in filter"; + } + } +} diff --git a/test/WebSites/RazorPagesWebSite/Pages/ViewData/ViewDataInPage.cshtml b/test/WebSites/RazorPagesWebSite/Pages/ViewData/ViewDataInPage.cshtml new file mode 100644 index 0000000000..119fbf7b6e --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/ViewData/ViewDataInPage.cshtml @@ -0,0 +1,6 @@ +@page +@model ViewDataInPage +@{ + Layout = "_Layout"; +} +Sample that shows ViewData attributes being set in a PageModel. diff --git a/test/WebSites/RazorPagesWebSite/Pages/ViewData/ViewDataInPageWithoutModel.cshtml b/test/WebSites/RazorPagesWebSite/Pages/ViewData/ViewDataInPageWithoutModel.cshtml new file mode 100644 index 0000000000..b64538b461 --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/ViewData/ViewDataInPageWithoutModel.cshtml @@ -0,0 +1,18 @@ +@page +@{ + Layout = "_Layout"; +} +@functions +{ + [ViewData] + public string Title { get; set; } = "Default value"; + + [ViewData] + public string Description { get; set; } + + public void OnGet() + { + Description = "Description set in page handler"; + } +} +Sample that shows ViewData being set from a page without handler. diff --git a/test/WebSites/RazorPagesWebSite/Pages/ViewData/ViewDataToViewComponentPage.cs b/test/WebSites/RazorPagesWebSite/Pages/ViewData/ViewDataToViewComponentPage.cs new file mode 100644 index 0000000000..d38ab3b08e --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/ViewData/ViewDataToViewComponentPage.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.RazorPages; +using RazorPagesWebSite.Components; + +namespace RazorPagesWebSite +{ + public class ViewDataToViewComponentPage : PageModel + { + [ViewData] + public string Title => "View Data in Pages"; + + [ViewData] + public string Message { get; private set; } + + public IActionResult OnGet() + { + Message = "Message set in handler"; + return new ViewComponentResult + { + ViewComponentType = typeof(ViewDataViewComponent), + }; + } + } +} diff --git a/test/WebSites/RazorPagesWebSite/Pages/ViewData/ViewDataToViewComponentPage.cshtml b/test/WebSites/RazorPagesWebSite/Pages/ViewData/ViewDataToViewComponentPage.cshtml new file mode 100644 index 0000000000..82b4340edb --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/ViewData/ViewDataToViewComponentPage.cshtml @@ -0,0 +1,2 @@ +@page +@model ViewDataToViewComponentPage diff --git a/test/WebSites/RazorPagesWebSite/Pages/ViewData/_Layout.cshtml b/test/WebSites/RazorPagesWebSite/Pages/ViewData/_Layout.cshtml new file mode 100644 index 0000000000..a9a9d31edf --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/ViewData/_Layout.cshtml @@ -0,0 +1,11 @@ + + + + + + @ViewData["Title"] + + + @RenderBody() + + diff --git a/test/WebSites/RazorPagesWebSite/Pages/ViewData/_ViewImports.cshtml b/test/WebSites/RazorPagesWebSite/Pages/ViewData/_ViewImports.cshtml new file mode 100644 index 0000000000..aaf882de29 --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/ViewData/_ViewImports.cshtml @@ -0,0 +1 @@ +@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"