如何在 ASP.NET MVC 中集成 AngularJS(1)

介绍

当涉及到计算机软件的开发时,我想运用全部的最新技术。例如,前端使用最新的 JavaScript 技术,服务器端使用最新的基于 REST 的 Web API 服务。另外,还有最新的数据库技术、最新的设计模式和技术。html

当选择最新的软件技术时,有几个因素在起做用,其中包括如何将这些技术整合起来。过去两年中,我最喜欢的一项技术就是设计单页面应用(SPA)的 AngularJS。做为一个微软stack开发者,我也是使用 ASP.NET MVC 平台实现 MVC 设计模式和并进行研究的粉丝,包括它的捆绑和压缩功能以及实现其对 RESTful 服务的 Web API 控制器。前端

为了兼得二者,本文介绍了在 ASP.NET MVC 中集成 AngularJS 的一箭双鵰的方案。html5

因为本文篇幅较长,故会分为3篇,分别进行介绍。web

概述

本文中示例的 Web 应用程序将有三个目标:数据库

  • 在前端页面中实现 AngularJS 和 JavaScript AngularJS 控制器
  • 使用微软的 ASP.NET MVC 平台来创建、引导并捆绑一个应用
  • 根据功能模型的需求,动态的加载 AngularJS 的控制器和服务

本文的示例应用程序将包含三个主要文件夹:关于联系和索引的主文件夹、容许你建立,更新和查询客户的客户文件夹、容许你建立,更新和查询产品的产品文件夹。bootstrap

除了使用 AngularJS 和 ASP.NET MVC,这个应用程序也将实现使用微软的 ASP.NET Web API 服务来建立 RESTful 服务。微软的实体框架将用于生成并更新一个 SQL Server Express 数据库。设计模式

此应用程序也将用到一些使用 Ninject 的依赖注入。此外,也会运用流畅的界面和 lambda 表达式,来合并使用称为 FluentValidation的.NET 的小型验证库,用于构建驻留在应用业务层的验证业务规则。浏览器

 

AngularJS VS ASP.NET Razor 视图

几年来,我一直在使用完整的 Microsoft ASP.NET MVC 平台来开发 Web 应用程序。相比于使用传统的 ASP.NET Web 窗体的 postback 模型, ASP.NET MVC 平台使用的是 Razor 视图。 这带来的是:适当的业务逻辑、数据和表示逻辑之间关注点的分离。在使用它的约定优于配置和简洁的设计模式进行 MVC 开发以后,你将永远不会想回过头去作 Web 窗体的开发。缓存

 

ASP.NET MVC 平台及其 Razor 视图引擎,不但比 Web 窗体简洁,还鼓励和容许你将 .NET 服务器端代码和样式混合。在 Razor 视图中的 HTML 混合的 .NET 代码看起来像套管代码。另外,在 ASP.NET MVC 模式下,一些业务逻辑是能够被最终写入在 MVC 的控制器中。在MVC控制器中,写入代码来控制表示层中的信息,这是颇有诱惑力的。安全

AngularJS 提供了如下对微软 ASP.NET MVC Razor 视图的加强功能:

  • AngularJS 视图是纯 HTML 的
  • AngularJS 视图被缓存在客户端上以实现更快的响应,并在每次请求不产生服务器端响应
  • AngularJS 提供了一个完整的框架,编写高质量的客户端 JavaScript 代码
  • AngularJS 提供了 JavaScript 控制器和 HTML 视图之间的彻底分离

 

ASP.NET MVC 捆绑和压缩

捆绑和压缩是两种你能够用来缩短 Web 应用程序的请求负载时间的技术。这是经过减小对服务器的请求数量和减少请求规模,来实现缩短请求负载时间的(如 CSS 和 JavaScript)。压缩技术经过复杂的代码逻辑也使得别人更难的侵入你的 JavaScript 代码。

当涉及到捆绑技术和 AngularJS 框架时,你会发现捆绑和压缩过程当中会自动使用 Grunt 和 Gulp 之类的框架,Grunt 和 Gulp 技术是一种流行的 web 库并配有插件,它容许你自动化你的每一项工做。

