Scott的ASP.net MVC框架系列文章之一

  [原文地址]  [查看该系列所有文章]

Two weeks ago I blogged about a new MVC (Model View Controller) framework for ASP.NET that we are going to be supporting as an optional feature soon.  It provides a structured model that enforces a clear separation of concerns within applications, and makes it easier to unit test your code and support a TDD workflow.  It also helps provide more control over the URLs you publish in your applications, and can optionally provide more control over the HTML that is emitted from them.
两周前我发表了介绍将作为ASP.net特性的 MVC框架的技术文章,该框架降低应用程序各部分之间的耦合程度,更有利于单元测试的进行并支持TDD工作流,同时它可以通过应用程序中的URL路径及其中的HTML代码提供更多的控制。
 
Since then I've been answering a lot of questions from people eager to learn more about it.  Given the level of interest I thought it might make sense to put together a few blog posts that describe how to use it in more detail.  This first post is one of several I'll be doing in the weeks ahead.
之后我答复了很多朋友关于这篇文章的问题,我想又必须要继续对该框架的使用做进一步的介绍,这篇文章是我本系列文章的第一篇。
 
A Simple E-Commerce Storefront Application
一个简单的电子商务前端应用
 
I'm going to use a simple e-commerce store application to help illustrate how the ASP.NET MVC Framework works.  For today's post I'll be implementing a product listing/browsing scenario in it.
我将用一个简单的网上商店应用程序来演示ASP.net MVC框架的工作原理。这篇文章将要演示其中产品列表/浏览的应用。
 
Specifically, we are going to build a store-front that enables end-users to browse a list of product categories when they visit the /Products/Categories URL on the site:
首先我们要建立一个前台显示页面,使得用户在访问网站的/Products/Categories路径时可以浏览产品目录:
 


 
When a user clicks an individual product, they'll navigate to a product details URL - /Products/Detail/ProductID - that displays more details about the specific product they selected:
当用户点击某一个具体产品,将会进入选择产品的详细信息显示页面/Products/Detail/ProductID。
 

 
We'll build all of the above functionality using the new ASP.NET MVC Framework.  This will enable us to maintain a "clean separation of concerns" amongst the different components of the application, and enable us to more easily integrate unit testing and test driven development.
我们要使用ASP.net MVC 框架实现上述功能。这让我们可以清晰划分应用程序的不同组件,并使单元测试和测试驱动开发模型更容易被集成。
 
Creating A New ASP.NET MVC Application
创建一个新的ASP.net MVC 应用程序
 
The ASP.NET MVC Framework includes Visual Studio Project Templates that make it easy to create a new web application with it.  Simply select the File->New Project menu item and choose the "ASP.NET MVC Web Application" template to create a new web application using it.
包含Visual Studio项目模板的ASP.net MVCFramework使创建web应用程序变得更加方便。选择“文件”->“新建项目”菜单项,并点击“ASP.net MVC Web Application”模板创建一个新的Web应用程序。
 
By default when you create a new application using this option, Visual Studio will create a new solution for you and add two projects into it.  The first project is a web project where you'll implement your application.  The second is a testing project that you can use to write unit tests against it:
当你用该选项创建项目时, Visual Studio默认创建一个解决方案并向其中添加两个项目,第一个项目是实现应用程序的Web应用程序,另一个是用于针对web应用程序编写单元测试的测试项目。
 

 
You can use any unit testing framework (including NUnit, MBUnit, MSTest, XUnit, and others) with the ASP.NET MVC Framework.  VS 2008 Professional now includes built-in testing project support for MSTest (previously in VS 2005 this required a Visual Studio Team System SKU), and our default ASP.NET MVC project template automatically creates one of these projects when you use VS 2008. 
在ASP.net MVC框架中你可以使用任何一种单元测试框架(包括Nunit, MBUnit, MSTest, Xunit等等)。VS2008专业版内置了对MSTest测试项目(在先前的VS2005版本中需要安装Visual Studio Team System SKU)的支持,同时ASP.net MVC框架在VS2008中自动创建了这个项目。
 
We'll also be shipping project template downloads for NUnit, MBUnit and other unit test frameworks as well, so if you prefer to use those instead you'll also have an easy one click way to create your application and have a test project immediately ready to use with it.
我们会陆续提供适用于Nunit, MBUnit和其他单元测试框架的ASP.net MVC项目模板下载,因此如果你偏向于使用上述这些单元测试工具,你仍然可以通过这种简单的方式创建需要的应用程序以及对应的单元测试项目。
 
Understanding the Folder Structure of a Project
理解项目的目录结构
 
The default directory structure of an ASP.NET MVC Application has 3 top-level directories:
ASP.net MVC 应用程序默认有3个父级目录
/Controllers
/Models
/Views
As you can probably guess, we recommend putting your Controller classes underneath the /Controllers directory, your data model classes underneath your /Models directory, and your view templates underneath your /Views directory. 
正如你所想象的那样,我们建议你将控制类放在/Conrollers目录中,数据实体类放在/Model文件夹中,视图模板放在/Views文件夹中。
 
While the ASP.NET MVC framework doesn't force you to always use this structure, the default project templates use this pattern and we recommend it as an easy way to structure your application.  Unless you have a good reason to use an alternative file layout, I'd recommend using this default pattern.
虽然ASP.net MVC框架并没有强制要求你使用这种文件结构,但是除非你有更好的理由去使用其他的文件组织方式,我们仍然推荐你用这种默认方式组织你的应用程序。
 
Mapping URLs to Controller Classes
将URL地址映射到控制类
 
In most web frameworks (ASP, PHP, JSP, ASP.NET WebForms, etc), incoming URLs typically map to template files stored on disk.  For example, a "/Products.aspx" or "/Products.php" URL typically has an underlying Products.aspx or Products.php template file on disk that handles processing it.  When a http request for a web application comes into the web server, the web framework runs code specified by the template file on disk, and this code then owns handling the processing of the request.  Often this code uses the HTML markup within the Products.aspx or Products.php file to help with generating the response sent back to the client.
在多数Web框架中(ASP, PHP, JSP, ASP.net Form等等)URL地址通常映射到磁盘上具体的物理文件,例如,对于路径“/Products.aspx”或“/Products.php”通常在磁盘上都有一个Products.aspx或Products.php文件与之相对应。当一个针对Web应用程序的http请求发送到服务器时,web框架将请求的处理权交由请求的文件,通常这个文件通过使用HTML标记来生成发送到客户端的回应内容。
 
