BrnShop开源网上商城第五讲:自定义视图引擎

今天这篇博文主要讲解自定义视图引擎,大家都知道在asp.net mvc框架中默认自带一个Razor视图引擎,除此之外我们也可以自定义自己的视图引擎,只需要实现IViewEngine接口,接口定义如下:

  • ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
  • ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
  • void ReleaseView(ControllerContext controllerContext, IView view)

  下面我们详细介绍下这三个方法,首先是FindView方法:这个方法的作用是使用指定的控制器上下文来查找指定的视图,说的通俗点就是当我们在控制器中使用诸如"return View("视图名称")"方法时来查找指定的视图文件时使用的。它的四个参数解释如下:

  • controllerContext:控制器上下文。这个大家都知道什么意思所以就不说了
  • viewName:视图名称。在控制器中我们一般这样指定:return View("视图名称")
  • masterName:母版页视图名称。也就是我们在视图文件中通过"Layout"指定的视图名称
  • useCache:是否使用缓存。如果使用缓存就自动在缓存记录中查找,此时如果找到就直接使用,如果没有找到再去磁盘上查找。如果不使用缓存需要每次都到磁盘本地进行查找

  再来说下FindPartialView方法:这个方法的作用是使用指定的控制器上下文查找指定的分部视图,和上面方法对比就知道这个方法是用来查找分部视图的。我们在控制器中使用诸如"return PartialView("分部视图名称")"方法时来查找指定的分部视图文件时使用的。它的三个参数(由于分部视图没有母版页所以不需要masterName参数)解释如下:

  • controllerContext:控制器上下文。
  • partialViewName:分部视图名称。在控制器中我们一般这样指定:return PartialView("分部视图名称")
  • useCache:是否使用缓存。具体解释参见FindView方法

  最后是ReleaseView方法,这个方法的作用是使用指定的控制器上下文来释放指定的视图,说白了就是释放视图使用的资源。它的参数比较简单,具体如下:

  • controllerContext:控制器上下文。
  • view:视图。我们知道所有的视图文件在运行时都会编译成一个类,这个类实现了IView 接口,所以这个参数就是指的这个类。

  

  如果我们想自定义自己的视图引擎只需要实现自定义一个类,然后此类继承IViewEngine接口并实现它的3个方法就可以了。但这样做我们的工作量会很大,所以有没有一种更方便的方法呢?答案是有的,那就是继承VirtualPathProviderViewEngine类,然后重写FindView和FindPartialView方法就可以了(ReleaseView方法不要重写,可以直接使用VirtualPathProviderViewEngine中的实现)。现在我们来看看VirtualPathProviderViewEngine到底是何方神物,代码如下:

1
abstract class VirtualPathProviderViewEngine : IViewEngine

  由于VirtualPathProviderViewEngine类的代码太多,所以我只帖出了它的定义,大家可以看到VirtualPathProviderViewEngine类已经实现了IViewEngine接口,所以我们可以借助这个类来简化我们自定义视图引擎的复杂度和工作量。

  下面我们以多店版网上商城BrnMall为例带着大家做一个例子:

  首先我们定义一个类ThemeVirtualPathProviderViewEngine,并继承自VirtualPathProviderViewEngine,代码如下:

1
2
3
4
/// <summary>
/// 主题路径提供者视图引擎
/// </summary>
public abstract class ThemeVirtualPathProviderViewEngine : VirtualPathProviderViewEngine

  继承了这个类后我们就可以重写它的FindView和FindPartialView方法了,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#region 重写方法
 
/// <summary>
/// 使用指定的控制器上下文和母版视图名称来查找指定的视图
/// </summary>
/// <param name="controllerContext">控制器上下文</param>
/// <param name="viewName">视图的名称</param>
/// <param name="masterName">母版视图的名称</param>
/// <param name="useCache">若为 true,则使用缓存的视图</param>
/// <returns>页视图</returns>
public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
    //判断是否为移动访问
    bool mobile = WebHelper.IsMobile();
 
    //如果为移动访问,则构建一个新的视图名称
    string overrideViewName = mobile ? string.Format("{0}.{1}", viewName, _mobileviewmodifier) : viewName;
    //构建一个视图引擎结果
    ViewEngineResult result = FindThemeView(controllerContext, overrideViewName, masterName, useCache, mobile);
 
    //如果为移动访问且没有对应视图文件时采用原视图名称解析
    if (mobile && (result == null || result.View == null))
        result = FindThemeView(controllerContext, viewName, masterName, useCache, false);
    return result;
 
}
 
