首页 > 代码库 > C# 6 与 .NET Core 1.0 高级编程 - 41 ASP.NET MVC(上)

C# 6 与 .NET Core 1.0 高级编程 - 41 ASP.NET MVC(上)

译文,个人原创,转载请注明出处(C# 6 与 .NET Core 1.0 高级编程 - 41 ASP.NET MVC(上)),不对的地方欢迎指出与交流。   

章节出自《Professional C# 6 and .NET Core 1.0》。水平有限,各位阅读时仔细分辨,唯望莫误人子弟。   

附英文版原文:Professional C# 6 and .NET Core 1.0 - Chapter 41 ASP.NET MVC

------------------------------------ 

本章内容

  • ASP.NET MVC 6的特性
  • 路由
  • 创建控制器
  • 创建视图
  • 验证用户输入
  • 使用过滤器
  • 使用HTML和标签助手
  • 创建数据驱动的Web应用程序
  • 实现认证和授权

Wrox.com网站下载本章代码
wrox.com中关于本章的代码下载位于 http://www.wrox.com/go/professionalcsharp6 的“下载代码”选项卡上。本章的代码主要有以下主要示例:

  • MVC Sample App
  • Menu Planner

设置 ASP.NET MVC 6 服务 

第40章“ASP.NET Core”展示了ASP.NET MVC的基础:ASP.NET Core 1.0 第40章展示了中间件以及依赖注入如何与ASP.NET结合使用。本章通过注入ASP.NET MVC服务来使用依赖注入。

ASP.NET MVC是基于MVC(模型 - 视图 - 控制器)模式。如 图41.1所示,这种标准模式(一个文档中描述的设计模式:由Gang of Four [Addison-Wesley Professional,1994]的可重复使用的面向对象软件的元素)定义了一个实现数据实体和数据访问的模型,向用户展示信息的视图,以及利用模型向视图发送数据的控制器。控制器从浏览器接收请求并返回响应。要创建响应,控制器可以使用模型提供一些数据,以及一个视图来定义返回的HTML。
技术分享

 

图41.1
ASP.NET MVC 的控制器和模型通常使用运行服务器端的C#和.NET代码创建。视图是带有JavaScript的HTML代码,视图中有时也会有少量用于访问服务器端信息的C#代码。

MVC模式中的这种分离的最大优点是,可以使用单元测试轻松测试功能。控制器只包含具有参数和返回值的方法,这些参数和返回值可以通过单元测试轻松遍历。

现在开始为ASP.NET MVC 6设置服务。ASP.NET Core 1.0的依赖注入是深入集成的,如第40章中所示。可以选择ASP.NET Core 1.0模板“Web应用程序”创建一个ASP.NET MVC 6项目。该模板已经包括ASP.NET MVC 6所需的NuGet包以及帮助组织应用程序的目录结构。但是,这里将从空模板开始(类似于第40章),所以你可以看到创建一个ASP.NET MVC 6项目所需要的东西,不会有项目可能不需要的额外的东西。

创建的第一个项目名为 MVCSampleApp。要在 Web应用程序MVCSampleApp 使用ASP.NET MVC,需要添加NuGet包 Microsoft.AspNet.Mvc 。该包准备就绪后,通过在ConfigureServices方法中调用扩展方法AddMvc来添加MVC服务(代码文件MVCSampleApp/Startup.cs):

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
// etc.
namespace MVCSampleApp
{
  public class Startup
 {
    // etc.
    public void ConfigureServices(IServiceCollection services)
    {
      services.AddMvc();
      // etc.

    }
    // etc.
    public static void Main(string[] args)
     {
      var host = new WebHostBuilder()
         .UseDefaultConfiguration(args)
         .UseStartup<Startup>()
         .Build();
       host.Run();
   }
  }
}

AddMvc 扩展方法添加和配置几个 ASP.NET MVC核心服务,如配置功能(IConfigureOptions与MvcOptions和RouteOptions);控制器工厂和控制器触发器(IControllerFactory,IControllerActivator)、动作方法选择器,调用者和约束提供者(IActionSelector,IActionInvokerFactory,IActionConstraintProvider)、参数绑定器和模型验证器(IControllerActionArgumentBinder,IObjectModelValidator)、和过滤器提供程序(IFilterProvider)。
它除了增加核心服务,AddMvc方法还添加ASP.NET MVC服务以支持授权,CORS,数据注释,视图,Razor视图引擎等。

定义路由

第40章解释了 IapplicationBuilder 的 Map 扩展方法如何定义一个简单的路由。本章展示了 ASP.NET MVC 路由如何基于映射提供了一种灵活的路由机制,用于将URL映射到控制器和操作方法。

控制器是基于路由选择的。创建默认路由的一种简单方法是在启动类中调用UseMvcWithDefaultRoute方法(代码文件MVCSampleApp/Startup.cs):

public void Configure(IApplicationBuilder app)
{
  // etc.
  app.UseIISPlatformHandler();
  app.UseStaticFiles();
  app.UseMvcWithDefaultRoute();
  // etc.
}

注意 扩展方法 UseStaticFiles 已在第40章讨论。此方法需要添加 Microsoft.AspNet.StaticFiles  NuGet包。

使用默认路由后,控制器类型的名称(不带Controller后缀)和方法名称组成路由,例如  http://server[:port]/controller/action 。还可以使用名为id的可选参数,例如:  http://server[:port]/controller/action/id  。控制器的默认名称为 Home ; action方法的默认名称为Index。
以下代码段显示了指定相同默认路由的另一种方法。 UseMvc方法可以接收类型 Action<IRouteBuilder> 的参数。此IRouteBuilder接口包含映射的路由列表。使用 MapRoute 扩展方法定义路由:

app.UseMvc(routes =< with => routes.MapRoute(
    name:"default",
    template:"{controller}/{action}/{id?}",
    defaults: new {controller ="Home", action ="Index"}
  ));

此路由定义与默认路由定义相同。template 参数定义URL,id? 定义了该参数是可选的; defaults 参数定义URL的控制器和动作部分的默认值。

让我们看看这个URL:

http://localhost:[port]/UseAService/GetSampleStrings

这个URL 中 UseAService 映射到控制器的名称,因为 Controller 后缀是自动添加的,类型名称为UseAServiceController,GetSampleStrings 是操作,它表示 UseAServiceController 类中的一个方法。

添加路由

添加或更改路由有几个原因。例如,可以修改路由去使用链接操作,将Home定义为默认控制器,向链接添加实体或使用多个参数。

可以定义路由让用户使用链接(例如 http://<server>/About )在Home控制器中寻址“关于”操作方法,而不传递控制器名称,如以下代码段所示。请注意,控制器名称不在URL中。 controller关键字对于路由是必需的,但可以为其提供默认值:

app.UseMvc(routes => routes.MapRoute(
    name:"default",
    template:"{action}/{id?}",
    defaults: new {controller ="Home", action ="Index"}
  ));

更改路由的另一个方案如以下代码段所示。在此代码段中,向路径中添加变量 language 。该变量设置为URL中位于服务器名称之后并放置在控制器之前,例如   http://server/en/Home/About   。可以使用它来指定语言:

app.UseMvc(routes => routes.MapRoute(
    name:"default",
    template:"{controller}/{action}/{id?}",
    defaults: new {controller ="Home", action ="Index"}
  ).MapRoute(
    name:"language",
    template:"{language}/{controller}/{action}/{id?}",
    defaults: new {controller ="Home", action ="Index"}
);

如果一个路由匹配并且找到了控制器和动作方法,则采取路由,否则选择下一条路由,直到找到一条路由匹配。

使用路由约束

映射路由时,可以指定约束。这种情况下,不符合约束定义的URL 是不能被解析的。以下约束定义  language 参数使用正则表达式(en)|(de)来指定只能是 en或de。例如 只有  http://<server>/en/Home/About  或  http://<server>/de/Home/About  的URL有效:

app.UseMvc(routes => routes.MapRoute(
  name:"language",
  template:"{language}/{controller}/{action}/{id?}",
  defaults: new {controller ="Home", action ="Index"},
  constraints: new {language = @"(en)|(de)"}
));

如果链接只应启用数字(例如,要访问具有产品编号的产品),正则表达式  \d+ 匹配任意数量的数字,但必须至少匹配一个数字:

app.UseMvc(routes => routes.MapRoute(
  name:"products",
  template:"{controller}/{action}/{productId?}",
  defaults: new {controller ="Home", action ="Index"},
constraints: new {productId = @"\d+"}
)); 

现在已经看到了路由如何指定使用的控制器和控制器的动作。下一节“创建控制器”介绍了控制器的详细信息。

创建控制器

控制器对来自用户的请求做出反应并发送响应。本节所讲述的内容,可以不需要视图。

使用ASP.NET MVC时有一些约定要优于配置。控制器也会看到一些约定。可以在目录 Controllers 中找到控制器,并且控制器类的名称必须以名称 Controller 为后缀。

在创建第一个控制器之前,先创建Controllers目录。然后,可以通过在解决方案资源管理器中选择此目录来创建控制器,从上下文菜单中选择 Add -> New Item,并选择MVC控制器类项目模板。 HomeController是为指定的路由创建的。

生成的代码包含从基类Controller派生的HomeController类。此类还包含与 Index 操作对应的 Index 方法。当请求由路由定义的操作时,将调用控制器中的方法(代码文件MVCSampleApp/Controllers/HomeController.cs):

  public class HomeController : Controller
  {
    public IActionResult Index() => View();
  }

了解操作方法

控制器包含操作(译者注: action,也译作 动作)方法。以下代码片段有一个简单的动作 Hello 方法(代码文件MVCSampleApp/Controllers/HomeController.cs):

public string Hello() =>"Hello, ASP.NET MVC 6"; 

可以使用链接 http://localhost:5000/Home/Hello 在Home控制器中调用Hello操作。当然,端口号取决于设置,可以使用项目设置中的网络属性进行配置。从浏览器打开此链接时,控制器只返回字符串“Hello, ASP.NET MVC 6”,没有HTML - 只是一个字符串。浏览器可以显示字符串。

动作可以返回任何内容,例如,图像,视频,XML或JSON数据的字节,当然,也可以是HTML。视图对于返回HTML有很大的帮助。

使用参数

可以使用参数声明动作方法,如下面的代码片段(代码文件MVCSampleApp/Controllers/HomeController.cs):

public string Greeting(string name) =>
  HtmlEncoder.Default.Encode($"Hello, {name}");

注意 HtmlEncoder需要NuGet包System.Text.Encodings.Web。

有了这个声明,可以调用Greeting操作方法去请求以下的URL,在URL中传递带有name参数的值:http://localhost:18770/Home/Greeting?name=Stephanie。

要使用更容易记住的链接,可以使用路由信息来指定参数。 Greeting2操作方法指定名为id的参数。

public string Greeting2(string id) =>
  HtmlEncoder.Default.Encode($"Hello, {id}");

这与默认路由{controller}/{action}/{id?}匹配,其中id被指定为可选参数。现在使用下面的链接,id参数包含字符串Matthias:http://localhost:5000/Home/Greeting2/Matthias。

还可以使用任意数量的参数声明操作方法。例如,可以使用两个参数将“Add”操作方法添加到Home控制器,如下所示:

public int Add(int x, int y) => x + y;

 

可以使用URL  http://localhost:18770/Home/Add?x=4&y=5 调用此操作以填充x和y参数。

使用多个参数,还可以定义一个路由以使用不同的链接传递值。以下代码片段显示了在路由表中定义的一个附加路由,以指定填充变量x和y的多个参数(代码文件MVCSampleApp/Startup.cs):

app.UseMvc(routes =< routes.MapRoute(
    name:"default",
    template:"{controller}/{action}/{id?}",
    defaults: new {controller ="Home", action ="Index"}
  ).MapRoute(
    name:"multipleparameters",
    template:"{controller}/{action}/{x}/{y}",
    defaults: new {controller ="Home", action ="Add"},
    constraints: new {x = @"\d", y = @"\d"}
  ));

现在,可以使用此URL调用与之前相同的操作:http://localhost:18770/Home/Add/7/2。

注意 本章后面的“将数据传递到视图”部分中,将看到可以使用自定义类型的参数,以及客户端的数据如何映射到属性。

返回数据

目前为止,只从控制器返回字符串值。通常,返回实现接口 IActionResult 的对象。

以下是 ResultController 类的几个示例。第一个代码片段使用 ContentResult 类返回简单的文本内容。可以使用来自基类Controller 的方法来返回 ActionResults,而不是创建 ContentResult 类的实例并返回该实例。在以下示例中,方法 Content 用于返回文本内容。 Content方法允许指定内容,MIME类型和编码(代码文件MVCSampleApp/Controllers/ResultController.cs):

public IActionResult ContentDemo() =>
  Content("Hello World","text/plain");

要返回JSON格式的数据,可以使用Json方法。以下示例代码创建一个Menu对象:

public IActionResult JsonDemo()
{
  var m = new Menu
  {
    Id = 3,
    Text ="Grilled sausage with sauerkraut and potatoes",
    Price = 12.90,
    Date = new DateTime(2016, 3, 31),
    Category ="Main"
  };
  return Json(m);
}

Menu类在Models目录中定义,它定义了一个简单的仅有一些属性的POCO 类(代码文件MVCSampleApp/Models/Menu.cs):

public class Menu
{
  public int Id {get; set;}
  public string Text {get; set;}
  public double Price {get; set;}
  public DateTime Date {get; set;}
  public string Category {get; set;}
}

客户端在响应正文中看到此JSON数据。 JSON数据可以轻松地作为JavaScript对象使用:

{"Id":3,"Text":"Grilled sausage with sauerkraut and potatoes",
"Price":12.9,"Date":"2016-03-31T00:00:00","Category":"Main"}

使用Controller类的Redirect方法,客户端接收HTTP重定向请求。在接收到重定向请求后,浏览器请求它接收的链接。 Redirect方法返回一个RedirectResult(代码文件MVCSampleApp/Controllers/ResultController.cs):

public IActionResult RedirectDemo() => 
Redirect("http://www.cninnovation.com");

还可以通过指定重定向到另一个控制器和操作来为客户端生成重定向请求。 RedirectToRoute 返回一个 RedirectToRouteResult,用于指定路由名称,控制器,操作和参数。这将构建一个使用HTTP重定向请求返回到客户端的链接:

public IActionResult RedirectRouteDemo() =>
  RedirectToRoute(new {controller ="Home", action="Hello"});

Controller基类的File方法定义了返回不同类型的不同重载。此方法可以返回FileContentResult,FileStreamResult和VirtualFileResult。不同的返回类型取决于所使用的参数 - 例如,VirtualFileResult的字符串,FileStreamResult的Stream和FileContentResult的字节数组。

下一个代码段返回一个图像。创建 Images 文件夹并添加一个JPG文件。要使下一个代码段工作,请在 wwwroot 目录中创建一个Images文件夹,并添加文件Matthias.jpg。示例代码返回一个VirtualFileResult,它在第一个参数指定文件名,第二个参数指定MIME类型为image/jpeg的contentType参数:

public IActionResult FileDemo() =>
  File("~/images/Matthias.jpg","image/jpeg");

下一节显示如何返回不同的ViewResult。

使用控制器基类和POCO控制器

到目前为止,所有创建的控制器都是从基类 Controller 派生的。 ASP.NET MVC 6还支持不从这个基类派生的控制器 - 被称为POCO(普通旧CLR对象)控制器。这样,可以用自定义的基类来定义控制器类型层次结构。

从Controller基类中得到什么?这个基类能使控制器直接访问基类的属性。下表描述了这些属性及其功能。 

属性 说明
ActionContext 此属性包装其他的属性。这里可以获取有关操作描述符的信息,其中包含操作的名称,控制器,过滤器和方法信息; HttpContext可以从Context属性直接访问;可以从ModelState属性直接访问的模型的状态以及可以从RouteData属性直接访问路由信息??。
Context 此属性返回HttpContext。通过它可以访问 ServiceProvider 以访问注册了依赖注入(ApplicationServices属性)的服务,身份验证和用户信息,请求和响应信息 都可以从 Request 和 Response属性直接访问的,以及Web套接字(如果它们正在使用)。
BindingContext 此属性可以访问将接收到的数据绑定到操作方法的参数的绑定器。将绑定请求信息绑定到自定义类型将在本章后面的“从客户端提交数据”一节中讨论。
MetadataProvider 使用绑定器绑定参数。绑定器可以使用与模型相关联的元数据。MetadataProvider属性可以访问有关配置为处理元数据信息的提供程序的信息。
ModelState ModelState属性可以知道模型绑定是成功还是有错误。如果出现错误,可以阅读有关导致错误的属性的信息。
Request

此属性可以访问有关 HTTP请求的所有信息:标题和正文信息,查询字符串,表单数据和Cookie。 头信息包含一个User-Agent字符串,提供有关浏览器和客户端平台的信息。

Response  此属性保存返回给客户端的信息。可以发送cookie,更改标题信息,并直接写入正文。 在本章前面的章节“启动”中,已经看到了如何使用Response属性将一个简单的字符串返回给客户端。
Resolver Resolver属性返回ServiceProvider,可以在其中访问注册了依赖注入的服务。
RouteData RouteData属性提供有关在启动代码中注册的完整路由表的信息。
ViewBag 可以使用这些属性向视图发送信息。这将在稍后的“将数据传递到视图”部分中解释。
ViewData  
TempData 此属性写入在多个请求之间共享的用户状态(而写入ViewBag和ViewData的数据可以写入以在单个请求中共享视图和控制器之间的信息)。默认情况下,TempData将信息写入会话状态。
User User属性返回有关已验证用户的信息,包括身份和声明。

POCO控制器没有Controller基类,但访问这些信息仍然很重要。以下代码片段定义了从对象基类派生的POCO控制器(当然您可以使用自定义类型作为基类)。要使用POCO类创建一个ActionContext,可以创建此类型的属性。 POCO Controller类使用ActionContext 作为此属性的名称,这类似于Controller类的方式。但是,有一个属性不会自动设置。需要应用ActionContext属性。使用此属性注入实际的ActionContext。 Context属性直接从ActionContext访问HttpContext属性。 Context属性用于从UserAgentInfo 操作方法访问并返回请求中的User-Agent头信息(代码文件MVCSampleApp/Controllers/POCOController.cs):

public class POCOController
{
  public string Index() =>
    "this is a POCO controller";
  [ActionContext]
  public ActionContext ActionContext {get; set;}
  public HttpContext Context => ActionContext.HttpContext;
  public ModelStateDictionary ModelState => ActionContext.ModelState;
  public string UserAgentInfo()
  {
    if (Context.Request.Headers.ContainsKey("User-Agent"))
    {
     return Context.Request.Headers["User-Agent"];
    }
    return"No user-agent information";
  }
} 

创建视图

返回到客户端的HTML代码最好使用视图来指定。本节中的示例将创建 ViewsDemoController 。视图都在Views文件夹中定义。 ViewsDemo 控制器的视图需要一个 ViewsDemo子目录。这是视图的约定(代码文件MVCSampleApp/Controllers/ViewsDemoController.cs):

public ActionResult Index() => View();

注意 另一个搜索视图的地方是  Shared  目录。可以将多个控制器(以及多个视图使用的特殊部分视图)要使用的视图放入 Shared 目录。

在Views目录中创建 ViewsDemo 目录之后,可以使用Add -> New Item并选择 “MVC View Page” 项目模板来创建视图。因为action方法具有名称Index,所以视图文件被命名为Index.cshtml。

操作方法Index使用没有参数的View方法,因此视图引擎将在ViewsDemo目录中搜索与操作名称相同名称的视图文件。在控制器中使用的View方法有允许传递不同视图名称的重载。在这种情况下,视图引擎将查找传递给View方法的名称的视图。

视图包含混合了一些服务器端代码的HTML代码,如下面的代码段(代码文件 VCSampleApp/Views/ViewsDemo/Index.cshtml)所示:

@{
  Layout = null;
}
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Index</title>
</head>
<body>
  <div>
  </div>
</body>
</html>

服务器端代码使用 @符号 写入,它会启动 Razor 语法,本章后面将讨论。在介绍Razor语法的详细信息之前,下一节将介绍如何将数据从控制器传递到视图。

传递数据到视图

控制器和视图在同一进程中运行。视图直接从控制器内创建。这使得将数据从控制器传递到视图变得容易。为了传递数据,可以使用ViewDataDictionary。此字典将键存储为字符串,并启用对象值。可以将 ViewDataDictionary 与 Controller 类的 ViewData 属性一起使用 - 例如,可以将字符串传递到使用键值 MyData 的字典: ViewData[“MyData”] =“Hello”  。一个更简单的语法是使用ViewBag属性。 ViewBag 是一个动态类型,可以分配任何属性名称来传递数据到视图(代码文件MVCSampleApp/Controllers/SubmitDataController.cs):

public IActionResult PassingData()
{
  ViewBag.MyData ="Hello from the controller";
  return View();
} 

注意 使用动态类型的优点是没有从视图到控制器的直接依赖。动态类型在第16章“反射,元数据和动态编程”中有详细说明。

在视图中,可以用类似于控制器的方式访问从控制器传递来的数据。视图的基类(WebViewPage)定义了一个ViewBag属性(代码文件MVCSampleApp/Views/ViewsDemo/PassingData.cshtml):

<div>
  <div>@ViewBag.MyData</div>
</div>

了解 Razor 语法

如上所述的视图,视图包含HTML和服务器端代码。ASP.NET MVC 中可以使用Razor语法在视图中编写C#代码。 Razor使用@字符 作为转换字符。从@后面就是C#代码。

使用Razor需要区分返回值的语句和不返回值的方法。可以直接使用返回的值。例如,ViewBag.MyData 返回一个字符串。该字符串直接放在HTML div标签之间,如下所示:

<div>@ViewBag.MyData</div>

当调用返回 void 的方法或指定一些不返回值的其他语句时,需要一个Razor代码块。以下代码块定义了一个字符串变量:

@{
  string name ="Angela";
}

现在可以使用简单语法的变量,只需使用转换字符@访问变量:

<div>@name</div>

使用Razor语法,引擎会在找到HTML元素时自动检测C#代码的结束。在某些情况下可能无法自动检测到C#代码的结束。可以使用括号来解决此问题,如以下示例所示,以标记变量,然后正常文本继续:

<div>@(name), Stephanie</div>

另一种启动Razor代码块的方法是使用foreach语句:

@foreach(var item in list)
{
  <li>The item name is @item.</li>
}

注意 通常 Razor 自动检测文本内容,例如,Razor检测到打开的尖括号或带有变量的括号。也有一些情况下不起作用。这里可以明确使用  @:  来定义文本的开始。

创建强类型视图

将数据传递给视图,我们已经看到了操作中的ViewBag。有另一种方法来传递数据到视图 - 传递模型到视图。使用模型允许您创建强类型视图。

现在 ViewsDemoController 使用动作方法PassingAModel扩展。以下示例创建一个新的菜单项列表,并将此列表作为模型传递给Controller基类的View方法(代码文件MVCSampleApp/Controllers/ViewsDemoController.cs):

public IActionResult PassingAModel()
{
  var menus = new List<Menu>
  {
    new Menu
    {
      Id=1,
      Text="Schweinsbraten mit Kn&ouml;del und Sauerkraut",
      Price=6.9,
      Category="Main"
    },
    new Menu
    {
      Id=2,
      Text="Erd&auml;pfelgulasch mit Tofu und Geb&auml;ck",
      Price=6.9,
      Category="Vegetarian"
    },
    new Menu
    {
      Id=3,
      Text="Tiroler Bauerngr&ouml;st‘l mit Spiegelei und Krautsalat",
      Price=6.9,
      Category="Main"
    }
  };
  return View(menus);
}

当模型信息从动作方法传递到视图时,可以创建强类型视图。强类型视图使用 model 关键字声明。传递给视图的模型类型必须与模型指令的声明相匹配。在下面的代码片段中,强类型视图声明类型  IEnumerable<Menu>  ,它与模型类型匹配。因为 Menu 类在命名空间 MVCSampleApp.Models 中定义,所以这个命名空间使用 using 关键字打开。

从.cshtml文件创建的视图的基类派生自基类 RazorPage 。有了一个模型,基类是 RazorPage<TModel>  类型;以下代码片段基类是 RazorPage<IEnumerable<Menu>> 。此通用参数依次定义类型为 IEnumerable<Menu> 的Model属性。代码片段基类的Model属性用于通过@foreach 遍历 Menu 项,并显示每个菜单的列表项(代码文件MVCSampleApp/ViewsDemo/PassingAModel.cshtml):

@using MVCSampleApp.Models
@model IEnumerable<Menu>
@{
  Layout = null;
}
<!DOCTYPE html>
<html>
<head>
  <meta name="viewport" content="width=device-width" />
  <title>PassingAModel</title>
</head>
<body>
  <div>
    <ul>
      @foreach (var item in Model)
      {
        <li>@item.Text</li>
      }
    </ul>
  </div>
</body>
</html>

可以将任何对象作为模型传递 - 任何视图需要的都可以。例如,当编辑单个Menu对象时,可以使用类型为Menu的模型。显示或编辑列表时,可以使用  IEnumerable<Menu> 。

运行应用程序显示定义视图时,会在浏览器中看到菜单列表,如图41.2所示。

技术分享

 

图41.2

定义布局

通常,许多网页应用程序共享相同的内容,例如版权信息、徽标和主导航结构。目前为止,所有视图都包含完整的HTML内容,但是有一个更简单的方法来管理共享的内容。这是布局页面发挥作用的地方。

要定义布局,要设置视图的 Layout 属性。要定义所有视图的默认属性,可以创建视图起始页。将此文件放入Views文件夹,可以使用项目模板 MVC View Start Page 来创建它。创建文件_ViewStart.cshtml(代码文件MVCSampleApp/Views/_ViewStart.cshtml):

@{
  Layout ="_Layout";
}

对于不需要布局的所有视图,可以将Layout属性设置为null:

@{
  Layout = null;
}

使用默认布局页

可以使用项目模板 MVC View Layout Page 创建默认布局页面。在共享文件夹中创建此页面,以便它可用于来自不同控制器的所有视图。项目模板 MVC View Layout Page 创建以下代码:

<!DOCTYPE html>
<html>
<head>
  <meta name="viewport" content="width=device-width" />
  <title>@ViewBag.Title</title>
</head>
<body>
  <div>
    @RenderBody()
  </div>
</body>
</html>

布局页面包含使用此布局页面的所有页面通用的HTML内容(例如,页眉,页脚和导航)。您已经了解了视图和控制器如何用ViewBag通信。布局页面可以使用相同的机制。在内容页面中定义 ViewBag.Title 的值,在布局页面,上述代码段中它显示在HTML标题元素中。基类 RazorPage 的 RenderBody 方法呈现内容页面的内容,从而定义内容应该放置的位置。

以下代码段中,生成的布局页面将更新为引用样式表,并向每个页面添加页眉,页脚和导航部分。environment,asp-controller和asp-action 是创建HTML元素的标签助手。标签助手将在本章后面的“助手”部分中讨论(代码文件MVCSampleApp/Views/Shared/_Layout.cshtml):

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <environment names="Development">
    <link rel="stylesheet" href="~/css/site.css" />
  </environment>
  <environment names="Staging,Production">
    <link rel="stylesheet" href="~/css/site.min.css"
      asp-append-version="true" />
  </environment>
  <title>@ViewBag.Title - My ASP.NET Application</title>
</head>
<body>
  <div class="container">
    <header>
      <h1>ASP.NET MVC Sample App</h1>
    </header>
    <nav>
      <ul>
        <li><a asp-controller="ViewsDemo" asp-action="LayoutSample">
          Layout Sample</a></li>
        <li><a asp-controller="ViewsDemo" asp-action="LayoutUsingSections">
          Layout Using Sections</a></li>
      </ul>
    </nav>
    <div>
      @RenderBody()
    </div>
    <hr />
    <footer>
      <p>
        <div>Sample Code for Professional C#</div>
        &copy; @DateTime.Now.Year - My ASP.NET Application
      </p>
    </footer>
  </div>
</body>
</html>

为LayoutSample 操作 创建视图(代码文件MVCSampleApp/Views/ViewsDemo/LayoutSample.cshtml)。该视图不设置Layout属性,因此使用默认布局。以下代码段设置ViewBag.Title,它在布局中的HTML标题元素中使用:

@{
  ViewBag.Title ="Layout Sample";
}
<h2>LayoutSample</h2>
<p>
  This content is merged with the layout page
</p>

运行应用程序时,将合并布局和视图中的内容,如图41.3所示。

技术分享

图41.3

使用部件

渲染正文和使用ViewBag不是在布局和视图之间交换数据仅有的方法。部件区域可以定义命名内容应放置在视图中的位置。以下代码片段使用名为PageNavigation的部件。默认情况下,这些部件是必需的,如果未定义部件,则加载视图将失败。当required参数设置为false时,该部件变为可选(代码文件MVCSampleApp/Views/Shared/_Layout.cshtml):

<!-- etc. -->
<div>
  @RenderSection("PageNavigation", required: false)
</div>
<div>
  @RenderBody()
</div>
<!-- etc. -->

在视图页面中,section关键字定义了该部件。放置该部件的位置完全独立于其他内容。视图不定义页面中的位置,这是由布局定义的(代码文件MVCSampleApp/Views/ViewsDemo/LayoutUsingSections.cshtml):

@{
    ViewBag.Title ="Layout Using Sections";
}
<h2>Layout Using Sections</h2>
Main content here
@section PageNavigation
{
  <div>Navigation defined from the view</div>
  <ul>
    <li>Nav1</li>
    <li>Nav2</li>
  </ul>
}

运行应用程序时,视图和布局中的内容将根据布局定义的位置合并,如图41.4所示。

技术分享

图41.4

注意 部件不仅用于在HTML页面的正文中放置某些内容,它们也可用于视图在头部中放置某些东西 - 例如,来自页面的元数据。

部分视图定义内容

布局为Web应用程序中的多个页面提供了一个整体定义,也可以使用部分视图来定义视图中的内容。部分视图没有布局。

除此之外,部分视图类似于正常视图。部分视图使用与普通视图相同的基类,它们都有一个模型。

以下是部分视图的示例。这里首先需要一个模型,该模型包含由 EventsAndMenusContext 类定义的独立集合,事件和菜单的属性(代码文件MVCSampleApp/Models/EventsAndMenusContext.cs):

public class EventsAndMenusContext
{
  private IEnumerable<Event> events = null;
  public IEnumerable<Event> Events
  {
    get
    {
      return events ?? (events = new List<Event>()
      {
        new Event
        {
          Id=1,
          Text="Formula 1 G.P. Australia, Melbourne",
          Day=new DateTime(2016, 4, 3)
        },
        new Event
        {
          Id=2,
          Text="Formula 1 G.P. China, Shanghai",
          Day = new DateTime(2016, 4, 10)
        },
        new Event
        {
          Id=3,
          Text="Formula 1 G.P. Bahrain, Sakhir",
          Day = new DateTime(2016, 4, 24)
        },
        new Event
        {
          Id=4,
          Text="Formula 1 G.P. Russia, Socchi",
          Day = new DateTime(2016, 5, 1)
        }
      });
    }
  }
  private List<Menu> menus = null;
  public IEnumerable<Menu> Menus
  {
    get
    {
      return menus ?? (menus = new List<Menu>()
      {
        new Menu
        {
          Id=1,
          Text="Baby Back Barbecue Ribs",
          Price=16.9,
          Category="Main"
        },
        new Menu
        {
          Id=2,
          Text="Chicken and Brown Rice Piaf",
          Price=12.9,
          Category="Main"
        },
        new Menu
        {
          Id=3,
          Text="Chicken Miso Soup with Shiitake Mushrooms",
          Price=6.9,
          Category="Soup"
        }
      });
    }
  }
}

上下文类已注册到依赖注入启动代码,以使用控制器构造函数注入类型(代码文件MVCSampleApp/Startup.cs):

public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddScoped<EventsAndMenusContext>();
}

现在该模型将在以下代码中用于部分视图示例,从服务器端代码加载的部分视图以及使用客户端上的JavaScript代码请求的视图。

使用来自服务器端代码的部分视图

在 ViewsDemoController 类中,构造函数被修改为注入 EventsAndMenusContext 类型(代码文件MVCSampleApp/Controllers/ViewsDemoController.cs):

public class ViewsDemoController : Controller
{
  private EventsAndMenusContext _context;
  public ViewsDemoController(EventsAndMenusContext context)
  {
    _context = context;
  }
  // etc.

UseAPartialView1动作方法将 EventsAndMenus的 一个实例传递给视图(代码文件MVCSampleApp/Controllers/ViewsDemoController.cs):

public IActionResult UseAPartialView1() => View(_context);

视图页面被定义为使用 EventsAndMenusContext 类型的模型。可以使用HTML Helper 的方法 Html.PartialAsync 显示部分视图,该方法返回  Task<HtmlString>。后面的示例代码中,该字符串使用 Razor 语法作为 div 元素的内容写入。 PartialAsync方法的第一个参数接受部分视图的名称,第二个参数可以传递模型。如果没有传递模型,则部分视图可以访问与视图相同的模型。这里视图使用类型 EventsAndMenusContext 的模型,部分视图只是使用类型  IEnumerable<Event>的一部分(代码文件MVCSampleApp/Views/ViewsDemo/UseAPartialView1.cshtml):

@model MVCSampleApp.Models.EventsAndMenusContext
@{
  ViewBag.Title ="Use a Partial View";
  ViewBag.EventsTitle ="Live Events";
}
<h2>Use a Partial View</h2>
<div>this is the main view</div>
<div>
  @await Html.PartialAsync("ShowEvents", Model.Events)
</div>

如果不使用异步方法,可以使用同步变量Html.Partial。这是一个返回 HtmlString 的扩展方法。

在视图中渲染局部视图的另一种方法是使用 HTML Helper 方法Html.RenderPartialAsync,它被定义为返回 Task 。此方法直接将部分视图内容写入响应流。这样一可以在Razor代码块中使用 RenderPartialAsync 。

用创建正常视图的方式去创建局部视图即可,可以访问模型以及通过使用ViewBag属性访问的字典。部分视图接收字典的副本以接收可以使用的相同字典数据(代码文件MVCSampleApp/Views/ViewsDemo/ShowEvents.cshtml):

@using MVCSampleApp.Models
@model IEnumerable<Event>
<h2>
  @ViewBag.EventsTitle
</h2>
<table>
  @foreach (var item in Model)
  {
    <tr>
      <td>@item.Day.ToShortDateString()</td>
      <td>@item.Text</td>
    </tr>
  }
</table>

运行应用程序时,将渲染视图,局部视图和布局,如图41.5所示。

技术分享

图41.5

从控制器返回部分视图

目前为止部分视图都是直接加载,而im有与控制器交互,但也可以使用控制器返回部分视图。

以下代码片段中,在 ViewsDemoController 类中定义了两个操作方法。第一个动作方法 UsePartialView2 返回正常视图,第二个动作方法 ShowEvents 使用基类方法PartialView返回部分视图。局部视图 ShowEvents 之前已经创建和使用,并在此处使用。 PartialView方法将包含事件列表的模型将传递到部分视图(代码文件 MVCSampleApp/Controllers/ViewDemoController.cs):

public ActionResult UseAPartialView2() => View();
public ActionResult ShowEvents()
{
  ViewBag.EventsTitle ="Live Events";
  return PartialView(_context.Events);
}

从控制器提供部分视图时,可以直接从客户端代码调用部分视图。以下代码片段使用jQuery:事件处理程序链接到按钮的 click 事件。在事件处理程序内,使用jQuery load 函数向服务器发出GET请求,以请求 /ViewsDemo/ShowEvents 。该请求返回部分视图,部分视图的结果放在名为 events 的 div 元素中(代码文件MVCSampleApp/Views/ViewsDemo/UseAPartialView2.cshtml):

@model MVCSampleApp.Models.EventsAndMenusContext
@{
  ViewBag.Title ="Use a Partial View";
}
<script src="~/lib/jquery/dist/jquery.js"></script>
<script>
  $(function () {
    $("#getEvents").click(function () {
      $("#events").load("/ViewsDemo/ShowEvents");
    });
  });
</script>
<h2>Use a Partial View</h2>
<div>this is the main view</div>
<button id="FileName_getEvents">Get Events</button>
<div id="FileName_events">
</div>

使用视图组件

ASP.NET MVC 6 提供了一个新的替代部分视图的方式:视图组件。视图组件与局部视图非常类似,主要区别是视图组件与控制器无关。这使得它们易于与多个控制器一起使用。视图组件非常有用的示例是菜单的动态导航,登录面板或博客中的侧边栏内容。这些场景独立于单个控制器是非常有用的。

与控制器和视图一样,视图组件有两个部分。视图组件中控制器功能由从ViewComponent(或具有ViewComponent属性的POCO类)派生的类接管。用户界面类似于视图定义,但是调用视图组件的方法不同。

以下代码片段定义了从基类 ViewComponent 派生的视图组件。该类使用先前在 Startup 类中注册的 EventsAndMenusContext 类型,以便可用于依赖注入。这与构造函数注入的控制器类似。 InvokeAsync 方法定义为从显示视图组件的视图中调用。该方法可以有任意数量和类型的参数,因为由 IViewComponentHelper 接口定义的方法使用 params关键字定义了灵活数量的参数。不要使用异步方法实现,而是同步实现此方法返回 IViewComponentResult  而不是  Task<IViewComponentResult>  。然而,通常异步方式是最好的使用,例如,用于访问数据库。视图组件需要存储在 ViewComponents 目录中。这个目录本身可以放置在项目中的任何地方(代码文件MVCSampleApp/ViewComponents/EventListViewComponent.cs):

public class EventListViewComponent : ViewComponent
{
  private readonly EventsAndMenusContext _context;
  public EventListViewComponent(EventsAndMenusContext context)
  {
    _context = context;
  }
  public Task<IViewComponentResult> InvokeAsync(DateTime from, DateTime to)
  {
    return Task.FromResult<IViewComponentResult>(
      View(EventsByDateRange(from, to)));
  }
  private IEnumerable<Event> EventsByDateRange(DateTime from, DateTime to)
  {
    return _context.Events.Where(e => e.Day >= from && e.Day <= to);
  }
}

视图组件的用户界面在以下代码段中定义。可以使用项目模板 MVC View Page 创建视图组件的视图,它使用相同的 Razor 语法。具体来说,它必须放在 Components/[viewcomponent] 文件夹中,例如 Components/EventList 。要使视图组件可用于所有控件,需要在视图的 Shared 文件夹中创建Components文件夹。仅使用来自一个特定控制器的视图组件时,可以将其放入视图控制器文件夹中。这个视图与众不同的地方是它需要被命名为default.cshtml。也可以创建其他视图名称,需要使用从InvokeAsync方法返回的View方法的参数指定这些视图(代码文件MVCSampleApp/Views/Shared/Components/EventList/default.cshtml):

@using MVCSampleApp.Models;
@model IEnumerable<Event>
<h3>Formula 1 Calendar</h3>
<ul>
  @foreach (var ev in Model)
  {
    <li><div>@ev.Day.ToString("D")</div><div>@ev.Text</div></li>
  }
</ul>

视图组件完成后,可以通过调用InvokeAsync方法显示它。Component是视图动态创建的属性,返回实现IViewComponentHelper的对象。 IviewComponentHelper允许您调用同步或异步方法,如Invoke,InvokeAsync,RenderInvoke和RenderInvokeAsync。当然,只能调用由视图组件实现的这些方法,并且要使用相应的参数(代码文件MVCSampleApp/Views/ViewsDemo/UseViewComponent.cshtml):

@{
  ViewBag.Title ="View Components Sample";
}
<h2>@ViewBag.Title</h2>
<p>
  @await Component.InvokeAsync("EventList", new DateTime(2016, 4, 10),
    new DateTime(2016, 4, 24))
</p>

运行应用程序,您可以看到呈现的视图组件,如图41.6所示。

技术分享

图41.6

在视图中使用依赖注入

如果直接从视图中需要服务,可以使用 inject 关键字注入它:

@using MVCSampleApp.Services
@inject ISampleService sampleService
<p>
    @string.Join("*", sampleService.GetSampleStrings())
</p>

要这样做时,最好使用AddScoped方法注册服务。如上所述,以这种方式注册服务意味着它只对一个HTTP请求实例化一次。使用AddScoped,在控制器和视图中注入相同的服务,它只为请求实例化一次。

导入多个视图的命名空间

所有以前的视图示例都使用了using关键字打开所有需要的命名空间。其实可以使用Visual Studio项目模板 MVC View Imports Page 来创建文件(_ViewImports.cshml)定义所有使用声明,而不是在每个视图打开命名空间(代码文件MVCSampleApp/Views/_ViewImports.cshtml):

@using MVCSampleApp.Models
@using MVCSampleApp.Services

有了这个文件,不需要在所有视图中都添加 using 关键字。 

 

---------------------(未完待续)

C# 6 与 .NET Core 1.0 高级编程 - 41 ASP.NET MVC(上)