MVC frameworks typically map URLs to server code in a different way.  Instead of mapping URLs to template files on disk, they instead map URLs directly to classes.  These classes are called "Controllers" and they own processing incoming requests, handling user input and interactions, and executing appropriate application and data logic based on them.  A Controller class will then typically call a separate "View" component that owns generating the actual HTML output for the request.
MVC框架使用另一种不同的方式将URL路径直接映射到服务器端的代码,也就是将URL直接映射到类而不是映射到物理文件。这些被影射到的类被称为“控制类”,他们可以处理服务器请求,处理用户输入和与用户进行交互,执行应用逻辑和数据逻辑等。控制类将会调用一个独立的“视图”组件来产生针对请求的HTML输出。
 

 
The ASP.NET MVC Framework includes a very powerful URL mapping engine that provides a lot of flexibility in how you map URLs to Controller classes.  You can use it to easily setup routing rules that ASP.NET will then use to evaluate incoming URLs and pick a Controller to execute.  You can also then have the routing engine automatically parse out variables that you define within the URL and have ASP.NET automatically pass these to your Controller as parameter arguments.  I'll be covering more advanced scenarios involving the URL routing engine in a future blog post in this series.
ASP.net MVC框架包含了一个强大的URL映射引擎,可以处理多种复杂的URL到控制类的关系映射。你可以轻松的设置映射规则,使ASP.net遵循这些规则解析URL路径并调用相应的控制类,也可以使用URL映射引擎自动解析URL路径中的变量并将它们作为参数传递给控制类。我将在这个系列的后续文章中详细介绍URL映射引擎的高级使用。
 
Default ASP.NET MVC URL Routing to Controller Classes
ASP.NET MVC URL到控制类的默认映射
 
By default ASP.NET MVC projects have a preconfigured set of URL routing rules that enable you to easily get started on an application without needing to explicitly configure anything.  Instead you can start coding using a default set of name-based URL mapping conventions that are declared within the ASP.NET Application class of the Global.asax file created by the new ASP.NET MVC project template in Visual Studio. 
ASP.net MVC项目预置了一系列URL映射规则让用户不需要进行任何配置就可以开始开发应用程序,当然你也可以依照Visual Studio的ASP.NET MVC 项目模板提供的的Global.asax文件中定义的应用程序类中声明的一系列默认URL名称映射规范进行开发。
 
The default naming convention is to map the leading URL path of an incoming HTTP request (for example: /Products/) to a class whose name follows the pattern UrlPathController (for example: by default a URL leading with /Products/ would map to a class named ProductsController).
默认的命名规范是将一个HTTP请求的URL的起始路径部分(例如:/Products)映射到一个具有UrlPathController名称的类(例如:一个由/Products/开始的URL路径,将被映射到ProductsController类中)。
 
To build our e-commerce product browsing functionality, we'll add a new "ProductsController" class to our project (you can use the "Add New Item" menu in Visual Studio to easily create a Controller class from a template):
我们在项目中建立一个“ProductsController”类来实现电子商务的产品浏览功能(你可以点击Visual Studio中的“添加新项目”菜单项,根据模板创建一个控制类)。
 

 
Our ProductsController class will derive from the System.Web.MVC.Controller base class.  Deriving from this base class isn't required - but it contains some useful helper methods and functionality that we'll want to take advantage of later:
我们的ProductsController类从System.Web.MVC.Controller基类中继承而来,但控制类并非必须继承自该积累,只是该基类中包含了很多我们后面要用到的很多有用的辅助方法和功能。
 

 
Once we define this ProductsController class within our project, the ASP.NET MVC framework will by default use it to process all incoming application URLs that start under the "/Products/" URL namespace.  This means it will be automatically called to process the "/Products/Categories", "/Products/List/Beverages", and "/Products/Detail/3" URLs that we are going to want to enable within our store-front application.
当我们在项目中创建了ProductsController类之后,ASP.NET MVC框架默认便将所有“/Products”引导的URL请求交由该类处理,这将意味着在处理诸如"/Products/Categories", “/Products/List/Beverages",或"/Products/Detail/3"这些我们在后面将要提到前端应用时都将调用这个控制类进行处理。
 
In a future blog post we'll also add a ShoppingCartController (to enable end users to manage their shopping carts) and an AccountController (to enable end users to create new membership accounts on the site and login/logout of it).  Once we add these two new controller classes to our project, URLs that start with /ShoppingCart/ and /Account/ will automatically be routed to them for processing.
在下一篇文章里,我们将要继续添加ShoppingCartController(用于管理用户的购物车)和AccountController(用于管理用户的账号和登录注销操作)两个控制类,同样以”/ShoppingCart/”和”Account”开头的URL请求也将分别由这两个类进行处理。
 
Note: The ASP.NET MVC framework does not require that you always use this naming convention pattern.  The only reason our application uses this by default is because there is a mapping rule that configures this that was automatically added to our ASP.NET Application Class when we created the new ASP.NET MVC Project using Visual Studio.  If you don't like this rule, or want to customize it to use a different URL mapping pattern, just go into the ASP.NET Application Class (in Global.asax) and change it. I'll cover how to-do this in a future blog post (when I'll also show some of the cool scenarios the URL routing engine enables).
提示:ASP.net MVC 框架并不要求你一定遵循这个命名样式,我们之所以这样做是因为在Visual Studio中创建ASP.net MVC项目时,该默认的URL映射规则被自动添加到了项目的应用程序类中。如果你认为这种规则并不合理,你可以打开应用程序类文件(Global.asax文件)修改规则。在后面的文章里我将介绍这以操作(同时我会展示一些URL引擎提供的强大功能)。
 
Understanding Controller Action Methods
理解控制方法
 
Now that we have a created a ProductsController class in our project we can start adding logic to handle the processing of incoming "/Products/" URLs to the application.
在创建ProductController类之后我们可以开始添加处理”/Products/”请求的逻辑了。
 
When defining our e-commerce storefront use cases at the beginning of this blog post, I said we were going to implement three scenarios on the site: 1) Browsing all of the Product Categories, 2) Listing Products within a specific Category, and 3) Showing Details about a Specific Product.  We are going to use the following SEO-friendly URLs to handle each of these scenarios:
在文章开头部分定义电子商务的前端用例时,我曾提到我们要实现的三个功能:1)浏览产品目录,2)列出某一目录的所有产品和3)展示某一具体产品的详细信息。我们将要使用下面的SEO(Search Engine Optimize)友好路径来实现每种功能。
 
