Adding support for inheriting chunks from _ViewStarts as part of host

parsing.

Fixes #881
This commit is contained in:
Pranav K 2014-08-05 08:20:00 -07:00
Родитель 7a1242f162
Коммит a490abc6e8
42 изменённых файлов: 1388 добавлений и 209 удалений

Просмотреть файл

@ -0,0 +1,83 @@
// 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 Microsoft.AspNet.Mvc.Razor.Host;
using Microsoft.AspNet.Razor.Generator.Compiler;
namespace Microsoft.AspNet.Mvc.Razor.Directives
{
/// <summary>
/// Contains helper methods for dealing with Chunks
/// </summary>
public static class ChunkHelper
{
private const string TModelToken = "<TModel>";
/// <summary>
/// Attempts to cast the passed in <see cref="Chunk"/> to type <typeparamref name="TChunk"/> and throws if the
/// cast fails.
/// </summary>
/// <typeparam name="TChunk">The type to cast to.</typeparam>
/// <param name="chunk">The chunk to cast.</param>
/// <returns>The <paramref name="Chunk"/> cast to <typeparamref name="TChunk"/>.</returns>
/// <exception cref="ArgumentException"><paramref name="chunk"/> is not an instance of
/// <typeparamref name="TChunk"/>.</exception>
public static TChunk EnsureChunk<TChunk>([NotNull] Chunk chunk)
where TChunk : Chunk
{
var chunkOfT = chunk as TChunk;
if (chunkOfT == null)
{
var message = Resources.FormatArgumentMustBeOfType(typeof(TChunk).FullName);
throw new ArgumentException(message, "chunk");
}
return chunkOfT;
}
/// <summary>
/// Returns the <see cref="ModelChunk"/> used to determine the model name for the page generated
/// using the specified <paramref name="codeTree"/>
/// </summary>
/// <param name="codeTree">The <see cref="CodeTree"/> to scan for <see cref="ModelChunk"/>s in.</param>
/// <returns>The last <see cref="ModelChunk"/> in the <see cref="CodeTree"/> if found, null otherwise.
/// </returns>
public static ModelChunk GetModelChunk([NotNull] CodeTree codeTree)
{
// If there's more than 1 model chunk there will be a Razor error BUT we want intellisense to show up on
// the current model chunk that the user is typing.
return codeTree.Chunks
.OfType<ModelChunk>()
.LastOrDefault();
}
/// <summary>
/// Returns the type name of the Model specified via a <see cref="ModelChunk"/> in the
/// <paramref name="codeTree"/> if specified or the default model type.
/// </summary>
/// <param name="codeTree">The <see cref="CodeTree"/> to scan for <see cref="ModelChunk"/>s in.</param>
/// <param name="defaultModelName">The <see cref="Type"/> name of the default model.</param>
/// <returns>The model type name for the generated page.</returns>
public static string GetModelTypeName([NotNull] CodeTree codeTree,
[NotNull] string defaultModelName)
{
var modelChunk = GetModelChunk(codeTree);
return modelChunk != null ? modelChunk.ModelType : defaultModelName;
}
/// <summary>
/// Returns a string with the &lt;TModel&gt; token replaced with the value specified in
/// <paramref name="modelName"/>.
/// </summary>
/// <param name="value">The string to replace the token in.</param>
/// <param name="modelName">The model name to replace with.</param>
/// <returns>A string with the token replaced.</returns>
public static string ReplaceTModel([NotNull] string value,
[NotNull] string modelName)
{
return value.Replace(TModelToken, modelName);
}
}
}

Просмотреть файл

@ -0,0 +1,147 @@
// 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.IO;
using System.Linq;
using Microsoft.AspNet.FileSystems;
using Microsoft.AspNet.Razor;
using Microsoft.AspNet.Razor.Generator.Compiler;
using Microsoft.AspNet.Razor.Parser;
namespace Microsoft.AspNet.Mvc.Razor.Directives
{
/// <summary>
/// A utility type for supporting inheritance of chunks into a page from _ViewStart pages that apply to it.
/// </summary>
public class ChunkInheritanceUtility
{
private readonly IReadOnlyList<Chunk> _defaultInheritedChunks;
/// <summary>
/// Instantiates a new instance of <see cref="ChunkInheritanceUtility"/>.
/// </summary>
/// <param name="codeTree">The <see cref="CodeTree"/> instance to add <see cref="Chunk"/>s to.</param>
/// <param name="defaultInheritedChunks">The list of <see cref="Chunk"/>s inherited by default.</param>
/// <param name="defaultModel">The model type used in the event no model is specified via the
/// <c>@model</c> keyword.</param>
public ChunkInheritanceUtility([NotNull] CodeTree codeTree,
[NotNull] IReadOnlyList<Chunk> defaultInheritedChunks,
[NotNull] string defaultModel)
{
CodeTree = codeTree;
_defaultInheritedChunks = defaultInheritedChunks;
ChunkMergers = GetMergerMappings(codeTree, defaultModel);
}
/// <summary>
/// Gets the CodeTree to add inherited <see cref="Chunk"/> instances to.
/// </summary>
public CodeTree CodeTree { get; private set; }
/// <summary>
/// Gets a dictionary mapping <see cref="Chunk"/> type to the <see cref="IChunkMerger"/> used to merge
/// chunks of that type.
/// </summary>
public IDictionary<Type, IChunkMerger> ChunkMergers { get; private set; }
/// <summary>
/// Gets the list of chunks that are to be inherited by a specified page.
/// Chunks are inherited from _ViewStarts that are applicable to the page.
/// </summary>
/// <param name="razorHost">The <see cref="MvcRazorHost"/> used to parse _ViewStart pages.</param>
/// <param name="fileSystem">The filesystem that represents the application.</param>
/// <param name="appRoot">The root of the application.</param>
/// <param name="pagePath">The path of the page to locate inherited chunks for.</param>
/// <returns>A list of chunks that are applicable to the given page.</returns>
public List<Chunk> GetInheritedChunks([NotNull] MvcRazorHost razorHost,
[NotNull] IFileSystem fileSystem,
[NotNull] string appRoot,
[NotNull] string pagePath)
{
var inheritedChunks = new List<Chunk>();
var templateEngine = new RazorTemplateEngine(razorHost);
foreach (var viewStart in ViewStartUtility.GetViewStartLocations(appRoot, pagePath))
{
IFileInfo fileInfo;
if (fileSystem.TryGetFileInfo(viewStart, out fileInfo))
{
var parsedTree = ParseViewFile(templateEngine, fileInfo);
var chunksToAdd = parsedTree.Chunks
.Where(chunk => ChunkMergers.ContainsKey(chunk.GetType()));
inheritedChunks.AddRange(chunksToAdd);
}
}
inheritedChunks.AddRange(_defaultInheritedChunks);
return inheritedChunks;
}
/// <summary>
/// Merges a list of chunks into the <see cref="CodeTree"/> instance.
/// </summary>
/// <param name="inherited">The list of chunks to merge.</param>
public void MergeInheritedChunks(List<Chunk> inherited)
{
var current = CodeTree.Chunks;
// We merge chunks into the codeTree in two passes. In the first pass, we traverse the CodeTree visiting
// a mapped IChunkMerger for types that are registered.
foreach (var chunk in current)
{
if (ChunkMergers.TryGetValue(chunk.GetType(), out var merger))
{
merger.VisitChunk(chunk);
}
}
// In the second phase we invoke IChunkMerger.Merge for each chunk that has a mapped merger.
// During this phase, the merger can either add to the CodeTree or ignore the chunk based on the merging
// rules.
foreach (var chunk in inherited)
{
if (ChunkMergers.TryGetValue(chunk.GetType(), out var merger))
{
// TODO: When mapping chunks, we should remove mapping information since it would be incorrect
// to generate it in the page that inherits it. Tracked by #945
merger.Merge(CodeTree, chunk);
}
}
}
private static Dictionary<Type, IChunkMerger> GetMergerMappings(CodeTree codeTree, string defaultModel)
{
var modelType = ChunkHelper.GetModelTypeName(codeTree, defaultModel);
return new Dictionary<Type, IChunkMerger>
{
{ typeof(UsingChunk), new UsingChunkMerger() },
{ typeof(InjectChunk), new InjectChunkMerger(modelType) },
{ typeof(SetBaseTypeChunk), new SetBaseTypeChunkMerger(modelType) }
};
}
// TODO: This needs to be cached (#1016)
private CodeTree ParseViewFile(RazorTemplateEngine engine,
IFileInfo fileInfo)
{
using (var stream = fileInfo.CreateReadStream())
{
using (var streamReader = new StreamReader(stream))
{
var parseResults = engine.ParseTemplate(streamReader);
var className = ParserHelpers.SanitizeClassName(fileInfo.Name);
var language = engine.Host.CodeLanguage;
var codeGenerator = language.CreateCodeGenerator(className,
engine.Host.DefaultNamespace,
fileInfo.PhysicalPath,
engine.Host);
codeGenerator.Visit(parseResults);
return codeGenerator.Context.CodeTreeBuilder.CodeTree;
}
}
}
}
}

