Introduce ViewDataAttribute

Allow properties on controllers, Razor Page and Razor Page models annotatted with [ViewDataAttribute]
to populate ViewDataDictionary

Fixes https://github.com/aspnet/Mvc/issues/6525
This commit is contained in:
Pranav K 2018-03-22 15:45:32 -07:00
parent c515cece8e
commit 07a1907918
No known key found for this signature in database
GPG Key ID: 1963DA6D96C3057A
47 changed files with 1257 additions and 17 deletions

View File

@ -3,6 +3,7 @@
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
</PropertyGroup>
<PropertyGroup Label="Package Versions">
<AngleSharpPackageVersion>0.9.9</AngleSharpPackageVersion>
<BenchmarkDotNetPackageVersion>0.10.13</BenchmarkDotNetPackageVersion>
<InternalAspNetCoreSdkPackageVersion>2.1.0-preview2-15749</InternalAspNetCoreSdkPackageVersion>
<MicrosoftAspNetCoreAntiforgeryPackageVersion>2.1.0-preview2-30478</MicrosoftAspNetCoreAntiforgeryPackageVersion>

View File

@ -101,6 +101,8 @@ namespace Microsoft.Extensions.DependencyInjection
ServiceDescriptor.Singleton<IPageApplicationModelProvider, AuthorizationPageApplicationModelProvider>());
services.TryAddEnumerable(
ServiceDescriptor.Singleton<IPageApplicationModelProvider, TempDataFilterPageApplicationModelProvider>());
services.TryAddEnumerable(
ServiceDescriptor.Singleton<IPageApplicationModelProvider, ViewDataAttributePageApplicationModelProvider>());
services.TryAddEnumerable(
ServiceDescriptor.Singleton<IPageApplicationModelProvider, ResponseCacheFilterApplicationModelProvider>());

View File

@ -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<IViewDataValuesProviderFeature>();
if (viewDataValuesProvider != null)
{
viewDataValuesProvider.ProvideViewDataValues(pageContext.ViewData);
}
}
}
}

View File

@ -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;

View File

@ -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<LifecycleProperty> properties)
{
Properties = properties;
}
public IReadOnlyList<LifecycleProperty> Properties { get; }
public object Subject { get; set; }
public void OnPageHandlerExecuted(PageHandlerExecutedContext context)
{
}
public void OnPageHandlerExecuting(PageHandlerExecutingContext context)
{
Subject = context.HandlerInstance;
context.HttpContext.Features.Set<IViewDataValuesProviderFeature>(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;
}
}
}
}
}

View File

@ -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<LifecycleProperty> properties)
{
Properties = properties;
}
public IReadOnlyList<LifecycleProperty> Properties { get; }
// PageViewDataAttributeFilter is stateful and cannot be reused.
public bool IsReusable => false;
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
return new PageViewDataAttributeFilter(Properties);
}
}
}

View File

@ -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
{
/// <inheritdoc />
/// <remarks>This order ensures that <see cref="ViewDataAttributePageApplicationModelProvider"/> runs after the <see cref="DefaultPageApplicationModelProvider"/>.</remarks>
public int Order => -1000 + 10;
/// <inheritdoc />
public void OnProvidersExecuted(PageApplicationModelProviderContext context)
{
}
/// <inheritdoc />
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);
}
}
}

View File

@ -203,6 +203,8 @@ namespace Microsoft.Extensions.DependencyInjection
//
services.TryAddEnumerable(
ServiceDescriptor.Transient<IApplicationModelProvider, TempDataApplicationModelProvider>());
services.TryAddEnumerable(
ServiceDescriptor.Transient<IApplicationModelProvider, ViewDataAttributeApplicationModelProvider>());
services.TryAddSingleton<SaveTempDataFilter>();

View File

@ -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<LifecycleProperty> properties)
{
Properties = properties;
}
public object Subject { get; set; }
public IReadOnlyList<LifecycleProperty> Properties { get; }
public void OnActionExecuted(ActionExecutedContext context)
{
}
public void OnActionExecuting(ActionExecutingContext context)
{
Subject = context.Controller;
context.HttpContext.Features.Set<IViewDataValuesProviderFeature>(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;
}
}
}
}
}