URL Format Behavior URL Example
/Products/Categories Browse all Product Categories /Products/Categories
/Products/List/Category List Products within a Category /Products/List/Beverages
/Products/Detail/ProductID Show Details about a Specific Product /Products/Detail/34

 
URL 格式 行为 示例
/Products/Categories 显示所有目录 /Products/Categories
/Products/List/Category 列出某一目录的产品 /Products/List/Beverages
/Products/Detail/ProductID 显示某产品的信息 /Products/Detail/34

 
There are a couple of ways we could write code within our ProductsController class to process these three types of incoming URLs.  One way would be to override the "Execute" method on the Controller base class and write our own manual if/else/switching logic to look at the incoming URL being requested and then execute the appropriate logic to process it.
我们可以通过多种方式来实现ProductsController类中对3种URL的处理功能,其中一种方式是通过重写控制基类Controller中的”Execute”方法,在其冲添加if/else/switching逻辑判断传入的URL并决定相应的处理逻辑。
 
A much easier approach, though, is to use a built-in feature of the MVC framework that enables us to define "action methods" on our controller, and then have the Controller base class automatically invoke the appropriate action method to execute based on the URL routing rules in use for our application.
然而,另一种更简单的方式是使用MVC框架为我们提供的内置特性:MVC框架允许我们自定义操作方法(Action Method),同时Controller基类会根据当前应用程序中配置的URL映射规则自动调用相应的操作方法。
 
For example, we could add the below three controller action methods to our ProductsController class to handle our three e-commerce URL scenarios above:
例如,我们可以在ProductsController类中添加下面三个控制类操作方法处理我们前面提及的三种功能:
 

 
The URL routing rules that are configured by default when a new project is created treat the URL sub-path that follows the controller name as the action name of the request.  So if we receive a URL request of /Products/Categories, the routing rule will treat "Categories" as the name of the action, and the Categories() method will be invoked to process the request.  If we receive a URL request of /Products/Detail/5, the routing rule will treat "Detail" as the name of the action, and the Detail() method will be invoked to process the request, etc. 
项目创建时默认的URL映射规则规定URL中紧跟控制类名称的子路径是请求的控制方法名称,例如如果我们收到一个/Products/Categories请求,路由规则将把”Categories”作为操作方法名,Categories()方法将用于处理该请求等等。
 
Note: The ASP.NET MVC framework does not require that you always use this action naming convention pattern.   If you want to use a different URL mapping pattern, just go into the ASP.NET Application Class (in Global.asax) and change it.
提示:ASP.net MVC框架并不要求你一定遵循这个规范,如果你使用不同的映射规则,你只需要打开应用程序类并进行修改。
 
Mapping URL Parameters to Controller Action Methods
URL的参数到控制类方法的映射
 
There are several ways to access URL parameter values within the action methods of Controller classes.
在控制类操作方法中有多种访问URL参数的方式。
 
The Controller base class exposes a set of Request and Response objects that can be used.  These objects have the exact same API structure as the HttpRequest/HttpResponse objects that you are already familiar with in ASP.NET.  The one important difference is that these objects are now interface based instead of sealed classes (specifically: the MVC framework ships with new System.Web.IHttpRequest and System.Web.IHttpResponse interfaces).  The benefit of having these be interfaces is that it is now easy to mock them - which enables easy unit testing of controller classes.  I'll cover this in more depth in a future blog post. 
控制基类封装了Request和Response对象以供使用,这些对象与ASP.net中你所熟悉的HttpRequest/HttpResponse对象拥有相同的API接口,但他们之间一个重要的区别是这些对象现在是基于接口的而不是抽象基类(说明:MVC框架中同时包含了System.Web.IhttpRequest和System.Web.IhttpResponse接口)。使用接口的优点是可以方便对控制类进行单元测试,在后面的文章里我将详细阐述这一点。
 
Below is an example of how we could use the Request API to manually retrieve an ID querystring value from within our Detail action method in the ProductsController class:
下面是这个例子介绍了我们如何使用Request获取查询字符串(QueryString)中的ID变量,并传递给ProductController控制类中的Detail操作方法:
 

 
The ASP.NET MVC framework also supports automatically mapping incoming URL parameter values as parameter arguments to action methods.  By default, if you have a parameter argument on your action method, the MVC framework will look at the incoming request data to see if there is a corresponding HTTP request value with the same name.  If there is, it will automatically pass it in as a parameter to your action method.
ASP.net MVC框架也支持将URL参数映射到操作方法的操作。如果你的操作方法中具有一个参数,MVC框架默认查找请求数据中是否存在具有相同名称的相关HTTP请求变量,如果存在,它将作为函数参数自动传递给操作方法。
 
For example, we could re-write our Detail action method to take advantage of this support and make it cleaner like below:
例如,利用这种特性,我们可以按照下面更简洁的方式实现Detail方法:
 

 
In addition to mapping argument values from the querystring/form collection of a request, the ASP.NET MVC framework also allows you to use the MVC URL route mapping infrastructure to embed parameter values within the core URL itself (for example: instead of /Products/Detail?id=3 you could instead use /Products/Detail/3). 
除了从请求的查询字符串和Form变量中实现参数值映射,ASP.net MVC框架同样通过 URL映射规则将参数值集成在URL中(例如:使用/Products/Detail/3代替/Products/Detail?id=3)
 
The default route mapping rule declared when you create a new MVC project is one with the format: "/[controller]/[action]/[id]".  What this means is that if there is any URL sub-path after the controller and action names in the URL, it will by default be treated as a parameter named "id" - and which can be automatically passed into our controller action method as a method argument.
MVC 项目的默认URL映射规则是"/[controller]/[action]/[id]",这表明在URL子路径中控制类名称(Controller)和动作方法名称(action)之后的部分将被自动作为方法参数传入控制类操作方法。
 