Просмотреть файл

@ -0,0 +1,26 @@
// 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 Microsoft.AspNet.Razor.Generator.Compiler;
namespace Microsoft.AspNet.Mvc.Razor.Directives
{
/// <summary>
/// Defines the contract for merging <see cref="Chunk"/> instances from _ViewStart files.
/// </summary>
public interface IChunkMerger
{
/// <summary>
/// Visits a <see cref="Chunk"/> from the <see cref="CodeTree"/> to merge into.
/// </summary>
/// <param name="chunk">A <see cref="Chunk"/> from the tree.</param>
void VisitChunk(Chunk chunk);
/// <summary>
/// Merges an inherited <see cref="Chunk"/> into the <see cref="CodeTree"/>.
/// </summary>
/// <param name="codeTree">The <see cref="CodeTree"/> to merge into.</param>
/// <param name="chunk">The <see cref="Chunk"/> to merge.</param>
void Merge(CodeTree codeTree, Chunk chunk);
}
}

Просмотреть файл

@ -0,0 +1,61 @@
// 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.Linq;
using Microsoft.AspNet.Razor.Generator.Compiler;
namespace Microsoft.AspNet.Mvc.Razor.Directives
{
/// <summary>
/// A <see cref="IChunkMerger"/> that merges <see cref="InjectChunk"/> instances.
/// </summary>
public class InjectChunkMerger : IChunkMerger
{
private readonly HashSet<string> _addedMemberNames = new HashSet<string>(StringComparer.Ordinal);
private string _modelType;
/// <summary>
/// Initializes a new instance of <see cref="InjectChunkMerger"/>.
/// </summary>
/// <param name="modelType">The model type to be used to replace &lt;TModel&gt; tokens.</param>
public InjectChunkMerger([NotNull] string modelType)
{
_modelType = '<' + modelType + '>';
}
/// <inheritdoc />
public void VisitChunk([NotNull] Chunk chunk)
{
var injectChunk = ChunkHelper.EnsureChunk<InjectChunk>(chunk);
injectChunk.TypeName = ChunkHelper.ReplaceTModel(injectChunk.TypeName, _modelType);
_addedMemberNames.Add(injectChunk.MemberName);
}
/// <inheritdoc />
public void Merge([NotNull] CodeTree codeTree, [NotNull] Chunk chunk)
{
var injectChunk = ChunkHelper.EnsureChunk<InjectChunk>(chunk);
if (!_addedMemberNames.Contains(injectChunk.MemberName))
{
_addedMemberNames.Add(injectChunk.MemberName);
codeTree.Chunks.Add(TransformChunk(injectChunk));
}
}
private InjectChunk TransformChunk(InjectChunk injectChunk)
{
var typeName = ChunkHelper.ReplaceTModel(injectChunk.TypeName, _modelType);
if (typeName != injectChunk.TypeName)
{
return new InjectChunk(typeName, injectChunk.MemberName)
{
Start = injectChunk.Start,
Association = injectChunk.Association
};
}
return injectChunk;
}
}
}

Просмотреть файл

@ -0,0 +1,62 @@
// 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 Microsoft.AspNet.Razor.Generator.Compiler;
namespace Microsoft.AspNet.Mvc.Razor.Directives
{
/// <summary>
/// A <see cref="IChunkMerger"/> that merges <see cref="SetBaseTypeChunk"/> instances.
/// </summary>
public class SetBaseTypeChunkMerger : IChunkMerger
{
private readonly string _modelType;
private bool _isBaseTypeSet;
/// <summary>
/// Initializes a new instance of <see cref="SetBaseTypeChunkMerger"/>.
/// </summary>
/// <param name="defaultModelType">The type name of the model used by default.</param>
public SetBaseTypeChunkMerger(string modelType)
{
_modelType = '<' + modelType + '>';
}
/// <inheritdoc />
public void VisitChunk([NotNull] Chunk chunk)
{
var setBaseTypeChunk = ChunkHelper.EnsureChunk<SetBaseTypeChunk>(chunk);
setBaseTypeChunk.TypeName = ChunkHelper.ReplaceTModel(setBaseTypeChunk.TypeName, _modelType);
_isBaseTypeSet = true;
}
/// <inheritdoc />
public void Merge([NotNull] CodeTree codeTree, [NotNull] Chunk chunk)
{
if (!_isBaseTypeSet)
{
var baseTypeChunk = ChunkHelper.EnsureChunk<SetBaseTypeChunk>(chunk);
// The base type can set exactly once and the first one we encounter wins.
_isBaseTypeSet = true;
codeTree.Chunks.Add(TransformChunk(baseTypeChunk));
}
}
private SetBaseTypeChunk TransformChunk(SetBaseTypeChunk setBaseTypeChunk)
{
var typeName = ChunkHelper.ReplaceTModel(setBaseTypeChunk.TypeName, _modelType);
if (typeName != setBaseTypeChunk.TypeName)
{
return new SetBaseTypeChunk
{
TypeName = typeName,
Start = setBaseTypeChunk.Start,
Association = setBaseTypeChunk.Association
};
}
return setBaseTypeChunk;
}
}
}

Просмотреть файл

@ -0,0 +1,36 @@
// 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 Microsoft.AspNet.Razor.Generator.Compiler;
namespace Microsoft.AspNet.Mvc.Razor.Directives
{
/// <summary>
/// A <see cref="IChunkMerger"/> that merges <see cref="UsingChunk"/> instances.
/// </summary>
public class UsingChunkMerger : IChunkMerger
{
private readonly HashSet<string> _currentUsings = new HashSet<string>(StringComparer.Ordinal);
/// <inheritdoc />
public void VisitChunk([NotNull] Chunk chunk)
{
var namespaceChunk = ChunkHelper.EnsureChunk<UsingChunk>(chunk);
_currentUsings.Add(namespaceChunk.Namespace);
}
/// <inheritdoc />
public void Merge([NotNull] CodeTree codeTree, [NotNull] Chunk chunk)
{
var namespaceChunk = ChunkHelper.EnsureChunk<UsingChunk>(chunk);
if (!_currentUsings.Contains(namespaceChunk.Namespace))
{
_currentUsings.Add(namespaceChunk.Namespace);
codeTree.Chunks.Add(namespaceChunk);
}
}
}
}

Просмотреть файл

@ -10,8 +10,8 @@ namespace Microsoft.AspNet.Mvc.Razor
/// <summary>
/// Represents the chunk for an @inject statement.
/// </summary>
/// <param name="typeName">The type of object that would be injected</param>
/// <param name="propertyName">The member name the field is exposed to the page as.</param>
/// <param name="typeName">The type name of the property to be injected</param>
/// <param name="propertyName">The member name of the property to be injected.</param>
public InjectChunk(string typeName,
string propertyName)
{
@ -19,8 +19,14 @@ namespace Microsoft.AspNet.Mvc.Razor
MemberName = propertyName;
}
public string TypeName { get; private set; }
/// <summary>
/// Gets or sets the type name of the property to be injected.
/// </summary>
public string TypeName { get; set; }
public string MemberName { get; private set; }
/// <summary>
/// Gets or sets the name of the property to be injected.
/// </summary>
public string MemberName { get; set; }
}
}

Просмотреть файл