若是你是一个微软开发者,你可使用它们在 Visual Studio 中一键式发布你的 Web 应用,而不用学习使用任何第三发工具和库类。幸运的是,捆绑和压缩是 ASP.NET 4.5 ASP.NET 中的一项功能,能够很容易地将多个文件合并或捆绑到一个文件中。你能够建立 CSS,JavaScript 和其余包。较少的文件意味着更少的 HTTP 请求,这也能够提升第一个页面的加载性能。

 

使用 RequireJS 来实现 MVC 捆绑的动态加载

在开发 AngularJS 单页的应用程序时,其中有一件事情是不肯定的。因为应用开始时会被引导和下载,因此在主页面索引时,AngularJS 会请求全部的 JavaScript 文件和控制器。对于可能包含数百个 JavaScript 文件的大规模应用,这可能不是很理想。由于我想使用 ASP.NET 的捆绑来加载全部的 AngularJS 控制器。一旦开始索引,一个 ASP.NET 捆绑中的巨大的挑战将会出如今服务器端。

为了实现示例程序动态地绑定 ASP.NET 文件包,我决定用 RequireJS JavaScript 库。RequireJS 是一个众所周知的 JavaScript 模块和文件加载器,最新版本的浏览器是支持 RequireJS 的。起初,这彷佛是一个很简单的事情,但随着时间的推移,我完成了大量的代码的编写,却并无解决使用服务器端 rendered bundle 与客户端 AngularJS 等技术的问题。

最终,在大量的研究和反复试验和失败后,我想出了少许代码却行之有效的解决方案。

本文的接下来部分将会展现,在 ASP.NET MVC 中集成 AngularJS 的过程。

 

建立 MVC 项目并安装 Angular NuGet 包

为了开始示例应用程序,我经过在 Visual Studio 2013 专业版中选择 ASP.NET Web 应用程序模板来建立一个 ASP.NET MVC 5 Web 应用程序。以后,我选择了 MVC 工程并在应用中会用到 MVC Web API 添加文件夹和引用。下一步是选择工具菜单中的“管理 NuGet 包的解决方案”,来下载并安装 NuGet AngularJS。

对于此示例应用程序,我安装了全部的如下的 NuGet 包:

  • AngularJS - 安装整个 AngularJS 库
  • AngularJS UI - AngularJS 框架的伙伴套件UI工具和脚本。
  • AngularJS UI引导 - 包含一组原生 AngularJS 指令的引导标记和CSS
  • AngularJS 块UI - AngularJS BlockUI 指令,块状化 HTTP 中的请求
  • RequireJS - RequireJS 是一个 JavaScript 文件和模块加载
  • Ninject – 提供了支持 MVC 和 MVC Web API 支持的依赖注入
  • 实体框架 - 微软推荐的数据访问技术的新应用
  • 流畅的验证 - 创建验证规则的 .NET 验证库。
  • 优美字体- CSS 可当即定制的可升级的矢量图标

NuGet 是一个很好的包管理器。当你使用 NuGet 安装一个软件包,它会拷贝库文件到你的解决方案,并自动更新项目中的引用和配置文件。若是你删除一个包, NuGet 会让全部删除过程不会留下任何痕迹。

 

优美的URLS

对于此示例应用程序,我想在浏览器的地址栏中实现优美的网址。默认状况下,AngularJS 会将 URL 用#标签进行路由:

例如:

  • http://localhost:16390/
  • http://localhost:16390/#/contact
  • http://localhost:16390/#/about
  • http://localhost:16390/#/customers/CustomerInquiry
  • http://localhost:16390/#/products/ProductInquiry

经过转向 html5Mode 和设置基本的 URL,能够很方便的清除 URLS 并去除 URL 中的#。在 HTML5 模式下,AngularJS 的$位置服务会和使用 HTML5 History API 的浏览器 URL 地址进行交互。HTML5 History API 是经过脚原本操做浏览器历史记录的标准方法,以这点为核心,是实现单页面应用的重点。

要打开 html5Mode,你须要在 Angular 的配置过程当中,将 $locationProviderhtml5Mode 设置为 true,以下所示:

// CodeProjectRouting-production.js
angular.module("codeProject").config('$locationProvider', function ($locationProvider) {
    $locationProvider.html5Mode(true);
}]);