This means that we can now use our Detail method to also handle taking the ID argument from the URL path (e.g: /Products/Detail/3):
这表明我们可以从路径(例如: /Products/Detail/3)中获取Detail方法的ID参数。
 

 
I can use a similar approach for the List action so that we can pass in the category name as part of the URL (for example: /Products/List/Beverages).  In the interest of making the code more readable, I made one tweak to the routing rules so that instead of having the argument name be called "id" it will be called "category" for this action.
利用同样的方式,我们可以通过URL路径向List方法传入产品目录名称参数(例如:/Products/List/Beverages)。为了增强代码的可读性,我修改了映射规则,使用“Category”作为这个方法的参数而不是“id”。
 
Below is a version of our ProductsController class that now has full URL routing and parameter mapping support implemented:
下面是具备了完整的URL映射和参数映射的ProductController类的实现:
 

 
Note above how the List action takes the category parameter as part of the URL, and then an optional page index parameter as a querystring (we'll be implementing server-side paging and using that value to indicate which page of category data to display with the request). 
注意上面的例子中List方法是如何从URL中的查询字符串(querystring)获取目录参数以及可选的页码参数(我们将利用这个参数在服务器端实现分页显示的逻辑代码)。
 
Optional parameters in our MVC framework are handled using nullable type arguments on Controller Action methods.  Because the page parameter on our List action is a nullable int (that is what "int?" means syntactically), the MVC framework will either pass in a value if it is present in the URL - or pass in null if not.  Check out my previous post on the ?? null coalescing operator to learn a useful tip/trick on how to work with nullable types that are passed as arguments like this.
MVC框架中的操作方法的可选参数使用nullable类型的参数,因为List方法的页码参数是可选参数(int?代表可以为空),所以MVC框架会根据传入的URL自动为参数赋值。关于nullable类型的使用请阅读我的另一篇相关文章。
 
Building our Data Model Objects
创建数据模型对象
 
We now have a ProductsController class and three action methods on it ready to process incoming web requests.  Our next step will be to build some classes that will help us work with our database to retrieve the appropriate data needed to handle these web requests.
 现在我们已经完成了ProductContoller类并添加了三个操作方法处理Web请求,我们的下一步是创建其他的类用于访问数据库并获取请求的数据。
 
In an MVC world "models" are the components of an application that are responsible for maintaining state.  With web applications this state is typically persisted inside a database (for example: we might have a Product object that is used to represent product data from the Products table inside our SQL database).
在MVC中“模型(model)”是用于维护状态的应用程序组件。在Web应用中,状态通常在数据库中保持(例如我们可以使用Product对象代表产品SQL数据库中Product表的数据)。
 
The ASP.NET MVC Framework enables you to use any data access pattern or framework you want in order to retrieve and manage your models.  If you want to use ADO.NET DataSets/DataReaders (or abstractions built on top of them) you can.  If you prefer to use an object relational mapper (ORM) like NHibernate, LLBLGen, WilsonORMapper, LINQ to SQL/LINQ to Entities you can absolutely use those as well.
ASP.net MVC框架允许使用多种数据访问模式和框架创建和管理模型。你可以使用ADO.net DataSet/DataReader(或任何他们的抽象对象),也可以使用像Nhibernate,LLBLGen, WilsonORMapper, LINQ to SQL/LINQ to Entity之类的数据库实体映射工具(ORM).
 
For our e-commerce sample application I'm going to use the built-in LINQ to SQL ORM shipped in .NET 3.5 and VS 2008.  You can learn more about LINQ to SQL from my ongoing blog tutorial series that covers it (in particular make sure to check out my Part1, Part2, Part3 and Part4 posts). 
在我们的电子商务示例程序中,我们将要使用.NET3.5和VS2008中内置的LINQ to SQL ORM映射工具,在以后的文章里我介绍更多关于LINQ to SQL的知识(包括Part1, Part2, Part3 和Part4)。
 
I'll start by right-clicking on the "Models" sub-directory of our MVC web project inside VS and choose the "Add New Item" option to add a LINQ to SQL model.  Within the LINQ to SQL ORM designer I'll define three data model classes that map to the Categories, Products, and Suppliers table inside the SQL Server Northwind sample database (read Part 2 of my LINQ to SQL series to learn how to-do this):
在VS项目中的”Model”目录上右单击选择“添加新项目”,添加一个LINQ to SQL模型。在LINQ to SQL ORM设计器中我会定义3个数据对象类对应SQL Server的示例数据库Northwind中的类别(Categories)、产品(Products)和供应商(Suppliers)表(阅读我LINQ to SQL系列文章的Part 2了解如何完成该操作)。
 

 
Once we've defined our LINQ to SQL data model classes, I'll then add a new NorthwindDataContext partial class to our Models directory as well:
完成LINQ to SQL数据模型类后,我将在模型文件夹Model中添加NorthwindDataContext分布类:
 

 
Within this class I'll define a few helper methods that encapsulate some LINQ expressions that we can use to retrieve the unique Category objects from our database, retrieve all Product objects within a specific category in our database, as well as retrieve an individual Product object based on a supplied ProductID:
在这个类中我将定义一个封装了一些LINQ表达式的辅助方法,以用于从数据库中获取指定的产品目录类别、获取指定产品目录的所有产品和指定ProductID的产品信息。
 

 
These helper methods will make it easy for us to cleanly retrieve the data model objects needed from our ProductsController class (without having to write the LINQ expressions within the Controller class itself):
辅助方法让ProductController类中获取所需要的数据模型的代码更加整洁(不需要在控制类中编写LINQ 表达式):
 

 
We now have all of the data code/objects we need to finish implementing our ProductsController functionality. 
现在我们已经创建完成了在ProductController功能重所需要的所有数据代码和对象。
 
Finishing the Implementation of our ProductsController Class
完成ProductController类的实现
 
Controllers in a MVC based application are responsible for processing incoming requests, handling user input and interactions, and executing appropriate application logic based on them (retrieving and updating model data stored in a database, etc).
MVC应用程序中的控制类用于处理请求,与用户交互和执行应用逻辑(创建和更新数据库中的对象模型)。
 
Controllers typically do not generate the specific HTML response for a request.  The task of generating an HTML response is instead owned by "View" components within the application - which are implemented as separate classes/templates from Controllers.  Views are intended to be focused entirely on encapsulating presentation logic, and should not contain any application logic or database retrieval code (instead all app logic should be handled by the Controller).
控制类通常不生成HTML代码,生成HTML代码的工作由“视图(View)”组件完成,视图是与控制类相独立的类和模板。视图主要完成封装显示逻辑,不应该包含任何应用逻辑或数据库访问代码(所有的应用逻辑都应该由控制类进行处理)。
 
In a typical MVC web workflow, Controller action methods will handle the incoming web request, use the incoming parameter values to execute appropriate application logic code, retrieve or update data model objects from a database, and then select a "View" to use to render an appropriate UI response back to a browser.  As part of picking the appropriate View to render, the Controller will explicitly pass in (as arguments) all of the data and variables required by the "View" in order to for it to render the appropriate response:
在MVC Web工作流程中,控制类操作方法将处理Web请求,接受传入参数并执行相应的逻辑代码,根据数据库创建或获取对象模型,并选择一个“视图”用于生成相应的UI发送到浏览器。控制类将向视图传入所有需要的数据和参数用于显示数据。
 

 
You might be wondering - what is the benefit of separating the Controller and the View like this?  Why not just put them in the same class?  The primary motivation in partitioning the application like this is to help enforce the separation of your application/data logic from your UI generation code.  This makes it much easier to unit test your application/data logic in isolation from your UI rendering logic.  It can also help make your application more maintainable over time - since it makes it harder for you to accidentally add application/data logic in your view templates.
你或许想知道像这样将控制类和视图分离的有点是什么,为什么不把他们放在一个相同的类中?这样做主要的优点是有效的将应用程序和数据逻辑分离,使针对应用程序和数据逻辑的单元测试与UI呈现分离,同时可以使应用程序更容易维护,因为在视图模板中不允许添加应用程序和数据逻辑。
 
When implementing the three controller action methods of our ProductsController class, we'll use the incoming URL parameter values to retrieve the appropriate model objects from our database, and then pick a "View" component to use to render an appropriate HTML response.  We'll use one of the RenderView() methods on the Controller base class to specify the View we want to use, as well as explicitly pass in the specific data that we want the View to use to render its response.
在实现ProductController类中的三个控制方法时,我们使用传入URL路径中的参数从数据库中获取对应的对象模型,然后选择一个“视图”组件用于生成相应的HTML响应。我们使用Contoller基类中的RenderView()方法指明我们希望使用的视图,同时传入视图显示时所需要的数据。
 
Below is the final result of our ProductsController implementation:
下面是ProductsController类的最终实现:
 

 
Notice that the number of lines of code in our action methods above is pretty small (two lines each).  This is partly because the URL parameter parsing logic is handled entirely for us by the MVC framework (saving us from having to write a lot of this code).  This is also partly because the product browsing scenario is fairly simple from a business logic perspective (the action methods are all read-only display scenarios). 
注意到上面的操作方法代码都很少(每个方法只有两行)。这得益于MVC框架啊为我们提供的URL参数解析逻辑(避免书写很多逻辑代码),同时也因为产品浏览的逻辑功能比较简单(操作方法只有只读的显示功能)
 
In general, though, you'll often find that you'll have what are sometimes called "skinny controllers" - meaning controller methods full of relatively terse action methods (less than 10 lines of code).  This is often a good sign that you have cleanly encapsulated your data logic and factored your controller logic well.
你通常会发现你的一些方法代码非常短小,这表明你对数据逻辑和控制逻辑进行了很好的封装。
 
Unit Testing our ProductsController
对ProductController进行单元测试
 
You might be surprised that the next step we are going to work on is to test our application logic and functionality.  You might ask - how is that even possible?  We haven't implemented our Views, and our application currently doesn't render a single tag of HTML.  Well, part of what makes an MVC approach attractive is that we can unit test the Controller and Model logic of applications completely independently of the View/Html generation logic.  As you'll see below we can even unit test these before we create our Views.
或许你会对我们下一步将要进行程序测试而感到奇怪,你或许会问这怎么可能?我们还没有实现视图,我们的应用程序现在甚至没有产生一个HTML标记,MVC吸引人的特性之一就是可以在视图/HTML产生逻辑完成之前独立进行针对控制类和模型逻辑的单元测试。正像你将要在下面看到的,我们甚至可以在创建视图之前进行单元测试。
 
To unit test the ProductsController class that we've been working on, we'll add a ProductsControllerTest class into the Test Project that was added to our solution by default when we created our ASP.NET MVC Application using Visual Studio:
为了对ProductController类进行单元测试我们在项目默认添加的测试项目中新建了一个ProductControllerTest类:
 

 
We'll then define a simple unit test that tests the Detail action of our ProductsController:
接下来我们要哦啊定义一个简单的单元测试对ProductController类中的Detail函数进行测试:
 

The ASP.NET MVC framework has been designed specifically to enable easy unit testing.  All core APIs and contracts within the framework are interfaces, and extensibility points are provided to enable easy injection and customization of objects (including the ability to use IOC containers like Windsor, StructureMap, Spring.NET, and ObjectBuilder).  Developers will be able to use built-in mock classes, or use any .NET type-mocking framework to simulate their own test versions of MVC related objects.
ASP.net MVC框架转为利于单元测试而设计。框架中所有的核心API和协议都是基于接口的
In the unit test above, you can see an example of how we are injecting a dummy "ViewFactory" implementation on our ProductsController class before calling the Detail() action method.  By doing this we are overriding the default ViewFactory that would otherwise handle creating and rendering our View.  We can use this test ViewFactory implementation to isolate the testing of just our ProductController's Detail action behavior (and not have to invoke the actual View to-do this).  Notice how we can then use the three Assert statements after the Detail() action method is called to verify that the correct behavior occurred within it (specifically that the action retrieved the correct Product object and then passed it to the appropriate View).
在上面的单元测试中,你可以看到我们是如何在调用个ProductsController类的Detail()方法前对“视图工厂(ViewFactory)”的。通过这种方式我们重写了将要处理创建和产生视图的视图工厂。我们可以使用这个视图工厂的
 
Because we can mock and simulate any object in the MVC framework (including IHttpRequest and IHttpResponse objects), you do not have to run unit tests in the context of an actual web-server.  Instead, we can create our ProductsController within a normal class library and test it directly.  This can significantly speed up the execution of unit tests, as well as simplify the configuration and running of them.
因为在MVC框架中哦我们可以模拟各种对象(包括IhttpRequest和IhttpResponse对象),你不需要在Web Server的环境中进行单元测试,取而代之的是我们可以将ProductController类创建在一个普通的类库项目中并对他进行测试,这可以有效提高单元测试的执行效率,同时简化了其配置和运行过程。
 
If we use the Visual Studio 2008 IDE, we can also easily track the success/failure of our test runs (this functionality is now built-into VS 2008 Professional):
如果我们使用Visual Studio 2008,我们可以轻松的跟踪测试运行的成功/失败状态(该功能已经被集成入VS2008专业版中)。
 

 
I think you'll find that the ASP.NET MVC Framework makes writing tests easy, and enables a nice TDD workflow.
我想你会发现ASP.net MVC框架式的编写测试更加容易,同时更好地支持了TDD开发流程。
 
Rendering UI with Views
用视图产生UI
 
We've finished implementing and testing the application + data logic for the product browsing section of our e-commerce application.  Now we need to implement the HTML UI for it. 
我们已经完成并测试了该电子商务程序中产品浏览部分的应用和数据逻辑,现在我们需要实现他的HTML UI界面。
 

 
We'll do this by implementing "Views" that render the appropriate UI using the view-related data objects that our ProductsController action method provided when calling the RenderView() method:
我们将实现当RenderView()方法时被调用是,视图会根据数据ProductController操作方法提供的数据产生相应的界面。
 

 
In the code example above the RenderView method's "Categories" parameter is indicating the name of the view we want to render, and the second parameter is a list of category objects that we want to pass to the view and have it use as data to generate the appropriate HTML UI for.
在上面的例子中,RenderView方法的”Categories”参数指明了我们希望用于显示的视图名称,第二个参数是一个目录类的集合,我们将在视图中根据该集合数据产生相应的HTML界面。
 
The ASP.NET MVC Framework supports the ability to use any templating engine to help with generating UI (including existing templating engines like NVelocity, Brail - as well as new ones you write yourself).  By default the ASP.NET MVC Framework uses the existing ASP.NET Page (.aspx), Master Page (.master), and UserControl (.ascx) support already within ASP.NET. 
ASP.net MVC框架支持多种模板引擎产生UI(包括像Nvelocity, Brail以及自定义的模板引擎),ASP.net MVC框架默认使用ASP.net已经支持的ASPX, MASTER和ASCX。
 
We'll be using the built-in ASP.NET view engine to implement our E-commerce Application UI.
我们将使用ASP.net内置的视图引擎产生我们的电子商务程序界面。
 
Defining a Site.Master File
定义Site.Master文件
 
Because we are going to be building many pages for our site, we'll start our UI work by first defining a master page that we can use to encapsulate the common HTML layout/chrome across the site. We'll do this in a file called "Site.Master" that we'll create under the /Views/Shared directory of our project:
因为我们要为站点创建很多页面,我们将要首先为所有的UI定义一个母板页,以便封装常用的HTML布局,我们在、Views/Shared文件夹中创建一个名为”Site.Master”的文件实现该功能。
 

 
We can reference an external CSS stylesheet to encapsulate all styles for the site, and then use the master page to define the overall layout of the site, as well as identify content placeholder regions where we'll want pages to be able to fill in page specific content.  We can optionally use all of the cool new VS 2008 designer features when doing this (including the HTML split-view designer, CSS Authoring, and Nested Master Pages support).
我们引用一个包含了站点样式的外部CSS样式表文件,并使用母板页定义站点的布局和内容显示区域,在开发时我们可以选择使用所有VS2008设计器中提供的强大功能特性(包括拆分设计视图(HTML split-view designer), CSS编辑 (CSS Authoring), 和嵌入模板支持(Nested Master Pages support)
 

 
Understanding the /Views Directory Structure
理解/Views目录结构
 
By default when you create a new ASP.NET MVC Project using Visual Studio, it will create a "Shared" sub-directory underneath the "Views" directory root.  This is the recommended place to store Master Pages, User Controls, and Views that we want to share across multiple Controllers within the application.
当你在Visual Studio中创建ASP.net MVC项目时,它默认会在View根目录下创建一个”Shared”子目录,你可以在这里存放母板页,用户控件和在多个控件中共享的视图。
 
When building views that are specific to an individual controller, the default ASP.NET MVC convention is to store them in sub-directories under the /Views root.  By default the name of a sub-directory should correspond to the Controller name.  For example, because the Controller class we have been building is called "ProductsController", we will by default store the Views specific to it within the /Views/Products sub-directory:
当创建针对某一个控制类的视图是,ASP.net MVC默认的规则是将它们创建在/Views根文件夹下,子文件夹名称默认与控制类的名称相同。例如,因为我们刚创建的控制类是ProductsController,那么我们默认在/Views/Products子文件夹中存放视图文件。
 

 
When we call the RenderView(string viewName) method within a specific Controller, the MVC framework will automatically first look for a corresponding .aspx or .ascx view template underneath the /Views/ControllerName directory, and then if it can't find an appropriate view template there it will check the /Views/Shared directory for one:
当我们调用某一控制类中的RenderView(string viewName)方法时,MVC框架在、Views/ControllerName文件夹中自动搜索对应的.aspx或.ascx视图模板文件,如果其中没有对应的视图模板,则将自动转到/Views/Share目录进行搜索。
 
Creating a Categories View
创建目录视图
 
We can create the "Categories" View for our ProductsController within Visual Studio by using the "Add New Item" menu option on the Products directory and selecting the "MVC View Page" item template.  This will create a new .aspx page that we can optionally associate with our Site.Master master page to pick up the overall look and feel of the site (and just like with master pages you get full WYSIWYG designer support):
我们可以通过在Products目录中通过“添加新项目”菜单项选择”MVC View Page”选项创建ProductsController类的目录视图文件。这将创建一个新的.aspx页面,我们可以设置这个文件与Site.Master母板文件的关联使得他具备站点的一致外观:
 

 
When building applications using an MVC pattern, you want to keep your View code as simple as possible, and make sure that the View code is purely about rendering UI.  Application and data retrieval logic should only be written inside Controller classes.  Controller classes can then choose to pass on the necessary data objects needed to render a view when they call their RenderView method.  For example, below in the Categories action method of our ProductsController class we are passing a List collection of Category objects to the Categories view:
在使用MVC模式创建应用程序时,你应当使你的视图代码尽可能简化并完全用于产生视图,而把应用和数据逻辑在控制类中实现。控制类在调用RenderView方法时应向视图中传入用于生成视图的必要数据。例如,下面的ProductController类的Categories方法中我们向视图传入一个Category集合对象:
 

 
MVC View Pages by default derive from the System.Web.Mvc.ViewPage base class, which provides a number of MVC specific helper methods and properties that we can use in constructing our UI.  One of these ViewPage properties is named "ViewData", and it provides access to the view-specific data objects that the Controller passed as arguments to the RenderView() method.
MVC视图页面默认集成自System.Web.Mvc.ViewPage基类,该积累定义了很多在我们构建UI过程中将要用到的辅助方法和属性,ViewPage的一个属性是ViewData,它提供对控制类从RenderView()方法中传来的数据的访问。
 
From within your View you can access the "ViewData" in either a late-bound or strongly-typed way.  If your View derives from ViewPage, the ViewData property will be typed as a late-bound dictionary.  If your View derives from the generics based ViewPage<T> - where T indicates the data object type of the ViewData the Controller is passing to the View - then the ViewData property will be strongly typed to match the same type that your Controller passed in.
在你的视图中你可以通过迟绑定或强类型的方式访问视图数据(ViewData)。如果你的视图继承自ViewPage类,那么ViewData属性将被设定为迟绑定(late-bound dictionary)类型。如果你的视图继承自泛型ViewPage<T>类,那么ViewData属性将被设定为与控制类传入的参数相同的类型。
 
For example, my Categories View code-behind class below is deriving from ViewPage<T> - where I am indicating that T is a List of Category objects:
例如,我下面的目录视图后台代码继承自ViewPage<T>,同时我指明了T是一个目录对象的集合:
 

 
This means that I get full type-safety, intellisense, and compile-time checking within my View code when working against the ProductsController.Categories() supplied List<Category> ViewData:
这表明我在访问ProductsController.Categories()方法提供的List<Category>类型的ViewData时具备了完全的类型安全,智能感知和编译时检查的特性:
 

 
Rendering our Categories View:
产生目录视图
 
If you remember from the screenshots at the very beginning of this post, we want to display a list of the product categories within our Categories view:
或许你还记得这篇文章开始部分的截图,我们希望在目录视图中显示所有的产品目录:
 

 
I can write this HTML UI generation code in one of two ways within my Categories View implementation: 1) Using Inline Code within the .aspx file, or 2) Using Server Controls within the .aspx file and Databinding from my Code Behind
我有如下两种方式在目录视图的实现部分产生HTML代码:1)在.aspx文件中嵌入代码2)在.aspx页面使用服务器控件并在后台代码中实现绑定。
 