/// <summary>
/// 寻找分部视图的方法
/// </summary>
/// <param name="controllerContext">控制器上下文</param>
/// <param name="partialViewName">分部视图的名称</param>
/// <param name="useCache">若为 true,则使用缓存的分部视图</param>
/// <returns>分部视图</returns>
public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
{
    //判断是否为移动访问
    bool mobile = WebHelper.IsMobile();
 
    //如果为移动访问,则构建一个新的分部视图名称
    string overrideViewName = mobile ? string.Format("{0}.{1}", partialViewName, _mobileviewmodifier) : partialViewName;
    //构建一个分部视图引擎结果
    ViewEngineResult result = FindThemePartialView(controllerContext, overrideViewName, useCache, mobile);
 
    //如果为移动访问且没有对应分部视图文件时采用原分部视图名称解析
    if (mobile && (result == null || result.View == null))
        result = FindThemePartialView(controllerContext, partialViewName, useCache, false);
    return result;
}
 
#endregion

  在这两个方法中我们可以根据自己的需要添加必要的东西。例如BrnMall商城需要判断访问者是否使用手机浏览器访问商城,如果是则切换到专门针对手机浏览器优化的视图(即在视图名称后面添加.Mobile,构建一个新的视图名称)。具体代码实现大家可以参考上面。

  现在我们已经将我们的业务需要融合进视图引擎中,接下来就是去磁盘查找对应视图文件,不过在查找之前我们还需要做点事情,那就是构建磁盘查找路径结果,方便在视图文件没有找到时给出提示,所以我们在上面两个重写方法中并没有马上去磁盘查找视图文件,而是调用了构建查找路径结果的两个方法,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/// <summary>
/// 构建视图引擎结果
/// </summary>
private ViewEngineResult FindThemeView(ControllerContext controllerContext, string viewName, string masterName, bool useCache, bool mobile)
{
    //视图文件路径搜索列表
    string[] strArray1 = null;
    //布局文件路径搜索列表
    string[] strArray2 = null;
 
    //获取控制器名称
    string controllerName = controllerContext.RouteData.GetRequiredString("controller");
 
    //获取视图文件路径
    string viewPath = GetPath(controllerContext, viewName, controllerName, "View", useCache, mobile, out strArray1);
 
    //当视图文件存在时
    if (!string.IsNullOrWhiteSpace(viewPath))
    {
        if (string.IsNullOrWhiteSpace(masterName))
        {
            return new ViewEngineResult(CreateView(controllerContext, viewPath, string.Empty), this);
        }
        else
        {
            //获取布局文件的路径
            string masterPath = GetPath(controllerContext, masterName, controllerName, "Master", useCache, mobile, out strArray2);
            if (!string.IsNullOrWhiteSpace(masterPath))
            {
                return new ViewEngineResult(CreateView(controllerContext, viewPath, masterPath), this);
            }
        }
    }
 
    //当视图文件或布局文件不存在时将搜索路径返回
    if (strArray2 == null)
    {
        return new ViewEngineResult(strArray1);
    }
    else
    {
        return new ViewEngineResult(strArray1.Union<string>(strArray2));
    }
}
 