@ -1,10 +1,8 @@
// 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;
using Microsoft.AspNet.Mvc.Razor.Directives;
using Microsoft.AspNet.Razor.Generator;
using Microsoft.AspNet.Razor.Generator.Compiler;
using Microsoft.AspNet.Razor.Generator.Compiler.CSharp;
@ -13,13 +11,16 @@ namespace Microsoft.AspNet.Mvc.Razor
{
public class MvcCSharpCodeBuilder : CSharpCodeBuilder
{
private readonly MvcRazorHostOptions _hostOptions;
private readonly string _defaultModel;
private readonly string _activateAttribute;
public MvcCSharpCodeBuilder([NotNull] CodeGeneratorContext context,
[NotNull] MvcRazorHostOptions hostOptions)
[NotNull] string defaultModel,
[NotNull] string activateAttribute)
: base(context)
{
_hostOptions = hostOptions;
_defaultModel = defaultModel;
_activateAttribute = activateAttribute;
}
private string Model { get; set; }
@ -27,12 +28,9 @@ namespace Microsoft.AspNet.Mvc.Razor
protected override CSharpCodeWritingScope BuildClassDeclaration(CSharpCodeWriter writer)
{
// Grab the last model chunk so it gets intellisense.
// NOTE: If there's more than 1 model chunk there will be a Razor error BUT we want intellisense to
// show up on the current model chunk that the user is typing.
var modelChunk = Context.CodeTreeBuilder.CodeTree.Chunks.OfType<ModelChunk>()
.LastOrDefault();
var modelChunk = ChunkHelper.GetModelChunk(Context.CodeTreeBuilder.CodeTree);
Model = modelChunk != null ? modelChunk.ModelType : _hostOptions.DefaultModel;
Model = modelChunk != null ? modelChunk.ModelType : _defaultModel;
// If there were any model chunks then we need to modify the class declaration signature.
if (modelChunk != null)
@ -62,7 +60,7 @@ namespace Microsoft.AspNet.Mvc.Razor
writer.WriteLineHiddenDirective();
var injectVisitor = new InjectChunkVisitor(writer, Context, _hostOptions.ActivateAttributeName);
var injectVisitor = new InjectChunkVisitor(writer, Context, _activateAttribute);
injectVisitor.Accept(Context.CodeTreeBuilder.CodeTree.Chunks);
writer.WriteLine();

Просмотреть файл

@ -1,21 +1,21 @@
// 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.IO;
using System.Linq;
using Microsoft.AspNet.FileSystems;
using Microsoft.AspNet.Mvc.Razor.Directives;
using Microsoft.AspNet.Razor;
using Microsoft.AspNet.Razor.Generator;
using Microsoft.AspNet.Razor.Generator.Compiler;
using Microsoft.AspNet.Razor.Parser;
using Microsoft.Framework.Runtime;
namespace Microsoft.AspNet.Mvc.Razor
{
public class MvcRazorHost : RazorEngineHost, IMvcRazorHost
{
private const string ViewNamespace = "ASP";
private const string BaseType = "Microsoft.AspNet.Mvc.Razor.RazorPage";
private static readonly string[] _defaultNamespaces = new[]
{
"System",
@ -24,26 +24,48 @@ namespace Microsoft.AspNet.Mvc.Razor
"Microsoft.AspNet.Mvc",
"Microsoft.AspNet.Mvc.Rendering",
};
private static readonly Chunk[] _defaultInheritedChunks = new[]
{
new InjectChunk("Microsoft.AspNet.Mvc.Rendering.IHtmlHelper<TModel>", "Html"),
new InjectChunk("Microsoft.AspNet.Mvc.IViewComponentHelper", "Component"),
new InjectChunk("Microsoft.AspNet.Mvc.IUrlHelper", "Url"),
};
private readonly MvcRazorHostOptions _hostOptions;
private readonly string _appRoot;
private readonly IFileSystem _fileSystem;
// CodeGenerationContext.DefaultBaseClass is set to MyBaseType<dynamic>.
// This field holds the type name without the generic decoration (MyBaseType)
private readonly string _baseType;
public MvcRazorHost(Type baseType)
: this(baseType.FullName)
/// <summary>
/// Initializes a new instance of <see cref="MvcRazorHost"/> with the specified
/// <param name="appEnvironment"/>.
/// </summary>
/// <param name="appEnvironment">Contains information about the executing application.</param>
public MvcRazorHost(IApplicationEnvironment appEnvironment)
: this(appEnvironment.ApplicationBasePath,
new PhysicalFileSystem(appEnvironment.ApplicationBasePath))
{
}
public MvcRazorHost(string baseType)
/// <summary>
/// Initializes a new instance of <see cref="MvcRazorHost"/> at the specified application root
/// and <paramref name="fileSystem"/>.
/// </summary>
/// <param name="applicationBasePath">The base path of the application.</param>
/// <param name="fileSystem">
/// A <see cref="IFileSystem"/> rooted at the <paramref name="applicationBasePath"/>.
/// </param>
protected internal MvcRazorHost(string applicationBasePath,
IFileSystem fileSystem)
: 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 + '<' + _hostOptions.DefaultModel + '>';
_appRoot = applicationBasePath;
_fileSystem = fileSystem;
_baseType = BaseType;
DefaultBaseClass = BaseType + '<' + DefaultModel + '>';
DefaultNamespace = "Asp";
GeneratedClassContext = new GeneratedClassContext(
executeMethodName: "ExecuteAsync",
writeMethodName: "Write",
@ -62,51 +84,64 @@ namespace Microsoft.AspNet.Mvc.Razor
}
}
/// <summary>
/// Gets the model type used by default when no model is specified.
/// </summary>
/// <remarks>This value is used as the generic type argument for the base type </remarks>
public virtual string DefaultModel
{
get { return "dynamic"; }
}
/// <summary>
/// Gets the list of chunks that are injected by default by this host.
/// </summary>
public virtual IReadOnlyList<Chunk> DefaultInheritedChunks
{
get { return _defaultInheritedChunks; }
}
/// <summary>
/// Gets or sets the name attribute that is used to decorate properties that are injected and need to be
/// activated.
/// </summary>
public virtual string ActivateAttribute
{
get { return "Microsoft.AspNet.Mvc.ActivateAttribute"; }
}
/// <inheritdoc />
public GeneratorResults GenerateCode(string rootRelativePath, Stream inputStream)
{
var className = ParserHelpers.SanitizeClassName(rootRelativePath);
using (var reader = new StreamReader(inputStream))
{
var engine = new RazorTemplateEngine(this);
return engine.GenerateCode(reader, className, ViewNamespace, rootRelativePath);
return engine.GenerateCode(reader, className, DefaultNamespace, rootRelativePath);
}
}
public override ParserBase DecorateCodeParser(ParserBase incomingCodeParser)
/// <inheritdoc />
public override ParserBase DecorateCodeParser([NotNull] ParserBase incomingCodeParser)
{
return new MvcRazorCodeParser(_baseType);
}
public override CodeBuilder DecorateCodeBuilder(CodeBuilder incomingBuilder, CodeGeneratorContext context)
/// <inheritdoc />
public override CodeBuilder DecorateCodeBuilder([NotNull] CodeBuilder incomingBuilder,
[NotNull] CodeGeneratorContext context)
{
UpdateCodeBuilder(context);
return new MvcCSharpCodeBuilder(context, _hostOptions);
return new MvcCSharpCodeBuilder(context, DefaultModel, ActivateAttribute);
}
private void UpdateCodeBuilder(CodeGeneratorContext context)
{
var currentChunks = context.CodeTreeBuilder.CodeTree.Chunks;
var existingInjects = new HashSet<string>(currentChunks.OfType<InjectChunk>()
.Select(c => c.MemberName),
StringComparer.Ordinal);
var modelChunk = currentChunks.OfType<ModelChunk>()
.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("<TModel>", model);
currentChunks.Add(new InjectChunk(typeName, property.MemberName));
}
var chunkUtility = new ChunkInheritanceUtility(context.CodeTreeBuilder.CodeTree,
DefaultInheritedChunks,
DefaultModel);
var inheritedChunks = chunkUtility.GetInheritedChunks(this, _fileSystem, _appRoot, context.SourceFile);
chunkUtility.MergeInheritedChunks(inheritedChunks);
}
}
}

Просмотреть файл

@ -1,42 +0,0 @@
// 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
{
/// <summary>
/// Represents configuration options for the Razor Host
/// </summary>
public class MvcRazorHostOptions
{
public MvcRazorHostOptions()
{
DefaultModel = "dynamic";
ActivateAttributeName = "Microsoft.AspNet.Mvc.ActivateAttribute";
DefaultInjectedProperties = new List<InjectDescriptor>()
{
new InjectDescriptor("Microsoft.AspNet.Mvc.Rendering.IHtmlHelper<TModel>", "Html"),
new InjectDescriptor("Microsoft.AspNet.Mvc.IViewComponentHelper", "Component"),
new InjectDescriptor("Microsoft.AspNet.Mvc.IUrlHelper", "Url"),
};
}
/// <summary>
/// Gets or sets the model that is used by default for generated views
/// when no model is explicily specified. Defaults to dynamic.
/// </summary>
public string DefaultModel { get; set; }
/// <summary>
/// Gets or sets the attribue that is used to decorate properties that are injected and need to
/// be activated.
/// </summary>
public string ActivateAttributeName { get; set; }
/// <summary>
/// Gets the list of properties that are injected by default.
/// </summary>
public IList<InjectDescriptor> DefaultInjectedProperties { get; private set; }
}
}

