// 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 Microsoft.AspNet.Mvc.Razor.OptionDescriptors; using Microsoft.AspNet.Mvc.Rendering; using Microsoft.AspNet.PageExecutionInstrumentation; using Microsoft.Framework.DependencyInjection; namespace Microsoft.AspNet.Mvc.Razor { /// /// Represents a view engine that is used to render a page that uses the Razor syntax. /// public class RazorViewEngine : IViewEngine { private const string ViewExtension = ".cshtml"; internal const string ControllerKey = "controller"; internal const string AreaKey = "area"; private static readonly IEnumerable _viewLocationFormats = new[] { "/Views/{1}/{0}" + ViewExtension, "/Views/Shared/{0}" + ViewExtension, }; private static readonly IEnumerable _areaViewLocationFormats = new[] { "/Areas/{2}/Views/{1}/{0}" + ViewExtension, "/Areas/{2}/Views/Shared/{0}" + ViewExtension, "/Views/Shared/{0}" + ViewExtension, }; private readonly IRazorPageFactory _pageFactory; private readonly IReadOnlyList _viewLocationExpanders; private readonly IViewLocationCache _viewLocationCache; /// /// Initializes a new instance of the class. /// /// The page factory used for creating instances. public RazorViewEngine(IRazorPageFactory pageFactory, IViewLocationExpanderProvider viewLocationExpanderProvider, IViewLocationCache viewLocationCache) { _pageFactory = pageFactory; _viewLocationExpanders = viewLocationExpanderProvider.ViewLocationExpanders; _viewLocationCache = viewLocationCache; } /// /// Gets the locations where this instance of will search for views. /// public virtual IEnumerable ViewLocationFormats { get { return _viewLocationFormats; } } /// /// Gets the locations where this instance of will search for views within an /// area. /// public virtual IEnumerable AreaViewLocationFormats { get { return _areaViewLocationFormats; } } /// public ViewEngineResult FindView([NotNull] ActionContext context, [NotNull] string viewName) { return CreateViewEngineResult(context, viewName, partial: false); } /// public ViewEngineResult FindPartialView([NotNull] ActionContext context, [NotNull] string partialViewName) { return CreateViewEngineResult(context, partialViewName, partial: true); } private ViewEngineResult CreateViewEngineResult(ActionContext context, string viewName, bool partial) { var nameRepresentsPath = IsSpecificPath(viewName); if (nameRepresentsPath) { if (viewName.EndsWith(ViewExtension, StringComparison.OrdinalIgnoreCase)) { var page = _pageFactory.CreateInstance(viewName); if (page != null) { return CreateFoundResult(context, page, viewName, partial); } } return ViewEngineResult.NotFound(viewName, new[] { viewName }); } else { return LocateViewFromViewLocations(context, viewName, partial); } } private ViewEngineResult LocateViewFromViewLocations(ActionContext context, string viewName, bool partial) { // Initialize the dictionary for the typical case of having controller and action tokens. var routeValues = context.RouteData.Values; var areaName = routeValues.GetValueOrDefault(AreaKey); // Only use the area view location formats if we have an area token. var viewLocations = !string.IsNullOrEmpty(areaName) ? AreaViewLocationFormats : ViewLocationFormats; var expanderContext = new ViewLocationExpanderContext(context, viewName); if (_viewLocationExpanders.Count > 0) { expanderContext.Values = new Dictionary(StringComparer.Ordinal); // 1. Populate values from viewLocationExpanders. foreach (var expander in _viewLocationExpanders) { expander.PopulateValues(expanderContext); } } // 2. With the values that we've accumumlated so far, check if we have a cached result. var viewLocation = _viewLocationCache.Get(expanderContext); if (!string.IsNullOrEmpty(viewLocation)) { var page = _pageFactory.CreateInstance(viewLocation); if (page != null) { // 2a. We found a IRazorPage at the cached location. return CreateFoundResult(context, page, viewName, partial); } } // 2b. We did not find a cached location or did not find a IRazorPage at the cached location. // The cached value has expired and we need to look up the page. foreach (var expander in _viewLocationExpanders) { viewLocations = expander.ExpandViewLocations(expanderContext, viewLocations); } // 3. Use the expanded locations to look up a page. var controllerName = routeValues.GetValueOrDefault(ControllerKey); var searchedLocations = new List(); foreach (var path in viewLocations) { var transformedPath = string.Format(CultureInfo.InvariantCulture, path, viewName, controllerName, areaName); var page = _pageFactory.CreateInstance(transformedPath); if (page != null) { // 3a. We found a page. Cache the set of values that produced it and return a found result. _viewLocationCache.Set(expanderContext, transformedPath); return CreateFoundResult(context, page, transformedPath, partial); } searchedLocations.Add(transformedPath); } // 3b. We did not find a page for any of the paths. return ViewEngineResult.NotFound(viewName, searchedLocations); } private ViewEngineResult CreateFoundResult(ActionContext actionContext, IRazorPage page, string viewName, bool partial) { // A single request could result in creating multiple IRazorView instances (for partials, view components) // and might store state. We'll use the service container to create new instances as we require. var services = actionContext.HttpContext.RequestServices; var view = services.GetService(); view.Contextualize(page, partial); return ViewEngineResult.Found(viewName, view); } private static bool IsSpecificPath(string name) { return name[0] == '~' || name[0] == '/'; } } }