diff --git a/src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewResult.cs b/src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewResult.cs index 4b41874026..cca67cabf4 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewResult.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewResult.cs @@ -2,7 +2,6 @@ // 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.IO; using System.Threading.Tasks; using Microsoft.AspNet.Mvc.Core; diff --git a/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj b/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj index 95345dd69e..46a286d607 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj +++ b/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj @@ -261,12 +261,14 @@ + + diff --git a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/DefaultViewComponentActivator.cs b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/DefaultViewComponentActivator.cs new file mode 100644 index 0000000000..9a17d44f7e --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/DefaultViewComponentActivator.cs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.Concurrent; +using System.Collections.Generic; +using System.Reflection; +using Microsoft.AspNet.Mvc.Rendering; + +namespace Microsoft.AspNet.Mvc +{ + /// + /// Represents the that is registered by default. + /// + public class DefaultViewComponentActivator : IViewComponentActivator + { + private readonly Func[]> _getPropertiesToActivate; + private readonly IReadOnlyDictionary> _valueAccessorLookup; + private readonly ConcurrentDictionary[]> _injectActions; + + /// + /// Initializes a new instance of class. + /// + public DefaultViewComponentActivator() + { + _valueAccessorLookup = CreateValueAccessorLookup(); + _injectActions = new ConcurrentDictionary[]>(); + _getPropertiesToActivate = type => + PropertyActivator.GetPropertiesToActivate( + type, typeof(ActivateAttribute), CreateActivateInfo); + } + + /// + public void Activate([NotNull] object viewComponent, [NotNull] ViewContext context) + { + var propertiesToActivate = _injectActions.GetOrAdd(viewComponent.GetType(), + _getPropertiesToActivate); + + for (var i = 0; i < propertiesToActivate.Length; i++) + { + var activateInfo = propertiesToActivate[i]; + activateInfo.Activate(viewComponent, context); + } + } + + /// + /// Creates a lookup dictionary for the values to be activated. + /// + /// Returns a readonly dictionary of the values corresponding to the types. + protected virtual IReadOnlyDictionary> CreateValueAccessorLookup() + { + return new Dictionary> + { + { typeof(ViewContext), (context) => context }, + { + typeof(ViewDataDictionary), + (context) => + { + return new ViewDataDictionary(context.ViewData) + { + Model = null + }; + } + } + }; + } + + private PropertyActivator CreateActivateInfo(PropertyInfo property) + { + Func valueAccessor; + if (!_valueAccessorLookup.TryGetValue(property.PropertyType, out valueAccessor)) + { + valueAccessor = (viewContext) => + { + var serviceProvider = viewContext.HttpContext.RequestServices; + var service = serviceProvider.GetService(property.PropertyType); + if (typeof(ICanHasViewContext).IsAssignableFrom(property.PropertyType)) + { + ((ICanHasViewContext)service).Contextualize(viewContext); + } + + return service; + }; + } + + return new PropertyActivator(property, valueAccessor); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/DefaultViewComponentInvoker.cs b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/DefaultViewComponentInvoker.cs index e2f334d218..680e59fbf4 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/DefaultViewComponentInvoker.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/DefaultViewComponentInvoker.cs @@ -15,15 +15,18 @@ namespace Microsoft.AspNet.Mvc { private readonly IServiceProvider _serviceProvider; private readonly TypeInfo _componentType; + private readonly IViewComponentActivator _viewComponentActivator; private readonly object[] _args; public DefaultViewComponentInvoker( [NotNull] IServiceProvider serviceProvider, + [NotNull] IViewComponentActivator viewComponentActivator, [NotNull] TypeInfo componentType, object[] args) { _serviceProvider = serviceProvider; _componentType = componentType; + _viewComponentActivator = viewComponentActivator; _args = args ?? new object[0]; } @@ -73,16 +76,7 @@ namespace Microsoft.AspNet.Mvc { var activator = _serviceProvider.GetService(); var component = activator.CreateInstance(_serviceProvider, _componentType.AsType()); - - Injector.InjectProperty(component, "ViewContext", context); - - // We're flowing the viewbag across, but the concept of model doesn't really apply here - var viewData = new ViewDataDictionary(context.ViewData); - viewData.Model = null; - Injector.InjectProperty(component, "ViewData", viewData); - - Injector.CallInitializer(component, _serviceProvider); - + _viewComponentActivator.Activate(component, context); return component; } @@ -148,4 +142,4 @@ namespace Microsoft.AspNet.Mvc typeof(IViewComponentResult).Name)); } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/DefaultViewComponentInvokerProvider.cs b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/DefaultViewComponentInvokerProvider.cs index 2f9b583554..9f85e7a51b 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/DefaultViewComponentInvokerProvider.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/DefaultViewComponentInvokerProvider.cs @@ -2,16 +2,20 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using Microsoft.AspNet.Mvc.Core; namespace Microsoft.AspNet.Mvc { public class DefaultViewComponentInvokerProvider : IViewComponentInvokerProvider { private readonly IServiceProvider _serviceProvider; + private readonly IViewComponentActivator _viewComponentActivator; - public DefaultViewComponentInvokerProvider(IServiceProvider serviceProvider) + public DefaultViewComponentInvokerProvider(IServiceProvider serviceProvider, + IViewComponentActivator viewComponentActivator) { _serviceProvider = serviceProvider; + _viewComponentActivator = viewComponentActivator; } public int Order @@ -22,7 +26,8 @@ namespace Microsoft.AspNet.Mvc public void Invoke([NotNull] ViewComponentInvokerProviderContext context, [NotNull] Action callNext) { context.Result = - new DefaultViewComponentInvoker(_serviceProvider, context.ComponentType, context.Arguments); + new DefaultViewComponentInvoker( + _serviceProvider, _viewComponentActivator, context.ComponentType, context.Arguments); callNext(); } } diff --git a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/IViewComponentActivator.cs b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/IViewComponentActivator.cs new file mode 100644 index 0000000000..e103adeabb --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/IViewComponentActivator.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Mvc +{ + /// + /// Provides methods to activate an instantiated ViewComponent + /// + public interface IViewComponentActivator + { + /// + /// When implemented in a type, activates an instantiated ViewComponent. + /// + /// The ViewComponent to activate. + /// The for the executing . + void Activate(object viewComponent, ViewContext context); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ViewComponent.cs b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ViewComponent.cs index 0b42aaf172..5ae35e94ab 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ViewComponent.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ViewComponent.cs @@ -10,7 +10,6 @@ namespace Microsoft.AspNet.Mvc public abstract class ViewComponent { private dynamic _viewBag; - private ICompositeViewEngine _viewEngine; public HttpContext Context { @@ -30,20 +29,20 @@ namespace Microsoft.AspNet.Mvc } } + [Activate] public ViewContext ViewContext { get; set; } + [Activate] public ViewDataDictionary ViewData { get; set; } + [Activate] + public ICompositeViewEngine ViewEngine { get; set; } + public ContentViewComponentResult Content([NotNull] string content) { return new ContentViewComponentResult(content); } - public void Initialize(ICompositeViewEngine viewEngine) - { - _viewEngine = viewEngine; - } - public JsonViewComponentResult Json([NotNull] object value) { return new JsonViewComponentResult(value); @@ -72,7 +71,7 @@ namespace Microsoft.AspNet.Mvc viewData.Model = model; } - return new ViewViewComponentResult(_viewEngine, viewName, viewData); + return new ViewViewComponentResult(ViewEngine, viewName, viewData); } } } diff --git a/src/Microsoft.AspNet.Mvc/MvcServices.cs b/src/Microsoft.AspNet.Mvc/MvcServices.cs index d76c0a0940..de8a7580cc 100644 --- a/src/Microsoft.AspNet.Mvc/MvcServices.cs +++ b/src/Microsoft.AspNet.Mvc/MvcServices.cs @@ -79,6 +79,7 @@ namespace Microsoft.AspNet.Mvc yield return describe.Scoped(); yield return describe.Transient(); + yield return describe.Singleton(); yield return describe.Transient(); yield return describe.Transient, DefaultViewComponentInvokerProvider>(); diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/DefaultViewComponentActivatorTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultViewComponentActivatorTests.cs new file mode 100644 index 0000000000..5669943186 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultViewComponentActivatorTests.cs @@ -0,0 +1,143 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#if NET45 +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNet.Mvc.ModelBinding; +using Microsoft.AspNet.Mvc.Rendering; +using Microsoft.AspNet.PipelineCore; +using Microsoft.AspNet.Routing; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc +{ + public class DefaultViewComponentActivatorTests + { + [Fact] + public void DefaultViewComponentActivatorSetsAllPropertiesMarkedAsActivate() + { + // Arrange + var activator = new DefaultViewComponentActivator(); + var instance = new TestViewComponent(); + var helper = Mock.Of>(); + var serviceProvider = new Mock(); + serviceProvider.Setup(p => p.GetService(typeof(IHtmlHelper))).Returns(helper); + var viewContext = GetViewContext(serviceProvider.Object); + + // Act + activator.Activate(instance, viewContext); + + // Assert + Assert.Same(helper, instance.Html); + Assert.Same(viewContext, instance.ViewContext); + Assert.IsType(instance.ViewData); + } + + [Fact] + public void DefaultViewComponentActivatorSetsModelAsNull() + { + // Arrange + var activator = new DefaultViewComponentActivator(); + var helper = Mock.Of>(); + var serviceProvider = new Mock(); + serviceProvider.Setup(p => p.GetService(typeof(IHtmlHelper))).Returns(helper); + var viewContext = GetViewContext(serviceProvider.Object); + + // Act + activator.Activate(new TestViewComponent(), viewContext); + + // Assert + Assert.Null(viewContext.ViewData.Model); + } + + [Fact] + public void DefaultViewComponentActivatorActivatesNonBuiltInTypes() + { + // Arrange + var activator = new DefaultViewComponentActivator(); + var helper = Mock.Of>(); + var myTestService = new MyService(); + var serviceProvider = new Mock(); + serviceProvider.Setup(p => p.GetService(typeof(IHtmlHelper))).Returns(helper); + serviceProvider.Setup(p => p.GetService(typeof(MyService))).Returns(myTestService); + var viewContext = GetViewContext(serviceProvider.Object); + var instance = new TestViewComponentWithCustomDataType(); + + // Act + activator.Activate(instance, viewContext); + + // Assert + Assert.Equal(myTestService, instance.TestMyServiceObject); + + } + + [Fact] + public void DefaulViewComponentActivatorContextualizesService() + { + // Arrange + var activator = new DefaultViewComponentActivator(); + var instance = new TestClassUsingMyService(); + var myTestService = new MyService(); + var serviceProvider = new Mock(); + serviceProvider.Setup(p => p.GetService(typeof(MyService))).Returns(myTestService); + var viewContext = GetViewContext(serviceProvider.Object); + + // Act + activator.Activate(instance, viewContext); + + // Assert + Assert.Same(myTestService, instance.MyTestService); + Assert.Same(viewContext, instance.MyTestService.ViewContext); + } + + private ViewContext GetViewContext(IServiceProvider serviceProvider) + { + var httpContext = new Mock(); + httpContext.SetupGet(c => c.RequestServices) + .Returns(serviceProvider); + var routeContext = new RouteContext(httpContext.Object); + var actionContext = new ActionContext(routeContext, new ActionDescriptor()); + return new ViewContext(actionContext, + Mock.Of(), + new ViewDataDictionary(Mock.Of()), + TextWriter.Null); + } + + private class TestViewComponent : ViewComponent + { + [Activate] + public IHtmlHelper Html { get; private set; } + + public Task ExecuteAsync() + { + throw new NotImplementedException(); + } + } + + private class TestViewComponentWithCustomDataType : TestViewComponent + { + [Activate] + public MyService TestMyServiceObject { get; set; } + } + + private class MyService : ICanHasViewContext + { + public ViewContext ViewContext { get; private set; } + + public void Contextualize(ViewContext viewContext) + { + ViewContext = viewContext; + } + } + + private class TestClassUsingMyService + { + [Activate] + public MyService MyTestService { get; set; } + } + } +} +#endif \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj b/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj index 971a81b273..3b8eddb6e7 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj @@ -34,6 +34,7 @@ +