Просмотреть файл

@ -0,0 +1,6 @@
// 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.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.AspNet.Mvc.Razor.Host.Test")]

Просмотреть файл

@ -26,6 +26,22 @@ namespace Microsoft.AspNet.Mvc.Razor.Host
return GetString("ArgumentCannotBeNullOrEmpy");
}
/// <summary>
/// Argument must be an instance of type '{0}'.
/// </summary>
internal static string ArgumentMustBeOfType
{
get { return GetString("ArgumentMustBeOfType"); }
}
/// <summary>
/// Argument must be an instance of type '{0}'.
/// </summary>
internal static string FormatArgumentMustBeOfType(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ArgumentMustBeOfType"), p0);
}
/// <summary>
/// The 'inherits' keyword is not allowed when a '{0}' keyword is used.
/// </summary>

Просмотреть файл

@ -120,6 +120,9 @@
<data name="ArgumentCannotBeNullOrEmpy" xml:space="preserve">
<value>Argument cannot be null or empty.</value>
</data>
<data name="ArgumentMustBeOfType" xml:space="preserve">
<value>Argument must be an instance of '{0}'.</value>
</data>
<data name="MvcRazorCodeParser_CannotHaveModelAndInheritsKeyword" xml:space="preserve">
<value>The 'inherits' keyword is not allowed when a '{0}' keyword is used.</value>
</data>

Просмотреть файл

