asp.net mvc theme主题皮肤的实现方法(二)

上一文中,我们基本上实现了主题模板。我们来个更高级点的。^-^ 

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 里面的文件,)