Rendering Approach 1: Using Inline Code
实现方式1:使用嵌入代码
 
ASP.NET Pages, User Controls and Master Pages today support <% %> and <%= %> syntax to embed rendering code within html markup.  We could use this technique within our Categories View to easily write a foreach loop that generates a bulleted HTML category list:
ASP.net页,用户控件和母板页都支持<%%>和<%=%>标记在HTML标签中嵌入代码,我们采用这种技术在目录视图中编写一个”foreach”循环生成HTML格式的目录列表:
 

 
VS 2008 provides full code intellisense within the source editor for both VB and C#.  This means we'll get intellisense against our Category model objects passed to the View:
VS2008提供了对VB和C#两种语言代码编辑器的完全代码智能感知支持,这意味着我们在访问传入视图的目录对象是将获得智能感知的支持:
 

 
VS 2008 also provides full debugger support for inline code as well (allowing us to set breakpoints and dynamically inspect anything in the View with the debugger):
VS2008同样提供了对迁入代码的调试支持(允许我们在代码中设置断点):
 

 
Rendering Approach 2: Using Server Side Controls
实现方式2:使用服务器控件
 
ASP.NET Pages, User Controls and Master Pages also support the ability to use declarative server controls to encapsulate HTML UI generation.  Instead of using inline code like above, we could use the new <asp:listview> control in .NET 3.5 to generate our bulleted list UI instead:
ASP.net 页面,用户控件和母板页同样支持使用服务器控件封装HTML代码的产生,除了用上面的迁入代码,我们还可以使用.NET3.5中的<asp:listView>控件来产生列表界面:
 

 
Notice above how the ListView control encapsulates both rendering a list of values, as well as handles the scenario where no items are in the list (the <EmptyDataTemplate> saves us from having to write an if/else statement in the markup).  We could then databind our list of category objects to the listview control using code-behind code like below:
注意上面的代码中,ListView控件是如何封装产生列表值和空列表值(<EmptyDataTemplate>省去了我们编写if/else判断逻辑)两种情况的。我们可以在后台代码中将目录列表绑定到该控件。
 

 
Important: In a MVC world we only want to put rendering logic in our View's code-behind class (and not any application or data logic).  Notice above how the only logic we have is to assign the strongly typed ViewData collection of Category objects to the ListView control.   Our ProductsController Controller class is the one that actually retrieves the list of Categories from the database - not the View. 
重要:在MVC中我们只希望在视图的后台代码中放置显示逻辑(不包括任何其他的业务逻辑),注意上面的大妈中我们唯一的一个逻辑操作时将强类型的目录列表ViewData赋值给ListView控件。我们的ProductController控制类实际承担从数据库中获取目录列表工作而不是视图。
 