@ -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.Generic;
using System.IO;
using System.Linq;
namespace Microsoft.AspNet.Mvc.Razor
{
public static class ViewStartUtility
{
private const string ViewStartFileName = "_viewstart.cshtml";
/// <summary>
/// Determines if the given path represents a view start file.
/// </summary>
/// <param name="path">The path to inspect.</param>
/// <returns>True if the path is a view start file, false otherwise.</returns>
public static bool IsViewStart([NotNull] string path)
{
var fileName = Path.GetFileName(path);
return string.Equals(ViewStartFileName, fileName, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Gets the view start locations that are applicable to the specified path.
/// </summary>
/// <param name="applicationBase">The base of the application.</param>
/// <param name="path">The path to locate view starts for.</param>
/// <returns>A sequence of paths that represent potential view start locations.</returns>
/// <remarks>
/// This method returns paths starting from the directory of <paramref name="path"/> and moves
/// upwards until it hits the application root.
/// e.g.
/// /Views/Home/View.cshtml -> [ /Views/Home/_ViewStart.cshtml, /Views/_ViewStart.cshtml, /_ViewStart.cshtml ]
/// </remarks>
public static IEnumerable<string> GetViewStartLocations(string applicationBase, string path)
{
if (string.IsNullOrEmpty(path))
{
return Enumerable.Empty<string>();
}
applicationBase = TrimTrailingSlash(applicationBase);
var viewStartLocations = new List<string>();
var currentDir = GetViewDirectory(applicationBase, path);
while (IsSubDirectory(applicationBase, currentDir))
{
viewStartLocations.Add(Path.Combine(currentDir, ViewStartFileName));
currentDir = Path.GetDirectoryName(currentDir);
}
return viewStartLocations;
}
private static bool IsSubDirectory(string appRoot, string currentDir)
{
return currentDir.StartsWith(appRoot, StringComparison.OrdinalIgnoreCase);
}
private static string GetViewDirectory(string appRoot, string viewPath)
{
if (viewPath.StartsWith("~/"))
{
viewPath = viewPath.Substring(2);
}
else if (viewPath[0] == Path.DirectorySeparatorChar ||
viewPath[0] == Path.AltDirectorySeparatorChar)
{
viewPath = viewPath.Substring(1);
}
var viewDir = Path.GetDirectoryName(viewPath);
return Path.GetFullPath(Path.Combine(appRoot, viewDir));
}
private static string TrimTrailingSlash(string path)
{
if (path.Length > 0 &&
path[path.Length - 1] == Path.DirectorySeparatorChar)
{
return path.Substring(0, path.Length - 1);
}
return path;
}
}
}

Просмотреть файл

@ -4,8 +4,10 @@
"warningsAsErrors": true
},
"dependencies": {
"Microsoft.AspNet.FileSystems": "1.0.0-*",
"Microsoft.AspNet.Mvc.Common": "",
"Microsoft.AspNet.Razor": "4.0.0-*"
"Microsoft.AspNet.Razor": "4.0.0-*",
"Microsoft.Framework.Runtime.Interfaces": "1.0.0-*"
},
"frameworks": {
"net45": {

Просмотреть файл

@ -99,11 +99,26 @@ namespace Microsoft.AspNet.Mvc.Razor
/// <inheritdoc />
public abstract Task ExecuteAsync();
/// <summary>
/// Writes the specified <paramref name="value"/> with HTML encoding to <see cref="Output"/>.
/// </summary>
/// <param name="value">The <see cref="object"/> to write.</param>
public virtual void Write(object value)
{
WriteTo(Output, value);
}
/// <summary>
/// Writes the specified <paramref name="value"/> with HTML encoding to <paramref name="writer"/>.
/// </summary>
/// <param name="writer">The <see cref="TextWriter"/> instance to write to.</param>
/// <param name="value">The <see cref="object"/> to write.</param>
/// <remarks>
/// <paramref name="value"/>s of type <see cref="HtmlString"/> are written without encoding and the
/// <see cref="HelperResult.WriteTo(TextWriter)"/> is invoked for <see cref="HelperResult"/> types.
/// For all other types, the encoded result of <see cref="object.ToString"/> is written to the
/// <paramref name="writer"/>.
/// </remarks>
public virtual void WriteTo(TextWriter writer, object content)
{
if (content != null)
@ -128,11 +143,20 @@ namespace Microsoft.AspNet.Mvc.Razor
}
}
public void WriteLiteral(object value)
/// <summary>
/// Writes the specified <paramref name="value"/> without HTML encoding to <see cref="Output"/>.
/// </summary>
/// <param name="value">The <see cref="object"/> to write.</param>
public virtual void WriteLiteral(object value)
{
WriteLiteralTo(Output, value);
}
/// <summary>
/// Writes the specified <paramref name="value"/> without HTML encoding to the <paramref name="writer"/>.
/// </summary>
/// <param name="writer">The <see cref="TextWriter"/> instance to write to.</param>
/// <param name="value">The <see cref="object"/> to write.</param>
public virtual void WriteLiteralTo(TextWriter writer, object text)
{
if (text != null)

Просмотреть файл

@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Framework.Runtime;
@ -12,21 +11,20 @@ namespace Microsoft.AspNet.Mvc.Razor
/// <inheritdoc />
public class ViewStartProvider : IViewStartProvider
{
private const string ViewStartFileName = "_ViewStart.cshtml";
private readonly string _appRoot;
private readonly IRazorPageFactory _pageFactory;
public ViewStartProvider(IApplicationEnvironment appEnv,
IRazorPageFactory pageFactory)
{
_appRoot = TrimTrailingSlash(appEnv.ApplicationBasePath);
_appRoot = appEnv.ApplicationBasePath;
_pageFactory = pageFactory;
}
/// <inheritdoc />
public IEnumerable<IRazorPage> GetViewStartPages([NotNull] string path)
{
var viewStartLocations = GetViewStartLocations(path);
var viewStartLocations = ViewStartUtility.GetViewStartLocations(_appRoot, path);
var viewStarts = viewStartLocations.Select(_pageFactory.CreateInstance)
.Where(p => p != null)
.ToArray();
@ -38,55 +36,5 @@ namespace Microsoft.AspNet.Mvc.Razor
return viewStarts;
}
internal IEnumerable<string> GetViewStartLocations(string path)
{
if (string.IsNullOrEmpty(path))
{
return Enumerable.Empty<string>();
}
var viewStartLocations = new List<string>();
var currentDir = GetViewDirectory(_appRoot, path);
while (IsSubDirectory(_appRoot, currentDir))
{
viewStartLocations.Add(Path.Combine(currentDir, ViewStartFileName));
currentDir = Path.GetDirectoryName(currentDir);
}
return viewStartLocations;
}
private static bool IsSubDirectory(string appRoot, string currentDir)
{
return currentDir.StartsWith(appRoot, StringComparison.OrdinalIgnoreCase);
}
private static string GetViewDirectory(string appRoot, string viewPath)
{
if (viewPath.StartsWith("~/"))
{
viewPath = viewPath.Substring(2);
}
else if (viewPath[0] == Path.DirectorySeparatorChar ||
viewPath[0] == Path.AltDirectorySeparatorChar)
{
viewPath = viewPath.Substring(1);
}
var viewDir = Path.GetDirectoryName(viewPath);
return Path.GetFullPath(Path.Combine(appRoot, viewDir));
}
private static string TrimTrailingSlash(string path)
{
if (path.Length > 0 &&
path[path.Length - 1] == Path.DirectorySeparatorChar)
{
return path.Substring(0, path.Length - 1);
}
return path;
}
}
}

Просмотреть файл

@ -42,7 +42,8 @@ namespace Microsoft.AspNet.Mvc
yield return describe.Transient<IControllerAssemblyProvider, DefaultControllerAssemblyProvider>();
yield return describe.Transient<IActionDiscoveryConventions, DefaultActionDiscoveryConventions>();
yield return describe.Instance<IMvcRazorHost>(new MvcRazorHost(typeof(RazorPage).FullName));
// The host is designed to be discarded after consumption and is very inexpensive to initialize.
yield return describe.Transient<IMvcRazorHost, MvcRazorHost>();
yield return describe.Singleton<ICompilationService, RoslynCompilationService>();
yield return describe.Singleton<IRazorCompilationService, RazorCompilationService>();

Просмотреть файл

@ -0,0 +1,50 @@
// 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.Diagnostics;
using System.Threading.Tasks;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.TestHost;
using RazorWebSite;
using Xunit;
namespace Microsoft.AspNet.Mvc.FunctionalTests
{
public class DirectivesTest
{
private readonly IServiceProvider _provider = TestHelper.CreateServices("RazorWebSite");
private readonly Action<IBuilder> _app = new Startup().Configure;
[Fact]
public async Task ViewsInheritsUsingsAndInjectDirectivesFromViewStarts()
{
var expected = @"Hello Person1";
var server = TestServer.Create(_provider, _app);
var client = server.Handler;
// Act
var result = await client.GetAsync("http://localhost/Directives/ViewInheritsInjectAndUsingsFromViewStarts");
// Assert
var body = await result.HttpContext.Response.ReadBodyAsStringAsync();
Assert.Equal(expected, body.Trim());
}
[Fact]
public async Task ViewInheritsBasePageFromViewStarts()
{
var expected = @"WriteLiteral says:layout:Write says:Write says:Hello Person2";
var server = TestServer.Create(_provider, _app);
var client = server.Handler;
// Act
var result = await client.GetAsync("http://localhost/Directives/ViewInheritsBasePageFromViewStarts");
// Assert
var body = await result.HttpContext.Response.ReadBodyAsStringAsync();
Assert.Equal(expected, body.Trim());
}
}
}

Просмотреть файл

@ -0,0 +1,153 @@
// 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 Microsoft.AspNet.Razor.Generator.Compiler;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc.Razor.Directives
{
public class ChunkInheritanceUtilityTest
{
[Fact]
public void GetInheritedChunks_ReadsChunksFromViewStartsInPath()
{
// Arrange
var appRoot = @"x:\myapproot";
var fileSystem = new TestFileSystem();
fileSystem.AddFile(@"x:\myapproot\views\accounts\_viewstart.cshtml", "@using AccountModels");
fileSystem.AddFile(@"x:\myapproot\views\Shared\_viewstart.cshtml", "@inject SharedHelper Shared");
fileSystem.AddFile(@"x:\myapproot\views\home\_viewstart.cshtml", "@using MyNamespace");
fileSystem.AddFile(@"x:\myapproot\views\_viewstart.cshtml",
@"@inject MyHelper<TModel> Helper
@inherits MyBaseType
@{
Layout = ""test.cshtml"";
}
");
var host = new MvcRazorHost(appRoot, fileSystem);
var utility = new ChunkInheritanceUtility(new CodeTree(), new Chunk[0], "dynamic");
// Act
var chunks = utility.GetInheritedChunks(host,
fileSystem,
appRoot,
@"x:\myapproot\views\home\Index.cshtml");
// Assert
Assert.Equal(3, chunks.Count);
var usingChunk = Assert.IsType<UsingChunk>(chunks[0]);
Assert.Equal("MyNamespace", usingChunk.Namespace);
var injectChunk = Assert.IsType<InjectChunk>(chunks[1]);
Assert.Equal("MyHelper<TModel>", injectChunk.TypeName);
Assert.Equal("Helper", injectChunk.MemberName);
var setBaseTypeChunk = Assert.IsType<SetBaseTypeChunk>(chunks[2]);
Assert.Equal("MyBaseType", setBaseTypeChunk.TypeName);
}
[Fact]
public void GetInheritedChunks_ReturnsEmptySequenceIfNoViewStartsArePresent()
{
// Arrange
var appRoot = @"x:\myapproot";
var fileSystem = new TestFileSystem();
fileSystem.AddFile(@"x:\myapproot\_viewstart.cs", string.Empty);
fileSystem.AddFile(@"x:\myapproot\views\_Layout.cshtml", string.Empty);
fileSystem.AddFile(@"x:\myapproot\views\home\_not-viewstart.cshtml", string.Empty);
var host = new MvcRazorHost(appRoot, fileSystem);
var utility = new ChunkInheritanceUtility(new CodeTree(), new Chunk[0], "dynamic");
// Act
var chunks = utility.GetInheritedChunks(host,
fileSystem,
appRoot,
@"x:\myapproot\views\home\Index.cshtml");
// Assert
Assert.Empty(chunks);
}
[Fact]
public void GetInheritedChunks_ReturnsDefaultInheritedChunks()
{
// Arrange
var appRoot = @"x:\myapproot";
var fileSystem = new TestFileSystem();
fileSystem.AddFile(@"x:\myapproot\views\_viewstart.cshtml",
@"@inject DifferentHelper<TModel> Html
@using AppNamespace.Models
@{
Layout = ""test.cshtml"";
}
");
var host = new MvcRazorHost(appRoot, fileSystem);
var defaultChunks = new Chunk[]
{
new InjectChunk("MyTestHtmlHelper", "Html"),
new UsingChunk { Namespace = "AppNamespace.Model" },
};
var utility = new ChunkInheritanceUtility(new CodeTree(), defaultChunks, "dynamic");
// Act
var chunks = utility.GetInheritedChunks(host,
fileSystem,
appRoot,
@"x:\myapproot\views\home\Index.cshtml");
// Assert
Assert.Equal(4, chunks.Count);
var injectChunk = Assert.IsType<InjectChunk>(chunks[0]);
Assert.Equal("DifferentHelper<TModel>", injectChunk.TypeName);
Assert.Equal("Html", injectChunk.MemberName);
var usingChunk = Assert.IsType<UsingChunk>(chunks[1]);
Assert.Equal("AppNamespace.Models", usingChunk.Namespace);
injectChunk = Assert.IsType<InjectChunk>(chunks[2]);
Assert.Equal("MyTestHtmlHelper", injectChunk.TypeName);
Assert.Equal("Html", injectChunk.MemberName);
usingChunk = Assert.IsType<UsingChunk>(chunks[3]);
Assert.Equal("AppNamespace.Model", usingChunk.Namespace);
}
[Fact]
public void MergeChunks_VisitsChunksPriorToMerging()
{
// Arrange
var codeTree = new CodeTree();
codeTree.Chunks.Add(new LiteralChunk());
codeTree.Chunks.Add(new ExpressionBlockChunk());
codeTree.Chunks.Add(new ExpressionBlockChunk());
var merger = new Mock<IChunkMerger>();
var mockSequence = new MockSequence();
merger.InSequence(mockSequence)
.Setup(m => m.VisitChunk(It.IsAny<LiteralChunk>()))
.Verifiable();
merger.InSequence(mockSequence)
.Setup(m => m.Merge(codeTree, It.IsAny<LiteralChunk>()))
.Verifiable();
var inheritedChunks = new List<Chunk>
{
new CodeAttributeChunk(),
new LiteralChunk()
};
var utility = new ChunkInheritanceUtility(codeTree, inheritedChunks, "dynamic");
// Act
utility.ChunkMergers[typeof(LiteralChunk)] = merger.Object;
utility.MergeInheritedChunks(inheritedChunks);
// Assert
merger.Verify();
}
}
}

Просмотреть файл

@ -0,0 +1,160 @@
// 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 Microsoft.AspNet.Razor.Generator.Compiler;
using Microsoft.AspNet.Testing;
using Xunit;
namespace Microsoft.AspNet.Mvc.Razor.Directives
{
public class InjectChunkMergerTest
{
[Fact]
public void Visit_ThrowsIfThePassedInChunkIsNotAInjectChunk()
{
// Arrange
var expected = "Argument must be an instance of 'Microsoft.AspNet.Mvc.Razor.InjectChunk'.";
var merger = new InjectChunkMerger("dynamic");
// Act and Assert
ExceptionAssert.ThrowsArgument(() => merger.VisitChunk(new LiteralChunk()), "chunk", expected);
}
[Theory]
[InlineData("MyApp.TestHelper<TModel>", "MyApp.TestHelper<Person>")]
[InlineData("TestBaseType", "TestBaseType")]
public void Visit_UpdatesTModelTokenToMatchModelType(string typeName, string expectedValue)
{
// Arrange
var chunk = new InjectChunk(typeName, "TestHelper");
var merger = new InjectChunkMerger("Person");
// Act
merger.VisitChunk(chunk);
// Assert
Assert.Equal(expectedValue, chunk.TypeName);
Assert.Equal("TestHelper", chunk.MemberName);
}
[Fact]
public void Merge_ThrowsIfThePassedInChunkIsNotAInjectChunk()
{
// Arrange
var expected = "Argument must be an instance of 'Microsoft.AspNet.Mvc.Razor.InjectChunk'.";
var merger = new InjectChunkMerger("dynamic");
// Act and Assert
ExceptionAssert.ThrowsArgument(() => merger.Merge(new CodeTree(), new LiteralChunk()), "chunk", expected);
}
[Fact]
public void Merge_AddsChunkIfChunkWithMatchingPropertyNameWasNotVisitedInCodeTree()
{
// Arrange
var expectedType = "MyApp.MyHelperType";
var expectedProperty = "MyHelper";
var merger = new InjectChunkMerger("dynamic");
var codeTree = new CodeTree();
// Act
merger.Merge(codeTree, new InjectChunk(expectedType, expectedProperty));
// Assert
var chunk = Assert.Single(codeTree.Chunks);
var injectChunk = Assert.IsType<InjectChunk>(chunk);
Assert.Equal(expectedType, injectChunk.TypeName);
Assert.Equal(expectedProperty, injectChunk.MemberName);
}
[Fact]
public void Merge_IgnoresChunkIfChunkWithMatchingPropertyNameWasVisitedInCodeTree()
{
// Arrange
var merger = new InjectChunkMerger("dynamic");
var codeTree = new CodeTree();
// Act
merger.VisitChunk(new InjectChunk("MyTypeA", "MyProperty"));
merger.Merge(codeTree, new InjectChunk("MyTypeB", "MyProperty"));
// Assert
Assert.Empty(codeTree.Chunks);
}
[Fact]
public void Merge_MatchesPropertyNameInCaseSensitiveManner()
{
// Arrange
var merger = new InjectChunkMerger("dynamic");
var codeTree = new CodeTree();
// Act
merger.VisitChunk(new InjectChunk("MyType", "MyProperty"));
merger.Merge(codeTree, new InjectChunk("MyType", "myproperty"));
merger.Merge(codeTree, new InjectChunk("MyTypeB", "different-property"));
// Assert
Assert.Equal(2, codeTree.Chunks.Count);
var injectChunk = Assert.IsType<InjectChunk>(codeTree.Chunks[0]);
Assert.Equal("MyType", injectChunk.TypeName);
Assert.Equal("myproperty", injectChunk.MemberName);
injectChunk = Assert.IsType<InjectChunk>(codeTree.Chunks[1]);
Assert.Equal("MyTypeB", injectChunk.TypeName);
Assert.Equal("different-property", injectChunk.MemberName);
}
[Fact]
public void Merge_ResolvesModelNameInTypesWithTModelToken()
{
// Arrange
var merger = new InjectChunkMerger("dynamic");
var codeTree = new CodeTree();
// Act
merger.Merge(codeTree, new InjectChunk("MyHelper<TModel>", "MyProperty"));
// Assert
var chunk = Assert.Single(codeTree.Chunks);
var injectChunk = Assert.IsType<InjectChunk>(chunk);
Assert.Equal("MyHelper<dynamic>", injectChunk.TypeName);
Assert.Equal("MyProperty", injectChunk.MemberName);
}
[Fact]
public void Merge_ReplacesTModelTokensWithModel()
{
// Arrange
var merger = new InjectChunkMerger("MyTestModel2");
var codeTree = new CodeTree();
// Act
merger.Merge(codeTree, new InjectChunk("MyHelper<TModel>", "MyProperty"));
// Assert
var chunk = Assert.Single(codeTree.Chunks);
var injectChunk = Assert.IsType<InjectChunk>(chunk);
Assert.Equal("MyHelper<MyTestModel2>", injectChunk.TypeName);
Assert.Equal("MyProperty", injectChunk.MemberName);
}
[Fact]
public void Merge_IgnoresChunkIfChunkWithMatchingPropertyNameWasPreviouslyMerged()
{
// Arrange
var merger = new InjectChunkMerger("dynamic");
var codeTree = new CodeTree();
// Act
merger.Merge(codeTree, new InjectChunk("SomeType", "Property"));
merger.Merge(codeTree, new InjectChunk("SomeOtherType", "Property"));
// Assert
var chunk = Assert.Single(codeTree.Chunks);
var injectChunk = Assert.IsType<InjectChunk>(chunk);
Assert.Equal("SomeType", injectChunk.TypeName);
Assert.Equal("Property", injectChunk.MemberName);
}
}
}

Просмотреть файл

@ -0,0 +1,104 @@
// 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 Microsoft.AspNet.Razor.Generator.Compiler;
using Microsoft.AspNet.Testing;
using Xunit;
namespace Microsoft.AspNet.Mvc.Razor.Directives
{
public class SetBaseTypeChunkMergerTest
{
[Fact]
public void Visit_ThrowsIfThePassedInChunkIsNotASetBaseTypeChunk()
{
// Arrange
var expected = "Argument must be an instance of "+
"'Microsoft.AspNet.Razor.Generator.Compiler.SetBaseTypeChunk'.";
var merger = new SetBaseTypeChunkMerger("dynamic");
// Act and Assert
ExceptionAssert.ThrowsArgument(() => merger.VisitChunk(new LiteralChunk()), "chunk", expected);
}
[Theory]
[InlineData("MyApp.BaseType<TModel>", "MyApp.BaseType<Person>")]
[InlineData("TestBaseType", "TestBaseType")]
public void Visit_UpdatesTModelTokenToMatchModelType(string typeName, string expectedValue)
{
// Arrange
var chunk = new SetBaseTypeChunk
{
TypeName = typeName,
};
var merger = new SetBaseTypeChunkMerger("Person");
// Act
merger.VisitChunk(chunk);
// Assert
Assert.Equal(expectedValue, chunk.TypeName);
}
[Fact]
public void Merge_ThrowsIfThePassedInChunkIsNotASetBaseTypeChunk()
{
// Arrange
var expected = "Argument must be an instance of " +
"'Microsoft.AspNet.Razor.Generator.Compiler.SetBaseTypeChunk'.";
var merger = new SetBaseTypeChunkMerger("dynamic");
// Act and Assert
ExceptionAssert.ThrowsArgument(() => merger.Merge(new CodeTree(), new LiteralChunk()), "chunk", expected);
}
[Fact]
public void Merge_SetsBaseTypeIfItHasNotBeenSetInCodeTree()
{
// Arrange
var expected = "MyApp.Razor.MyBaseType";
var merger = new SetBaseTypeChunkMerger("dynamic");
var codeTree = new CodeTree();
// Act
merger.Merge(codeTree, new SetBaseTypeChunk { TypeName = expected });
// Assert
var chunk = Assert.Single(codeTree.Chunks);
var setBaseTypeChunk = Assert.IsType<SetBaseTypeChunk>(chunk);
Assert.Equal(expected, setBaseTypeChunk.TypeName);
}
[Fact]
public void Merge_IgnoresSetBaseTypeChunksIfCodeTreeContainsOne()
{
// Arrange
var merger = new SetBaseTypeChunkMerger("dynamic");
var codeTree = new CodeTree();
// Act
merger.VisitChunk(new SetBaseTypeChunk { TypeName = "MyBaseType1" });
merger.Merge(codeTree, new SetBaseTypeChunk { TypeName = "MyBaseType2" });
// Assert
Assert.Empty(codeTree.Chunks);
}
[Fact]
public void Merge_IgnoresSetBaseTypeChunksIfSetBaseTypeWasPreviouslyMerged()
{
// Arrange
var merger = new SetBaseTypeChunkMerger("dynamic");
var codeTree = new CodeTree();
// Act
merger.Merge(codeTree, new SetBaseTypeChunk { TypeName = "MyBase1" });
merger.Merge(codeTree, new SetBaseTypeChunk { TypeName = "MyBase2" });
// Assert
var chunk = Assert.Single(codeTree.Chunks);
var setBaseTypeChunk = Assert.IsType<SetBaseTypeChunk>(chunk);
Assert.Equal("MyBase1", setBaseTypeChunk.TypeName);
}
}
}

Просмотреть файл

@ -0,0 +1,106 @@
// 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 Microsoft.AspNet.Razor.Generator.Compiler;
using Microsoft.AspNet.Testing;
using Xunit;
namespace Microsoft.AspNet.Mvc.Razor.Directives
{
public class UsingChunkMergerTest
{
[Fact]
public void Visit_ThrowsIfThePassedInChunkIsNotAUsingChunk()
{
// Arrange
var expected = "Argument must be an instance of 'Microsoft.AspNet.Razor.Generator.Compiler.UsingChunk'.";
var merger = new UsingChunkMerger();
// Act and Assert
ExceptionAssert.ThrowsArgument(() => merger.VisitChunk(new LiteralChunk()), "chunk", expected);
}
[Fact]
public void Merge_ThrowsIfThePassedInChunkIsNotAUsingChunk()
{
// Arrange
var expected = "Argument must be an instance of 'Microsoft.AspNet.Razor.Generator.Compiler.UsingChunk'.";
var merger = new UsingChunkMerger();
// Act and Assert
ExceptionAssert.ThrowsArgument(() => merger.Merge(new CodeTree(), new LiteralChunk()), "chunk", expected);
}
[Fact]
public void Merge_AddsNamespacesThatHaveNotBeenVisitedInCodeTree()
{
// Arrange
var expected = "MyApp.Models";
var merger = new UsingChunkMerger();
var codeTree = new CodeTree();
// Act
merger.VisitChunk(new UsingChunk { Namespace = "Microsoft.AspNet.Mvc" });
merger.Merge(codeTree, new UsingChunk { Namespace = expected });
// Assert
var chunk = Assert.Single(codeTree.Chunks);
var usingChunk = Assert.IsType<UsingChunk>(chunk);
Assert.Equal(expected, usingChunk.Namespace);
}
[Fact]
public void Merge_IgnoresNamespacesThatHaveBeenVisitedInCodeTree()
{
// Arrange
var merger = new UsingChunkMerger();
var codeTree = new CodeTree();
// Act
merger.VisitChunk(new UsingChunk { Namespace = "Microsoft.AspNet.Mvc" });
merger.Merge(codeTree, new UsingChunk { Namespace = "Microsoft.AspNet.Mvc" });
// Assert
Assert.Empty(codeTree.Chunks);
}
[Fact]
public void Merge_IgnoresNamespacesThatHaveBeenVisitedDuringMerge()
{
// Arrange
var merger = new UsingChunkMerger();
var codeTree = new CodeTree();
// Act
merger.Merge(codeTree, new UsingChunk { Namespace = "Microsoft.AspNet.Mvc" });
merger.Merge(codeTree, new UsingChunk { Namespace = "Microsoft.AspNet.Mvc" });
merger.Merge(codeTree, new UsingChunk { Namespace = "Microsoft.AspNet.Mvc.Razor" });
// Assert
Assert.Equal(2, codeTree.Chunks.Count);
var chunk = Assert.IsType<UsingChunk>(codeTree.Chunks[0]);
Assert.Equal("Microsoft.AspNet.Mvc", chunk.Namespace);
chunk = Assert.IsType<UsingChunk>(codeTree.Chunks[1]);
Assert.Equal("Microsoft.AspNet.Mvc.Razor", chunk.Namespace);
}
[Fact]
public void Merge_MacthesNamespacesInCaseSensitiveManner()
{
// Arrange
var merger = new UsingChunkMerger();
var codeTree = new CodeTree();
// Act
merger.Merge(codeTree, new UsingChunk { Namespace = "Microsoft.AspNet.Mvc" });
merger.Merge(codeTree, new UsingChunk { Namespace = "Microsoft.AspNet.mvc" });
// Assert
Assert.Equal(2, codeTree.Chunks.Count);
var chunk = Assert.IsType<UsingChunk>(codeTree.Chunks[0]);
Assert.Equal("Microsoft.AspNet.Mvc", chunk.Namespace);
chunk = Assert.IsType<UsingChunk>(codeTree.Chunks[1]);
Assert.Equal("Microsoft.AspNet.mvc", chunk.Namespace);
}
}
}

Просмотреть файл

@ -3,12 +3,14 @@
using System.Collections.Generic;
using System.IO;
using Microsoft.AspNet.FileSystems;
using Microsoft.AspNet.Razor;
using Microsoft.AspNet.Razor.Generator;
using Microsoft.AspNet.Razor.Generator.Compiler;
using Microsoft.AspNet.Razor.Generator.Compiler.CSharp;
using Microsoft.AspNet.Razor.Parser.SyntaxTree;
using Microsoft.AspNet.Razor.Text;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc.Razor
@ -114,7 +116,7 @@ MyType2 @MyPropertyName2
public void InjectVisitor_GeneratesCorrectLineMappings()
{
// Arrange
var host = new MvcRazorHost("RazorView")
var host = new MvcRazorHost("appRoot", Mock.Of<IFileSystem>())
{
DesignTimeMode = true
};
@ -124,8 +126,8 @@ MyType2 @MyPropertyName2
var expectedCode = ReadResource("TestFiles/Output/Inject.cs");
var expectedLineMappings = new List<LineMapping>
{
BuildLineMapping(1, 0, 1, 32, 3, 0, 17),
BuildLineMapping(28, 1, 8, 573, 26, 8, 20)
BuildLineMapping(1, 0, 1, 30, 3, 0, 17),
BuildLineMapping(28, 1, 8, 598, 26, 8, 20)
};
// Act
@ -146,7 +148,7 @@ MyType2 @MyPropertyName2
public void InjectVisitorWithModel_GeneratesCorrectLineMappings()
{
// Arrange
var host = new MvcRazorHost("RazorView")
var host = new MvcRazorHost("appRoot", Mock.Of<IFileSystem>())
{
DesignTimeMode = true
};
@ -156,9 +158,9 @@ MyType2 @MyPropertyName2
var expectedCode = ReadResource("TestFiles/Output/InjectWithModel.cs");
var expectedLineMappings = new List<LineMapping>
{
BuildLineMapping(7, 0, 7, 126, 6, 7, 7),
BuildLineMapping(24, 1, 8, 562, 26, 8, 20),
BuildLineMapping(54, 2, 8, 732, 34, 8, 22)
BuildLineMapping(7, 0, 7, 151, 6, 7, 7),
BuildLineMapping(24, 1, 8, 587, 26, 8, 20),
BuildLineMapping(54, 2, 8, 757, 34, 8, 23)
};
// Act
@ -188,7 +190,7 @@ MyType2 @MyPropertyName2
private static CodeGeneratorContext CreateContext()
{
return CodeGeneratorContext.Create(new MvcRazorHost("RazorView"),
return CodeGeneratorContext.Create(new MvcRazorHost("appRoot", Mock.Of<IFileSystem>()),
"MyClass",
"MyNamespace",
string.Empty,

Просмотреть файл

@ -4,12 +4,14 @@
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.AspNet.FileSystems;
using Microsoft.AspNet.Razor;
using Microsoft.AspNet.Razor.Generator;
using Microsoft.AspNet.Razor.Generator.Compiler;
using Microsoft.AspNet.Razor.Generator.Compiler.CSharp;
using Microsoft.AspNet.Razor.Parser.SyntaxTree;
using Microsoft.AspNet.Razor.Text;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc.Razor
@ -106,7 +108,7 @@ Environment.NewLine +
public void ModelVisitor_GeneratesCorrectLineMappings()
{
// Arrange
var host = new MvcRazorHost("RazorView")
var host = new MvcRazorHost("appRoot", Mock.Of<IFileSystem>())
{
DesignTimeMode = true
};
@ -116,7 +118,7 @@ Environment.NewLine +
var expectedCode = ReadResource("TestFiles/Output/Model.cs");
var expectedLineMappings = new List<LineMapping>
{
BuildLineMapping(7, 0, 7, 126, 6, 7, 30),
BuildLineMapping(7, 0, 7, 151, 6, 7, 30),
};
// Act
@ -146,7 +148,7 @@ Environment.NewLine +
private static CodeGeneratorContext CreateContext()
{
return CodeGeneratorContext.Create(new MvcRazorHost("RazorView"),
return CodeGeneratorContext.Create(new MvcRazorHost("appRoot", Mock.Of<IFileSystem>()),
"MyClass",
"MyNamespace",
string.Empty,

Просмотреть файл

@ -0,0 +1,45 @@
// 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.IO;
using System.Text;
using Microsoft.AspNet.FileSystems;
using Moq;
namespace Microsoft.AspNet.Mvc.Razor
{
public class TestFileSystem : IFileSystem
{
private readonly Dictionary<string, IFileInfo> _lookup =
new Dictionary<string, IFileInfo>(StringComparer.Ordinal);
public bool TryGetDirectoryContents(string subpath, out IEnumerable<IFileInfo> contents)
{
throw new NotImplementedException();
}
public void AddFile(string path, string contents)
{
var fileInfo = new Mock<IFileInfo>();
fileInfo.Setup(f => f.CreateReadStream())
.Returns(new MemoryStream(Encoding.UTF8.GetBytes(contents)));
fileInfo.SetupGet(f => f.PhysicalPath)
.Returns(path);
fileInfo.SetupGet(f => f.Name)
.Returns(Path.GetFileName(path));
AddFile(path, fileInfo.Object);
}
public void AddFile(string path, IFileInfo contents)
{
_lookup.Add(path, contents);
}
public bool TryGetFileInfo(string subpath, out IFileInfo fileInfo)
{
return _lookup.TryGetValue(subpath, out fileInfo);
}
}
}

Просмотреть файл

@ -1,4 +1,4 @@
namespace Razor
namespace Asp
{
#line 1 ""
using MyNamespace
@ -8,7 +8,7 @@ using MyNamespace
;
using System.Threading.Tasks;
public class __CompiledTemplate : RazorView<dynamic>
public class __CompiledTemplate : Microsoft.AspNet.Mvc.Razor.RazorPage<dynamic>
{
private static object @__o;
private void @__RazorDesignTimeHelpers__()

Просмотреть файл

@ -1,8 +1,8 @@
namespace Razor
namespace Asp
{
using System.Threading.Tasks;
public class __CompiledTemplate : RazorView<
public class __CompiledTemplate : Microsoft.AspNet.Mvc.Razor.RazorPage<
#line 1 ""
MyModel
@ -32,7 +32,7 @@
[Microsoft.AspNet.Mvc.ActivateAttribute]
public
#line 3 ""
MyService<TModel> Html
MyService<MyModel> Html
#line default
#line hidden

Просмотреть файл

@ -1,8 +1,8 @@
namespace Razor
namespace Asp
{
using System.Threading.Tasks;
public class __CompiledTemplate : RazorView<
public class __CompiledTemplate : Microsoft.AspNet.Mvc.Razor.RazorPage<
#line 1 ""
System.Collections.IEnumerable

Просмотреть файл

@ -2,12 +2,9 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.Framework.Runtime;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc.Razor.Test
namespace Microsoft.AspNet.Mvc.Razor
{
public class ViewStartProviderTest
{
@ -18,10 +15,9 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
{
// Arrange
var appPath = @"x:\test";
var provider = new ViewStartProvider(GetAppEnv(appPath), Mock.Of<IRazorPageFactory>());
// Act
var result = provider.GetViewStartLocations(viewPath);
var result = ViewStartUtility.GetViewStartLocations(appPath, viewPath);
// Assert
Assert.Empty(result);
@ -37,9 +33,9 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
"/Views/Home/View.cshtml",
new[]
{
@"x:\test\myapp\Views\Home\_ViewStart.cshtml",
@"x:\test\myapp\Views\_ViewStart.cshtml",
@"x:\test\myapp\_ViewStart.cshtml",
@"x:\test\myapp\Views\Home\_viewstart.cshtml",
@"x:\test\myapp\Views\_viewstart.cshtml",
@"x:\test\myapp\_viewstart.cshtml",
}
};
@ -49,9 +45,9 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
"Views/Home/View.cshtml",
new[]
{
@"x:\test\myapp\Views\Home\_ViewStart.cshtml",
@"x:\test\myapp\Views\_ViewStart.cshtml",
@"x:\test\myapp\_ViewStart.cshtml",
@"x:\test\myapp\Views\Home\_viewstart.cshtml",
@"x:\test\myapp\Views\_viewstart.cshtml",
@"x:\test\myapp\_viewstart.cshtml",
}
};
@ -61,9 +57,9 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
"Views/Home/View.cshtml",
new[]
{
@"x:\test\myapp\Views\Home\_ViewStart.cshtml",
@"x:\test\myapp\Views\_ViewStart.cshtml",
@"x:\test\myapp\_ViewStart.cshtml",
@"x:\test\myapp\Views\Home\_viewstart.cshtml",
@"x:\test\myapp\Views\_viewstart.cshtml",
@"x:\test\myapp\_viewstart.cshtml",
}
};
}
@ -75,22 +71,11 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
string viewPath,
IEnumerable<string> expected)
{
// Arrange
var provider = new ViewStartProvider(GetAppEnv(appPath), Mock.Of<IRazorPageFactory>());
// Act
var result = provider.GetViewStartLocations(viewPath);
var result = ViewStartUtility.GetViewStartLocations(appPath, viewPath);
// Assert
Assert.Equal(expected, result);
}
private static IApplicationEnvironment GetAppEnv(string appPath)
{
var appEnv = new Mock<IApplicationEnvironment>();
appEnv.Setup(p => p.ApplicationBasePath)
.Returns(appPath);
return appEnv.Object;
}
}
}

Просмотреть файл

@ -5,6 +5,7 @@
"resources": "TestFiles\\**",
"dependencies": {
"Microsoft.AspNet.Mvc.Razor.Host": "",
"Microsoft.AspNet.Testing": "1.0.0-*",
"Xunit.KRunner": "1.0.0-*"
},
"commands": {

Просмотреть файл

@ -0,0 +1,22 @@
// 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.Diagnostics;
using Microsoft.AspNet.Mvc;
namespace RazorWebSite
{
public class DirectivesController : Controller
{
public ViewResult ViewInheritsInjectAndUsingsFromViewStarts()
{
return View(new Person { Name = "Person1" });
}
public ViewResult ViewInheritsBasePageFromViewStarts()
{
return View("/views/directives/scoped/ViewInheritsBasePageFromViewStarts.cshtml",
new Person { Name = "Person2" });
}
}
}

Просмотреть файл

@ -0,0 +1,23 @@
// 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 Microsoft.AspNet.Mvc.Razor;
namespace RazorWebSite
{
public abstract class MyBasePage<TModel> : RazorPage<TModel>
{
public override void WriteLiteral(object value)
{
base.WriteLiteral("WriteLiteral says:");
base.WriteLiteral(value);
}
public override void Write(object value)
{
base.WriteLiteral("Write says:");
base.Write(value);
}
}
}

Просмотреть файл

@ -0,0 +1,13 @@
// 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 RazorWebSite
{
public class InjectedHelper
{
public string Greet(Person person)
{
return "Hello " + person.Name;
}
}
}

Просмотреть файл

@ -14,6 +14,7 @@ namespace RazorWebSite
{
// Add MVC services to the services container
services.AddMvc(configuration);
services.AddTransient<InjectedHelper>();
});
// Add MVC to the request pipeline

Просмотреть файл

@ -0,0 +1 @@
@MyHelper.Greet(Model)

Просмотреть файл

@ -0,0 +1 @@
layout:@RenderBody()

Просмотреть файл

@ -0,0 +1,4 @@
@inherits MyBasePage<TModel>
@{
Layout = "/Views/Directives/Scoped/_Layout.cshtml";
}

Просмотреть файл

@ -0,0 +1,2 @@
@model MyPerson
@MyHelper.Greet(Model)

Просмотреть файл

@ -0,0 +1,2 @@
@using MyPerson = RazorWebSite.Person
@inject InjectedHelper MyHelper

Просмотреть файл

@ -1,2 +1,2 @@
@model RazorWebSite.Address
@model Address
@ViewData.Model.ZipCode

Просмотреть файл

@ -0,0 +1 @@
@using RazorWebSite