第一境 ASP.Net MVC5项目初探 — 第三节:View层简单改造
看到这里,您一定已经迫不及待的想要动手了。下面,我们通过一些对视图的修改,来深入了解Razor能给我们带来什么惊喜。
一、修改页面标题
有过HTML开发经验的读者,都知道页面标题,是在HTML页面的<title>...</title>标签中指定的。
默认情况下,MVC视图的标题为[xxx] - My ASP.NET Application,我们想把“ - My ASP.NET Application”替换为“ - Honor Shop”。
查找顺序是由内而外,因为内层配置会覆盖外层,所以我们先看Home目录中的视图文件,有没有指定标题。默认是没有的。
接下来看Home目录中,有没有建立新的_ViewStart.cshtml,因为它也可以覆盖全局配置。默认也是没有的。
接下来再找上级目录,也就是/Views目录中的_ViewStart.cshtml,默认是可以找到的,这个文件里默认指定的Layout是"~/Views/Shared/_Layout.cshtml",其中“~/”代表虚拟根目录,是相对路径的表示方法,它指向了/Views/Shared/_Layout.cshtml文件。
打开_Layout.cshtml文件,我们可以看到<title>@ViewBag.Title - My ASP.NET Application</title>,可能现在还不太明白@ViewBag.Title是什么意思,不过先不用管它,总之,格式看起来很像“[xxx] - My ASP.NET Application”。
我们把<title>@ViewBag.Title - My ASP.NET Application</title>修改为<title>@ViewBag.Title - Honor Shop</title>。
运行一下,可以看到标题栏已经变成“[xxx] - Honor Shop”的格式了。
接下来,我们来看看[xxx]是从哪儿来的。还是按照刚才的顺序,这次比较幸运,在控制器视图文件里就发现了一些端倪。每个视图文件的开头都有如此一段代码:
@{ ViewBag.Title = "xxx"; }
有的同学会疑惑,这是个什么鬼,HTML里面,没见过啊。这里,结合刚才我们用到的<title>@ViewBag.Title - My ASP.NET Application</title>一起来说明一下,首先,我们使用的是Razor视图引擎,'@'符号,在Razor中有特殊的含义,它标明一段服务器端代码的开始。
C# 的主要 Razor 语法规则
Razor 代码封装于 @{ ... } 中
行内表达式(变量和函数)以 @ 开头
代码语句以分号结尾
字符串由引号包围
C# 代码对大小写敏感
C# 文件的扩展名是 .cshtml
C# 实例
<!-- 单行代码块 --> @{ var myMessage = "Hello World"; } <!-- 行内表达式或变量 --> <p>The value of myMessage is: @myMessage</p> <!-- 多行语句代码块 --> @{ var greeting = "Welcome to our site!"; var weekDay = DateTime.Now.DayOfWeek; var greetingMessage = greeting + " Here in Huston it is: " + weekDay; } <p>The greeting is: @greetingMessage</p>
至于ViewBag.Title = "xxx";,代码中并没有ViewBag的定义,首先想到它会不会是Razor内置的东东,要么,就是MVC提供的东东,但要解释清楚它,目前还有点复杂,这里讲解会牵扯的东西比较多,为了保持对视图的改造的流畅性,我们后面再介绍它。但现在可以猜测的出它是一个服务器端对象,并且它有一个Title属性。虽然这个猜测有点偏激,姑且先这么理解它吧。
那么,书归正传,就尝试替换一下吧,看看效果,正如所料,[xxx],被替换掉了。
也有细心的同学,会发现我的标题栏里的图标怎么于自己的不一样,下面我们就来更改标题栏的图标吧。
二、修改标题栏图标
学习过HTML的读者,都知道这是一个比较基础知识,我们不难发现,在项目的根目录下,“躺着”一个名叫favicon.ico的文件。对了,就是它,图标文件的扩展名为.ico,读者可以从网上随意下载一个ico文件,来替换它。我为Honor Shop精心设计了一个,简洁又富有科技感的图标:)自我陶醉一下。
也可以通过在html的<head>...</head>标签中加入如下代码来改变favicon的路径及名称。
<head> <title>@ViewBag.Title - Honor Shop</title> <link rel="icon" href="~/Content/Images/honorshop.ico" type="image/x-icon" /> <link rel="shortcut icon" href="~/Content/Images/honorshop.ico" type="image/x-icon" /> </head>
三、修改页脚
先修改页脚,不为别的,代码少,改起来简单:P,在页面底部,很轻松就找到了:
<footer> <p>© @DateTime.Now.Year - My ASP.NET Application</p> </footer>
简单修改为:
<footer> <p>© @DateTime.Now.AddYears(-3).Year - Honor Shop</p> </footer>
运行看看,效果实现,也简单使用了一下在Razor中进行服务端编码的快感。
不过这时发现,好像很多地方都用到了“Honor Shop”这段字符串,这是我的网站的名字,可以预料,之后,还会有很多地方会用到。当然,方法有很多了,比如写入配置文件,比如写入数据库,等等。这里为了折腾,暂时不考虑这些方法,既然我们现在是对视图的修改,本着学习的目的,先在视图层面想办法,比如在布局视图中加一个变量,然后,视图里就可以引用这个变量了,也比四处硬编码来得爽快。
我们现在_Layout.cshtml的顶端加入如下代码:
@{ var SiteName = "My Honor Shop"; }
接着,我们对页面标题进行更新:
<title>@ViewBag.Title - @SiteName</title>
再更新页脚
<p>© @DateTime.Now.AddYears(-3).Year - @SiteName</p>
运行一下看看,哎哟,不错哟。
不过,这里有个问题,如果我们的项目中,使用了多个布局文件,那不是要在每个布局文件中都定义一遍SiteName?而且,经过试验,在控制器视图中,也没有办法直接使用@SiteName,因为控制器视图与_Layout视图并没有继承关系。
这里提出第一个方案,还记得_ViewStart先于其他视图运行,它的代码里只是指定了一个Layout属性,别的什么都没做。但是这个Layout却可以在控制器视图中进行重写覆盖,由此灵感而发,这个Layout属性到底是谁的属性?在Layout上点击鼠标右键选择[Go To Definition]项或者使用快捷键[F12]跳转到Layout的声明处。
可以看到,Layout是一个抽象类StartPage的属性,既然这个类是一个抽象类,那么它就不能被实例化,但不管是哪个类继承自它,也就同样继承Layout属性,既然Layout能够在其他视图中使用,那么与Layout平起平坐的其他属性,肯定也可以。所以,第一眼就瞄到了PageData这个属性,它是一个字典,Key是object类型,Value是dynamic类型,看起来,都挺合适的。于是乎,在_ViewStart.cshtml中做些手脚:
@{ /* 在PageData中添加站点名称键值对 */ PageData.Add("SiteName", "My First Solution 4 Page Title - Honor Shop"); Layout = "~/Views/Shared/_Layout.cshtml"; }
以页面标题为例,对视图中所有使用@SiteName的地方进行更新:
<title>@ViewBag.Title - @PageData["SiteName"]</title>
运行一下,效果如同期望一样,在布局视图和控制器视图中,都可以使用:
第二个方案就是我们之前还很朦胧的ViewBag,与查看Layout属性的方法一样,故技重施,跳转到ViewBag的声明处。
可以看到,ViewBag是一个抽象类WebViewPage的属性,而且是动态属性,那么如法炮制,再对_ViewStart.cshtml中做些手脚:
但是很遗憾,在_ViewStart中不能操作ViewBag属性。这是为什么呢?再仔细看看上面两个抽象类,原来他们分别在不同的命名空间,也就是说,_ViewStart和控制器视图,处于不同命名空间。第二个方案宣告失败,放弃,就是这么随性:D
不过于此同时,我们还是有所收获的,在返回头来看看第二个抽象类WebViewPage,尤其是它内部声明的一系列属性,我可以很负责任的告诉你,你的整个.NET MVC生涯都是在与它们打交道,不信?你等着瞧……
关于页脚的修改,暂时告一段落,下面来修改导航栏吧。
四、修改导航栏
知己知彼方能百战不殆,我们先看看导航栏原来长得什么样子。
<div class="navbar navbar-inverse navbar-fixed-top"> <div class="container"> <div class="navbar-header"> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse"> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> @Html.ActionLink("Application name", "Index", "Home", new { area = "" }, new { @class = "navbar-brand" }) </div> <div class="navbar-collapse collapse"> <ul class="nav navbar-nav"> <li>@Html.ActionLink("Home", "Index", "Home")</li> <li>@Html.ActionLink("About", "About", "Home")</li> <li>@Html.ActionLink("Contact", "Contact", "Home")</li> </ul> </div> </div> </div>
忽略掉繁杂的HTML标签和各种样式,标重点,第9、13、14、15行。第13~15行很相似,只是参数的内容不同,第9行与其它几行,都是以@Html.ActionLink开头,参数个数不同。看来都是使用了Html的ActionLink的重载方法。
等等,'@'符我们说过了,表明后面要接服务端代码了,那么,Html又是什么鬼?我们还是跳转到声明处看看吧
首先,它是一个抽象类WebViewPage<TModel>的属性,WebViewPage<TModel>类又继承自WebViewPage类,WebViewPage类就是刚才我们看到的声明了ViewBag的类,兜兜转转啊,不过,WebViewPage类中也声明了一个Html,只不过是HtmlHelper<object>类型,这里的Html是HtmlHelper<TModel>类型,其实它们两个都是同根同源的,都是HtmlHelper<TModel>类的实例。同时,也可以看出,两个WebViewPage,一个是强类型的,一个是弱类型的。继续追踪HtmlHelper<TModel>类,它继承自HtmlHelper类,具有两个构造函数,还有两个只读属性,没有方法声明,那么,方法应该是都在HtmlHelper基类中声明的了,跟进去一看,大跌眼镜,虽然有声明了一堆方法,但是居然没有我们期待已久的ActionLink方法,此刻,必须要想到扩展方法,如果没有想到,那么请恶补C#的相关细节。返回到_Layout.cshtml,直接跳转到ActionLink方法的声明,眼前一亮,原来都在这里:
大家可以展开类及方法上的Summary信息来了解一些信息。从类名上可以看出,这是一个针对链接的扩展类。从方法名可以看出,链接可以分为两种,一种是ActionLink,另一种是RouteLink。RouteLink可以简单理解为通过路由(不是通常所说的路由器阿)跳转,用到时再详细介绍。我们先来看ActionLink。
ActionLink有10个重载,每个都讲,也是比较辛苦的,毕竟我也很懒……我们就拿导航栏中第9行代码使用的重载来做一个说明吧。
// // 根据指定的链接文字,操作名称,控制器名称,路由参数对象和html属性对象, // 返回一个锚点元素(也就是html中的a标签), // MvcHtmlString是一个特殊的字符串,是经过Html-encoding的html字符串,这点很重要。 // public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper // 锚点标签中显示的文字 , string linkText // 控制器中操作的名称 , string actionName // 控制器名称 , string controllerName // 包含路由参数的对象,通过反射检测routeValues对象的属性获取路由参数。 // 通常使用对象初始化器语法创建routeValues对象。 , object routeValues // 一个包含html属性列表的对象 , object htmlAttributes);
目前,我们也只能是修改第一个参数,动手:
<div class="navbar navbar-inverse navbar-fixed-top"> <div class="container"> <div class="navbar-header"> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse"> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> @Html.ActionLink(@PageData["SiteName"] as string // @PageData["SiteName"]的值是dynamic类型 , "Index", "Home", new { area = "" }, new { @class = "navbar-brand" }) </div> <div class="navbar-collapse collapse"> <ul class="nav navbar-nav"> <li>@Html.ActionLink("首页", "Index", "Home")</li> <li>@Html.ActionLink("关于我们", "About", "Home")</li> <li>@Html.ActionLink("联系我们", "Contact", "Home")</li> </ul> </div> </div> </div>
这样就完成了对导航信息的基本修改。
现在我们可以初步体会到,@Html.ActionLink可以帮助我们生成一个锚点元素,它是.NET MVC视图引擎提供的一个辅助方法。
Html就是对HtmlHelper的封装,它能帮助我们生成页面上所需要的各种元素。
但我现在又想把我辛辛苦苦设计的Logo放上去,替换干巴巴的“Honor Shop"文字。
五、添加带链接的图片
首先,将制作好的Logo文件(logo.png)拷贝到/Content/Images/下。将原来的锚点代码注释掉。
@*@Html.ActionLink(@PageData["SiteName"] as string // @PageData["SiteName"]的值是dynamic类型 , "Index", "Home", new { area = "" }, new { @class = "navbar-brand" })*@
Razor中使用@*....*@来注释,看起来,还挺有喜感的。
接下来,我们先立个目标,最终要生成一个什么样的html,能够满足我们添加的Logo的需求。
<a href="/Home/Index" class="navbar-brand" style="padding-top: 0px; padding-bottom:0px;"> <img src="/Content/Images/logo.png" alt="Honor Shop" title="Honor Shop" style="height: 100%; background-color:white;"> </a>
这里没有什么特别的,都是一些Html和CSS的基础的东西,不是本书的重点,不多介绍,看看效果。
目前,它可以很好的工作,但它是脆弱的。思考一个问题,如果,我们的应用,并没有部署在网站的根目录,或者修改了路由的定义,那么,锚点的href和图片的src的值,都有可能把浏览器导航到一个网站上并不存在的资源处。再做一个假设,我们的应用里有很多这样的锚点加图片的元素,他们在编译时,并不会报告错误,应用将变得非常难以维护。
更好的办法就是可以通过路由来计算路径,这样就可以有效的解决上面提出的部署位置和修改路由的问题了。
.NET Web应用为我们提供了一套路由机制,可以为我们计算路由路径,其核心就是RouteTable,我们可以通过
RouteTable.Routes.GetVirtualPath(RequestContext requestContext, RouteValueDictionary values).VirtualPath; RouteTable.Routes.GetVirtualPath(RequestContext requestContext, string routeName, RouteValueDictionary values).VirtualPath;
这两个重载方法来计算路径,是不是很开心,那么来看看参数列表,我们是不是都具备:
第一个参数,requestContext,不用操心,视图引擎已经为我们提供了,可以通过this.ViewContext.RequestContext来获取;
第二个参数,values,他的类型是RouteValueDictionary,是一个字典,我们可以通过这个字典,提交路由所需的参数;
第三个参数,routeName,可以用来指定我们需要使用哪条路由;
路由是MVC的一个重要机制,更是ASP.NET核心框架的一部分,还记得我们在本境第二节中介绍App_Start目录时,提及到RouteConfig,这个类就是用来管理配置路由的,并且在应用启动时,Global中的Application_Start方法中会调用它的RegisterRoutes方法来注册路由。
ASP.NET MVC框架中的路由主要有两个用途:
1. 匹配传入的请求(该请求不匹配服务器文件系统中的文件或资源),并把这些请求映射到控制器操作。
2. 构造传出的URL,用来响应控制器操作。
现在我们打开RouteConfig,看看默认提供的路由是什么样的。
using System.Web.Mvc; using System.Web.Routing; namespace HonorShop.Web { public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); // 注册一条路由配置 routes.MapRoute( // 定义路由的名称,名称可以自定义,但在路由表中不可重复。 name: "Default", // 定义一条url访问的模式 url: "{controller}/{action}/{id}", // 指定路由的默认值 defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } ); } } }
通过上面的代码,可以看出,注册一条路由所需要的参数,以及他们的含义。
对于构造首页的url,我们知道controller是Home,action是Index,id是可选的,可以不提供。
但是对于图片的url,就没有那么幸运了,因为图片是静态资源,并没有明确的controller和action。很显然,默认提供的路由,不适合我们。不如,我们动手来添加一条路由吧:
// 注册一条路由指向图片资源 routes.MapRoute( // 定义本条路由的名称为Images name: "Images", // 定义url匹配的模式 url: "Content/Images/{imgName}", // 定义本条路由的默认参数值,这里配置为可选; // 其实更好的建议是配置为一张“图片未找到”“图片已损坏”等含义的图片名称; defaults: new { imgName = UrlParameter.Optional } );
好了,准备工作都已经就绪了,让我们返回_Layout.cshtml,在它顶部开始撸代码:
@{ var context = this.ViewContext.RequestContext; var href_values = new RouteValueDictionary { { "controller", "home" }, { "action", "index" } }; var href = RouteTable.Routes.GetVirtualPath(context, href_values).VirtualPath; var src_values = new RouteValueDictionary { { "id", "logo.png" } }; var src = RouteTable.Routes.GetVirtualPath(context, "Images", src_values).VirtualPath; }
方法都很简单,前面也对细节都解释过了,下面接着修改我们的锚点和图片元素:
<a href="@href" class="navbar-brand" style="padding-top: 0px; padding-bottom:0px;"> <img src="@src" alt="Honor Shop" title="Honor Shop" style="height: 100%; background-color:white;" /> </a>
运行效果与图14一样,完美。完美是完美,不过实现的过程,未免复杂了些,而且这里只是解决了路径计算的问题,如果有很多路径需要计算,参数也是各种各样,可以想象计算路径给我们带来的如此的繁重的代码量,对于偷奸耍滑成性的MVC团队来说,这也太不能忍了。
值得庆幸的是,MVC给我们提供了很多与链接相关的辅助方法:
- Html.ActionLink:我们前面刚介绍过;
- Url.Action:根据给定的Controller,Action 生成链接,但是Html.ActionLink返回的是MvcHtmlString的一个带<a>标签的超链接,而Url.Action返回的是string,一个根据Controller,Action生成的URL地址,比Html.ActionLink少了<a>标签;
- Html.RouteLink 与 Url.RouteUrl:两者都是可以指定由哪一个路由来生成Url,其它与上面的ActionLInk,Action一样;
- Url.Content:将虚拟(相对)路径转换为应用程序绝对路径。
这次,我们来使用两个Url属性提供的方法,毕竟我们是用来计算路径,Url看起来比Html更贴切一些。将之前直接使用路由所做的更改,全都删除或者注释掉,对的,就是这么随性:
<a href="@Url.Action("Index", "Home")" class="navbar-brand" style="padding-top: 0px; padding-bottom:0px;"> <img src="@Url.Content("~/Content/Images/logo.png")" alt="Honor Shop" title="Honor Shop" style="height: 100%; background-color:white;" /> </a>
运行看看:
没有意外,一切按计划行事。但为了早日冲入第二境,我决定再用@Html提供的辅助方法折腾一遍,但是悲剧的是在@Html中并没有找到与图片相关的扩展方法。这就有点悲剧了。
如果只用@Html.ActionLink能不能实现呢,能,可以把Logo图片加到样式表的背景图里,ActionLink的htmlAttributes应用样式,也是可以的。也比较简单,但对SEO不够友好,特殊场景还是可以使用的,这里就不实操了。
不过这时,想起了之前提到的@Html.ActionLink是一系列扩展的重载方法,这些方法都可以辅助生成锚点元素。那么,我们是不是也可以通过扩展方法,来生成符合我们要求的自定义图片链接元素呢?说干就干。
在项目中创建一个Html目录,用来放置对HtmlHelper扩展方法的类文件;
在Html目录中新建类LinkExtensions,修改为静态类(必须,可以参考C#扩展方法的实现);
using System.Collections.Generic; using System.Web.Mvc; using System.Web.Routing; namespace HonorShop.Web.Html { public static class LinkExtensions { /// <summary> /// Summary: /// 扩展ActionLink方法,用来创建带锚点的图片元素; /// </summary> /// <param name="htmlHelper">扩展类</param> /// <param name="actionName">操作名</param> /// <param name="controllerName">控制器名</param> /// <param name="routeValues">路由参数列表</param> /// <param name="linkAttributes">锚点htmlAttributes</param> /// <param name="imagePath">图片路径</param> /// <param name="imageAttributes">图片htmlAttributes</param> /// <returns>An image element (img element) within an anchor element (a element).</returns> public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper , string actionName , string controllerName , RouteValueDictionary routeValues , IDictionary<string, string> linkAttributes , string imagePath , IDictionary<string, string> imageAttributes) { var urlHelper = new UrlHelper(htmlHelper.ViewContext.RequestContext); string imgUrl = urlHelper.Content(imagePath); TagBuilder imgTagBuilder = new TagBuilder("img"); imgTagBuilder.MergeAttribute("src", imgUrl); if (null != imageAttributes && 0 < imageAttributes.Count) imgTagBuilder.MergeAttributes(imageAttributes, true); string img = imgTagBuilder.ToString(TagRenderMode.SelfClosing); string url = urlHelper.Action(actionName, controllerName, routeValues); TagBuilder tagBuilder = new TagBuilder("a") { InnerHtml = img }; tagBuilder.MergeAttribute("href", url); if (null != linkAttributes && 0 < linkAttributes.Count) tagBuilder.MergeAttributes(linkAttributes, true); return new MvcHtmlString(tagBuilder.ToString(TagRenderMode.Normal)); } } }
在_Layout.cshtml的顶部添加引用
@using HonorShop.Web.Html;
修改导航栏Logo位置代码
@Html.ActionLink("Index", "Home", new RouteValueDictionary { { "area", string.Empty } }, new Dictionary<string, string> { { "alt", "Honor Shop" }, { "title", "Honor Shop" }, { "class", "navbar-brand" }, { "style", "padding-top: 0px; padding-bottom:0px;" } }, "~/Content/Images/logo.png", new Dictionary<string, string> { { "alt", "Honor Shop" }, { "title", "Honor Shop" }, { "style", "height: 100%; background-color:white;" } })
两个字典拼装的有点多,代码显得长了点,不过,一个方法搞定,还是简洁了很多,而且使用了扩展方法,在重用和灵活性上都得到了大幅度的提升。
回想一下,我们在第二部分说过如何修改标题栏图标时,使用了如下代码:
<link rel="icon" href="~/Content/Images/honorshop.ico" type="image/x-icon" /> <link rel="shortcut icon" href="~/Content/Images/honorshop.ico" type="image/x-icon" />
其中,也涉及到了路径问题,那么,我们应用学到的知识,来更新一下它们吧:
<link rel="icon" href="@Url.Content("~/Content/Images/honorshop.ico")" type="image/x-icon" /> <link rel="shortcut icon" href="@Url.Content("~/Content/Images/honorshop.ico")" type="image/x-icon" />
运行一下,效果如图15所示一样。大功告成。
到这里,我们也基本了解了HtmlHelper和UrlHelper两个辅助类的使用方法以及如何为其添加扩展方法。这将在我们后面的开发过程中,打下良好的基础,读者朋友需要细细品味,最好能够跟着动手实际操作一番。
下一节开始,我们就要使用这些知识,动手打造我们的第一个页面了。
喜欢本系列丛书的朋友,可以点击链接加入QQ交流群(994761602)【C# 破境之道】
方便各位在有疑问的时候可以及时给我个反馈。同时,也算是给各位志同道合的朋友提供一个交流的平台。
需要源码的童鞋,也可以在群文件中获取最新源代码。