This ListView server-control version of our View template will then generate the exact same HTML as our in-line code version.  Because we don't have a <form runat="server"> control on the page, no viewstate, ID values or other markup will be emitted.  Only pure CSS friendly HTML:
我们的视图模板中的ListView服务器控件将会产生向我们在嵌入代码中编写的一样的HTML标记,因为我们的页面上没有<form runat=”server”>控件,没有ViewStaes视图,ID和其他标记都将排除,只有CSS友好的HTML代码:
 
 
 
Html.ActionLink Method
HTML.ActionLink方法
 
One of the things you might have noticed in both the inline-code and the server-control versions of the View code snippets above are calls to an Html.ActionLink method:
或许你已经注意到了在两种方式的代码中都调用了Html.ActionLink方法:
 

 
The Html object is a helper property on the ViewPage base class, and the ActionLink method is a helper on it that makes it easy to dynamically generate HTML hyperlinks that link back to action methods on Controllers. If you look at the HTML output picture in the section above, you can see some example HTML output generated by this method:
Html对象是ViewPage类及其子类的的辅助属性,ActionLink方法用于产生链接回控制类方法的HTML代码。如果你看到了前一部分的HTML输出图片,你可以看到这个方法输出的HTML代码:
<a href="http://weblogs.asp.net/Products/List/Beverages">Beverages</a>
 