/// <summary>
/// 构建分部视图引擎结果
/// </summary>
private ViewEngineResult FindThemePartialView(ControllerContext controllerContext, string partialViewName, bool useCache, bool mobile)
{
    //分部视图文件路径搜索列表
    string[] strArray;
 
    //获取控制器名称
    string controllerName = controllerContext.RouteData.GetRequiredString("controller");
 
    //获取分部视图文件路径
    string partialViewPath = GetPath(controllerContext, partialViewName, controllerName, "Partial", useCache, mobile, out strArray);
 
    //当分部视图文件存在时
    if (!string.IsNullOrWhiteSpace(partialViewPath))
    {
        return new ViewEngineResult(CreatePartialView(controllerContext, partialViewPath), this);
    }
    //分部视图文件不存在时返回搜索路径
    return new ViewEngineResult(strArray);
}

  在上面的方法中我们根据GetPath方法返回的结果分别进行处理,具体如下:

  • 如果返回的结果为空,代表视图文件不存在,我们需要将视图文件查找路径构建成一个数组并调用ViewEngineResult类的public ViewEngineResult(IEnumerable<string> searchedLocations)构造函数
  • 如果返回的结果不为空,代表视图文件存在,那么我们调用ViewEngineResult类的public ViewEngineResult(IView view, IViewEngine viewEngine)构造函数。

  关于上面有两点需要补充下:

  • ViewEngineResult类:这个类代表一个视图引擎查找结果,它有两个属性分别是IEnumerable<string>类型的SearchedLocations,和IView类型的View。当我们使用构造函数ViewEngineResult(IEnumerable<string> searchedLocations)初始化时会将参数searchedLocations赋值给属性SearchedLocations,此时属性View为空。当我们使用构造函数ViewEngineResult(IView view, IViewEngine viewEngine)初始化时会将参数view赋值给属性View,此时属性SearchedLocations为空。MVC框架会根据属性View是否为空来呈现不同的结果,如果View属性为空则直接将视图路径查找结果输出,如果不为空则输出此视图。
  • CreateView和CreatePartialView方法:这两个方法可以把视图文件路径转换为对应的视图(即实现IView接口的类)

  万事俱备,现在可以查找视图文件的路径了,我们以多店版网上商城BrnMall为例,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/// <summary>
/// 获取文件的路径
/// </summary>
private string GetPath(ControllerContext controllerContext, string name, string controllerName, string cacheKeyPrefix, bool useCache, bool mobile, out string[] searchedLocations)
{
    searchedLocations = null;
 
    //视图位置列表
    string[] locations = null;
    //主题
    string theme = string.Empty;
 
    //获取区域
    string area = GetRouteDataTokenValue("area", controllerContext.RouteData.DataTokens).ToLower();
    if (string.IsNullOrWhiteSpace(area))//商城前台视图位置的处理
    {
        theme = GetRouteDataTokenValue("theme", controllerContext.RouteData.DataTokens);
        if (theme == "")//商城页面
        {
            locations = new string[2] { "~/Views/{1}/{0}.cshtml""~/Views/Shared/{0}.cshtml" };
        }
        else//店铺页面
        {
            locations = new string[1] { "~/Themes/{2}/Views/{0}.cshtml" };
        }
    }
    else//店铺后台和商城后台视图位置的处理
    {
        //不能通过移动访问后台
        if (mobile)
        {
            searchedLocations = new string[0];
            return string.Empty;
        }
        if (area == "storeadmin")//访问店铺后台管理区域
        {
            locations = new string[2] { "~/Admin_Store/Views/{1}/{0}.cshtml""~/Admin_Store/Views/Shared/{0}.cshtml" };
        }
        else if (area == "malladmin")//访问商城后台管理区域
        {
            locations = new string[2] { "~/Admin_Mall/Views/{1}/{0}.cshtml""~/Admin_Mall/Views/Shared/{0}.cshtml" };
        }
    }
 
    //是否为特殊路径的标识
    bool flag2 = IsSpecificName(name);
 
    //从缓存中获取视图位置
    string cacheKey = CreateCacheKey(cacheKeyPrefix, name, flag2 ? string.Empty : controllerName, area, theme);//视图位置的缓存键
    if (useCache)
    {
        //从缓存中得到视图位置
        var cachedPath = ViewLocationCache.GetViewLocation(controllerContext.HttpContext, cacheKey);
        if (cachedPath != null)
        {
            return cachedPath;
        }
    }
 
    //如果视图位置不在缓存中,则构建视图位置并存储到缓存中
    if (!flag2)//不是特殊路径时的操作
    {
        return GetPathFromGeneralName(controllerContext, locations, name, controllerName, theme, cacheKey, ref searchedLocations);
    }
    else//特殊路径时的操作
    {
        return GetPathFromSpecificName(controllerContext, name, cacheKey, ref searchedLocations);
    }
}

  在这个方法中我们根据当前访问的位置(商城页面,店铺页面,店铺后台页面,系统后台页面)构建不同的查找路径,具体大家可以参考商品的if else 语句。

  这里有两点需要注意下,第一点是视图文件名的格式问题:

  我们都知道在控制器的View方法中我们可以传入视图文件名称,也可以传入视图文件的路径,代码如下:

1
2
return View("视图文件名称");//只传入视图文件名称
return View("~/Views/视图文件名称.cshtml");//传入视图文件的路径

  所以我们需要判断此时视图名称的类型,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
/// <summary>
/// 判读视图名称是否以“~”或“/”开头
/// </summary>
private bool IsSpecificName(string name)
{
    char ch = name[0];
    if (ch != '~')
    {
        return (ch == '/');
    }
    return true;
}

  第二点是视图路径缓存问题,当参数useCache为真时我们首先通过缓存获取路径,如果缓存中存在则直接返回,否则去磁盘上查找。缓存键的生成代码如下:

1
2
3
4
5
6
7
/// <summary>
/// 创建视图位置的缓存键
/// </summary>
private string CreateCacheKey(string prefix, string name, string controllerName, string area, string theme)
{
    return string.Format(":ViewCacheKey:{0}:{1}:{2}:{3}:{4}:{5}"new object[] { base.GetType().AssemblyQualifiedName, prefix, name, controllerName, area, theme });
}

  最后就是真正的视图文件路径查找了,具体代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/// <summary>
/// 特殊名称时构建视图路径
/// </summary>
private string GetPathFromSpecificName(ControllerContext controllerContext, string name, string cacheKey, ref string[] searchedLocations)
{
    //将路径添加到视图位置缓存
    ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, cacheKey, name);
    //扩展名不为“.cshtml”或者文件不存在时
    if (!IsSupportedExtension(name) || !FileExists(controllerContext, name))
    {
        searchedLocations = new string[] { name };
        return string.Empty;
    }
    return name;
}
 
/// <summary>
/// 普通名称时构建视图路径
/// </summary>
private string GetPathFromGeneralName(ControllerContext controllerContext, string[] viewLocationFormats, string name, string controllerName, string theme, string cacheKey, ref string[] searchedLocations)
{
    int count = viewLocationFormats.Length;
    searchedLocations = new string[count];
 
    //循环视图位置
    for (int i = 0; i < count; i++)
    {
        string path = string.Format(viewLocationFormats[i], name, controllerName, theme);
        if (FileExists(controllerContext, path))
        {
            searchedLocations = null;
            ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, cacheKey, path);
            return path;
        }
        //将路径添加到搜索位置列表中
        searchedLocations[i] = path;
    }
 
    return string.Empty;
}

  如果视图名称为视图路径格式时直接查找路径指定的文件是否存在;如果视图名称为普通名称时根据路径格式列表依次构件真实路径并查找。无论上述哪种情况,当视图文件不存在时都要将查找路径输出,视图文件存在且使用缓存时将其保存到缓存中以便下次直接使用。

  到了这步还不算完,因为这只是自定义了路径查找类,还需要两步来构造完整的视图引擎。第一步实现主题构建引擎,代码如下:

1
2
3
4
5
6
7
8
9
10
11
/// <summary>
/// 主题构建引擎
/// </summary>
public abstract class ThemeBuildManagerViewEngine : ThemeVirtualPathProviderViewEngine
{
    //判读文件是否存在
    protected override bool FileExists(ControllerContext controllerContext, string virtualPath)
    {
        return BuildManager.GetObjectFactory(virtualPath, false) != null;
    }
}

  第二步是实现视图引擎,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/// <summary>
/// 主题视图引擎
/// </summary>
public class ThemeRazorViewEngine : ThemeBuildManagerViewEngine
{
    /// <summary>
    /// 创建Razor视图
    /// </summary>
    protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
    {
        return new RazorView(controllerContext, viewPath, masterPath, true, FileExtensions);
    }
 
    /// <summary>
    /// 创建Razor分部视图
    /// </summary>
    protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
    {
        return new RazorView(controllerContext, partialPath, nullfalse, FileExtensions);
    }
}

  至此我们的自定义视图引擎已经完成,如果想要使用此引擎只需要在Global.asax中替换掉默认视图引擎即可。代码如下:

1
2
3
4
5
6
7
protected void Application_Start()
{
    //将默认视图引擎替换为ThemeRazorViewEngine引擎
    ViewEngines.Engines.Clear();
    ViewEngines.Engines.Add(new ThemeRazorViewEngine());
 
}

  

  有对网上商城程序设计感兴趣的朋友,欢迎加入QQ群:235274151,大家可以交流下!

原文地址:https://www.cnblogs.com/Alex80/p/4266828.html