当你使用 html5Mode 配置 $locationProvider 时,你须要使用 href 标记来指定应用的基本 URL。基本 URL 用于在整个应用程序中,解决全部相对 URL 的问题。你能够在应用程序中设置,以下所示的母版页的 header 部分的基本 URL:

<!-- _Layout.cshtml -->
<html>
<head>
<basehref="http://localhost:16390/"/>
</head>

对于示例应用程序,我以程序设置的方式将基本 URL 存储在 Web 配置文件中。这是一种最好的方式使得基本 URL 成为一种配置,这样可以让你根据环境、配置或者你开发的应用的站点的状况,来将基本 URL 设定为不一样的值。此外,设置基本 URL 时,要确保基本 URL 以“/”为结尾,由于基本 URL 将是全部地址的前缀。

<!-- web.config.cs -->
<appsettings>
<addkey="BaseUrl"value="http://localhost:16390/"/>
</appsettings>

打开 html5Mode 并设置基本 URL 后,你须要以如下优美的 URL 做为结束:

  • http://localhost:16390/
  • http://localhost:16390/contact
  • http://localhost:16390/about
  • http://localhost:16390/customers/CustomerInquiry
  • http://localhost:16390/products/ProductInquiry

 

目录结构与配置

按照惯例,一个 MVC 项目模板要求全部的 Razor 视图驻留在视图文件夹中; 全部的 JavaScript 文件驻留在脚本文件夹; 全部的内容文件驻留在内容文件夹中。对于此示例应用程序,我想将全部的 Angular 视图和相关的 Angular JavaScript 控制器放入相同的目录下。基于 Web 的应用程序会变得很是大,我不想相关功能以整个应用程序的目录结构存储在不一样文件夹中。

在示例应用程序,会出现两个 Razor 视图被用到,Index.cshtml 和 _Layout.cshtml 母版页布局,这两个 Razor 视图将用于引导和配置应用程序。应用程序的其他部分将包括 AngularJS 视图和控制器。

对于示例应用程序,我在视图文件夹下建立了两个额外的文件夹,一个客户的子文件夹,一个产品的子文件夹。全部的客户的 Angular 视图和控件器将驻留在客户子文件夹中,全部的产品的 Angular 视图和控件器将驻留在产品子文件夹中 。

因为 Angular 视图是 HTML 文件,而 Angular 控制器是 JavaScript 文件,从 Views 文件夹到浏览器,ASP.NET MVC 必须被配置为容许 HTML 文件和 JavaScript文 件进行访问和传递。这是一个 ASP.NET MVC 默认的约定。幸运的是,你能够经过编辑视图文件下的 web.config 文件并添加一个 HTML 和 JavaScript 的处理器来更改此约定,这将会使这些文件类型可以被送达至浏览器进行解析。

复制代码

<!-- web.config under the Views folder -->
<system.webserver>
<handlers>
<addname="JavaScriptHandler"path="*.js"verb="*"precondition="integratedMode"
type="System.Web.StaticFileHandler"/>

<addname="HtmlScriptHandler"path="*.html"verb="*"precondition="integratedMode"
type="System.Web.StaticFileHandler"/>
</handlers>
</system.webserver>

复制代码

 

应用程序版本自动刷新和工程构建

对于此示例应用程序,我想跟踪每一次编译的版本和内部版本号,在属性文件夹下使用 AssemblyInfo.cs 文件的信息测试并发布这个应用。每次应用程序运行的时候,我想得到最新版本的应用程序和使用的版本号,以实现最新的 HTML 文件和 JavaScript 文件生成时,帮助浏览器从缓存中,获取最新的文件来替换那些旧文件。

对于这种应用,我使用的 Visual Studio 2013 专业版,这让一切变得简单,我为 Visual Studio2013 专业版下载了一个自动版本的插件

https://visualstudiogallery.msdn.microsoft.com/dd8c5682-58a4-4c13-a0b4-9eadaba919fe

它会自动刷新 C# 和 VB.NET 项目的版本。将安装插件下载到名为自动版本设置的工具菜单中。该插件自带了配置工具,它容许你配置主要和次要版本号,以便每次编译时,自动的更新 AssemblyInfo.cs 文件。目前,这个插件只是在 Visual Studio 2013 专业版中支持,或者你也能够手动更新版本号或使用相似微软的 TFS 以持续构建和配置管理环境的方式,来管理你的版本号。

