视图简介
在 ASP.NET MVC 应用程序中,所有传入的请求均由控制器处理,并将这些请求映射到控制器相应的方法上
控制器方法可能会返回一个视图,也可能执行其它类型的操作,例如重定向到另一个控制器方法上
如果使用 MVC 框架,最流行的创建 HTML 的方法是使用 ASP.NET MVC 的 Razor 视图引擎
为了使用 Razor 视图引擎,一个控制器方法将产生一个 ViewResult (实现IActionResult接口)对象,而一个 ViewResult 可以携带我们想要使用的 Razor 视图的名称。
即我们可以在控制器中组装数据,再将数据通过Model绑定或ViewBag、ViewData对象将组装的数据返传递给View,之后控制器将View(中的Html)返回给浏览器。
其过程如下:
控制器决定返回哪个视图、视图中的数据是什么样,而最终响应给浏览器的HTML代码是根据视图进行生成的。
创建一个视图
前一篇教程我们简单我们讲了,如何为控制器创建一个视图,即在项目中创建一个Views文件夹,再按控制器名称在Views目录下继续创建目录,然后创建跟控制器Action同名的.cshtml文件。
如DeptController下有名为”Index“、”Add“的Action,他们均需要返回一个ViewResult
则我们目录结构图下图所示:
在cshtml文件中我们可以直接使用Html代码进行编写,结构同Html结构
使用 Razor布局解决视图公共部分
比如我们的系统使用如下图所示的圣杯布局
除登录页外所有页面面的header、left、right、footer部分都相同,只有center部分根据不同页面显示不同内容。
如果我们不使用Razor布局,也可以使用iframe的方式进行引入,但这种方式存在一些弊端,比如以下几种:
- 第一次打开页面是浏览器需要多次向服务器请求页面内容,存在一定开销
- 我们没有办法将其中某一页的地址发给其他人打开,如果直接打开center部分的页面则header、left、right、footer这些部分的内容不会显示(除非你使用URL重写或将center部分地址带入到)。
- 必须控制center部分的页面能被哪些域嵌入,否则可能存在xss攻击的风险,因为parent页可以取到iframe中的cookies信息并修改页面的html代码内容。
因此我们可以考虑在每个视图中单独呈现这些公共部分,但每个页手动编写存在工作量大、修改麻烦(一旦一个地方需要改动,可能每个视图都得修改)。
好在,mvc为我们提供了一套解决方案,即”Razor布局“。
我们在上文提到的Views目录下再新建一个"shared"目录,在"shared"目录新建一个视图布局,名为_Layout.cshtml
之后打开这个Razor布局文件,我们可以将html的公共部分写在这个文件里,然后每个页需要展示不同信息的地方(如center部分)写如下代码
@RenderBody()
<!DOCTYPE html><html>
<head><meta name="viewport" content="width=device-width" /><title>@ViewBag.Title</title>
</head>
<body><div><img src="/img/logo.jpg" asp-append-verison="true" alt="Jxmaker.com" style="width:20%" /></div><div id="mainFrame"><!--这个DIV中的内容每个页面不同,在最后生成html时,这个地方的代码会被相应视图里面的内容(生成的html)所替换,除这个部分外,其余部分每个页面相同-->@RenderBody()</div></body>
</html>
然后我们再为控制器中的每个Action创建各自的视图,此时,请注意,因为公共部分已经写到了Razor布局文件,每个视图生成Html时都会有这些部分,因为、等标签是不需要在写的,我们只要在这些视图中写上要放在id为"mainFrame"的DIV中的内容即可
如Dept/Index的视图里面的代码:
<div><a asp-action="Add">Add</a></div><div><table><thead><tr><th>部门名称</th><th>部门地址</th><th>部门人数</th><th>操作</th></tr></thead><tbody>@Html.DisplayForModel();</tbody></table></div>
当然我们需要在各个action的视图中标明他需要使用哪个公共布局,即在视图的最顶部加上以下代码:
@{Layout = Url.Content("~/Views/Shared/_Layout.cshtml");
}
利用Razor视图开始(ViewStart)批量配置布局
当有非常多视图都需要引入相同的布局时,我们可以按上面的方法在每个试图单独写Layout=”*****“,也可以使用Razor视图开始,在Razor视图开始中编写的引用在其作用域下所有的视图都会被加上相应的代码,引用其规定的Razor布局
Razor视图开始文件作用域在当前目录及其子目录的所有视图。
也就是说
- 需要使用Razor视图开始则可以在内容页视图的文件夹或父级文件夹添加_ViewStart.cshtml文件,然后在_ViewStart.cshtml文件中引用布局页(这很与aspx开发的web.config文件类似)
- 当不同的文件夹内的如果要使用不同的布局时,可以在相应的文件夹下新建_ViewStart.cshtml文件
- 当相同文件夹内的文件要使用不同布局时,只能在内容页里使用Layout属性
现在我们在Viiews目录下创建一个Razor视图开始文件取名为”_ViewStart.cshtml“,文件中的全部代码如下
@{Layout = "_Layout";
}
之后运行网站(去掉所有视图的Layout="*********"这些代码),你就会看到所有视图都引用了该布局
视图引用的优先级(覆盖)
经过测试视图引用优先级规则如下:
在视图中使用@{Layout}的引用>写在与视图同目录的视图开始中的引用>写在父目录的视图开始的引用
说明:”>“号左边的引用方式优先级高于>“号右边的引用方式,即如果某一页需要单独引用不同的视图布局,则可以使用优先级更高的方式覆盖优先级低的方式。
在视图中使用C#代码
有时候,如果你需要在视图中进行一写值的调用或进行一些特殊的计算,需要让视图中能执行c#代码
视图中的特殊符号@就可以实现这个效果
@符号存在以下几种用法
- 代码体,单纯执行某些代码,不对页面内容进行修改,如需要在页面中执行一个当前页面访问量统计(当然最好可以通过控制器或接口来实现,这里只是提到具有这个用法,不推荐使用,否则会导致代码混乱不易维护)
@{ ViewBag.ViewCount=(ViewBag.ViewCount as int)+1;
}
在代码体中,每一行都需要用";“结束,代码区中,字母区分大小写。字符类型常量必须用”"括起来,
- 注释代码
使用 @注释内容@ 这种语法可以实现在视图中添加注释(当然更推荐用html的<! –注释内容–>方式注释)。例:
- 在Razor中使用局部变量,进行上下文调用,也可以用来拼接字符串,比如页面中某个位置需要显示当前的时间(当然还是建议你用JS实现)
@{
var time=DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
}
<div>当前时间: @time ,网站将于每晚21点准时关闭,请抓紧时机选购</div>
- 执行一些.netcore中自带的方法,如Url.Content(string path)方法能够实现自动将绝对地址转换为相对地址,如现有logo放在项目wwwroot/image目录下,如果直接写成"/image/logo.jpg",直接使用没有问题,如后期因部署方案调整,系统静态文件访问地址改为"/static/image/logo.jpg",但文件的物理地址还是不变,此时就必须跟着修改代码否则访问不到图片。但是如果使用Url.Content方法,则会根据静态文件配置自动修改成正确的图片的访问地址。
例:
<img src="@Url.Content("/img/logo.jpg")" alt="Jxmaker.com" />
其他更多方法我们后面还会学习到,这里不深究
- 控制显示内容,比如我们做一个随机抽签决定A或B下去拿外卖的小例子,取当前时间的毫秒数,如果当前毫秒为0或999,则显示“两个人一起去取餐”,否则如果毫秒数能被2整除则输出”A去取餐“,否则输入"B去取餐"。
@{var nowMillisecond = DateTime.Now.Millisecond;if (nowMillisecond == 0 || nowMillisecond == 999) {<h3>一起去取餐</h3>}else { <h3>@(nowMillisecond % 2 == 0 ? "A" : "B") 去取餐</h3>}}
Controller向视图传值的几种方式
1、ViewBag和ViewData传值
使用ViewBag和ViewData可以实现控制器的Action向视图传值,比如我有一个视图布局,但是我需要根据不同的页面地址在浏览器中显示不同的标题。其行为和响应如下:
事件 | 用户访问页面端点 | 浏览器显示标题 |
---|---|---|
用户访问首页 | Home/Index | 首页 ——Jxmaker |
用户访问登录页 | UserCenter/Login | 用户登录 ——Jxmaker |
用户访问个人中心首页 | UserCenter/Index | 首页 ——个人中心 ——Jxmaker |
我们可以看出,我的标题中“——Jxmaker”部分是固定的,但是这部分之前的文字是根据访问不同页面动态显示的,此时,我又使用了Razor布局,该处的代码位于布局文件中,我在控制器中需要将这个值传给视图让他在相应位置出现,我们就可以使用ViewBag或ViewData,ViewBag和ViewData只在Action中有效。他俩其实类似,只是有点小区别,看下面
ViewBag和ViewData的区别
ViewData | ViewBag |
---|---|
它是key/value字典集合 | 它是dynamic类型对象 |
从asp.net mvc1就有了 | 从asp.netmvc3才有 |
asp.netframework 3.5及以上支持 | asp.net framework4.0及以上支持 |
快 | 慢 |
页面查询数据时需要转换合适的类型 | 在页面查询数据时不需要转换合适的类型 |
有一些类型转换代码 | 可读性较好 |
使用方法:
Controller中控制要显示的Title并把它传入ViewBag/ViewData:
ViewBag.Title = "首页" //使用ViewBag的方式
ViewData["Title"]="首页" //使用ViewData方式
在视图中显示传入的值
<!--使用ViewBag方式--><title>@ViewBag.Title ——Jxmaker</title>
<!--使用ViewBag方式结束--><!--使用ViewData方式--><title>@ViewData["Title"] ——Jxmaker</title>
<!--使用ViewData方式结束-->
小贴士 ViewData和ViewBag的值可以相互访问,因为ViewBag是对ViewData的动态封装,因此,你可以这么使用,在Controller中 用 @ViewBag.Title=“****”把值传给视图,而视图中使用@ViewData[“Title”] 显示传入值,或者反过来使用。
视图绑定Model方式
使用这种方式,控制器的Action中使用将实体对象当作View的参数传递给View,而View中绑定实体的类型,就可以直接使用@Model得到传来的值
例如,我现在有一个页面需要显示部门的列表,此时我肯定是需要在Action中将部门列表传给View,View负责进行展示
我们首先在项目创建一个Models目录,然后在该目录新建类,比如我们现在新建一个部门类
public class DeptInfo{public int Id { get; set; }[Display(Name ="部门名称")]public string Name { get; set; }[Display(Name = "部门地址")]public string Addr { get; set; }[Display(Name = "员工总数")]public int EmployeeCount { get; set; }}
Action方法:
//async关键字表示这是异步的一个方法,调用时如果需要获取返回值需要使用await关键字等待,后面章节再讲, Task<IActionResult> 表示该方法异步执行完后会返回一个IActionResult类型的值 public async Task<IActionResult> Index(){ViewBag.Title = "Index";//_deptService.GetList()方法定义:public async Task<IEnumerable<DeptInfo>> GetList(),该方法也是一个异步方法,执行完返回一个IEnumerable<DeptInfo>类型对象var deptList = await _deptService.GetList();return View(deptList);//讲IEnumerable<DeptInfo>类型的部门列表传给View}
在View中,我们需要先绑定一下该视图接收到值的类型
@model IEnumerable<DeptInfo> //绑定Model类型为IEnumerable<DeptInfo>
然后我们就可以通过Model访问到传入的数据,比如我们遍历这个列表,并显示在表格中,比如我们要在视图中显示部门数量,我们就可以用如下代码:
<div>共有@Model.Count() 个部门</div>
视图中的Model就是你传入的那个对象,你可以在视图中调用这个Model中的函数、属性、字段。
HTMLHelper
@Html(HtmlHelper)基本包含了html中的表单控件和常用Html,简单地说就是你可以在页面上”.”出你所需要的html标签,@Html还自带了一些方法也是通过”.”来进行选择
@Html的用途很多,大家可以看这篇文章,我就不再讲了,后面用到自然就会(其实现在我都是前后端分离,服务端只管API返回,所以Razor页面也差不多忘光了):
https://www.cnblogs.com/wfy680/p/12213292.html
我们讲一个比较特殊的用法
@Html.DisplayForModel();
方法作用:
根据模板返回当前Model实例的HTML代码。也就是说,@Html.DisplayForModel()这个位置最后会被根据模板和传入的Model生成的Html代码给覆盖掉。最常见的一种情况就是如以列表方式展示(如各部门)信息,并提供一些操作按钮可以针对每行(显示的部门)进行操作。
我们以部门列表页为例,在View中,写一个表格代码的html和样式,thead节点是表头,因为表头是固定的,所以可以直接写html代码,而tbody部分需要根据控制器传入的列表进行动态渲染,所以这部分我们就用“@Html.DisplayForModel();”语句,告诉.net引擎这里帮我根据模板生成一堆代码
<table><thead><tr><th>部门名称</th><th>部门地址</th><th>部门人数</th><th>操作</th></tr></thead><tbody>@Html.DisplayForModel();</tbody></table>
接下来,需要创建模板。在项目的Views目录下创建一个DisplayTemplates目录,然后根据Model的类型名称(这里是DeptInfo),创建一个cshtml文件,如DeptInfo.cshtml。
在Model模板中,我们首先绑定传入的Model类型 :
@using WebApplication1.Models @*添加引用*@
@model DeptInfo
然后像普通的页面那样写模板(.net引擎会自动给你进行循环,所以你只要管一行的模板就可以了,会自动根据你的列表有多少个对象,就会生成多少行这样的html,这样就可以实现一个列表的循环渲染)
<tr><td>@Model.Name</td><td>@Model.Addr</td><td>@Model.EmployeeCount</td><td><a>查看职员</a></td>
</tr>
此外还有一个常用的Html.DisplayFor方法也可以实现类似的功能,如果想知道怎么用可以自己去百度一下。
其他的什么生成Input、RadioButton等功能你们可以在用到的时候自行百度,这里就不说了,HtmlHelper的作用就是帮助我们快速生成一段Html代码。
Html标签助手(TagHelper)
在.net framework中,如果你想使用HtmlHelper生成一个超链接(链接预览),点击这个超链接会跳转到 news/preview页面,并通过get方式给该页面传入id=1的值,就可以使用如下代码:
@Html.ActionLink("预览","preview","news",new { id=1})
.net core提供更加方便的taghelper,使我们写代码更简洁也更具易读性:
<a asp-controller="news" asp-action="preview" asp-route-id="1">预览</a>
那么为什么在Asp.Net Core MVC中要使用TagHelper呢?我们结合路由来看一下,假设路由开始是“/{controller=Home}/{action=Index}/{id?}”后来因为需要改为“/page/{controller=Home}/{action=Index}/{id?}”,如果我们直接在Razor视图上写死:
<a href="news/preview/1">预览</a>
那么你修改路由就需要修改各个超链接,容易出现遗漏,工作量也不小。
使用TagHelper前需要先在视图中导入
我们需要先在项目文件夹Views目录中创建一个Razor视图导入,取名为“_ViewImports.cshtml”
然后再这个导入文件里面添加如下代码
@addTagHelper "*,Microsoft.AspNetCore.Mvc.TagHelpers"
意思就是全局导入Microsoft.AspNetCore.Mvc.TagHelpers命名空间下的全部(*代表全部)TagHelper
这样所有的View都可以直接使用
自动生成Action访问的URL地址
如上面例子所述,如果我一个超链接到某个Controller下的Action,则我在A标签中可以使用asp-controller、asp-action两个属性,asp-controller的值为Controller的名称,asp-action的值就是Action的名称,则自动根据路由配置生成访问该Action的地址。
如果需要使用get方式进行传值,则可以使用“asp-route-参数名称”这种格式的属性,如asp-route-uid=“99999”,则生成的html将会变成:
<a href="action访问地址?uid=99999">点我 </a>
比如刚才系统的部门列表每一行都提供一个“查看职员”超链接,该按钮被点击后将会请求Employee控制器下的名为“Index”的Action,该Action访问需要传入对应的部门Id(deptId),如果部门Id没有问题则会返回当前部门下所有职员列表。
代码如下:
<a asp-controller="Employee" asp-action="Index" asp-route-deptId="@Model.Id">查看职员 </a>
防止图片缓存
假设我们现在需要修改网站的logo(网站logo地址在wwwroot/img/logo.jpg),我再服务器上修改后,客户访问网站由于浏览器缓存问题依然不会生效,看到的还是旧的logo图片。
为了解决这一个问题,我们一般会在图片地址后面添加一个没有意义的参数,该参数给请求地址传递一个随机数,每次页面刷新时这串随机数都会改变,相当于访问了一张新图片,浏览器没进行缓存。
<img src='~/wwwroot/img/logo.jpg?_v=随机数'/>
TagHelper为我们提供了一个更简易的方式,我们只要给img标签加上“asp-append-verison”属性,其值为true,则每次访问这个页面时图片地址后面会自动加入一个版本号,使用代码如下:
<img src="/img/logo.jpg" asp-append-verison="true" />
批量引入js/css
顾名思义,就是一个一个使用link标签引入外部css太麻烦了,我想要按规则批量引入某个目录下的css文件
直接上代码
<link rel="stylesheet" asp-href-include="css/*" asp-href-exclude="css/all.min.css" />
asp-href-include属性值用来设置需要引入的文件规则,asp-href-exclude属性用来排除部分不引入的文件的规则
以上代码执行后效果除了all.min.css外,全部css目录下的文件都会被引入
根据不同环境输出不同Html
使用environment标签可以实现在不同环境下向浏览器响应发送不同的Html代码
如,我在开发环境需要引入上述css目录下全部css(除all.min.css),而在生产环境或其他不属于开发的环境下直接引入压缩过后的 all.min.css,不引入其他css
<!--include="Development"表示在开发环境下,引入css目录下除all.min.css外的全部css--><environment include="Development"><link rel="stylesheet" asp-href-include="css/*" asp-href-exclude="css/all.min.css" /></environment><!--exclude="Development"表示不在开发环境下,则引入all.min.css--><environment exclude="Development"><link rel="stylesheet" asp-href-include="all.min.css" /></environment>
自动生成表单域中表单项的标题和控件
- 给Model类配置每个属性的显示名称,不如刚才的DeptInfo,改成如下:
public class DeptInfo{public int Id { get; set; }[Display(Name ="部门名称")]public string Name { get; set; }[Display(Name = "部门地址")]public string Addr { get; set; }[Display(Name = "员工总数")]public int EmployeeCount { get; set; }}
}
然后在DeptController里增加一个Add的Action,需要传入一个Model给视图
//[HttpGet]public IActionResult Add(){ViewBag.Title = "新增部门";return View(new DeptInfo());}
为这个Action添加视图,先绑定Model
@model DeptInfo
然后写一个form标签,其提交地址可以使用asp-action指定
<form asp-action="Add"> <!--默认以post方式提交到Add Action'--></form>
在form标签中可以用 asp-for指定输入框的名称和 input的name属性
全部代码:
@using WebApplication1.Models
@model DeptInfo<form asp-action="Add"><div><label asp-for="Name"></label><input asp-for="Name" /></div><div><label asp-for="Addr"></label><input asp-for="Addr" /></div><div><label asp-for="EmployeeCount"></label><input asp-for="EmployeeCount" /></div><div><input type="submit" value="提交"/></div></form>
然后在DeptController写一个接受表单post方式传值的action,命名为“Add”,该Action接收一个DeptInfo 类型的参数model
[HttpPost] //指定该Action必须使用Post方式请求,不接受其它方式请求//该方法是一个异步方法,返回类型为IActionResultpublic async Task<IActionResult> Add(DeptInfo model){if (!ModelState.IsValid)return null;//model验证不通过时直接return,验证后面再讲await _deptService.Add(model);//等待Server中异步方法执行完成return RedirectToAction(nameof(Index)); //当添加成功后跳转的名为Index的Action,这里也可以直接传入"Index"字符串。}
当我们使用Get方式访问(在浏览器中通过输入地址访问或点击超链接跳转),则会自动进入设置为[HttpGet]或不设置的Action,当我们使用Post方式,默认会去找具有[HttpPost]申明的Action。
最后生成的表单效果(文字是我输入进去的):
附录:Service代码
接口
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using WebApplication1.Models;namespace WebApplication1.IServices
{public interface IDeptService{Task<IEnumerable<DeptInfo>> GetList();Task<DeptInfo> GetSingle(int id);Task Add(DeptInfo model);}public interface IEmployeeService{public Task Add(EmployeeInfo model);public Task<IEnumerable<EmployeeInfo>> GetList(int? deptId,string name,bool? isFired);Task<EmployeeInfo> Fire(int id);}
}
DeptService(对部门进行管理的服务)代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using WebApplication1.Models;
using WebApplication1.IServices;namespace WebApplication1.Services
{public class DeptService : IDeptService{private readonly List<DeptInfo> _deptList = new List<DeptInfo>(){new DeptInfo(){ Id=1, Name="技术部", Addr="北京",EmployeeCount=10},new DeptInfo(){ Id=2, Name="人力资源部", Addr="厦门",EmployeeCount=1},new DeptInfo(){ Id=3, Name="研发部", Addr="烟台",EmployeeCount=3}};public Task Add(DeptInfo model){model.Id = _deptList.Max(r => r.Id) + 1;_deptList.Add(model);return Task.CompletedTask;}public Task<IEnumerable<DeptInfo>> GetList(){return Task.Run(() => _deptList.AsEnumerable());}public Task<DeptInfo> GetSingle(int id){return Task.Run(() => _deptList.SingleOrDefault(r => r.Id.Equals(id)));}}
}
EmployeeService(对职员进行管理的服务)代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using WebApplication1.Models;
using WebApplication1.IServices;
using System.Runtime.InteropServices.ComTypes;namespace WebApplication1.Services
{public class EmployeeService : IEmployeeService{private readonly List<EmployeeInfo> _employeeList = new List<EmployeeInfo>(){new EmployeeInfo(){ Id=1 ,DeptId=1, Fired=false, FirstName="西西", LastName="张", Gender=Gender.男},new EmployeeInfo(){ Id=2 ,DeptId=1, Fired=false, FirstName="东东", LastName="张", Gender=Gender.女},new EmployeeInfo(){ Id=3 ,DeptId=3, Fired=false, FirstName="东东", LastName="陈", Gender=Gender.女},new EmployeeInfo(){ Id=4 ,DeptId=2, Fired=false, FirstName="楠楠", LastName="陈", Gender=Gender.女},new EmployeeInfo(){ Id=5 ,DeptId=3, Fired=false, FirstName="倩倩", LastName="陈", Gender=Gender.男}};public Task Add(EmployeeInfo model){model.Id = _employeeList.Max(r => r.Id) + 1;_employeeList.Add(model);return Task.CompletedTask;}public Task<EmployeeInfo> Fire(int id){return Task.Run(()=> {var model = _employeeList.SingleOrDefault(r => r.Id.Equals(id));if (model == null)return null;model.Fired = true;return model;});}public Task<IEnumerable<EmployeeInfo>> GetList(int? deptId, string name, bool? isFired){return Task.Run(() => {var query = _employeeList.AsEnumerable();if (deptId.HasValue)query = query.Where(r => r.DeptId.Equals(deptId.Value));if (!string.IsNullOrWhiteSpace(name))query = query.Where(r => r.LastName.Contains(name) || r.FirstName.Contains(name));if (isFired.HasValue)query = query.Where(r => r.Fired);return query;});}}
}