The signature of the Html.ActionLink helper method I am using looks like this:
Html.ActionLink辅助方法的原型如下:
 
string ActionLink(string text, object values);
 
The first argument represents the inner content of the hyperlink to render (for example: <a>text goes here</a>).  The second argument is an anonymous object that represents a sequence of values to use to help generate the actual URL (you can think of this as a cleaner way to generate dictionaries).  I will go into more detail on how exactly this works in a future blog post that covers the URL routing engine.  The short summary, though, is that you can use the URL routing system both to process incoming URLs, as well as to generate URLs that you can emit in outgoing HTML.  If we have a routing rule like this:
第一个参数代表超级链接中间的文字(例如<a>text goes here</a>).第二个参数是一个匿名对象(anonymous object)代表一系列用于产生URL路径的参数(你可以认为他是一种产生字典的有简洁方式)。在后面介绍URL映射引擎的文章中我会进一步介绍它究竟是如何工作的。概括的将,你不但可以使用URL映射系统处理URL路径同时可以生成需要的HTML代码,如果我们有如下的映射规则:
 
/<controller>/<action>/<category>
 
And then write this code within a ProductController's Category View:
我们在ProductController的目录视图中编写下面的代码:
 
<%= Html.ActionLink("Click Me to See Beverages", new { action="List", category="Beverages" } %>
 
The ActionLink method will use the URL mapping rules of your application to swap in your parameters and generate this output:
ActionLink方法将要使用应用程序的URL映射规则将处理你传入的参数并产生输出:
<a href="http://weblogs.asp.net/Products/List/Beverages">Click Me to See Beverages</a>
 
This makes it easy within your application to generate URLs and AJAX callbacks to your Controllers.  It also means you can update your URL routing rules in one place and have the code throughout your application automatically pick up the changes for both incoming URL processing and outgoing URL generation.
这使得生成到控制类的URL和AJAX回调变得更加容易,同时意味着你只要修改URL映射规则,就可以让应用程序中所有处理URL请求和产生URL路径的代码发生相应的变更。
 
Important Note: To help enforce testability, the MVC framework today does not support postback events directly to server controls within your Views.  Instead, ASP.NET MVC applications generate hyperlink and AJAX callbacks to Controller actions - and then use Views (and any server controls within them) solely to render output.  This helps ensure that your View logic stays minimal and solely focused on rendering, and that you can easily unit test your Controller classes and verify all Application and Data Logic behavior independent of your Views.  I'll blog more about this in the future.
重要提示:为了增强可测试性,今天的MVC框架不支持视图中控件的直接回送事件,取而代之的,ASP.net MVC应用程序产生到控制类的超级链接和AJAX回调,然后使用视图(以及其中的其他服务器控件)产生输出。这是的你的视图逻辑更加简洁并专注于控制显示,是应用和数据逻辑与视图分离,你可以更方便的对控制类进行单元测试。后面我将会做详细的介绍。
 
Summary
总结
 
This first blog post is a pretty long one, but hopefully helps provide a reasonably broad look at how all the different components of the new ASP.NET MVC Framework fit together, and how you can build a common real world scenario with it.  The first public preview of the ASP.NET MVC bits will be available in a few weeks, and you'll be able to use them to do all of the steps I outlined above.
第一篇文章相当长,旨在对ASP.net MVC框架中各部分之间的关系有一个大致的描述,以及如何利用它创建一个更加普遍并与真实世界相吻合的应用场景,ASP.net MVC第一个预览版本将在几周后发布,你将可以使用他尝试我上面提到的操作。
 
While many of the concepts inherent to MVC (in particular the idea of separation of concerns) are probably new to a lot of people reading this, hopefully this blog post has also show how the ASP.NET MVC implementation we've been working on fits pretty cleanly into the existing ASP.NET, .NET, and Visual Studio feature-set.  You can use .ASPX, .ASCX and .MASTER files and ASP.NET AJAX to create your ASP.NET MVC Views.  Non-UI features in ASP.NET today like Forms Authentication, Windows Authentication, Membership, Roles, Url Authorization, Caching, Session State, Profiles, Health Monitoring, Configuration, Compilation, Localization, and HttpModules/HttpHandlers all fully support the MVC model.
MVC中的很多概念(尤其是对应用程序各部分分离的思想)或许在很多人看来是很新的,希望这篇文章同时也可以展示我们一直在研究的ASP.net MVC是如何在已有的ASP.net, .NET和Visual Studio基础上分离的。你可以使用.ASPX, . ASCX和.MASTER文件和ASP.net AJAX创建ASP.net MVC视图。ASP.net的非UI特性例如窗体验证,Windows验证,用户权限,角色管理,URL权限设置,缓存,Session, Profile, 运行监控,配置,编译,区域化设置和HttpModules/HttpHanlders仍然被MVC模型支持。
 
If you don't like the MVC model or don't find it natural to your style of development, you definitely don't have to use it.  It is a totally optional offering - and does not replace the existing WebForms Page Controller model.  Both WebForms and MVC will be fully supported and enhanced going forward.  You can even build a single application and have parts of it written using WebForms and parts written using an MVC approach if you want.
如果你并不喜欢MVC模型或发现他并不符合你的开发习惯,你可以选择不使用它,这完全是一个可选项,并不会取代已有的Web页面控制模型,WebForms和MVC都会被继续被支持和强化功能。你甚至可以创建一个同时兼有WebPage和MVC模型的应用程序。
 
If you do like what you've seen from the above MVC post (or are intrigued and want to learn more), keep an eye on my blog over the weeks ahead.  I'll be covering more MVC concepts and use them to build out our e-commerce application to show more features of it.
如果你确实喜欢MVC并希望了解更多的知识,请继续关注后面的文章,我会介绍更多MVC的概念并使用他们完成创建我们的电子商务应用程序以展示MVC的诸多特性。
 
Hope this helps,
希望这对你有所帮助,
 
Scott


本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/drummery/archive/2007/12/29/2002363.aspx

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值