下面是一个使用更新的 AssemblyVersion 和 AssemlyFileVersion 号的示例,这个示例在版本编译以后会经过插件自动地进行更新。

复制代码

// AssemblyInfo.cs
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

[assembly: AssemblyTitle("CodeProject.Portal")]
[assembly: AssemblyProduct("CodeProject.Portal")]
[assembly: AssemblyCopyright("Copyright &copy; 2015")]

// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("1d9cf973-f876-4adb-82cc-ac4bdf5fc3bd")]
// Version information for an assembly consists of the following four values:

// Major Version
// Minor Version
// Build Number
// Revision

// You can specify all the values or you can default the Revision and Build Numbers
// by using the '*' as shown below:

[assembly: AssemblyVersion("2015.9.12.403")]

复制代码

 

使用 Angular 视图和控制器更换联系咱们和关于 Razor 视图

要想使用 MVC 工程,首先要作的事情之一就是使用 AngularJS 视图和控制器来更换联系咱们和关于 Razor 视图。这是一个很好的起点来测试你的配置是否可以使 AngularJS 正常创建并运行。随后若是不须要这些页面,你能够删除关于和联系咱们的视图和控制器。

 

AngularJS 的这种建立控制器的方式是经过注入 $scope 实现的。示例应用程序的视图和控制器使用“controller as”语法。此语法并不是使用控制器中的 $scope,而是简化你的控制器的语法。当你声明一个“controller as”语法的控制器时,你会获得该控制器的一个实例。

 

使用“controller as”语法,你的全部的链接到控制器(视图模式)的属性必须以你视图的别名做为前缀。在下面的视图代码片断,属性标题前面就加上了“VM”的别名。

<!-- aboutController.js -->
<div ng-controller="aboutController as vm" ng-init="vm.initializeController()">
   <h4 class="page-header">{{vm.title}}</h4>
</div>

当控制器构造函数被调用时,使用“controller as”的语法,叫作“this”的控制器示例就会被建立。不须要使用 Angular 提供的 $scope 变量,你只须要简单的声明一个 vm 变量并分配“this”给它。全部被分配给 vm 对象的变量都会替换掉 $scope。有了分配给控制器功能的示例的变量,咱们就可使用这些别名并访问这些变量。

此外,全部示例应用程序中的控制器都是使用“use strict”JavaScript 命令以一种严格的模式运行的。这种严格模式能够更容易地编写“安全”的 JavaScript 代码。严格模式将此前“不严格的语法”变成了真正的错误。做为一个例子,在通常的 JavaScript 中,错误输入变量名称会建立一个新的全局变量。在严格模式下,这将抛出一个错误,所以没法意外建立一个全局变量。

复制代码