View File

@ -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<LifecycleProperty> properties)
{
Properties = properties;
}
public IReadOnlyList<LifecycleProperty> Properties { get; }
// ControllerViewDataAttributeFilter is stateful and cannot be reused.
public bool IsReusable => false;
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
return new ControllerViewDataAttributeFilter(Properties);
}
}
}

View File

@ -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);
}
}

View File

@ -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
{
/// <inheritdoc />
/// <remarks>This order ensures that <see cref="ViewDataAttributeApplicationModelProvider"/> runs after the <see cref="DefaultApplicationModelProvider"/>.</remarks>
public int Order => -1000 + 10;
/// <inheritdoc />
public void OnProvidersExecuted(ApplicationModelProviderContext context)
{
}
/// <inheritdoc />
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);
}
}
}
}

View File

@ -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<LifecycleProperty> GetViewDataProperties(Type type)
{
List<LifecycleProperty> 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<ViewDataAttribute>();
if (tempDataAttribute != null)
{
if (results == null)
{
results = new List<LifecycleProperty>();
}
var key = tempDataAttribute.Key;
if (string.IsNullOrEmpty(key))
{
key = property.Name;
}
results.Add(new LifecycleProperty(property, key));
}
}
return results;
}
}
}

View File

@ -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
{
/// <summary>
/// Properties decorated with <see cref="ViewDataAttribute"/> will have their values stored in
/// and loaded from the <see cref="ViewDataDictionary"/>. <see cref="ViewDataDictionary"/>
/// is supported on properties of Controllers, and Razor Page handlers.
/// </summary>
[AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
public sealed class ViewDataAttribute : Attribute
{
/// <summary>
/// Gets or sets the key used to get or add the property from value from <see cref="ViewDataDictionary"/>.
/// When unspecified, the key is the property name.
/// </summary>
public string Key { get; set; }
}
}

View File

@ -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<IViewComponentHelper>();
(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<IViewDataValuesProviderFeature>();
if (viewDataValuesProvider != null)
{
viewDataValuesProvider.ProvideViewDataValues(viewContext.ViewData);
}
}
private Task<IHtmlContent> GetViewComponentResult(IViewComponentHelper viewComponentHelper, ILogger logger, ViewComponentResult result)
{
if (result.ViewComponentType == null && result.ViewComponentName == null)

View File

@ -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<IViewDataValuesProviderFeature>();
if (viewDataValuesProvider != null)
{
viewDataValuesProvider.ProvideViewDataValues(viewContext.ViewData);
}
}
}
}

View File

@ -167,7 +167,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
var view = viewEngineResult.View;
using (view as IDisposable)
{
await ExecuteAsync(
context,
view,

View File

@ -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);
}
}
}

View File

@ -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<IHtmlDocument> 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<HttpResponseMessage> 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;
}
}
}
}
}

View File

