diff --git a/src/Microsoft.AspNet.Mvc.Common/Microsoft.AspNet.Mvc.Common.kproj b/src/Microsoft.AspNet.Mvc.Common/Microsoft.AspNet.Mvc.Common.kproj index c76849b092..caf1d98985 100644 --- a/src/Microsoft.AspNet.Mvc.Common/Microsoft.AspNet.Mvc.Common.kproj +++ b/src/Microsoft.AspNet.Mvc.Common/Microsoft.AspNet.Mvc.Common.kproj @@ -23,6 +23,7 @@ + diff --git a/src/Microsoft.AspNet.Mvc.Common/PropertyActivator.cs b/src/Microsoft.AspNet.Mvc.Common/PropertyActivator.cs new file mode 100644 index 0000000000..79ee86d091 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Common/PropertyActivator.cs @@ -0,0 +1,51 @@ +// 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.Linq; +using System.Reflection; + +namespace Microsoft.AspNet.Mvc +{ + internal class PropertyActivator + { + private readonly Func _valueAccessor; + private readonly Action _fastPropertySetter; + + public PropertyActivator(PropertyInfo propertyInfo, + Func valueAccessor) + { + PropertyInfo = propertyInfo; + _valueAccessor = valueAccessor; + _fastPropertySetter = PropertyHelper.MakeFastPropertySetter(propertyInfo); + } + + public PropertyInfo PropertyInfo { get; private set; } + + public object Activate(object view, TContext context) + { + var value = _valueAccessor(context); + _fastPropertySetter(view, value); + return value; + } + + /// + /// Returns a list of properties on a type that are decorated with + /// the specified activateAttributeType and have setters. + /// + public static PropertyActivator[] GetPropertiesToActivate( + Type type, + Type activateAttributeType, + Func> createActivateInfo) + { + return type.GetRuntimeProperties() + .Where(property => + property.IsDefined(activateAttributeType) && + property.GetIndexParameters().Length == 0 && + property.SetMethod != null && + !property.SetMethod.IsStatic) + .Select(createActivateInfo) + .ToArray(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/DefaultControllerActivator.cs b/src/Microsoft.AspNet.Mvc.Core/DefaultControllerActivator.cs index 87918e6172..a3846764f2 100644 --- a/src/Microsoft.AspNet.Mvc.Core/DefaultControllerActivator.cs +++ b/src/Microsoft.AspNet.Mvc.Core/DefaultControllerActivator.cs @@ -4,8 +4,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; using System.Reflection; using Microsoft.AspNet.Http; using Microsoft.AspNet.Mvc.Core; @@ -19,10 +17,9 @@ namespace Microsoft.AspNet.Mvc /// public class DefaultControllerActivator : IControllerActivator { - private readonly Func _getPropertiesToActivate; - private readonly Func _createActivateInfo; - private readonly ReadOnlyDictionary> _valueAccessorLookup; - private readonly ConcurrentDictionary _injectActions; + private readonly Func[]> _getPropertiesToActivate; + private readonly IReadOnlyDictionary> _valueAccessorLookup; + private readonly ConcurrentDictionary[]> _injectActions; /// /// Initializes a new instance of the DefaultControllerActivator class. @@ -30,9 +27,11 @@ namespace Microsoft.AspNet.Mvc public DefaultControllerActivator() { _valueAccessorLookup = CreateValueAccessorLookup(); - _getPropertiesToActivate = GetPropertiesToActivate; - _createActivateInfo = CreateActivateInfo; - _injectActions = new ConcurrentDictionary(); + _injectActions = new ConcurrentDictionary[]>(); + _getPropertiesToActivate = type => + PropertyActivator.GetPropertiesToActivate(type, + typeof(ActivateAttribute), + CreateActivateInfo); } /// @@ -59,7 +58,7 @@ namespace Microsoft.AspNet.Mvc } } - protected virtual ReadOnlyDictionary> CreateValueAccessorLookup() + protected virtual IReadOnlyDictionary> CreateValueAccessorLookup() { var dictionary = new Dictionary> { @@ -78,20 +77,11 @@ namespace Microsoft.AspNet.Mvc } } }; - return new ReadOnlyDictionary>(dictionary); + return dictionary; } - private PropertyActivator[] GetPropertiesToActivate(Type controllerType) - { - var bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; - return controllerType.GetProperties(bindingFlags) - .Where(property => property.IsDefined(typeof(ActivateAttribute)) && - property.GetSetMethod(nonPublic: true) != null) - .Select(_createActivateInfo) - .ToArray(); - } - - private PropertyActivator CreateActivateInfo(PropertyInfo property) + private PropertyActivator CreateActivateInfo( + PropertyInfo property) { Func valueAccessor; if (!_valueAccessorLookup.TryGetValue(property.PropertyType, out valueAccessor)) @@ -103,29 +93,7 @@ namespace Microsoft.AspNet.Mvc }; } - return new PropertyActivator(property, - valueAccessor); - } - - private sealed class PropertyActivator - { - private readonly PropertyInfo _propertyInfo; - private readonly Func _valueAccessor; - private readonly Action _fastPropertySetter; - - public PropertyActivator(PropertyInfo propertyInfo, - Func valueAccessor) - { - _propertyInfo = propertyInfo; - _valueAccessor = valueAccessor; - _fastPropertySetter = PropertyHelper.MakeFastPropertySetter(propertyInfo); - } - - public void Activate(object instance, ActionContext context) - { - var value = _valueAccessor(context); - _fastPropertySetter(instance, value); - } + return new PropertyActivator(property, valueAccessor); } } } diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/InjectChunkVisitor.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/InjectChunkVisitor.cs index 9194926bcc..0136b2a873 100644 --- a/src/Microsoft.AspNet.Mvc.Razor.Host/InjectChunkVisitor.cs +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/InjectChunkVisitor.cs @@ -11,11 +11,14 @@ namespace Microsoft.AspNet.Mvc.Razor public class InjectChunkVisitor : MvcCSharpCodeVisitor { private readonly List _injectChunks = new List(); + private readonly string _activateAttribute; public InjectChunkVisitor([NotNull] CSharpCodeWriter writer, - [NotNull] CodeGeneratorContext context) + [NotNull] CodeGeneratorContext context, + [NotNull] string activateAttributeName) : base(writer, context) { + _activateAttribute = '[' + activateAttributeName + ']'; } public List InjectChunks @@ -25,7 +28,14 @@ namespace Microsoft.AspNet.Mvc.Razor protected override void Visit([NotNull] InjectChunk chunk) { - if (Context.Host.DesignTimeMode) + Writer.WriteLine(_activateAttribute); + + // Some of the chunks that we visit are either InjectDescriptors that are added by default or + // are chunks from _ViewStart files and are not associated with any Spans. Invoking + // CreateExpressionMapping to produce line mappings on these chunks would fail. We'll skip + // generating code mappings for these chunks. This makes sense since the chunks do not map + // to any code in the current view. + if (Context.Host.DesignTimeMode && chunk.Association != null) { Writer.WriteLine("public"); var code = string.Format(CultureInfo.InvariantCulture, diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/InjectDescriptor.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/InjectDescriptor.cs new file mode 100644 index 0000000000..36b7a566a9 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/InjectDescriptor.cs @@ -0,0 +1,40 @@ +// 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 Microsoft.AspNet.Mvc.Razor.Host; + +namespace Microsoft.AspNet.Mvc.Razor +{ + /// + /// Represents information about an injected property. + /// + public class InjectDescriptor + { + public InjectDescriptor(string typeName, string memberName) + { + if (string.IsNullOrEmpty(typeName)) + { + throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpy, "typeName"); + } + + if (string.IsNullOrEmpty(memberName)) + { + throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpy, "memberName"); + } + + TypeName = typeName; + MemberName = memberName; + } + + /// + /// Gets the type name of the injected property + /// + public string TypeName { get; private set; } + + /// + /// Gets the name of the injected property. + /// + public string MemberName { get; private set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/Microsoft.AspNet.Mvc.Razor.Host.kproj b/src/Microsoft.AspNet.Mvc.Razor.Host/Microsoft.AspNet.Mvc.Razor.Host.kproj index 227e4f8f8a..3029d6ab23 100644 --- a/src/Microsoft.AspNet.Mvc.Razor.Host/Microsoft.AspNet.Mvc.Razor.Host.kproj +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/Microsoft.AspNet.Mvc.Razor.Host.kproj @@ -24,6 +24,7 @@ + @@ -33,6 +34,7 @@ + diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcCSharpCodeBuilder.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcCSharpCodeBuilder.cs index 9026fc2741..2c1cbe5e7b 100644 --- a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcCSharpCodeBuilder.cs +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcCSharpCodeBuilder.cs @@ -1,6 +1,7 @@ // 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.Generic; using System.Globalization; using System.Linq; @@ -12,11 +13,17 @@ namespace Microsoft.AspNet.Mvc.Razor { public class MvcCSharpCodeBuilder : CSharpCodeBuilder { - public MvcCSharpCodeBuilder([NotNull] CodeGeneratorContext context) + private readonly MvcRazorHostOptions _hostOptions; + + public MvcCSharpCodeBuilder([NotNull] CodeGeneratorContext context, + [NotNull] MvcRazorHostOptions hostOptions) : base(context) { + _hostOptions = hostOptions; } + private string Model { get; set; } + protected override CSharpCodeWritingScope BuildClassDeclaration(CSharpCodeWriter writer) { // Grab the last model chunk so it gets intellisense. @@ -25,6 +32,8 @@ namespace Microsoft.AspNet.Mvc.Razor var modelChunk = Context.CodeTreeBuilder.CodeTree.Chunks.OfType() .LastOrDefault(); + Model = modelChunk != null ? modelChunk.ModelType : _hostOptions.DefaultModel; + // If there were any model chunks then we need to modify the class declaration signature. if (modelChunk != null) { @@ -46,26 +55,18 @@ namespace Microsoft.AspNet.Mvc.Razor protected override void BuildConstructor([NotNull] CSharpCodeWriter writer) { + // TODO: Move this to a proper extension point. Right now, we don't have a place to print out properties + // in the generated view. + // Tracked by #773 + base.BuildConstructor(writer); + writer.WriteLineHiddenDirective(); - var injectVisitor = new InjectChunkVisitor(writer, Context); + var injectVisitor = new InjectChunkVisitor(writer, Context, _hostOptions.ActivateAttributeName); injectVisitor.Accept(Context.CodeTreeBuilder.CodeTree.Chunks); writer.WriteLine(); writer.WriteLineHiddenDirective(); - - var arguments = injectVisitor.InjectChunks - .Select(chunk => new KeyValuePair(chunk.TypeName, - chunk.MemberName)); - using (writer.BuildConstructor("public", Context.ClassName, arguments)) - { - foreach (var inject in injectVisitor.InjectChunks) - { - writer.WriteStartAssignment("this." + inject.MemberName) - .Write(inject.MemberName) - .WriteLine(";"); - } - } } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs index 6a08c7806a..2610845937 100644 --- a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs @@ -2,7 +2,9 @@ // 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.Linq; using Microsoft.AspNet.Razor; using Microsoft.AspNet.Razor.Generator; using Microsoft.AspNet.Razor.Generator.Compiler; @@ -24,6 +26,8 @@ namespace Microsoft.AspNet.Mvc.Razor "Microsoft.AspNet.Mvc.Rendering" }; + private readonly MvcRazorHostOptions _hostOptions; + // CodeGenerationContext.DefaultBaseClass is set to MyBaseType. // This field holds the type name without the generic decoration (MyBaseType) private readonly string _baseType; @@ -36,8 +40,11 @@ namespace Microsoft.AspNet.Mvc.Razor public MvcRazorHost(string baseType) : base(new CSharpRazorCodeLanguage()) { + // TODO: this needs to flow from the application rather than being initialized here. + // Tracked by #774 + _hostOptions = new MvcRazorHostOptions(); _baseType = baseType; - DefaultBaseClass = baseType + ""; + DefaultBaseClass = baseType + '<' + _hostOptions.DefaultModel + '>'; GeneratedClassContext = new GeneratedClassContext( executeMethodName: "ExecuteAsync", writeMethodName: "Write", @@ -73,7 +80,34 @@ namespace Microsoft.AspNet.Mvc.Razor public override CodeBuilder DecorateCodeBuilder(CodeBuilder incomingBuilder, CodeGeneratorContext context) { - return new MvcCSharpCodeBuilder(context); + UpdateCodeBuilder(context); + return new MvcCSharpCodeBuilder(context, _hostOptions); + } + + private void UpdateCodeBuilder(CodeGeneratorContext context) + { + var currentChunks = context.CodeTreeBuilder.CodeTree.Chunks; + var existingInjects = new HashSet(currentChunks.OfType() + .Select(c => c.MemberName), + StringComparer.Ordinal); + + var modelChunk = currentChunks.OfType() + .LastOrDefault(); + var model = _hostOptions.DefaultModel; + if (modelChunk != null) + { + model = modelChunk.ModelType; + } + model = '<' + model + '>'; + + // Locate properties by name that haven't already been injected in to the View. + var propertiesToAdd = _hostOptions.DefaultInjectedProperties + .Where(c => !existingInjects.Contains(c.MemberName)); + foreach (var property in propertiesToAdd) + { + var typeName = property.TypeName.Replace("", model); + currentChunks.Add(new InjectChunk(typeName, property.MemberName)); + } } } } diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHostOptions.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHostOptions.cs new file mode 100644 index 0000000000..3ab5f0359c --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHostOptions.cs @@ -0,0 +1,40 @@ +// 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.Collections.Generic; + +namespace Microsoft.AspNet.Mvc.Razor +{ + /// + /// Represents configuration options for the Razor Host + /// + public class MvcRazorHostOptions + { + public MvcRazorHostOptions() + { + DefaultModel = "dynamic"; + ActivateAttributeName = "Microsoft.AspNet.Mvc.ActivateAttribute"; + DefaultInjectedProperties = new List() + { + new InjectDescriptor("Microsoft.AspNet.Mvc.Rendering.IHtmlHelper", "Html") + }; + } + + /// + /// Gets or sets the model that is used by default for generated views + /// when no model is explicily specified. Defaults to dynamic. + /// + public string DefaultModel { get; set; } + + /// + /// Gets or sets the attribue that is used to decorate properties that are injected and need to + /// be activated. + /// + public string ActivateAttributeName { get; set; } + + /// + /// Gets the list of properties that are injected by default. + /// + public IList DefaultInjectedProperties { get; private set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/Properties/Resources.Designer.cs index 1dba347970..4f2663c2b8 100644 --- a/src/Microsoft.AspNet.Mvc.Razor.Host/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/Properties/Resources.Designer.cs @@ -10,6 +10,22 @@ namespace Microsoft.AspNet.Mvc.Razor.Host private static readonly ResourceManager _resourceManager = new ResourceManager("Microsoft.AspNet.Mvc.Razor.Host.Resources", typeof(Resources).GetTypeInfo().Assembly); + /// + /// Argument cannot be null or empty. + /// + internal static string ArgumentCannotBeNullOrEmpy + { + get { return GetString("ArgumentCannotBeNullOrEmpy"); } + } + + /// + /// Argument cannot be null or empty. + /// + internal static string FormatArgumentCannotBeNullOrEmpy() + { + return GetString("ArgumentCannotBeNullOrEmpy"); + } + /// /// The 'inherits' keyword is not allowed when a '{0}' keyword is used. /// diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/Resources.resx b/src/Microsoft.AspNet.Mvc.Razor.Host/Resources.resx index f6562cd582..cfa565507e 100644 --- a/src/Microsoft.AspNet.Mvc.Razor.Host/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/Resources.resx @@ -117,6 +117,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Argument cannot be null or empty. + The 'inherits' keyword is not allowed when a '{0}' keyword is used. diff --git a/src/Microsoft.AspNet.Mvc.Razor/IRazorViewActivator.cs b/src/Microsoft.AspNet.Mvc.Razor/IRazorViewActivator.cs new file mode 100644 index 0000000000..e3b2966170 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor/IRazorViewActivator.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.Razor +{ + /// + /// Provides methods to activate properties on a view instance. + /// + public interface IRazorViewActivator + { + /// + /// When implemented in a type, activates an instantiated view. + /// + /// The view to activate. + /// The for the view. + void Activate(RazorView view, ViewContext context); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/Microsoft.AspNet.Mvc.Razor.kproj b/src/Microsoft.AspNet.Mvc.Razor/Microsoft.AspNet.Mvc.Razor.kproj index 19160b8e8a..86c67fcad0 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Microsoft.AspNet.Mvc.Razor.kproj +++ b/src/Microsoft.AspNet.Mvc.Razor/Microsoft.AspNet.Mvc.Razor.kproj @@ -30,9 +30,11 @@ + + @@ -42,4 +44,4 @@ - + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs index ad44f215f9..0187e832a0 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs @@ -186,6 +186,22 @@ namespace Microsoft.AspNet.Mvc.Razor return string.Format(CultureInfo.CurrentCulture, GetString("SectionsNotRendered"), p0); } + /// + /// View of type '{0}' cannot be activated by '{1}'. + /// + internal static string ViewCannotBeActivated + { + get { return GetString("ViewCannotBeActivated"); } + } + + /// + /// View of type '{0}' cannot be activated by '{1}'. + /// + internal static string FormatViewCannotBeActivated(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ViewCannotBeActivated"), p0, p1); + } + /// /// View '{0}' must have extension '{1}' when the view represents a full path. /// diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorViewActivator.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorViewActivator.cs new file mode 100644 index 0000000000..83752dc4f5 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorViewActivator.cs @@ -0,0 +1,127 @@ +// 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.Reflection; +using Microsoft.AspNet.Mvc.Rendering; +using Microsoft.Framework.DependencyInjection; + +namespace Microsoft.AspNet.Mvc.Razor +{ + /// + public class RazorViewActivator : IRazorViewActivator + { + // Name of the "public TModel Model" property on RazorView + private const string ModelPropertyName = "Model"; + private readonly ITypeActivator _typeActivator; + private readonly ConcurrentDictionary _activationInfo; + + /// + /// Initializes a new instance of the RazorViewActivator class. + /// + public RazorViewActivator(ITypeActivator typeActivator) + { + _typeActivator = typeActivator; + _activationInfo = new ConcurrentDictionary(); + } + + /// + /// Activates the specified view by using the specified ViewContext. + /// + /// The view to activate. + /// The ViewContext for the executing view. + public void Activate([NotNull] RazorView view, [NotNull] ViewContext context) + { + var activationInfo = _activationInfo.GetOrAdd(view.GetType(), + CreateViewActivationInfo); + + context.ViewData = CreateViewDataDictionary(context, activationInfo); + + for (var i = 0; i < activationInfo.PropertyActivators.Length; i++) + { + var activateInfo = activationInfo.PropertyActivators[i]; + activateInfo.Activate(view, context); + } + } + + private ViewDataDictionary CreateViewDataDictionary(ViewContext context, ViewActivationInfo activationInfo) + { + // Create a ViewDataDictionary if the ViewContext.ViewData is not set or the type of + // ViewContext.ViewData is an incompatibile type. + if (context.ViewData == null) + { + // Create ViewDataDictionary(metadataProvider); + return (ViewDataDictionary)_typeActivator.CreateInstance(context.HttpContext.RequestServices, + activationInfo.ViewDataDictionaryType); + } + else if (context.ViewData.GetType() != activationInfo.ViewDataDictionaryType) + { + // Create ViewDataDictionary(ViewDataDictionary); + return (ViewDataDictionary)_typeActivator.CreateInstance(context.HttpContext.RequestServices, + activationInfo.ViewDataDictionaryType, + context.ViewData); + } + + return context.ViewData; + } + + private ViewActivationInfo CreateViewActivationInfo(Type type) + { + // Look for a property named "Model". If it is non-null, we'll assume this is + // the equivalent of TModel Model property on RazorView + var modelProperty = type.GetRuntimeProperty(ModelPropertyName); + if (modelProperty == null) + { + var message = Resources.FormatViewCannotBeActivated(type.FullName, GetType().FullName); + throw new InvalidOperationException(message); + } + + var modelType = modelProperty.PropertyType; + var viewDataType = typeof(ViewDataDictionary<>).MakeGenericType(modelType); + + return new ViewActivationInfo + { + ViewDataDictionaryType = viewDataType, + PropertyActivators = PropertyActivator.GetPropertiesToActivate(type, + typeof(ActivateAttribute), + CreateActivateInfo) + }; + } + + private PropertyActivator CreateActivateInfo(PropertyInfo property) + { + Func valueAccessor; + if (typeof(ViewDataDictionary).IsAssignableFrom(property.PropertyType)) + { + valueAccessor = context => context.ViewData; + } + else + { + valueAccessor = context => + { + var serviceProvider = context.HttpContext.RequestServices; + var value = serviceProvider.GetService(property.PropertyType); + var canHasViewContext = value as ICanHasViewContext; + if (canHasViewContext != null) + { + canHasViewContext.Contextualize(context); + } + + return value; + }; + } + + return new PropertyActivator(property, valueAccessor); + } + + private class ViewActivationInfo + { + public PropertyActivator[] PropertyActivators { get; set; } + + public Type ViewDataDictionaryType { get; set; } + + public Action ViewDataDictionarySetter { get; set; } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorViewOfT.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorViewOfT.cs index 80790fe364..0523215dc5 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorViewOfT.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorViewOfT.cs @@ -1,10 +1,7 @@ // 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.IO; using System.Threading.Tasks; -using Microsoft.AspNet.Mvc.ModelBinding; -using Microsoft.AspNet.Mvc.Rendering; using Microsoft.Framework.DependencyInjection; namespace Microsoft.AspNet.Mvc.Razor @@ -19,43 +16,15 @@ namespace Microsoft.AspNet.Mvc.Razor } } - public ViewDataDictionary ViewData { get; private set; } - - public IHtmlHelper Html { get; set; } + [Activate] + public ViewDataDictionary ViewData { get; set; } public override Task RenderAsync([NotNull] ViewContext context) { - ViewData = context.ViewData as ViewDataDictionary; - if (ViewData == null) - { - if (context.ViewData != null) - { - ViewData = new ViewDataDictionary(context.ViewData); - } - else - { - var metadataProvider = context.HttpContext.RequestServices.GetService(); - ViewData = new ViewDataDictionary(metadataProvider); - } - - // Have new ViewDataDictionary; make sure it's visible everywhere. - context.ViewData = ViewData; - } - - InitHelpers(context); + var viewActivator = context.HttpContext.RequestServices.GetService(); + viewActivator.Activate(this, context); return base.RenderAsync(context); } - - private void InitHelpers(ViewContext context) - { - Html = context.HttpContext.RequestServices.GetService>(); - - var contextable = Html as ICanHasViewContext; - if (contextable != null) - { - contextable.Contextualize(context); - } - } } } diff --git a/src/Microsoft.AspNet.Mvc.Razor/Resources.resx b/src/Microsoft.AspNet.Mvc.Razor/Resources.resx index df7106bffa..208fa3dbcd 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Razor/Resources.resx @@ -150,6 +150,9 @@ The following sections have been defined but have not been rendered: '{0}'. + + View of type '{0}' cannot be activated by '{1}'. + View '{0}' must have extension '{1}' when the view represents a full path. diff --git a/src/Microsoft.AspNet.Mvc/MvcServices.cs b/src/Microsoft.AspNet.Mvc/MvcServices.cs index a61f9c31b1..966a937df1 100644 --- a/src/Microsoft.AspNet.Mvc/MvcServices.cs +++ b/src/Microsoft.AspNet.Mvc/MvcServices.cs @@ -43,6 +43,7 @@ namespace Microsoft.AspNet.Mvc yield return describe.Scoped(); yield return describe.Transient(); yield return describe.Transient(); + yield return describe.Singleton(); yield return describe.Transient, ReflectedActionDescriptorProvider>(); diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/InjectChunkVisitorTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/InjectChunkVisitorTest.cs index 62a9f76623..718069b57c 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/InjectChunkVisitorTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/InjectChunkVisitorTest.cs @@ -22,7 +22,7 @@ namespace Microsoft.AspNet.Mvc.Razor var writer = new CSharpCodeWriter(); var context = CreateContext(); - var visitor = new InjectChunkVisitor(writer, context); + var visitor = new InjectChunkVisitor(writer, context, "ActivateAttribute"); // Act visitor.Accept(new Chunk[] @@ -41,13 +41,15 @@ namespace Microsoft.AspNet.Mvc.Razor { // Arrange var expected = -@"public MyType1 MyPropertyName1 { get; private set; } +@"[ActivateAttribute] +public MyType1 MyPropertyName1 { get; private set; } +[ActivateAttribute] public MyType2 @MyPropertyName2 { get; private set; } "; var writer = new CSharpCodeWriter(); var context = CreateContext(); - var visitor = new InjectChunkVisitor(writer, context); + var visitor = new InjectChunkVisitor(writer, context, "ActivateAttribute"); var factory = SpanFactory.CreateCsHtml(); var node = (Span)factory.Code("Some code") .As(new InjectParameterGenerator("MyType", "MyPropertyName")); @@ -69,13 +71,15 @@ public MyType2 @MyPropertyName2 { get; private set; } public void Visit_WithDesignTimeHost_GeneratesPropertiesAndLinePragmas_ForInjectChunks() { // Arrange - var expected = @"public + var expected = @"[Microsoft.AspNet.Mvc.ActivateAttribute] +public #line 1 """" MyType1 MyPropertyName1 #line default #line hidden { get; private set; } +[Microsoft.AspNet.Mvc.ActivateAttribute] public #line 1 """" MyType2 @MyPropertyName2 @@ -88,7 +92,7 @@ MyType2 @MyPropertyName2 var context = CreateContext(); context.Host.DesignTimeMode = true; - var visitor = new InjectChunkVisitor(writer, context); + var visitor = new InjectChunkVisitor(writer, context, "Microsoft.AspNet.Mvc.ActivateAttribute"); var factory = SpanFactory.CreateCsHtml(); var node = (Span)factory.Code("Some code") .As(new InjectParameterGenerator("MyType", "MyPropertyName")); @@ -121,7 +125,40 @@ MyType2 @MyPropertyName2 var expectedLineMappings = new List { BuildLineMapping(1, 0, 1, 32, 3, 0, 17), - BuildLineMapping(28, 1, 8, 442, 21, 8, 20) + BuildLineMapping(28, 1, 8, 573, 26, 8, 20) + }; + + // Act + GeneratorResults results; + using (var buffer = new StringTextBuffer(source)) + { + results = engine.GenerateCode(buffer); + } + + // Assert + Assert.True(results.Success); + Assert.Equal(expectedCode, results.GeneratedCode); + Assert.Empty(results.ParserErrors); + Assert.Equal(expectedLineMappings, results.DesignTimeLineMappings); + } + + [Fact] + public void InjectVisitorWithModel_GeneratesCorrectLineMappings() + { + // Arrange + var host = new MvcRazorHost("RazorView") + { + DesignTimeMode = true + }; + host.NamespaceImports.Clear(); + var engine = new RazorTemplateEngine(host); + var source = ReadResource("TestFiles/Input/InjectWithModel.cshtml"); + var expectedCode = ReadResource("TestFiles/Output/InjectWithModel.cs"); + var expectedLineMappings = new List + { + BuildLineMapping(7, 0, 7, 126, 6, 7, 7), + BuildLineMapping(24, 1, 8, 562, 26, 8, 20), + BuildLineMapping(54, 2, 8, 732, 34, 8, 22) }; // Act diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/Microsoft.AspNet.Mvc.Razor.Host.Test.kproj b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/Microsoft.AspNet.Mvc.Razor.Host.Test.kproj index 9b048f99ba..f2dc6d012b 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/Microsoft.AspNet.Mvc.Razor.Host.Test.kproj +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/Microsoft.AspNet.Mvc.Razor.Host.Test.kproj @@ -37,6 +37,8 @@ + + diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFiles/Input/InjectWithModel.cshtml b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFiles/Input/InjectWithModel.cshtml new file mode 100644 index 0000000000..b4d4a5589a --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFiles/Input/InjectWithModel.cshtml @@ -0,0 +1,3 @@ +@model MyModel +@inject MyApp MyPropertyName +@inject MyService Html \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFiles/Output/Inject.cs b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFiles/Output/Inject.cs index 27799209ba..aae5630d99 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFiles/Output/Inject.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFiles/Output/Inject.cs @@ -17,6 +17,11 @@ using MyNamespace #pragma warning restore 219 } #line hidden + public __CompiledTemplate() + { + } + #line hidden + [Microsoft.AspNet.Mvc.ActivateAttribute] public #line 2 "" MyApp MyPropertyName @@ -24,12 +29,10 @@ using MyNamespace #line default #line hidden { get; private set; } + [Microsoft.AspNet.Mvc.ActivateAttribute] + public Microsoft.AspNet.Mvc.Rendering.IHtmlHelper Html { get; private set; } #line hidden - public __CompiledTemplate(MyApp MyPropertyName) - { - this.MyPropertyName = MyPropertyName; - } #pragma warning disable 1998 public override async Task ExecuteAsync() diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFiles/Output/InjectWithModel.cs b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFiles/Output/InjectWithModel.cs new file mode 100644 index 0000000000..e1dc486e03 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFiles/Output/InjectWithModel.cs @@ -0,0 +1,49 @@ +namespace Razor +{ + using System.Threading.Tasks; + + public class __CompiledTemplate : RazorView< +#line 1 "" + MyModel + +#line default +#line hidden + > + { + private static object @__o; + private void @__RazorDesignTimeHelpers__() + { + #pragma warning disable 219 + #pragma warning restore 219 + } + #line hidden + public __CompiledTemplate() + { + } + #line hidden + [Microsoft.AspNet.Mvc.ActivateAttribute] + public +#line 2 "" + MyApp MyPropertyName + +#line default +#line hidden + { get; private set; } + [Microsoft.AspNet.Mvc.ActivateAttribute] + public +#line 3 "" + MyService Html + +#line default +#line hidden + { get; private set; } + + #line hidden + + #pragma warning disable 1998 + public override async Task ExecuteAsync() + { + } + #pragma warning restore 1998 + } +} diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFiles/Output/Model.cs b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFiles/Output/Model.cs index 73f1cdc53b..71dde59fe9 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFiles/Output/Model.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFiles/Output/Model.cs @@ -16,12 +16,15 @@ #pragma warning disable 219 #pragma warning restore 219 } - #line hidden - #line hidden public __CompiledTemplate() { } + #line hidden + [Microsoft.AspNet.Mvc.ActivateAttribute] + public Microsoft.AspNet.Mvc.Rendering.IHtmlHelper Html { get; private set; } + + #line hidden #pragma warning disable 1998 public override async Task ExecuteAsync() diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/Microsoft.AspNet.Mvc.Razor.Test.kproj b/test/Microsoft.AspNet.Mvc.Razor.Test/Microsoft.AspNet.Mvc.Razor.Test.kproj index 449bf94258..4bafa70d9c 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/Microsoft.AspNet.Mvc.Razor.Test.kproj +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/Microsoft.AspNet.Mvc.Razor.Test.kproj @@ -23,9 +23,10 @@ + - + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewActivatorTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewActivatorTest.cs new file mode 100644 index 0000000000..3e9c788fc3 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewActivatorTest.cs @@ -0,0 +1,244 @@ +// 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.Globalization; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc.ModelBinding; +using Microsoft.AspNet.Mvc.Rendering; +using Microsoft.AspNet.Routing; +using Microsoft.Framework.DependencyInjection; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Razor +{ + public class RazorViewActivatorTest + { + [Fact] + public void Activate_ActivatesAndContextualizesPropertiesOnViews() + { + // Arrange + var activator = new RazorViewActivator(Mock.Of()); + var instance = new TestView(); + + var myService = new MyService(); + var helper = Mock.Of>(); + var serviceProvider = new Mock(); + serviceProvider.Setup(p => p.GetService(typeof(MyService))) + .Returns(myService); + serviceProvider.Setup(p => p.GetService(typeof(IHtmlHelper))) + .Returns(helper); + var httpContext = new Mock(); + httpContext.SetupGet(c => c.RequestServices) + .Returns(serviceProvider.Object); + var routeContext = new RouteContext(httpContext.Object); + var actionContext = new ActionContext(routeContext, new ActionDescriptor()); + var viewContext = new ViewContext(actionContext, + instance, + new ViewDataDictionary(Mock.Of()), + TextWriter.Null); + + // Act + activator.Activate(instance, viewContext); + + // Assert + Assert.Same(helper, instance.Html); + Assert.Same(myService, instance.MyService); + Assert.Same(viewContext, myService.ViewContext); + Assert.Null(instance.MyService2); + } + + [Fact] + public void Activate_ThrowsIfTheViewDoesNotDeriveFromRazorViewOfT() + { + // Arrange + var activator = new RazorViewActivator(Mock.Of()); + var instance = new DoesNotDeriveFromRazorViewOfT(); + + var myService = new MyService(); + var helper = Mock.Of>(); + var serviceProvider = new Mock(); + var httpContext = new Mock(); + httpContext.SetupGet(c => c.RequestServices) + .Returns(serviceProvider.Object); + var routeContext = new RouteContext(httpContext.Object); + var actionContext = new ActionContext(routeContext, new ActionDescriptor()); + var viewContext = new ViewContext(actionContext, + instance, + new ViewDataDictionary(Mock.Of()), + TextWriter.Null); + + // Act and Assert + var ex = Assert.Throws(() => activator.Activate(instance, viewContext)); + var message = string.Format(CultureInfo.InvariantCulture, + "View of type '{0}' cannot be activated by '{1}'.", + instance.GetType().FullName, + typeof(RazorViewActivator).FullName); + + Assert.Equal(message, ex.Message); + } + + [Fact] + public void Activate_InstantiatesNewViewDataDictionaryType_IfTheTypeDoesNotMatch() + { + // Arrange + var typeActivator = new TypeActivator(); + var activator = new RazorViewActivator(typeActivator); + var instance = new TestView(); + + var myService = new MyService(); + var helper = Mock.Of>(); + var serviceProvider = new Mock(); + serviceProvider.Setup(p => p.GetService(typeof(MyService))) + .Returns(myService); + serviceProvider.Setup(p => p.GetService(typeof(IHtmlHelper))) + .Returns(helper); + var httpContext = new Mock(); + httpContext.SetupGet(c => c.RequestServices) + .Returns(serviceProvider.Object); + var routeContext = new RouteContext(httpContext.Object); + var actionContext = new ActionContext(routeContext, new ActionDescriptor()); + var viewData = new ViewDataDictionary(Mock.Of()) + { + Model = new MyModel() + }; + var viewContext = new ViewContext(actionContext, + instance, + viewData, + TextWriter.Null); + + // Act + activator.Activate(instance, viewContext); + + // Assert + Assert.IsType>(viewContext.ViewData); + } + + [Fact] + public void Activate_UsesPassedInViewDataDictionaryInstance_IfPassedInTypeMatches() + { + // Arrange + var typeActivator = new TypeActivator(); + var activator = new RazorViewActivator(typeActivator); + var instance = new TestView(); + var myService = new MyService(); + var helper = Mock.Of>(); + var serviceProvider = new Mock(); + serviceProvider.Setup(p => p.GetService(typeof(MyService))) + .Returns(myService); + serviceProvider.Setup(p => p.GetService(typeof(IHtmlHelper))) + .Returns(helper); + var httpContext = new Mock(); + httpContext.SetupGet(c => c.RequestServices) + .Returns(serviceProvider.Object); + var routeContext = new RouteContext(httpContext.Object); + var actionContext = new ActionContext(routeContext, new ActionDescriptor()); + var viewData = new ViewDataDictionary(Mock.Of()) + { + Model = new MyModel() + }; + var viewContext = new ViewContext(actionContext, + instance, + viewData, + TextWriter.Null); + + // Act + activator.Activate(instance, viewContext); + + // Assert + Assert.Same(viewData, viewContext.ViewData); + } + + [Fact] + public void Activate_DeterminesModelTypeFromProperty() + { + // Arrange + var typeActivator = new TypeActivator(); + var activator = new RazorViewActivator(typeActivator); + var instance = new DoesNotDeriveFromRazorViewOfTButHasModelProperty(); + var myService = new MyService(); + var helper = Mock.Of>(); + var serviceProvider = new Mock(); + serviceProvider.Setup(p => p.GetService(typeof(MyService))) + .Returns(myService); + serviceProvider.Setup(p => p.GetService(typeof(IHtmlHelper))) + .Returns(helper); + var httpContext = new Mock(); + httpContext.SetupGet(c => c.RequestServices) + .Returns(serviceProvider.Object); + var routeContext = new RouteContext(httpContext.Object); + var actionContext = new ActionContext(routeContext, new ActionDescriptor()); + var viewData = new ViewDataDictionary(Mock.Of()); + var viewContext = new ViewContext(actionContext, + instance, + viewData, + TextWriter.Null); + + // Act + activator.Activate(instance, viewContext); + + // Assert + Assert.IsType>(viewContext.ViewData); + } + + private abstract class TestViewBase : RazorView + { + [Activate] + public MyService MyService { get; set; } + + public MyService MyService2 { get; set; } + } + + private class TestView : TestViewBase + { + [Activate] + internal IHtmlHelper Html { get; private set; } + + public override Task ExecuteAsync() + { + throw new NotImplementedException(); + } + } + + private abstract class DoesNotDeriveFromRazorViewOfTBase : RazorView + { + } + + private class DoesNotDeriveFromRazorViewOfT : DoesNotDeriveFromRazorViewOfTBase + { + public override Task ExecuteAsync() + { + throw new NotImplementedException(); + } + } + + private class DoesNotDeriveFromRazorViewOfTButHasModelProperty : DoesNotDeriveFromRazorViewOfTBase + { + public string Model { get; set; } + + public override Task ExecuteAsync() + { + throw new NotImplementedException(); + } + } + + private class MyService : ICanHasViewContext + { + public ViewContext ViewContext { get; private set; } + + public void Contextualize(ViewContext viewContext) + { + ViewContext = viewContext; + } + } + + + + private class MyModel + { + } + } +} \ No newline at end of file