很多时候在mvc项目中我们需要去扩展自己的视图引擎,大概看起来应该下面这个样子的:
public class RazorEngineExpand : RazorViewEngine { private void SetAdmin() { AreaPartialViewLocationFormats = new[] { "~/Admin/Views/{2}/{1}/Shared/{0}.cshtml", "~/Admin/Views/{2}/Views/{1}/{0}.cshtml", "~/Admin/Views/Shared/{0}.cshtml" }; AreaViewLocationFormats = new[] { "~/Admin/Views/{2}/{1}/{0}.cshtml", "~/Admin/Views/Shared/{0}.cshtml" }; AreaMasterLocationFormats = new[] { "~/Admin/Views/{2}/Shared/{0}.cshtml", "~/Admin/Views/Shared/{0}.cshtml", "~/Views/Shared/{0}.cshtml" }; } protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath) { return new RazorView(controllerContext, partialPath, layoutPath: null, runViewStartPages: false, viewStartFileExtensions: FileExtensions, viewPageActivator: ViewPageActivator); } protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath) { var view = new RazorView(controllerContext, viewPath, layoutPath: masterPath, runViewStartPages: true, viewStartFileExtensions: FileExtensions, viewPageActivator: ViewPageActivator); return view; } }
我们要扩展自己的Razor视图引擎,则要继承自RazorViewEngine,并重写里面的相关的方法,RazorViewEngine最终实现的是IViewEngine接口,但是IViewEngine接口里面的方法:
public interface IViewEngine
{
ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache);
ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache);
void ReleaseView(ControllerContext controllerContext, IView view);
}
两个寻找视图的方法和一个释放视图资源的方法。根据传入的参数,大体的实现,我们几乎可以猜到。在抽象类VirtualPathProviderViewEngine中实现了这三个方法,我们注意到在该类中可以看到一些在RazorViewEngine中看到的字符串数组:
public string[] AreaMasterLocationFormats { get; set; } public string[] AreaPartialViewLocationFormats { get; set; } public string[] AreaViewLocationFormats { get; set; } public string[] FileExtensions { get; set; } public string[] MasterLocationFormats { get; set; } public string[] PartialViewLocationFormats { get; set; } public IViewLocationCache ViewLocationCache { get; set; } public string[] ViewLocationFormats { get; set; }
这些属性最终会在RazorViewEngine中得到初始化。我们主要来看FindView方法,FindPartialView方法的具体实现和FindView无多大区别,我们只介绍FindView即可:
public virtual ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache) { if (controllerContext == null) { throw new ArgumentNullException("controllerContext"); } if (String.IsNullOrEmpty(viewName)) { throw new ArgumentException(MvcResources.Common_NullOrEmpty, "viewName"); } string[] viewLocationsSearched; string[] masterLocationsSearched; string controllerName = controllerContext.RouteData.GetRequiredString("controller"); string viewPath = GetPath(controllerContext, ViewLocationFormats, AreaViewLocationFormats, "ViewLocationFormats", viewName, controllerName, CacheKeyPrefixView, useCache, out viewLocationsSearched); string masterPath = GetPath(controllerContext, MasterLocationFormats, AreaMasterLocationFormats, "MasterLocationFormats", masterName, controllerName, CacheKeyPrefixMaster, 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); }
我们可以看到return的语句可以发现最终是将查找到的view经过一些列的处理包装成ViewEngineResult对象返回了。我们逐一来看,首先获取到控制器,获取view文件的虚拟路径和母版文件的虚拟路径,然后根据这些参数,主要是view文件的虚拟路径来创建RazorView对象,最后封装成ViewEngineResult对象返回。
这里我们主要来看一下是如何获取到view文件的虚拟路径的,我们来看GetPath方法的具体实现:
private string GetPath(ControllerContext controllerContext, string[] locations, string[] areaLocations, string locationsPropertyName, string name, string controllerName, string cacheKeyPrefix, bool useCache, out string[] searchedLocations) { searchedLocations = _emptyLocations; if (String.IsNullOrEmpty(name)) { return String.Empty; } string areaName = AreaHelpers.GetAreaName(controllerContext.RouteData); bool usingAreas = !String.IsNullOrEmpty(areaName); List<ViewLocation> viewLocations = GetViewLocations(locations, (usingAreas) ? areaLocations : null); if (viewLocations.Count == 0) { throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, MvcResources.Common_PropertyCannotBeNullOrEmpty, locationsPropertyName)); } bool nameRepresentsPath = IsSpecificPath(name); string cacheKey = CreateCacheKey(cacheKeyPrefix, name, (nameRepresentsPath) ? String.Empty : controllerName, areaName); 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, ref searchedLocations); } }
首先判断是否使用了区域功能,并以此判断作为条件获取到所有的view的位置集合。然后看到一个是否使用了缓存功能,应该是对查找的view的虚拟路径进行的缓存,如果是的话,直接从缓存中获取要得到的view的虚拟路径。我们跳过最后,看到他返回的是什么?根据nameRepresentsPath调用不同的方法,nameRepresentsPath是由IsSpecificPath返回:
private static bool IsSpecificPath(string name) { char c = name[0]; return (c == '~' || c == '/'); }
了解之前首先的知道传入的name代表的是什么?追朔到FindView中我们可以发现name代表的就是viewName或是masterName。根据viewName来判断他是否是一个特殊的路径?感觉有点奇怪,应该说这个viewName和path应该都是指路径的意思。因此我们可以解释说是根据传入的路径来判断是虚拟路径还是绝对路径。然后我们再根据GetPathFromSpecificName方法的实现:
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; }
这个方法直接就是将name给返回去,也就是我们前面所说的view绝对路径了---nameRepresentsPath(代表路径嘛)GetPathFromGeneralName方法的实现:
private string GetPathFromGeneralName(ControllerContext controllerContext, List<ViewLocation> locations, string name, string controllerName, string areaName, string cacheKey, 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); 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; }
这个方法里面我们首先来看searchedLocations这个字符数组变量,由virtualPath赋值,virtualPath里面存储的就是有viewName,controllerName和areaName(假如有的话)构成的虚拟路径。因此searchedLocations数组里面存储的就是查找view的时候搜索过的所有路径集合。类似于一下这张图所示,代表着搜索view时的查找“路线”:
我们看返回的result,实际上代表的就是view的虚拟路径。因此我们可以总结一下这里面查找view做的事情:网上有位仁兄有个很好的总结:
- 获取视图位置(GetViewLocations)
- 检查是否使用了区域(Area)
- 如果使用了区域,则把areaLocations传入
- GetViewLocations方法会将locations和areaLocations这两个字符串数组包装和合并成一个ViewLocation的集合
- 如果集合没有东西,那么抛异常
- 缓存检索
- 获取路径
- 如果名称像是一个绝对路径("/"或"~"开头)
- 检查虚拟路径所指向的文件是否存在(FileExists)
- 存在则返回名称(当作路径)。
- 否则返回空字符串。
- 如果名称不像是一个绝对路径
- 遍历所有的视图位置生成虚拟路径
- 如果虚拟路径所指向的文件存在,则返回这个虚拟路径。
- 如果所有生成的虚拟路径所指向的文件都不存在,则返回空字符串。
- 如果名称像是一个绝对路径("/"或"~"开头)
缓存处理部分我并不关心,现在从外部来看GetPath方法,那么它的参数分为三大部分:
- 缓存部分
- controllerContext(主要利用里面的HttpContext.Cache模块)
- cacheKeyPrefix
- useCache
- 位置部分:
- locations和areaLocations,这是虚拟路径的模版,使用的值是VirtualPathProviderViewEngine的公开属性。
- locationsPropertyName,这个用于抛异常的时候指示使用的哪个Property。
- 名称部分:
- name,这个参数会是viewName或者masterName
- controllerName,这个参数标识了控制器的名称
- areaName,没有出现在参数中,但利用controllerContext提取了出来,事实上controllerName也是从controllerContext中提取的,性质一样。