// aboutController.js
angular.module("codeProject").register.controller('aboutController', 
['$routeParams', '$location', function ($routeParams, $location) {
{
    "use strict";
    var vm = this;

    this.initializeController = function () {
        vm.title = "About Us";
    }
}]);

复制代码

如前所述,在 MVC Razor 视图中使用 AngularJS 视图和控制器的优点之一,就是 Angular 提供了很好的机制来编写高质量的 JavaScript 模块、一种纯 HTML 视图和 JavaScript 控制器之间的彻底分离的编码方式。你再也不须要使用 AngularJS 双向数据绑定技术来解析浏览器的文件对象模型,这也就使得你可以编写单元测试的 JavaScript 代码。

做为一个注脚,您将在 aboutController 看到一个名为 register.controller 的方法。在本文的后面,你会看到注册方法是从哪儿来的和它用来作什么。

 

主页索引的 Razor 视图和 MVC 路由

ASP.NET MVC 中集成 AngularJS 的一件有趣的事情,就是应用程序其实是如何启动和实现路由的。当你启动应用程序时,ASP.NET MVC 将会以以下默认的方式进入并查看路由表:

复制代码

// RouteConfig.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace CodeProject.Portal
{
    publicclass RouteConfig
    {
        publicstaticvoid RegisterRoutes(RouteCollection routes)
        {
          routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
          routes.MapRoute(
          name: "Default",
          url: "{controller}/{action}/{id}",
          defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
          );
        }
    }
}

复制代码

应用开始时,以上外装配置的 MVC 路由表中的配置,会将应用路由到 MVC Home 主控制器,并执行主控制器中的索引方法。这样会以 MVC 默认工程模板的形式,将 Index.cshtml MVC Razor 视图传递到用户输出的主页面内容中。

这个应用程序的目标是使用 Angular 视图取代全部的 MVC 视图。但问题是,甚至在 AngularJS 被启动以前,主页的 Razor 视图索引就已经被执行和注入了 _Layout.cshtml 主页面中。

自从我决定,将主页面改成 AngularJS 视图,我就使用包含 AngularJS ng-view 标签的 div 标签删除了索引 Razor 视图的全部内容。

<!-- Index.cshtml -->
<divng-view></div>

该 AngularJS ngView 标签是一个指令,能以一种将当前路由的模板渲染成主页面布局的方式补充 $route service。我有两个选择,要么直接嵌入 NG-View 代码到母版页 _Layout.cshtml 或使用 Razor 视图将它注入到母版页。我决定简单地从索引 Razor 视图中注入标签。本质上,索引 Razor 视图在应用程序的引导过程当中被简单的使用,而且在应用程序启动后不会被引用。

一旦应用程序被引导并开始启动,AngularJS 将会执行本身的路由系统并以路由表中配置来执行本身的默认路由。基于这一点,我建立了一个单独 AngularJS index.html 和主页的 IndexController.js 文件。

<!-- Index.html -->
<divng-controller="indexController as vm"ng-init="vm.initializeController()">
<h4class="page-header">{{vm.title}}</h4>
</div>

当视图加载时,索引 Angular 视图将会经过 ng-init 指令来执行索引控制器的初始化功能。

复制代码

// indexController.js
angular.module("codeProject").register.controller('indexController',
['$routeParams', '$location', function ($routeParams, $location) {
"use strict";
var vm = this;
this.initializeController = function () {
        vm.title = "Home Page";
    }
}]);

复制代码

 

RouteConfig.cs

当开发一个 AngularJS 应用时,首先将会发生的一件事,就是你须要先开发一个像驻留在路由文件中的 CustomerInquiry 同样的页面

/Views/Customers/ CustomerInquiry 

当你在 HTML 页面寻找这个视图时,点击 Visual Studio 中的运行按钮来直接执行这个页面,MVC 将会执行并尝试去查找一个用于客户路由的 MVC 控制器和视图。将会发生的是,你会得到一个叫作找不到该路由的视图或控制器的错误。

你固然会遇到这个错误,由于/View/Customers/CustomerInquiry的路由是个 Angular 路由,而不是 MVC 路由。MVC 并不知道这个路由。若是你还想直接运行这个页面,则须要解决这一问题,给 MVC 路由表增长另外的路由以便告诉 MVC 将全部的请求路由到 MVC 主控制器,并渲染Razor 视图、经过路由引导这个应用。

因为我有三个视图文件夹,主文件夹、客户文件夹和产品文件夹,我增长了一下的 MVC 路由配置类以便将全部的请求路由到主/索引路由中。当应用程序运行时点击 F5,一样也会进入 MVC 路由表。就 Angular 和单页面如何运行而言,当你点击 F5 时,基本上就是重启了 AngularJS 应用。

有了这些额外的路由,如今就能够直接执行 AngularJS 路由了。你能够在 MVC 路由表中以一种通配符的路由来处理你的路由,但我更愿意使用明确的路由表,并使得 MVC 拒绝全部无效的路由。

要记住的基本的事情是,MVC 路由将会在 AngularJS 启动以前发生,一旦引导开始,AngularJS 将会接管全部之后路由请求。

复制代码

// RouteConfig.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace CodeProject.Portal 
{
    publicclass RouteConfig
    {
        publicstaticvoid RegisterRoutes(RouteCollection routes)
        {

             routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
             routes.MapRoute(
             name: "HomeCatchAllRoute",
             url: "Home/{*.}",
             defaults: new { controller = "Home", action = "Index", 
             id = UrlParameter.Optional }
             );
             routes.MapRoute(
             name: "CustomersCatchAllRoute",
             url: "Customers/{*.}",
             defaults: new { controller = "Home", action = "Index", 
             id = UrlParameter.Optional }
             );

             routes.MapRoute(
             name: "ProductsCatchAllRoute",
             url: "Products/{*.}",
             defaults: new { controller = "Home", action = "Index", 
             id = UrlParameter.Optional }
             );

             routes.MapRoute(
             name: "Default",
             url: "{controller}/{action}/{id}",
             defaults: new { controller = "Home", action = "Index", 
             id = UrlParameter.Optional }
          );
       }
   }
}

复制代码

 

$ controllerProvider 和动态加载控制器

当示例应用程序启动时,该应用程序将会预加载应用程序的核心控制器和服务。这包括 Home 目录中的全部控制器和应用程序的共享服务。

此应用程序的共享服务,将在全部模块中执行- 包括一个 Ajax 服务和提醒服务。如前所述,此应用程序具备三个功能模块:基本的关于、联系咱们和主页的模块、一个客户模块和产品模块。

因为此应用程序可随时间而增加,我不但愿该在应用程序的配置和引导阶段中,预加载全部的功能模块。应用程序启动后,我仅但愿当用户请求时,再加载这些控制器和产品模块。

默认状况下,AngularJS 被设计为预加载全部的控制器。一个典型的控制器看起来这样:

复制代码

// aboutController.js
angular.module("codeProject").controller('aboutController',
['$routeParams', '$location', function ($routeParams, $location) {
"use strict";
var vm = this;
this.initializeController = function () {
        vm.title = "About";
    }
}]);

复制代码

若是在配置阶段以后,你尝试动态加载上述控制器,将会收到一个 Angular 错误。你须要作的是使用 $controllerProvider 服务器在配置阶段以后,动态地加载控制器。Angular 使用 $controllerProvider 服务来建立新的控制器。这种方法容许经过注册方法来实现控制器注册。

复制代码

// aboutController.js
angular.module("codeProject").register.controller('aboutController',
['$routeParams', '$location', function ($routeParams, $location) {
"use strict";
var vm = this;
this.initializeController = function () {
         vm.title = "About";
     }
}]);

复制代码

上述有关控制器被修改成执行 $controllerProvider 的寄存器方法。为了使这种注册方法有效,必须在配置阶段配置这种注册。下面的代码片断在应用程序启动以后,使用了 $controllerProvider 来使注册方法有效。在下面的例子中,提供了一种用于注册和动态加载两个控制器和服务的注册方法。若是你愿意,也能够包括 Angular 所有库和指令的注册功能。

复制代码

// CodeProjectBootStrap.js
(function () {
var app = angular.module('codeProject', ['ngRoute', 'ui.bootstrap', 'ngSanitize', 'blockUI']);

app.config(['$controllerProvider', '$provide', function ($controllerProvider, $provide) {
        app.register =
        {
             controller: $controllerProvider.register, 
             service: $provide.service
        }
    }
}

复制代码

以上是如何在 ASP.NET MVC 中集成 AngularJS 的第一部份内容,后续内容会在本系列的后两篇文章中呈现,敬请期待!

经过第一部份内容的学习,相信你们已经对实如今 ASP.NET MVC 中集成 AngularJS 的基本思路有所了解。当咱们在进行 ASP.NET MVC 和 AngularJS 开始时,还能够借助开发工具来助力开发过程。ASP.NET MVC开发时,能够借助 ComponentOne Studio ASP.NET MVC 这一款轻量级控件,它与 Visual Studio 无缝集成,彻底与 MVC6 和 ASP.NET 5.0 兼容,将大幅提升工做效率;AngularJS 开发时,能够借助 Wijmo 这款为企业应用程序开发而推出的一系列包含 HTML5 和 JavaScript 的开发控件集,不管应用程序是移动端、PC端、仍是必需要支持IE6,Wijmo 均能知足需求。

 

文章来源:By Mark J. Caplin 

原文连接:http://www.codeproject.com/Articles/1033076/Integrating-AngularJS-with-ASP-NET-MVC

相关文章
相关标签/搜索