在上一文中,我们基本上实现了主题模板。我们来个更高级点的。^-^
PS:本内容参考了nopcommerce 主题实现的代码。
我们继续查看 RazorViewEngine 的源代码,RazorViewEngine 类继承 BuildManagerViewEngine 类,而 BuildManagerViewEngine 类 又继承 VirtualPathProviderViewEngine 类。VirtualPathProviderViewEngine 类 是继承 IViewEngine 接口。 IViewEngine 接口 是 asp.net mvc ViewEngine 的一个底层接口类。
从类名我们可以看出,VirtualPathProviderViewEngine 类 是定义了 VirtualPath 的一个操作。它也是一个抽象类。
我们从它定义的方法中可以看出,重点 就在 FindPartialView 和 FindView 这两个方法,且可以重写。回到前面,我们要实现的主题模板目录结构是这样的。
用 VirtualPath 来表示就是 : ~/Themes/{themename}/views 。其中 {themename} 就是主题所在文件夹,这个文件夹下,包含了 views 文件夹, views 文件夹 和原来的 views 文件夹 结构保持一致,即 views/{controller}/{action} 。
好了。 在上一篇中,我们发现视图文件查找的路径是在 ~/views 文件夹,对应的属性是 ViewLocationFormats 这种后缀是 Formats 的属性, 且参数只有 {controller} 和 {action},我们需要在加一个 {theme} 。我们需要重写 VirtualPathProviderViewEngine 类 中的 FindPartialView 和 FindView 这两个方法 ,让它查找的时候 加上 {theme} 。
定义一个类 继承 VirtualPathProviderViewEngine 类。重写 FindPartialView 和 FindView 。我们可以直接从 源码中 复制过来。
写好之后:
ThemeableVirtualPathProviderViewEngine.cs :
public abstract class ThemeableVirtualPathProviderViewEngine : VirtualPathProviderViewEngine
{ // format is ":ViewCacheEntry:{cacheType}:{prefix}:{name}:{controllerName}:{areaName}:{theme}:" private const string CacheKeyFormat = ":ViewCacheEntry:{0}:{1}:{2}:{3}:{4}:{5}:"; private const string CacheKeyPrefixMaster = "Master";
private const string CacheKeyPrefixPartial = "Partial";
private const string CacheKeyPrefixView = "View";
private static readonly string[] _emptyLocations = new string[0];
internal Func<string, string> GetExtensionThunk = VirtualPathUtility.GetExtension;
protected IThemeContext _themeContext;
public ThemeableVirtualPathProviderViewEngine()
: this(new ThemeContext())
{
}
public ThemeableVirtualPathProviderViewEngine(IThemeContext themeContext)
{
this._themeContext = themeContext;
}
public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
if (String.IsNullOrEmpty(partialViewName))
{
throw new ArgumentNullException("partialViewName");
}
string theme = _themeContext.CurentThemeName;
string[] searched;
string controllerName = controllerContext.RouteData.GetRequiredString("controller");
string partialPath = GetPath(controllerContext, PartialViewLocationFormats, AreaPartialViewLocationFormats, "PartialViewLocationFormats", partialViewName, controllerName, CacheKeyPrefixPartial, theme, useCache, out searched);
if (String.IsNullOrEmpty(partialPath))
{
return new ViewEngineResult(searched);
}
return new ViewEngineResult(CreatePartialView(controllerContext, partialPath), this);
//return base.FindPartialView(controllerContext, partialViewName, useCache);
}
public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
if (String.IsNullOrEmpty(viewName))
{
throw new ArgumentNullException("viewName");
}
string theme = _themeContext.CurentThemeName;
string[] viewLocationsSearched;
string[] masterLocationsSearched;
string controllerName = controllerContext.RouteData.GetRequiredString("controller");
string viewPath = GetPath(controllerContext, ViewLocationFormats, AreaViewLocationFormats, "ViewLocationFormats", viewName, controllerName, CacheKeyPrefixView, theme, useCache, out viewLocationsSearched);
string masterPath = GetPath(controllerContext, MasterLocationFormats, AreaMasterLocationFormats, "MasterLocationFormats", masterName, controllerName, CacheKeyPrefixMaster, theme, useCache, out masterLocationsSearched);
if (String.IsNullOrEmpty(viewPath) || (String.IsNullOrEmpty(masterPath) && !String.IsNullOrEmpty(masterName)))
{
return new ViewEngineResult(viewLocationsSearched.Union(masterLocationsSearched));
}
return new ViewEngineResult(CreateView(controllerContext, viewPath, masterPath), this);
}
protected string CreateCacheKey(string prefix, string name, string controllerName, string areaName, string theme)
{
return String.Format(CultureInfo.InvariantCulture, CacheKeyFormat, GetType().AssemblyQualifiedName, prefix, name, controllerName, areaName, theme);
}
private string GetPath(ControllerContext controllerContext, string[] locations, string[] areaLocations, string locationsPropertyName, string name, string controllerName, string cacheKeyPrefix, string theme, bool useCache, out string[] searchedLocations)
{
searchedLocations = _emptyLocations;
if (String.IsNullOrEmpty(name))
{
return String.Empty;
}
string areaName = GetAreaName(controllerContext.RouteData);
bool usingAreas = !String.IsNullOrEmpty(areaName);
List<ViewLocation> viewLocations = GetViewLocations(locations, (usingAreas) ? areaLocations : null);
if (viewLocations.Count == 0)
{
throw new InvalidOperationException("locationsPropertyName");
}
bool nameRepresentsPath = IsSpecificPath(name);
string cacheKey = CreateCacheKey(cacheKeyPrefix, name, (nameRepresentsPath) ? String.Empty : controllerName, areaName, theme);
if (useCache)
{
// Only look at cached display modes that can handle the context.
IEnumerable<IDisplayMode> possibleDisplayModes = DisplayModeProvider.GetAvailableDisplayModesForContext(controllerContext.HttpContext, controllerContext.DisplayMode);
foreach (IDisplayMode displayMode in possibleDisplayModes)
{
string cachedLocation = ViewLocationCache.GetViewLocation(controllerContext.HttpContext, AppendDisplayModeToCacheKey(cacheKey, displayMode.DisplayModeId));
if (cachedLocation == null)
{
// If any matching display mode location is not in the cache, fall back to the uncached behavior, which will repopulate all of our caches.
return null;
}
// A non-empty cachedLocation indicates that we have a matching file on disk. Return that result.
if (cachedLocation.Length > 0)
{
if (controllerContext.DisplayMode == null)
{
controllerContext.DisplayMode = displayMode;
}
return cachedLocation;
}
// An empty cachedLocation value indicates that we don't have a matching file on disk. Keep going down the list of possible display modes.
}
// GetPath is called again without using the cache.
return null;
}
else
{
return nameRepresentsPath
? GetPathFromSpecificName(controllerContext, name, cacheKey, ref searchedLocations)
: GetPathFromGeneralName(controllerContext, viewLocations, name, controllerName, areaName, cacheKey, theme, ref searchedLocations);
}
}
private string GetPathFromGeneralName(ControllerContext controllerContext, List<ViewLocation> locations, string name, string controllerName, string areaName, string cacheKey, string theme, ref string[] searchedLocations)
{
string result = String.Empty;
searchedLocations = new string[locations.Count];
for (int i = 0; i < locations.Count; i++)
{
ViewLocation location = locations[i];
string virtualPath = location.Format(name, controllerName, areaName, theme);
DisplayInfo virtualPathDisplayInfo = DisplayModeProvider.GetDisplayInfoForVirtualPath(virtualPath, controllerContext.HttpContext, path => FileExists(controllerContext, path), controllerContext.DisplayMode);
if (virtualPathDisplayInfo != null)
{
string resolvedVirtualPath = virtualPathDisplayInfo.FilePath;
searchedLocations = _emptyLocations;
result = resolvedVirtualPath;
ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, AppendDisplayModeToCacheKey(cacheKey, virtualPathDisplayInfo.DisplayMode.DisplayModeId), result);
if (controllerContext.DisplayMode == null)
{
controllerContext.DisplayMode = virtualPathDisplayInfo.DisplayMode;
}
// Populate the cache for all other display modes. We want to cache both file system hits and misses so that we can distinguish
// in future requests whether a file's status was evicted from the cache (null value) or if the file doesn't exist (empty string).
IEnumerable<IDisplayMode> allDisplayModes = DisplayModeProvider.Modes;
foreach (IDisplayMode displayMode in allDisplayModes)
{
if (displayMode.DisplayModeId != virtualPathDisplayInfo.DisplayMode.DisplayModeId)
{
DisplayInfo displayInfoToCache = displayMode.GetDisplayInfo(controllerContext.HttpContext, virtualPath, virtualPathExists: path => FileExists(controllerContext, path));
string cacheValue = String.Empty;
if (displayInfoToCache != null && displayInfoToCache.FilePath != null)
{
cacheValue = displayInfoToCache.FilePath;
}
ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, AppendDisplayModeToCacheKey(cacheKey, displayMode.DisplayModeId), cacheValue);
}
}
break;
}
searchedLocations[i] = virtualPath;
}
return result;
}
private string GetPathFromSpecificName(ControllerContext controllerContext, string name, string cacheKey, ref string[] searchedLocations)
{
string result = name;
if (!(FilePathIsSupported(name) && FileExists(controllerContext, name)))
{
result = String.Empty;
searchedLocations = new[] { name };
}
ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, cacheKey, result);
return result;
}
private bool FilePathIsSupported(string virtualPath)
{
if (FileExtensions == null)
{
// legacy behavior for custom ViewEngine that might not set the FileExtensions property
return true;
}
else
{
// get rid of the '.' because the FileExtensions property expects extensions withouth a dot.
string extension = GetExtensionThunk(virtualPath).TrimStart('.');
return FileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
}
}
private static List<ViewLocation> GetViewLocations(string[] viewLocationFormats, string[] areaViewLocationFormats)
{
List<ViewLocation> allLocations = new List<ViewLocation>();
if (areaViewLocationFormats != null)
{
foreach (string areaViewLocationFormat in areaViewLocationFormats)
{
allLocations.Add(new AreaAwareViewLocation(areaViewLocationFormat));
}
}
if (viewLocationFormats != null)
{
foreach (string viewLocationFormat in viewLocationFormats)
{
allLocations.Add(new ViewLocation(viewLocationFormat));
}
}
return allLocations;
}
private static bool IsSpecificPath(string name)
{
char c = name[0];
return (c == '~' || c == '/');
}
protected virtual string GetAreaName(RouteData routeData)
{
object obj2;
if (routeData.DataTokens.TryGetValue("area", out obj2))
{
return (obj2 as string);
}
return GetAreaName(routeData.Route);
}
protected virtual string GetAreaName(RouteBase route)
{
var area = route as IRouteWithArea;
if (area != null)
{
return area.Area;
}
var route2 = route as Route;
if ((route2 != null) && (route2.DataTokens != null))
{
return (route2.DataTokens["area"] as string);
}
return null;
}
private string AppendDisplayModeToCacheKey(string cacheKey, string displayMode)
{
return cacheKey + displayMode + ":";
}
private class AreaAwareViewLocation : ViewLocation
{
public AreaAwareViewLocation(string virtualPathFormatString)
: base(virtualPathFormatString)
{
} public override string Format(string viewName, string controllerName, string areaName, string theme) { return String.Format(CultureInfo.InvariantCulture, _virtualPathFormatString, viewName, controllerName, areaName, theme); } }
private class ViewLocation
{
protected string _virtualPathFormatString;
public ViewLocation(string virtualPathFormatString)
{
_virtualPathFormatString = virtualPathFormatString;
} public virtual string Format(string viewName, string controllerName, string areaName, string theme) { return String.Format(CultureInfo.InvariantCulture, _virtualPathFormatString, viewName, controllerName, theme); } }
}
为了便于获取当前主题名称,我们定义了一个接口,IThemeContext ,以及接口的实现 ThemeContext
IThemeContext.cs :
public interface IThemeContext { string CurentThemeName { get; } }
ThemeContext.cs:
public class ThemeContext : IThemeContext
{
public string CurentThemeName
{
get
{
// TODO . 获取当前theme
return "default";
}
}
}
修改上次的 MyThemeViewEngine.cs 文件,让它继承 ThemeableVirtualPathProviderViewEngine.cs,
public class MyThemeViewEngine : ThemeableVirtualPathProviderViewEngine { public MyThemeViewEngine() { base.AreaViewLocationFormats = new string[] { "~/Areas/{2}/Themes/{3}/Views/{1}/{0}.cshtml", "~/Areas/{2}/Themes/{3}/Views/Shared/{0}.cshtml", "~/Areas/{2}/Views/{1}/{0}.cshtml", "~/Areas/{2}/Views/Shared/{0}.cshtml", }; base.AreaMasterLocationFormats = new string[] { "~/Areas/{2}/Themes/{3}/Views/{1}/{0}.cshtml", "~/Areas/{2}/Themes/{3}/Views/Shared/{0}.cshtml", "~/Areas/{2}/Views/{1}/{0}.cshtml", "~/Areas/{2}/Views/Shared/{0}.cshtml", }; base.AreaPartialViewLocationFormats = new string[] { "~/Areas/{2}/Themes/{3}/Views/{1}/{0}.cshtml", "~/Areas/{2}/Themes/{3}/Views/Shared/{0}.cshtml", "~/Areas/{2}/Views/{1}/{0}.cshtml", "~/Areas/{2}/Views/Shared/{0}.cshtml", }; base.ViewLocationFormats = new string[] { "~/Themes/{2}/Views/{1}/{0}.cshtml", "~/Themes/{2}/Views/Shared/{0}.cshtml", "~/Views/{1}/{0}.cshtml", "~/Views/Shared/{0}.cshtml", }; base.MasterLocationFormats = new string[] { "~/Themes/{2}/Views/{1}/{0}.cshtml", "~/Themes/{2}/Views/Shared/{0}.cshtml", "~/Views/{1}/{0}.cshtml", "~/Views/Shared/{0}.cshtml", }; base.PartialViewLocationFormats = new string[] { "~/Themes/{2}/Views/{1}/{0}.cshtml", "~/Themes/{2}/Views/Shared/{0}.cshtml", "~/Views/{1}/{0}.cshtml", "~/Views/Shared/{0}.cshtml", }; base.FileExtensions = new string[] { "cshtml" }; } protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath) { IEnumerable<string> fileExtensions = base.FileExtensions; return new RazorView(controllerContext, partialPath, null, false, fileExtensions); } protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath) { IEnumerable<string> fileExtensions = base.FileExtensions; return new RazorView(controllerContext, viewPath, masterPath, true, fileExtensions); } }
这样,我们就将 {theme} 写进里面去了。 更改 Formats 里面的 文件路径顺序,将改变文件查找时的优先级。(现在的结果是,如果 ~/views/ 和 ~/themes/ 里面有相同的文件,将优先使用 themes 里面的文件,)