使用 MVC 5、Web API 2、KnockoutJS、Ninject 和 NUnit 开发、架构和测试 Web 应用程序






4.96/5 (98投票s)
Microsoft 技术栈的最新技术
引言
成为一名 Microsoft 软件开发人员很像在国际煎饼屋点早餐。每道菜都会给你一堆煎饼,你必须从各种煎饼和糖浆口味中选择。对于 Web 应用程序而言,解决方案堆栈是提供完全功能解决方案(无论是产品还是服务)所需的一组软件子系统或组件。例如,要开发一个 Web 应用程序,Microsoft 开发人员需要使用并理解 Microsoft 组件堆栈,包括不断涌现的开源工具、设计模式和第三方产品。那可真是很多煎饼。概述
本文的目标是介绍一个客户维护 Web 应用程序示例,该应用程序实现了 Microsoft 堆栈的最新技术,包括 Microsoft .NET 4.5.1、Visual Studio Express 2013、MVC 5、WebAPI 2 和 Entity Framework 6 的最新测试版。在本文中,将实现各种设计模式和技术,以帮助培养一种**松耦合**设计,通过 n 层 Web 应用程序的各个层来促进**关注点分离 (SoC)**。总的来说,这个示例应用程序的实现是我之前在 Code Project 上发表的文章 使用 jQuery、JSON、Knockout 和 C# 的 MVC 技术 的变体。
POCO 类 - 纯旧 CLR 对象 (POCO) 类
Entity Framework 附带一个建模工具,允许您设计您的领域和数据库对象——包括使用以下三种选项之一:数据库优先、模型优先或代码优先技术。我选择使用**“代码优先”**方法来创建我的领域对象,即为我的领域对象创建 **POCO** 类。创建 POCO(Plain Old CLR Objects)意味着您可以创建标准 .NET 类(用任何 .NET 支持的语言编写)来定义您的领域设计——不受特定框架所需的属性或继承的限制。
// Customer Domain Entity
public class Customer
{
public Guid CustomerID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string EmailAddress { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string Region { get; set; }
public string PostalCode { get; set; }
public string Country { get; set; }
public string PhoneNumber { get; set; }
public string CreditCardNumber { get; set; }
public Guid PaymentTypeID { get; set; }
public DateTime? CreditCardExpirationDate { get; set; }
public string CreditCardSecurityCode { get; set; }
public Decimal CreditLimit { get; set; }
public DateTime? DateApproved { get; set; }
public int ApprovalStatus { get; set; }
public DateTime DateCreated { get; set; }
public DateTime DateUpdated { get; set; }
}
数据层接口
此应用程序的设计目标之一是创建应用程序不同层之间松散耦合的交互。此客户维护应用程序将具有一个业务层,该业务层将依赖于数据层与数据库交互。
以下接口将由数据层实现。为了使数据层与业务层松散耦合,业务层不会实例化数据层。数据层将被注入到业务层的构造函数中,并且只了解数据层的 **ICustomerDataService** 接口,而不了解数据访问层的具体实现。
随着此应用程序的增长,可能会决定我们希望用一种更传统的方法(结合 ADO.NET 和/或存储过程)替换 Entity Framework 数据访问层。此外,我们可以使用模拟数据层来执行**持续集成**单元测试。
public interface IDataService
{
void CreateSession();
void BeginTransaction();
void CommitTransaction(Boolean closeSession);
void RollbackTransaction(Boolean closeSession);
void CloseSession();
}
public interface ICustomerDataService : IDataService, IDisposable
{
void CreateCustomer(Customer customer);
void UpdateCustomer(Customer customer);
Customer GetCustomerByCustomerID(Guid customerID);
List<CustomerInquiry> CustomerInquiry(string firstName, string lastName);
List<PaymentType> GetPaymentTypes();
PaymentType GetPaymentType(Guid paymentTypeID);
}
客户维护视图
MVC 5 附带的默认项目模板包含 **Twitter Bootstrap**。Bootstrap 是一个免费的工具集合,用于创建网站和 Web 应用程序。它包含用于排版、表单、按钮、导航和其他界面组件的基于 HTML 和 CSS 的设计模板,以及可选的 JavaScript 扩展。此客户维护项目的视图均按 Bootstrap 样式表进行格式化。
纯粹的 HTML
过去几年,我一直在使用 MVC 视图和 **Razor 视图引擎**,我的设计偏好在视图方面发生了一些变化。我的方法已经改变为使视图尽可能干净。客户维护视图将主要包含纯 HTML,并且不包含任何服务器端脚本 Razor 代码。视图中也将很少或没有 JavaScript 引用。视图和 JavaScript 之间唯一的链接将通过 **KnockoutJS 数据绑定标签**。这种方法使视图成为一个独立的、易于阅读的 HTML 代码片段。当然,随着视图变得越来越复杂,最终将需要一些 Razor 代码。幸运的是,Razor 视图引擎是一种精确、有用、轻量级的语言,使您能够在 ASP.NET 中为 MVC 项目创建视图,同时保持关注点分离,并能够进行测试和遵循基于模式的开发。
<!--- Customer Maintenance View --->
<div class="hero-unit" data-bind="visible: DisplayContent" style="display:no">
<table class="table-condensed">
<tr>
<td class="input-label"><label>First Name:</label></td>
<td class="input-box">
<div data-bind="visible: EditMode" class="edit-div">
<input id="FirstName" type="text" data-bind="value: FirstName" />
<span class="required-indicator">*</span>
</div>
<div class="display-div" data-bind="text: FirstName, visible: DisplayMode "></div>
</td>
<td class="input-label"><label>Last Name:</label></td>
<td class="input-box">
<div data-bind="visible: EditMode" class="edit-div">
<input id="LastName" type="text" data-bind="value: LastName" />
<span class="required-indicator">*</span>
</div>
<div class="display-div" data-bind="text: LastName, visible: DisplayMode "></div>
</td>
</tr>
<tr>
<td class="input-label"><label>Email Address:</label></td>
<td class="input-box">
<div data-bind="visible: EditMode" class="edit-div">
<input id="EmailAddress" type="text" data-bind="value: EmailAddress" />
<span class="required-indicator">*</span>
</div>
<div class="display-div" data-bind="text: EmailAddress, visible: DisplayMode "></div>
</td>
<td class="input-label"><label>Phone Number:</label></td>
<td class="input-box">
<div data-bind="visible: EditMode" class="edit-div">
<input id="PhoneNumber" type="text" data-bind="value: PhoneNumber " />
</div>
<div class="display-div" data-bind="text: PhoneNumber, visible: DisplayMode "></div>
</td>
</tr>
</table>
</div>
<div style="clear:both"></div>
<span data-bind="visible: EnableCreateButton">
<input id="btnCreate" type="button" value="Create"
data-bind="click: CreateCustomer" class="btn btn-primary btn-medium" />
</span>
</div>
MVVM - 模型-视图-视图模型 KnockoutJS 和 JavaScript
客户维护视图的 JavaScript 将通过 **KnockoutJS** 与视图交互。Knockout 是 **Model-View-View Model (MVVM)** 模式的独立 JavaScript 实现。Knockout 有助于明确分离表示 HTML 标记和要显示的数据。Knockout 通过简洁易读的语法,帮助您轻松地将 DOM 元素与 Knockout 的**视图模型**数据关联起来。当您的数据模型的状态发生变化时,您的 UI 会自动更新。客户维护项目中的 JavaScript 将不直接引用视图的 **文档对象模型 (DOM)**。通过 HTML 中的数据绑定标签,视图将数据绑定到 JavaScript 中创建的视图模型。对视图数据和视图元素的访问将通过直接访问 Knockout 视图模型进行。MVVM 设计模式使视图和视图的配套 JavaScript 更易于阅读、编写和测试。
//KnockoutJS View Model Setup and Initialization
$(document).ready(function () {
InitializeCustomerMaintenanceViewModel();
GetCustomerInformation();
});
function CustomerMaintenanceViewModel() {
this.CustomerID = ko.observable("");
this.FirstName = ko.observable("");
this.LastName = ko.observable("");
this.EmailAddress = ko.observable("");
this.Address = ko.observable("");
this.City = ko.observable("");
this.Region = ko.observable("");
this.PostalCode = ko.observable("");
this.Country = ko.observable("");
this.PhoneNumber = ko.observable("");
this.CreditCardNumber = ko.observable("");
this.PaymentTypes = ko.observableArray();
this.PaymentTypeID = ko.observable("");
this.PaymentTypeDescription = ko.observable("");
this.CreditCardExpirationDate = ko.observable("");
this.CreditCardSecurityCode = ko.observable("");
this.CreditLimit = ko.observable("");
this.MessageBox = ko.observable("");
}
function InitializeCustomerMaintenanceViewModel() {
customerMaintenanceViewModel = new CustomerMaintenanceViewModel();
ko.applyBindings(customerMaintenanceViewModel);
}
ASP.NET Web Forms
如果您正在使用 ASP.NET Web Forms,您可以通过在 ASPX 页面中只包含 HTML 控件、从页面中排除 ASP.NET 服务器控件并关闭视图状态来实施上述相同的客户端技术。实质上,这将使您远离 ASP.NET Web Forms POSTBACK 模型。除了在 Web Form 的 **Page_Init** 或 **Page_Load** 事件中进行任何初始用户身份验证和安全需求外,您的代码隐藏文件将为空。您的所有服务器端代码交互都将从 Web 服务或 RESTful 服务发起,或者如果您愿意,可以在 ASP.NET Web Form 中使用 ASP.NET 页面方法。最终,您将使用 Web Form 模拟 MVC 行为。MVC 控制器
由于这是一个 MVC 项目,视图将通过 MVC 控制器渲染。为了保持代码整洁和简单,该项目的控制器将承担最小的责任,即简单地验证用户并向客户端渲染视图。在编辑客户的情况下,CustomerID 将传递到控制器方法中,然后 CustomerID 将通过视图模型类对象传递到视图中。public class CustomersController : Controller
{
/// Customer Maintenance
[AuthenicationAction]
public ActionResult CustomerMaintenance()
{
return View("CustomerMaintenance");
}
/// Customer Inquiry
[AuthenicationAction]
public ActionResult CustomerInquiry()
{
return View("CustomerInquiry");
}
/// Edit Customer
[AuthenicationAction]
public ActionResult EditCustomer(string customerID)
{
CustomerMaintenanceViewModel viewModel = new CustomerMaintenanceViewModel();
viewModel.Customer.CustomerID = new Guid(customerID);
return View("CustomerMaintenance", viewModel);
}
}
jQuery AJAX
当客户维护视图加载时,它将进行 jQuery AJAX 调用以检索视图中所需的所有数据。这种方法提供了更好的用户体验,允许视图部分渲染,并使页面看起来更具响应性,而不是让用户等待整个页面内容一次性渲染。function GetCustomerInformation()
{
MVC5WebApplication.DisplayAjax();
var customer = new function () { };
customer.CustomerID = customerMaintenanceViewModel.CustomerID();
var jqxhr = $.get(
"/api/customers/GetCustomerMaintenanceInformation",
customer,
function (response) {
GetCustomerCompleted(response);
},
"json")
.fail(function (response) {
RequestFailed(response);
}
);
}
function GetCustomerCompleted(response) {
customerMaintenanceViewModel.PaymentTypes(response.PaymentTypes);
customerMaintenanceViewModel.FirstName(response.Customer.FirstName);
customerMaintenanceViewModel.LastName(response.Customer.LastName);
customerMaintenanceViewModel.Address(response.Customer.Address);
customerMaintenanceViewModel.City(response.Customer.City);
customerMaintenanceViewModel.Region(response.Customer.Region);
customerMaintenanceViewModel.PostalCode(response.Customer.PostalCode);
customerMaintenanceViewModel.Country(response.Customer.Country);
customerMaintenanceViewModel.PhoneNumber(response.Customer.PhoneNumber);
customerMaintenanceViewModel.EmailAddress(response.Customer.EmailAddress);
MVC5WebApplication.HideAjax();
}
jQuery AJAX HTTP POST
要创建新客户,用户将按下“创建客户”按钮,并执行以下 JavaScript。下面的 JavaScript 将阻塞 UI(使用来自 malsup.com 的 jQuery 插件),显示一个 AJAX 消息,表明服务器请求正在运行,并使用视图模型中的数据填充客户对象。使用 jQuery AJAX POST 方法,客户对象将自动序列化为 JSON 对象,并通过 HTTP 线缆发送到名为 **“/api/customers/create”** 的服务器路由。function CreateCustomer() {
MVC5WebApplication.DisplayAjax();
var customer = new PopulateCustomerInformation();
var jqxhr = $.post("/api/customers/create", customer,
function (response) {
CreateCustomerCompleted(response);
},
"json")
.fail(function (response) {
RequestFailed(response);
}
);
}
function PopulateCustomerInformation() {
var customer = new function () { };
customer.FirstName = customerMaintenanceViewModel.FirstName();
customer.LastName = customerMaintenanceViewModel.LastName();
customer.Address = customerMaintenanceViewModel.Address();
customer.City = customerMaintenanceViewModel.City();
customer.Region = customerMaintenanceViewModel.Region();
customer.PostalCode = customerMaintenanceViewModel.PostalCode();
customer.Country = customerMaintenanceViewModel.Country();
customer.PhoneNumber = customerMaintenanceViewModel.PhoneNumber();
customer.EmailAddress = customerMaintenanceViewModel.EmailAddress();
customer.PaymentTypeID = customerMaintenanceViewModel.PaymentTypeID();
return customer;
}
WebAPI 2 - RESTful 服务
客户维护项目的 AJAX 请求将通过最新版的 Microsoft ASP.NET WebAPI 2.0 组件与服务器进行交互。ASP.NET WebAPI 是一个框架,可以轻松构建 HTTP 服务,这些服务可触及广泛的客户端,包括浏览器和移动设备。ASP.NET WebAPI 是在 .NET 框架上构建 **RESTful 应用程序**的理想平台。该框架充满了抽象。有控制器、过滤器提供程序、模型验证器以及构成框架管道的许多其他组件。
您可以通过创建模仿 MVC 控制器并借鉴 MVC 路由系统的 WebAPI 控制器来创建 Web API。WebAPI 的第一个版本使用基于约定的路由。当框架收到请求时,它将 URI 与由 HTTP 动词(GET、POST、PUT、DELETE 等)驱动的路由进行匹配。
要创建客户,将使用 HTTP 动词 **POST**,而 **PUT** 动词将用于更新客户,**GET** 动词将用于检索客户。这在发布简单的公共 RESTful API 时是足够的。对于业务线应用程序,其中单个实体(发票、销售订单、采购订单等)需要多种类型的更新,包括更新、批准和作废实体,简单的基于约定的路由是不合适的。
WebAPI 的第 2 版引入了一种新的路由类型,称为属性路由。顾名思义,属性路由使用属性来定义路由。属性路由使您能够更好地控制 Web API 中的 URI。例如,您可以轻松地使用相同的 POST 动词来路由到执行不同类型功能的独立 WebAPI 方法调用。要在 MVC Web 应用程序中启用属性路由,您需要修改位于 **App_Start** 文件夹中的 **WebApiConfig** 类,并向 Register 方法添加 **config.MapHttpAttributeRoutes()** 行。
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
WebAPI 控制器
要创建 Web API,您需要将 MVC WebAPI 控制器添加到 MVC Web 项目的 Controllers 文件夹中(脚手架)。如果您使用过 ASP.NET MVC,那么您已经熟悉控制器了。它们在 WebAPI 中的工作方式类似,但 WebAPI 中的控制器派生自 **ApiController** 类而不是 Controller 类。您会注意到的主要区别是 WebAPI 控制器上的操作不返回视图,它们返回数据。[RoutePrefix("api/customers")]
public class CustomersApiController : ApiController
{
ICustomerDataService customerDataService;
/// Constructor with Dependency Injection using Ninject
public CustomersApiController(ICustomerDataService dataService)
{
customerDataService = dataService;
}
/// Create Customer
[WebApiAuthenication]
[HttpPost("create")]
public HttpResponseMessage CreateCustomer(
HttpRequestMessage request, [FromBody] CustomerMaintenanceDTO customerDTO)
{
TransactionalInformation transaction;
CustomerMaintenanceViewModel viewModel = new CustomerMaintenanceViewModel();
if (!ModelState.IsValid)
{
var errors = ModelState.Errors();
viewModel.ReturnMessage = ModelStateHelper.ReturnErrorMessages(errors);
viewModel.ReturnStatus = false;
var badResponse = Request.CreateResponse<CustomerMaintenanceViewModel>
(HttpStatusCode.BadRequest, viewModel);
return badResponse;
}
Customer customer = new Customer();
ModelStateHelper.UpdateViewModel(customerDTO, customer);
CustomerApplicationService service = new
CustomerApplicationService(customerDataService);
service.CreateCustomer(customer, out transaction);
viewModel.Customer = customer;
viewModel.ReturnStatus = transaction.ReturnStatus;
viewModel.ReturnMessage = transaction.ReturnMessage;
if (transaction.ReturnStatus == false)
{
var badResponse = Request.CreateResponse<CustomerMaintenanceViewModel>
(HttpStatusCode.BadRequest, viewModel);
return badResponse;
}
else
{
var response = Request.CreateResponse<CustomerMaintenanceViewModel>
(HttpStatusCode.Created, viewModel);
return response;
}
}
/// Get Customer Maintenance Information
[WebApiAuthenication]
[HttpGet("GetCustomerMaintenanceInformation")]
public HttpResponseMessage GetCustomerMaintenanceInformation(
HttpRequestMessage request,Guid customerID)
{
TransactionalInformation customerTransaction;
TransactionalInformation paymentTransaction;
CustomerMaintenanceViewModel viewModel = new CustomerMaintenanceViewModel();
CustomerApplicationService service =
new CustomerApplicationService(customerDataService);
if (customerID != Guid.Empty)
{
Customer customer = service.GetCustomerByCustomerID(
customerID, out customerTransaction);
viewModel.Customer = customer;
viewModel.ReturnStatus = customerTransaction.ReturnStatus;
viewModel.ReturnMessage = customerTransaction.ReturnMessage;
}
List<PaymentType> paymentTypes = service.GetPaymentTypes(out paymentTransaction);
viewModel.PaymentTypes = paymentTypes;
if (paymentTransaction.ReturnStatus == false)
{
viewModel.ReturnStatus = paymentTransaction.ReturnStatus;
viewModel.ReturnMessage = paymentTransaction.ReturnMessage;
}
if (viewModel.ReturnStatus == true)
{
var response = Request.CreateResponse<CustomerMaintenanceViewModel>
(HttpStatusCode.OK, viewModel);
return response;
}
var badResponse = Request.CreateResponse<CustomerMaintenanceViewModel>
(HttpStatusCode.BadRequest, viewModel);
return badResponse;
}
}
路由属性
**[HttpGet]** 属性定义一个 HTTP GET 方法。字符串 **"GetCustomerInformation"** 是 **"api/customers/GetCustomerInformation"** 路由的 URI 模板。**[HttPost]** 属性定义一个 HTTP POST 方法,其路由为 "create",映射到 **"api/customers/create"**。您可以不为每个方法指定完整的路由路径,而是使用 **[RoutePrefix("api/customers")]** 等属性在类级别添加路由前缀。HttpGet 和 HttpPost 等路由属性还限制客户端应用程序通过指定的 HTTP 动词调用这些路由。
[RoutePrefix("api/customers")]
public class CustomersApiController : ApiController
{
[WebApiAuthenication]
[HttpGet("GetCustomerInformation")]
public HttpResponseMessage GetCustomerMaintenanceInformation()
{
}
[WebApiAuthenication]
[HttpPost("create")]
public HttpResponseMessage CreateCustomer()
{
}
}
自定义操作过滤器
**[WebApiAuthenication]** 属性是一个自定义 WebAPI 动作过滤器,它将使用 FORMS 身份验证对用户进行身份验证,并在执行方法之前执行以下代码。此动作过滤器类似于用于在渲染视图时在 MVC 控制器方法中验证用户的动作过滤器 **[AuthenicationAction]**。[WebApiAuthenication] 动作过滤器将覆盖 WebAPI 控制器动作的 **OnActionExecuting** 事件。由于 WebAPI 控制器方法将仅从客户维护应用程序调用,因此过滤器将通过检查请求头中名为 **X-Requested-With** 的属性来验证方法调用是否正在从 AJAX 请求执行,以确定请求是否来自 AJAX 请求。此外,此自定义过滤器将检查用户是否已通过身份验证。如果这些条件中的任何一个不满足,则过滤器将向调用客户端返回一个错误的请求响应 HTTP 400 状态码,并且 WebAPI 动作将不会执行。
public class WebApiAuthenicationAttribute : System.Web.Http.Filters.ActionFilterAttribute
{
public override void OnActionExecuting(Http.Controllers.HttpActionContext actionContext)
{
var request = actionContext.Request;
var headers = request.Headers;
if (!headers.Contains("X-Requested-With") ||
headers.GetValues("X-Requested-With").FirstOrDefault() != "XMLHttpRequest")
{
TransactionalInformation tran = new TransactionalInformation();
tran.ReturnMessage.Add("Access has been denied.");
tran.ReturnStatus = false;
actionContext.Response = request.CreateResponse<TransactionalInformation>
(HttpStatusCode.BadRequest, tran);
}
else
{
HttpContext ctx = default(HttpContext);
ctx = HttpContext.Current;
if (ctx.User.Identity.IsAuthenticated == false)
{
TransactionalInformation tran = new TransactionalInformation();
tran.ReturnMessage.Add("Your session has expired.");
tran.ReturnStatus = false;
actionContext.Response = request.CreateResponse<TransactionalInformation>
(HttpStatusCode.BadRequest, tran);
}
}
}
}
模型绑定
当 WebAPI 接收到 HTTP 请求时,它根据操作的签名将请求转换为 .NET 类型。**动态模型绑定**用于自动填充操作的参数。在“创建客户”操作中,我创建了一个客户数据转换对象,该对象将通过 WebAPI 模型绑定器进行实例化和填充。由于“创建客户”请求是通过 POST 请求发送的,因此请求的值将位于请求的正文中。为了从请求正文读取值,需要一个 **[FromBody]** 属性来告知模型绑定器如何填充操作签名。由于 WebAPI 只允许一个参数用 [FromBody] 属性标记,因此需要一个客户数据转换对象。
客户数据转换对象
客户数据转换对象 (DTO) 类似于客户域实体对象。客户域实体对象可以在这里使用,但如果您打算构建一个将来会增长的应用程序,则应考虑使用 DTO 对象,因为域实体不适合数据转换。域实体在客户端拥有的属性总是比您需要的或多或少,这可能导致过度绑定并使模型绑定效率低下。public class CustomerMaintenanceDTO
{
public Guid CustomerID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string EmailAddress { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string Region { get; set; }
public string PostalCode { get; set; }
public string Country { get; set; }
public string PhoneNumber { get; set; }
public string CreditCardNumber { get; set; }
public Guid? PaymentTypeID { get; set; }
public DateTime? CreditCardExpirationDate { get; set; }
public string CreditCardSecurityCode { get; set; }
public Decimal? CreditLimit { get; set; }
}
模型绑定验证
当客户端向您的 Web API 发送数据时,模型绑定器将在进行任何处理之前验证数据。任何无法转换为 .NET 类型的参数都将把模型的设置为无效状态。此外,您还可以在 DTO 对象上添加数据验证的注解(例如将某些参数标记为必需等)。当验证失败时,WebAPI 不会自动向客户端返回错误。由 WebAPI 控制器操作来检查模型状态并进行适当响应。在下面的代码片段中,检查了模型状态,如果模型状态无效,控制器操作将向客户端返回 **HTTP 状态 400 错误请求**代码。if (!ModelState.IsValid)
{
var errors = ModelState.Errors();
viewModel.ReturnMessage = ModelStateHelper.ReturnErrorMessages(errors);
viewModel.ReturnStatus = false;
var badResponse = Request.CreateResponse<CustomerMaintenanceViewModel>
(HttpStatusCode.BadRequest, viewModel);
return badResponse;
}
HTTP 状态码
当“创建客户”操作成功执行时,它会返回一个 HTTP 201 状态码,表示请求的数据已创建。**RESTful 服务**传统上是在 HTTP 之上构建的。该协议通过 HTTP 状态码传达请求的状态。在应用程序中实现所有状态码很困难,但作为一项要求和最佳实践,您的所有 WebAPI 操作都应返回 HTTP 状态码。也许最好的方法是将公开的状态码数量限制在少数几个。您的客户端可以根据这些 HTTP 码采取行动并相当容易地实现它们。HTTP 请求消息
最后,WebAPI 还允许您查看发送到操作的整个 HTTP 请求消息。您可以通过在操作签名中提供 **HttpRequestMessage** 参数来访问此信息。依赖注入 - Ninject
此客户维护应用程序的目标之一是创建松散耦合的层。WebAPI 方法正在使用的客户业务层依赖于数据访问层。数据访问层将通过其构造函数传递到业务层,而不是直接在业务层中实例化数据访问层。数据访问层将使用 Ninject 注入到 WebAPI 控制器的构造函数中。这将允许我们替换数据访问层,而无需对业务层进行任何更改。出于单元测试目的,我们可能希望使用模拟的数据访问层版本,该版本实际上不会访问数据库。
Ninject 是一个快速、轻量级的 .NET 应用程序依赖注入器。它帮助您将应用程序拆分为一组松散耦合、高度内聚的组件。通过使用 Ninject 支持您的软件架构,您的代码将变得更易于编写、重用、测试和修改。
使用 NuGet 安装 Ninject
创建新的 MVC 应用程序后,从 Visual Studio 中的包管理器控制台运行 **Install-Package Ninject.MVC3**。这将下载并安装所有必需的程序集,并添加一些源代码以将其集成到应用程序中。安装后,您将在 **App_Start** 文件夹中找到一个 **NinjectWebCommon.cs** 文件。此类文件将包含配置 Ninject 所需的代码。NuGet 包管理器
NInject.MVC3 也可以从 Nuget 包库中找到并安装。**NuGet** 是 Visual Studio 扩展,可以轻松地在使用 .NET Framework 的 Visual Studio 项目中添加、删除和更新库和工具。当您添加库或工具时,NuGet 会将文件复制到您的解决方案,并自动对您的项目进行所需的更改,例如添加引用和更改您的 app.config 或 web.config 文件。当您删除库时,NuGet 会删除文件并撤销它在项目中进行的所有更改,以便不留下任何垃圾。下面的 NinjectWebCommon.cs 类文件注册您的依赖对象。public static class NinjectWebCommon
{
private static readonly Bootstrapper bootstrapper = new Bootstrapper();
/// Starts the application
public static void Start()
{
DynamicModuleUtility.RegisterModule(typeof(OnePerRequestHttpModule));
DynamicModuleUtility.RegisterModule(typeof(NinjectHttpModule));
bootstrapper.Initialize(CreateKernel);
}
/// Stops the application.
public static void Stop()
{
bootstrapper.ShutDown();
}
/// Creates the kernel that will manage your application.
private static IKernel CreateKernel()
{
var kernel = new StandardKernel();
kernel.Bind<Func<IKernel>>().ToMethod(ctx => () => new Bootstrapper().Kernel);
kernel.Bind<IHttpModule>().To<HttpApplicationInitializationHttpModule>();
RegisterServices(kernel);
// Install our Ninject-based IDependencyResolver into the Web API config
GlobalConfiguration.Configuration.DependencyResolver =
new NinjectDependencyResolver(kernel);
return kernel;
}
/// Load your modules or register your services here!
private static void RegisterServices(IKernel kernel)
{
kernel.Bind<MVC5DataServiceInterface.ICustomerDataService>().To
<MVC5EntityFrameworkDataAccess.EFCustomerService>();
}
}
使用 Ninject 注册对象
在 RegisterServices 方法中,您使用 **Ninject kernel.Bind** 命令来注册您的依赖对象。通过注册您的依赖对象,Ninject 将根据构造函数的签名实例化并将您注册的对象注入到您的类构造函数中。这就是将您的类实现为类接口的方式,允许您将实现相同接口的不同对象注入到您的构造函数中。在上面的示例中,我正在向 Ninject 注册实现 ICustomerDataService 接口的 Entity Framework 版本的客户数据服务组件。**依赖注入 (DI)** 实现了**控制反转 (IoC)** 编程技术。控制反转和依赖注入模式旨在消除代码中的依赖关系。在下面的 WebAPI 类构造函数中,对象耦合在运行时绑定,实际对象在编译时是未知的。通过 NinjectWebCommon Ninject 类中的一行代码,您可以告诉 Ninject 内核在运行时加载哪个对象。例如,您可以提供使用 Entity Framework 实现的数据访问层,或者可以切换到基于 ADO.NET 的数据访问层。出于单元测试目的,您还可以注入数据访问层的模拟对象。客户维护示例应用程序包含实现所有这三种不同数据访问层的代码。
ICustomerDataService customerDataService;
/// Constructor with Dependency Injection using Ninject
public CustomersApiController(ICustomerDataService dataService)
{
customerDataService = dataService;
}
依赖解析问题和 ApiController 类
WebAPI 遇到的困难是 Ninject 绑定不适用于 ApiController 实例。当您尝试访问您的某个 API 端点时,您将收到一条错误,指出您的控制器没有默认构造函数。要解决此问题,您需要设置一个依赖解析器。为了处理依赖解析,您需要创建两个自定义类,NinjectWebCommon 类将使用它们。下面的依赖解析器代码已添加到 NinjectWebCommon 类文件中,并且可以在各种技术博客上找到。请查看 Phil Haack 关于依赖解析器的博客。public class NinjectDependencyScope : IDependencyScope
{
IResolutionRoot resolver;
public NinjectDependencyScope(IResolutionRoot resolver)
{
this.resolver = resolver;
}
public object GetService(Type serviceType)
{
if (resolver == null)
throw new ObjectDisposedException("this", "This scope has been disposed");
return resolver.TryGet(serviceType);
}
public System.Collections.Generic.IEnumerable<object> GetServices(Type serviceType)
{
if (resolver == null)
throw new ObjectDisposedException("this", "This scope has been disposed");
return resolver.GetAll(serviceType);
}
public void Dispose()
{
IDisposable disposable = resolver as IDisposable;
if (disposable != null)
disposable.Dispose();
resolver = null;
}
}
public class NinjectDependencyResolver : NinjectDependencyScope, IDependencyResolver
{
IKernel kernel;
public NinjectDependencyResolver(IKernel kernel)
: base(kernel)
{
this.kernel = kernel;
}
public IDependencyScope BeginScope()
{
return new NinjectDependencyScope(kernel.BeginBlock());
}
}
业务层
客户维护项目的业务层包含创建、更新、验证和检索客户信息的关键方法。将 **ICustomerDataService** 接口传入业务层,可以轻松地使业务逻辑层独立于数据访问层。数据访问层的底层具体方法对业务层是隐藏的。客户对象和客户对象的泛型列表从数据访问层传入传出,无论访问数据库的数据访问技术是什么。这使得在不更改业务层的情况下轻松切换数据访问层。如下图所示,业务层中没有显式的数据访问代码。例如,**CreateSession** 方法可以初始化 Entity Framework 数据上下文对象,也可以使用标准 ADO.NET 创建数据库连接,具体取决于注入到业务层中的数据访问层。
public class CustomerApplicationService
{
ICustomerDataService _customerDataService;
private ICustomerDataService CustomerDataService
{
get { return _customerDataService; }
}
/// Constructor
public CustomerApplicationService(ICustomerDataService dataService)
{
_customerDataService = dataService;
}
/// Create Customer
public void CreateCustomer(Customer customer, out TransactionalInformation transaction)
{
transaction = new TransactionalInformation();
CustomerBusinessRules customerBusinessRules = new CustomerBusinessRules();
try
{
CustomerDataService.CreateSession();
customerBusinessRules.ValidateCustomer(customer, CustomerDataService);
if (customerBusinessRules.ValidationStatus == true)
{
CustomerDataService.BeginTransaction();
CustomerDataService.CreateCustomer(customer);
CustomerDataService.CommitTransaction(true);
transaction.ReturnStatus = true;
transaction.ReturnMessage.Add("Customer successfully created.");
}
else
{
transaction.ReturnStatus = customerBusinessRules.ValidationStatus;
transaction.ReturnMessage = customerBusinessRules.ValidationMessage;
transaction.ValidationErrors = customerBusinessRules.ValidationErrors;
}
}
catch (Exception ex)
{
CustomerDataService.RollbackTransaction(true);
transaction.ReturnMessage = new List<string>();
string errorMessage = ex.Message;
transaction.ReturnStatus = false;
transaction.ReturnMessage.Add(errorMessage);
}
finally
{
CustomerDataService.CloseSession();
}
}
}
单元测试 - NUnit
在为本文开发示例应用程序时,我决定通过 NuGet 包管理工具安装 NUnit 单元测试工具,以便可以针对应用程序的业务层编写一些示例单元测试。在编写了几个单元测试后,我意识到单元测试在理论上似乎比实践中更受欢迎。你认为这是为什么?嗯,因为设置真实有用的测试非常困难,所以存在巨大的入门障碍。在编写单元测试时,我还意识到我实际上是在编写集成测试,而不是单元测试。
我学到的关于编写良好单元测试的几个关键点是:编写良好单元测试包括先编写测试,然后使用**测试驱动开发 (TDD)** 方法编写代码。这可以确保您编写的是可测试的代码。您还需要开发和编程您的类以实现接口。这将允许您注入外部依赖项(如数据库和其他数据存储以及文件系统)的模拟对象。此外,您应该创建一个流程,以便在每次运行单元测试套件之前刷新测试数据。
如果您希望实施持续集成流程,在每次代码签入和软件构建运行后运行所有单元测试套件,这将很有帮助。此示例应用程序包含可刷新 SQL-Server Express 中数据库表的测试数据。您还可以使用此测试数据在模拟数据访问层中构建模拟数据,该模拟数据实际上不会访问数据库。为了刷新数据,我包含了一个 Seed 类库,可以从应用程序菜单或从模拟数据访问对象运行。
当下面的 NUnit 测试启动时,它会执行 **InitializeDependencies** 方法,因为它带有 **[SetUp]** 属性。此属性标记包含给定命名空间下所有测试夹具的一次性设置的类。该类最多可以包含一个用 [SetUp] 属性标记的方法和一个用 **[TearDown]** 属性标记的方法。
下面的 InitializeDependencies 方法将有条件地创建您想要用于测试目的的特定 ICustomerDataService 对象。在持续集成环境中,您将需要为需要外部资源(如数据库、数据存储等)的某些对象创建模拟对象。您需要这样做,因为您可能有数百个测试需要执行,并且您希望在开发人员签入代码以启动自动构建和测试时,您的测试能够及时运行。
此外,您可能还会运行应用程序的夜间构建,该构建使用不同的配置来执行您的依赖项。在下面的示例中,我有三个测试方法。NUnit 将执行带有 **[Test]** 属性的方法。您可以在测试方法中使用 NUnit **Assert** 方法来评估测试条件。最佳实践是将每个单元测试限制为每个单元测试只有一个断言。
[TestFixture]
public partial class Testing
{
ICustomerDataService customerDataService;
/// Initialize Dependencies
[SetUp]
public void InitializeDependencies()
{
string integrationType = ConfigurationManager.AppSettings["IntegrationType"];
if (integrationType == "EntityFramework")
customerDataService = new EFCustomerService();
else if (integrationType == "Ado")
customerDataService = new AdoCustomerService();
else
customerDataService = new MockedCustomerService();
}
/// Create Customer Integration Test
[Test]
public void CreateCustomerIntegrationTest()
{
string returnMessage;
TransactionalInformation transaction;
CustomerApplicationService service =
new CustomerApplicationService(customerDataService);
List<PaymentType> paymentTypes = service.GetPaymentTypes(out transaction);
var paymentType = (from p in paymentTypes where p.Description == "Visa"
select p).First();
Customer customer = new Customer();
customer.FirstName = "William";
customer.LastName = "Gates";
customer.EmailAddress = "bgates@microsoft.com";
customer.PhoneNumber = "(425)882-8080";
customer.Address = "One Microsoft Way";
customer.City = "Redmond";
customer.Region = "WA";
customer.PostalCode = "98052-7329";
customer.PaymentTypeID = paymentType.PaymentTypeID;
customer.CreditCardExpirationDate = Convert.ToDateTime("12/31/2014");
customer.CreditCardSecurityCode = "111";
customer.CreditCardNumber = "123111234";
service.CreateCustomer(customer, out transaction);
returnMessage = Utilities.GetReturnMessage(transaction.ReturnMessage);
Assert.AreEqual(true, transaction.ReturnStatus, returnMessage);
}
/// Test Valid Email Address
[Test]
public void TestValidEmailAddress()
{
Customer customer = new Customer();
customer.EmailAddress = "bgates@microsoft.com";
ValidationRules validationRules = new ValidationRules();
validationRules.InitializeValidationRules(customer);
Boolean returnStatus = validationRules.ValidateEmailAddress("EmailAddress");
Assert.AreEqual(true, returnStatus);
}
/// Test InValid Email Address
[Test]
public void TestInValidEmailAddress()
{
Customer customer = new Customer();
customer.EmailAddress = "bgates@microsoft";
ValidationRules validationRules = new ValidationRules();
validationRules.InitializeValidationRules(customer);
Boolean returnStatus = validationRules.ValidateEmailAddress("EmailAddress");
Assert.AreEqual(false, returnStatus);
}
}
NUnit GUI 测试运行器
nunit.exe **NUnit 测试运行器**程序(随 NUnit NuGet 包下载)是一个图形运行器。它在类似资源管理器的浏览器窗口中显示测试,并提供测试成功或失败的视觉指示。它允许您选择性地运行单个测试或套件,并在您修改和重新编译代码时自动重新加载。您还可以通过命令行运行测试,这可以包含在持续集成构建和测试环境中。
数据绑定和数据网格
最后,如果没有实现您最喜欢的数据网格控件,任何 Web 应用程序都不算完整。遵循此示例应用程序的设计原则,我希望所有 UI 呈现功能都驻留在客户端并执行。这意味着将数据网格数据绑定功能移动到客户端 JavaScript 中执行。传统上,数据绑定是在服务器端执行的,但我只是希望服务器端简单地将 JSON 集合序列化到客户端。所有这些让我寻找一个好的客户端数据网格。我的研究让我发现了大约二十种基于 jQuery 的数据网格和一些第三方供应商的数据网格控件。这一切都有些令人望而生畏和不知所措,所以我决定简单地编写一个普通的 HTML 表格,并使用 KnockoutJS 将从服务器序列化的 JSON 集合绑定到表格,只需几行 JavaScript 代码。

纯 HTML 数据网格
客户查询网格的 HTML 如下。它基本上是一个 HTML 表格,其中包含一个用 **thead** 标签定义的表头行和一个用于显示客户查询网格数据行的 **tbody** 标签。向 HTML 表格添加 KnockoutJS data-bind 标签将允许 KnockoutJS 自动将客户数据绑定到网格,以显示我们希望显示的每个客户。如您所见,您只需定义一行 HTML,其中包含一个带有 **foreach** 属性的 data-bind 标签。使用 foreach 属性,KnockoutJS 将自动为每个客户行生成 HTML。<table id="CustomerInquiryTable" border="0" class="table" style="width: 100%;">
<thead>
<tr>
<th style="width: 15%; height: 25px">Last Name</th>
<th style="width: 15%">First Name</th>
<th style="width: 35%">Email Address</th>
<th style="width: 15%">City</th>
<th style="width: 20%">Payment Type</th>
</tr>
</thead>
<tbody data-bind="foreach: Customers">
<tr>
<td><div data-bind="text: LastName "></div></td>
<td><div data-bind="text: FirstName "></div></td>
<td><div data-bind="text: EmailAddress"></div></td>
<td><div data-bind="text: City"></div></td>
<td><div data-bind="text: PaymentTypeDescription"></div></td>
</tr>
</tbody>
</table>
KnockoutJS 视图模型
为了使用 KnockoutJS 与客户查询数据网格进行交互,需要一些 JavaScript 来创建和初始化 KnockoutJS 视图模型。为了将客户数据绑定到 HTML 表格,在视图模型中定义了一个客户的 **KnockoutJS 可观察数组**。如果您想检测并响应单个对象的变化,您可以使用 **KnockoutJS 可观察对象**。如果您想检测并响应事物集合的变化,请使用 **observableArray**。这在许多场景中都很有用,例如数据网格,您在其中显示或编辑多个值,并且需要随着项目的添加和删除,重复的 UI 部分出现和消失。
$(document).ready(function () {
InitializeCustomerInquiryViewModel();
});
function InitializeCustomerInquiryViewModel() {
customerInquiryViewModel = new CustomerInquiryViewModel();
ko.applyBindings(customerInquiryViewModel);
}
function CustomerInquiryViewModel() {
this.Customers = ko.observableArray("");
this.TotalCustomers = ko.observable();
this.TotalPages = ko.observable();
this.PageSize = ko.observable(pageSize);
}
使用 KnockoutJS、AJAX 和 JSON 进行客户端数据绑定
传统的 ASP.NET 数据绑定总是发生在服务器端的代码隐藏中,尤其是在 ASP.NET Web Forms 中。为了遵循本项目关注点分离的设计目标,客户查询数据网格的数据绑定将在客户端 JavaScript 中进行。第一步是向服务器发出 AJAX 调用,访问 WebAPI 路由,该路由将返回一个包含客户数据 JSON 集合的 HTTP 响应。服务器将自动将 .NET 客户数据泛型列表序列化为 JSON 集合,并通过 HTTP 传输。将客户数据绑定到客户查询 HTML 表格就像在 JavaScript 中遍历 JSON 集合并将行填充(“推送”)到 KnockoutJS 视图模型中的客户可观察数组中一样简单。
for 循环完成后,KnockoutJS 将完成其余工作,通过 HTML 中的数据绑定标签自动渲染显示客户行所需的 HTML。最终,所有数据绑定都在客户端执行。我相信 JavaScript 中的客户端数据绑定促进了更清晰代码的开发。遵循关注点分离原则,数据绑定是表示层功能,应在客户端执行。
function CustomerInquiry() {
var jqxhr = $.get("/api/customers/GetCustomers", "", function (response) {
CustomerInquiryCompleted(response);
}, "json")
.fail(function (response) {
RequestFailed(response);
}
);
}
function CustomerInquiryCompleted(response)
{
customerInquiryViewModel.TotalPages(response.TotalPages);
customerInquiryViewModel.TotalCustomers(response.TotalRows);
customerInquiryViewModel.Customers.removeAll();
for (i = 0; i < response.Customers.length; i++) {
var customer = response.Customers[i];
customerInquiryViewModel.Customers.push(customer);
}
}
过滤、分页和排序
为了使客户查询数据网格更强大,我添加了过滤、分页和排序网格的功能。客户端将调用 GetCustomers WebAPI 路由并传入过滤、排序和分页信息。然后 WebAPI 控制器操作将调用业务层,同时也将数据访问层注入到业务层的构造函数中。
数据层分页
数据的过滤、分页和排序在客户数据访问层中实现。下面是使用 LINQ 查询针对 Entity Framework 上下文对象的客户数据的代码。Entity Framework 分页示例使用 **Dynamic LINQ**。为了使其工作,我必须从 Microsoft 下载 Dynamic LINQ 库源代码,并将其保存到名为 **DynamicLibrary.cs** 的 C# 类文件中,然后引用命名空间 **System.Linq.Dynamic**。Dynamic LINQ 库实现 **IQueryable** 接口来执行其操作。这是必需的,因为我需要能够将字面字符串值传递到 **LINQ 的 Lambda 表达式**语法中。下面的 LINQ 查询将客户表与支付类型表连接起来,因为我们希望显示每个客户的信用卡支付类型。数据层返回对象的泛型列表,因此需要一个 **CustomerInquiry** 类,该类包含客户表和支付类型表的属性。使用 **LINQ Skip 方法**允许查询根据我们希望为网格返回的页面大小和当前页码返回单页客户数据。
/// Customer Inquiry
public List<CustomerInquiry> CustomerInquiry(
string firstName, string lastName, DataGridPagingInformation paging)
{
string sortExpression = paging.SortExpression;
if (paging.SortDirection != string.Empty)
sortExpression = sortExpression + " " + paging.SortDirection;
var customerQuery = dbConnection.Customers.AsQueryable();
if (firstName != null && firstName.Trim().Length > 0)
{
customerQuery = customerQuery.Where(c => c.FirstName.StartsWith(firstName));
}
if (lastName != null && lastName.Trim().Length > 0)
{
customerQuery = customerQuery.Where(c => c.LastName.StartsWith(lastName));
}
var customers =
from c in customerQuery
join p in dbConnection.PaymentTypes on c.PaymentTypeID equals p.PaymentTypeID
select new { c.CustomerID,c.FirstName,c.LastName,c.EmailAddress,c.City,p.Description };
int numberOfRows = customers.Count();
customers = customers.OrderBy(sortExpression);
var customerList = customers.Skip((paging.CurrentPageNumber - 1)
* paging.PageSize).Take(paging.PageSize);
paging.TotalRows = numberOfRows;
paging.TotalPages = Utilities.CalculateTotalPages(numberOfRows, paging.PageSize);
List<CustomerInquiry> customerInquiry = new List<CustomerInquiry>();
foreach (var customer in customerList)
{
CustomerInquiry customerData = new CustomerInquiry();
customerData.CustomerID = customer.CustomerID;
customerData.FirstName = customer.FirstName;
customerData.LastName = customer.LastName;
customerData.EmailAddress = customer.EmailAddress;
customerData.City = customer.City;
customerData.PaymentTypeDescription = customer.Description;
customerInquiry.Add(customerData);
}
return customerInquiry;
}
使用 ADO.NET 和内联 T-SQL 进行分页
对于那些喜欢执行传统 SQL-Server T-SQL 语句的人,客户维护示例应用程序包含一个基于 ADO.NET 和 T-SQL 的数据访问层对象。下面的示例代码执行与 LINQ/Entity Framework 版本相同的分页功能。由于这两个数据访问层都实现了 ICustomerDataService 接口,因此您可以通过简单地更改 Ninject 配置文件在两者之间切换,而无需对客户维护示例应用程序进行任何额外的更改。
/// Customer Inquiry public List<CustomerInquiry> CustomerInquiry( string firstName, string lastName, DataGridPagingInformation paging) { List<Customer> customers = new List<Customer>(); string sortExpression = paging.SortExpression; int maxRowNumber; int minRowNumber; minRowNumber = (paging.PageSize * (paging.CurrentPageNumber - 1)) + 1; maxRowNumber = paging.PageSize * paging.CurrentPageNumber; StringBuilder sqlBuilder = new StringBuilder(); StringBuilder sqlWhereBuilder = new StringBuilder(); string sqlWhere = string.Empty; if (firstName != null && firstName.Trim().Length > 0) sqlWhereBuilder.Append(" c.FirstName LIKE @FirstName AND "); if (lastName != null && lastName.Trim().Length > 0) sqlWhereBuilder.Append(" c.LastName LIKE @LastName AND "); if (sqlWhereBuilder.Length > 0) sqlWhere = " WHERE " + sqlWhereBuilder.ToString().Substring(0, sqlWhereBuilder.Length - 4); sqlBuilder.Append(" SELECT COUNT(*) as total_records FROM Customers c "); sqlBuilder.Append(sqlWhere); sqlBuilder.Append(";"); sqlBuilder.Append(" SELECT * FROM ( "); sqlBuilder.Append(" SELECT (ROW_NUMBER() OVER (ORDER BY " + paging.SortExpression + " " + paging.SortDirection + ")) as record_number, "); sqlBuilder.Append(" c.*, p.Description as PaymentTypeDescription "); sqlBuilder.Append(" FROM Customers c "); sqlBuilder.Append(" INNER JOIN PaymentTypes p ON p.PaymentTypeID = c.PaymentTypeID "); sqlBuilder.Append(sqlWhere); sqlBuilder.Append(" ) Rows "); sqlBuilder.Append(" where record_number between " + minRowNumber + " and " + maxRowNumber); string sql = sqlBuilder.ToString(); SqlCommand sqlCommand = new SqlCommand(); sqlCommand.CommandText = sql; sqlCommand.Connection = dbConnection; if (firstName != null && firstName.Trim().Length > 0) { sqlCommand.Parameters.Add("@FirstName", System.Data.SqlDbType.VarChar); sqlCommand.Parameters["@FirstName"].Value = firstName + "%"; } if (lastName != null && lastName.Trim().Length > 0) { sqlCommand.Parameters.Add("@LastName", System.Data.SqlDbType.VarChar); sqlCommand.Parameters["@LastName"].Value = lastName + "%"; } SqlDataReader reader = sqlCommand.ExecuteReader(); reader.Read(); paging.TotalRows = Convert.ToInt32(reader["Total_Records"]); paging.TotalPages = Utilities.CalculateTotalPages(paging.TotalRows, paging.PageSize); reader.NextResult(); List<CustomerInquiry> customerList = new List<CustomerInquiry>(); while (reader.Read()) { CustomerInquiry customer = new CustomerInquiry(); DataReader dataReader = new DataReader(reader); customer.CustomerID = dataReader.GetGuid("CustomerID"); customer.FirstName = dataReader.GetString("FirstName"); customer.LastName = dataReader.GetString("LastName"); customer.EmailAddress = dataReader.GetString("EmailAddress"); customer.City = dataReader.GetString("City"); customer.PaymentTypeDescription = dataReader.GetString("PaymentTypeDescription"); customerList.Add(customer); } reader.Close(); return customerList; }
自己动手制作数据网格或使用第三方控件
这一切都很有趣,我喜欢完全控制数据网格。如果您更喜欢使用第三方供应商(如 Infragistics 或 Telerik)的数据网格,或者如果您更喜欢使用开源 jQuery 网格之一,请确保数据网格控件不与任何服务器端引用绑定或依赖。这将有助于您维护您的关注点分离 (SoC) 设计模式。
结论
自第一个软件系统实施以来,人们就明白模块化对其至关重要。软件工程中最重要的原则之一是关注点分离 (SoC):将软件系统分解为功能重叠尽可能小的部分。本文演示了实现这一原则的各种技术,用于 Web 应用程序开发。最终,您将获得一个架构,该架构允许不同的人独立地处理系统的各个部分,一个有助于系统可重用性和可维护性并允许轻松添加新功能的架构。也许更重要的是,它使每个人都能更好地理解系统。本文包含的技术
Visual Studio 2013 Express Preview for WebMicrosoft SQL-Server Express 2012
Microsoft .NET Framework 4.5.1
Microsoft Entity Framework 6.0 Beta
Microsoft ASP.NET MVC 5
Twitter Bootstrap
Microsoft WebAPI 2
KnockoutJS
Ninject
NUnit
Block UI
ToastrJS