@ -18,20 +18,10 @@
<ItemGroup>
<!-- For testing the testing infrastructure only -->
<WebApplicationFactoryContentRootAttribute
Include="BasicWebSite"
AssemblyName="BasicWebsite"
ContentRootPath="../../../../WebSites/BasicWebSite"
ContentRootTest="BasicWebSite.csproj"
Priority="-1" />
<WebApplicationFactoryContentRootAttribute Include="BasicWebSite" AssemblyName="BasicWebsite" ContentRootPath="../../../../WebSites/BasicWebSite" ContentRootTest="BasicWebSite.csproj" Priority="-1" />
<!-- For testing the testing infrastructure only. This attribute is
incorrect by design to ensure that the infrastructure falls back. -->
<WebApplicationFactoryContentRootAttribute
Include="FiltersWebSite"
AssemblyName="FiltersWebSite"
ContentRootPath="/../WebSites/FiltersWebSite"
ContentRootTest="Incorrect.csproj"
Priority="-1" />
<WebApplicationFactoryContentRootAttribute Include="FiltersWebSite" AssemblyName="FiltersWebSite" ContentRootPath="/../WebSites/FiltersWebSite" ContentRootTest="Incorrect.csproj" Priority="-1" />
</ItemGroup>
<ItemGroup>
@ -59,6 +49,7 @@
<ProjectReference Include="..\WebSites\WebApiCompatShimWebSite\WebApiCompatShimWebSite.csproj" />
<ProjectReference Include="..\WebSites\XmlFormattersWebSite\XmlFormattersWebSite.csproj" />
<PackageReference Include="AngleSharp" Version="$(AngleSharpPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.ChunkingCookieManager.Sources" PrivateAssets="All" Version="$(MicrosoftAspNetCoreChunkingCookieManagerSourcesPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="$(MicrosoftAspNetCoreHttpPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="$(MicrosoftAspNetCoreTestHostPackageVersion)" />

View File

@ -845,7 +845,6 @@ Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary`1[AspNetCore.InjectedPa
Assert.Equal(expected, response.Headers.Location.ToString());
}
[Fact]
public async Task RedirectToSelfWorks()
{

View File

@ -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);
}
}
}

View File

@ -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<IServiceProvider>());
// Assert
var filter = Assert.IsType<PageViewDataAttributeFilter>(result);
Assert.Same(properties, filter.Properties);
}
}
}

View File

@ -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<LifecycleProperty>());
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<string, object>(), 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<LifecycleProperty>());
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<string, object>(), 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; }
}
}
}

View File

@ -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<PageViewDataAttributeFilterFactory>(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<object>()),
};
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; }
}
}
}

View File

@ -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),
}
},

View File

@ -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<IServiceProvider>());
// Assert
var filter = Assert.IsType<ControllerViewDataAttributeFilter>(result);
Assert.Same(properties, filter.Properties);
}
}
}

View File

@ -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<LifecycleProperty>());
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<string, object>(), 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<LifecycleProperty>());
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<string, object>(), 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; }
}
}
}

View File

@ -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<ControllerViewDataAttributeFilterFactory>(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<ControllerViewDataAttributeFilterFactory>(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; }
}
}
}

View File

@ -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; }
}
}
}

View File

@ -3,7 +3,7 @@
using Microsoft.AspNetCore.Mvc;
namespace HtmlGenerationWebSite.Components
namespace BasicWebSite.Components
{
public class PassThroughViewComponent : ViewComponent
{

View File

@ -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();
}
}

View File

@ -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

View File

@ -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";
}
}
}

View File

@ -0,0 +1,8 @@
<html>
<head>
<title>@ViewData["Title"]</title>
</head>
<body>
<span id="message">@ViewData["Message"]</span>
</body>
</html>

View File

@ -0,0 +1,2 @@
Sample that shows value of ViewDataAttribute being passed to a ViewComponent
<span class="view-data">@ViewData["Message"]</span>

View File

@ -0,0 +1,2 @@
@{ Layout = "_Layout"; }
Sample that shows ViewDataAttribute being applied to a controller

View File

@ -0,0 +1,10 @@
<html>
<head>
<title>@ViewData["Title"]</title>
</head>
<body>
<span id="message">@ViewData["Message"]</span>
<span id="filter-message">@ViewData["FilterMessage"]</span>
@RenderBody()
</body>
</html>

View File

@ -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();
}
}
}

View File

@ -0,0 +1,8 @@
<html>
<head>
<title>@ViewData["Title"]</title>
</head>
<body>
<span id="message">@ViewData["Message"]</span>
</body>
</html>

View File

@ -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";
}
}
}

View File

@ -0,0 +1,6 @@
@page
@model ViewDataInPage
@{
Layout = "_Layout";
}
Sample that shows ViewData attributes being set in a PageModel.

View File

@ -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.

View File

@ -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),
};
}
}
}

View File

@ -0,0 +1,2 @@
@page
@model ViewDataToViewComponentPage

View File

@ -0,0 +1,11 @@
<html>
<head>
<meta name="description" content="@ViewData["Description"]" />
<meta name="keywords" content="@ViewData["Keywords"]" />
<meta name="author" content="@ViewData["Author"]" />
<title>@ViewData["Title"]</title>
</head>
<body>
@RenderBody()
</body>
</html>

View File

@ -0,0 +1 @@
@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"