客户发票系统/在ASP.NET MVC5和C#中使用访问者设计模式
本文向您展示了如何在实际软件场景中应用访问者设计模式。本文还涵盖了ASP.NET MVC-5、实体框架和C#语言的各种功能。
背景
最近我在这里发表了一篇关于`观察者`设计模式的文章。我的目的是展示`观察者`设计模式在实际场景中的应用。对此文章感兴趣的读者可以在这里阅读:实时股票行情仪表板(使用WPF和C#应用观察者设计模式)
在本文中,我将探索另一个流行的设计模式`访问者`并演示其实际应用。我为此开发的应用程序基于`Northwind`数据库。它是一个针对虚构公司`ABC LTD`的客户发票应用程序,用于跟踪客户列表及其订单。它还可以帮助您为任何订单生成发票,并将发票保存为XML格式以供将来参考。此应用程序使用asp.net MVC开发。我使用Visual Studio 2013开发此应用程序。
访问者设计模式是一种将算法与其操作的对象结构分离的方法。这种分离的一个实际结果是能够在不修改现有对象结构的情况下向其添加新操作。它是遵循开闭原则的一种方式。有关此设计模式的更多详细信息,请参阅此链接。
引言
本文的目的是演示访问者设计模式在实际场景中的应用。在本文中,我以客户发票系统为例,您可以跟踪客户列表及其订单。由于这是一个发票系统,您可以为任何订单生成发票并将其发送给客户。该应用程序还允许您将发票保存为XML格式。在此应用程序中,我使用了两种类型的访问者,如下所示
- 用于生成发票的访问者
- 用于将发票保存为XML格式的访问者
本文将详细介绍使用ASP.NET MVC和C#实现此模式。我还将探讨其他功能,例如
- 实现通用
PagedList<T>
类以实现分页 - Bootstrap.CSS 的功能
- 实体框架
- Lambda 表达式
设计概述
我准备了两张类图来解释此应用程序中的关键类及其作用。一张图会显得混乱,因此我根据它们的职责对其进行了逻辑分组。图1a显示了第一张类图,其中包含与发票生成和以XML格式保存发票相关的关键类。定义了两个关键接口`InvoiceElement`和`IVisitor`。下面给出了详细解释。
图 1a. 客户发票系统的类图
从上面的类图可以看出,Invoice
是一个概念类,负责定义发票的结构。它实现了InvoiceElement
接口,并维护了所有发票组件的列表。本例中的发票包含三个关键的InvoiceElement
,即HeaderElement
、OrderElement
和FooterElement
。这些类都实现了InvoiceElement
接口。InvoiceElement
接口公开了一个接受IVisitor
作为参数的accept()
方法。所有具体的访问者类都必须实现此接口。此方法允许通过实现不同类型的访问者来扩展原始类(在本例中为Invoice
)的功能,而无需修改这些类。因此,Invoice
类通过将其扩展功能的实现委托给不同类型的访问者,从而明显地简化了其功能。
IVisitor
接口公开了visit()
方法,并且有几个重载版本,以便它可以接受不同类型的InvoiceElement
作为其参数。因此,访问者的具体实现可以根据传递给它的特定类型的InvoiceElement
进行操作并采取适当的行动。在本例中,我们有两个具体的访问者类,即InvoiceController
和SaveInvoiceInXML
。我使用InvoiceController
作为其中一个具体访问者,因为我想生成发票并将其呈现在HTML中。它具有所有合适的引用和基础结构,可以与View
和Partial views
进行交互。我将在后面的部分详细介绍实现。SaveInvoiceInXML
是一个负责以XML格式保存Invoice
及其组件的访问者。
图 1b 显示了客户发票系统的另一个类图。这包含关键的控制器类 CustomerController
、OrderController
和业务层类以及其他辅助类。这些将在下面解释。
图 1b. 客户发票系统的类图
CustomerController
是一个控制器类,负责与 Customer
视图的交互。它还通过使用通用自定义类 PagedList<T>
的 PagedList<Customer>
列表实现分页。此类能够封装任何引用类型实体的列表,并公开实现分页功能所需的关键方法和属性。OrderController
类负责与 Order
视图的交互。它还通过使用 PagedList<Order>
实现分页。BONorthWindFacade
类充当门面,在控制器类和数据层之间提供统一接口。它公开了两个关键方法 GetCustomers()
和 GetOrdersForTheCustomer(string customerID)
。数据层已使用 Entity Framework 实现,NorthWindEntities
是一个关键类,它提供数据上下文并访问 Customer
、Order
、Product
和 Order_Details
等关键实体。
业务层
业务层由关键领域实体组成,例如Customer
、Order
和Product
。BONorthwindFacade
是一个充当门面的类,为与数据层和控制器的交互提供统一接口。下一节将解释数据层,Invoice
类、访问者类和其他功能特性的详细实现将在后面的章节中介绍。
数据层
数据层使用实体框架实现。NorthwindEntities
是一个关键类,它充当数据上下文,并提供对所有关键实体(如Customer
、Order
、Product
、Order_Details
)的访问。实体图如图2所示。所有数据层类都使用Visual Studio自动生成。有关更多详细信息,请参阅源代码。
图 2. 客户发票系统的 ER 图
访问者的实现
由于本文旨在解释访问者设计模式的应用,我将首先详细介绍Invoice
、InvoiceController
和SaveInvoiceInXML
这些类的实现。
Invoice 类的实现
Invoice
类是一个概念类,它保存了发票的结构及其所有组件。该类实现了InvoiceElement
接口,并具有以下关键方法。
initInvoiceElements()
accept(IVisitor visitor)。
下面的代码展示了该类的详细信息。
public class Invoice:InvoiceElement { private List <InvoiceElement> _invoiceElements; public Invoice(Order o){ InitInvoice(o); } private HeaderVM InitHeader(){ HeaderVM vm = new HeaderVM(); vm.Name = "ABC PLC"; vm.Address1="121,J.B.Road"; vm.Address2 = "Fleet Street"; vm.City = "London"; vm.Zip = "LS0 5TQ"; vm.Country = "UK"; vm.Email = "sales@abcplcltd.com"; vm.Website = "http://www.abcplc.co.uk"; return vm; } private void InitInvoice(Order o) { _invoiceElements = new List<InvoiceElementgt;(); HeaderElement header= new HeaderElement(InitHeader()); _invoiceElements.Add(header); OrderElement orderElemt = new OrderElement(o); _invoiceElements.Add(orderElemt); FooterElement footElement = new FooterElement("Thank you for doing Business with us!"); _invoiceElements.Add(footElement); } // Implementation of InvoiceElement Interface. public void accept(IVisitor visitor) { _invoiceElements.ForEach(ie => ie.accept(visitor)); } }
从上面的代码中可以看出,Invoice
类维护了其他InvoiceElement
组件的列表。它的构造函数接受Order
实体的实例作为参数,然后在构造函数内部调用InitInvoice()
方法。此方法初始化所有发票组件并将它们添加到列表中。主要方法是accept()
,它反过来调用之前初始化的所有InvoiceElement
的accept()
方法。请注意lambda表达式的使用。有关HeaderElement
、OrderElement
和FooterElement
实现细节的更多信息,请参阅源代码。
实现 InvoiceController 访问者
InvoiceController
类是一个具体的访问者,负责生成发票。由于我们以HTML
格式生成发票,并且控制器类具有与视图交互的所有合适基础结构,因此我通过实现IVisitor
接口扩展了此类。此类维护HTML
字符串列表。这些HTML从Partial Views
返回,并通过visit()
方法操作不同的InvoiceElement
组件进行渲染。请参阅下面的代码了解详细信息。
public class InvoiceController : Controller,IVisitor { // // Define data context private NORTHWNDEntities _dbContext = new NORTHWNDEntities(); // list of all htmlsection private List<string> _htmlsectionList = new List<string>(); /// <summary> /// Method:Index /// Purpose:Returns the Invoice view /// </summary> /// <param name="id"></param> /// <returns></returns> public ActionResult Index(int id=0) { Order od = _dbContext.Orders.Find(id); if (od == null) return HttpNotFound(); Invoice newInvoice = new Invoice(od); ViewBag.CustomerID = od.CustomerID; newInvoice.accept(this); ViewBag.OrderID = id; return View(_htmlsectionList); } public ActionResult BackToOrder(string id, int page = 1) { return RedirectToAction("Index", "Order", new { id = id, page = page }); } /// <summary> /// Method:Save /// Purpose:Handler to save the Invoice in XML format /// </summary> /// <param name="id"></param> /// <returns></returns> public ActionResult Save(int id=0) { Order od = _dbContext.Orders.Find(id); if (od == null) return HttpNotFound(); Invoice newInvoice = new Invoice(od); SaveInvoiceInXML saveVisitor = new SaveInvoiceInXML(); newInvoice.accept(saveVisitor); string filePath = string.Format("{0}Order_{1}_{2}.xml", Request.PhysicalApplicationPath, od.CustomerID, od.OrderID); try { if(System.IO.File.Exists(filePath)) { System.IO.File.Delete(filePath); } saveVisitor.InvoiceRoot.Save(filePath); } catch (Exception ex) { throw ex; } return View(od); } public void visit(HeaderElement headerElement) { _htmlsectionList.Add(RenderRazorViewToString("_Header", headerElement.Header)); } public void visit(OrderElement orderElement) { _htmlsectionList.Add(RenderRazorViewToString("_Order", orderElement.CurrentOrder)); } public void visit(FooterElement footerElement) { _htmlsectionList.Add(RenderRazorViewToString("_Footer", footerElement)); } public void visit(InvoiceElement invoiceElement) { //Do nothing... //throw new NotImplementedException(); } public string RenderRazorViewToString(string viewName, object model) { ViewData.Model = model; using (var sw = new StringWriter()) { var viewResult = ViewEngines.Engines.FindPartialView(ControllerContext, viewName); var viewContext = new ViewContext(ControllerContext, viewResult.View, ViewData, TempData, sw); viewResult.View.Render(viewContext, sw); viewResult.ViewEngine.ReleaseView(ControllerContext, viewResult.View); return sw.GetStringBuilder().ToString(); } } }
从上面的代码可以看出,每个版本的visit()
方法都作用于特定的InvoiceElement
,并从局部视图返回相应的Html
标记,并将其添加到_htmlSectionList
集合中。RenderRazorViewToString()
方法是一个重要的方法,因为它将PartialViewResult
转换为html字符串。Index()
方法简单但有趣。它首先根据传入的id
查找Order
。然后它创建Invoice
类的实例,并调用其accept()
方法。正是这个方法反过来调用相应的visit()
方法来填充_htmlSectionList
集合。最后,它通过将_htmlSectionList
集合作为模型传递给Index
视图来返回Invoice
视图。此视图的实现非常简单,因为它通过_htmlSectionList
集合渲染所有传递给它的partial views
。索引视图的代码如下所示。
@model List<string> @{ ViewBag.Title = "Invoice"; } <div class="container"> @foreach (var item in Model) { @Html.Raw(item); } <div id="footer"> <ul class="nav nav-pills"> <li> @Html.ActionLink("Back To Orders", "BackToOrder", new { id = ViewBag.CustomerID, page = 1 }) </li> <li> @Html.ActionLink("Save", "Save", new { id = ViewBag.OrderID }) </li> </ul> </div>
上面的代码很简单。它所做的就是使用Html.Raw()
方法渲染HTML。其余代码用于显示链接及其关联的处理程序。请参阅Bootstrap.CSS
的使用。示例Invoice
输出如图3所示。
图 3. 示例发票
SaveInvoiceInXML 访问者的实现
SaveInvoiceInXML
是另一个用于以XML格式保存发票的访问者。它的visit()
方法作用于各自的InvoiceElement
并将其内容保存为XML格式。您需要确保保存XML文件的文件夹具有读/写权限。下面的代码显示了该类的详细信息。
public class SaveInvoiceInXML:IVisitor { private XElement _root = new XElement("invoice"); public void visit(InvoiceElement invoiceElement) { //throw new NotImplementedException(); } /// <summary> /// Method:visit /// Purpose:save headerElement in XML format. /// </summary> /// <param name="headerElement"></param> public void visit(HeaderElement headerElement) { _root.Add(new XElement("header", new XElement("company",headerElement.Header.Name), new XElement("address1",headerElement.Header.Address1), new XElement("address2",headerElement.Header.Address1), new XElement("city",headerElement.Header.City), new XElement("country",headerElement.Header.Country), new XElement("zip",headerElement.Header.Zip), new XElement("email",headerElement.Header.Email), new XElement("website",headerElement.Header.Website) )); } /// <summary> /// Method:visit /// purpose:Saves the order content in XML format. /// </summary> /// <param name="orderElement"></param> public void visit(OrderElement orderElement) { // Add order and orderElement Decimal orderAmount = orderElement.CurrentOrder.Order_Details.Sum(odet=>(odet.UnitPrice*odet.Quantity)); Decimal taxAmount = 0.20M*orderAmount; orderAmount +=taxAmount; _root.Add( new XElement("order", new XAttribute("orderid",orderElement.CurrentOrder.OrderID), new XElement("orderdate",string.Format("{0:d}",orderElement.CurrentOrder.OrderDate)), new XElement("shipping_details", new XElement("contact_person",orderElement.CurrentOrder.Customer.ContactName), new XElement("company",orderElement.CurrentOrder.Customer.CompanyName), new XElement("address",orderElement.CurrentOrder.ShipAddress), new XElement("city",orderElement.CurrentOrder.ShipCity), new XElement("zip",orderElement.CurrentOrder.ShipPostalCode), new XElement("country",orderElement.CurrentOrder.ShipCountry) ), new XElement("orderdetails", from odet in orderElement.CurrentOrder.Order_Details select new XElement("order_detail", new XElement("productid", odet.ProductID), new XElement("description",odet.Product.ProductName), new XElement("unitprice",odet.UnitPrice), new XElement("quantity",odet.Quantity) ) ), new XElement("vat",string.Format("{0:0.00}",taxAmount)), new XElement("totalamount",string.Format("{0:0.00}",orderAmount)) )); } /// <summary> /// Method:visit /// Purpose:Saves footer. /// </summary> /// <param name="footerElement"></param> public void visit(FooterElement footerElement) { _root.Add(new XElement("footer",footerElement.Footer)); } public XElement InvoiceRoot { get { return _root; } } }
从上述代码中可以看到,_root
变量是XML的根,类型为XElement
。每个visit()
方法随后将每种类型的InvoiceElement
的内容添加到此根元素。InvoiceController
类中的Save()
处理方法初始化此访问者,并将其传递给Invoice
类的accept()
方法,该方法完成填充XML的工作。图4显示了发票的XML格式。
图 4. 示例发票 XML
其他控制器和 PagedList<T> 类的实现
CustomerController
和 OrderController
类负责显示 Customer
和 Order
视图。这两个视图都实现了分页功能,这是通过自定义的 PagedList<T>
类实现的。我决定从头开始构建此功能,而不是使用任何第三方控件。此类的功能以及 Bootstrap.CSS
的一些分页功能简化了此功能的实现。
PagedList<T> 的实现
PagedList<T>
是一个泛型类,它包含任何引用类型的原始集合,并公开分页所需的一些属性和方法。它公开了以下关键方法和属性。
getListFromPage(int page)
NoOfPages
CurrentPage
此类的实现很简单,代码如下所示。
/// <summary> /// Generic PagedList class /// </summary> /// <typeparam name="T"></typeparam> public class PagedList<T> where T:class { private List<T> _orgList; private int _noPages; private int _pageSize; private int _minPage = 1; private int _lastPage = 1; public PagedList (List<T> tlist,int pageSize){ _orgList =tlist; _pageSize = pageSize; if (tlist.Count % _pageSize == 0) { _noPages = tlist.Count / _pageSize; } else { _noPages = tlist.Count / _pageSize + 1; } } public int CurrentPage { get { return _lastPage; } set { _lastPage = value; } } public int NoPages { get { return _noPages; } } /// <summary> /// Method:GetListFromPage /// Purpose:Returns subset based on page index. /// </summary> /// <param name="pageNo"></param> /// <returns></returns> public List<T> GetListFromPage(int pageNo) { int pageCount = _pageSize; if (pageNo < _minPage) { pageNo = _minPage; } if (pageNo >= _noPages) { pageNo = _noPages; pageCount = _orgList.Count - (_pageSize * (pageNo - 1)); } _lastPage = pageNo; return _orgList.Skip(_pageSize * (pageNo - 1)).Take(pageCount).ToList<T>(); } }
从上面的代码中可以看出,PagedList<T>
类的构造函数接受任何泛型列表,其中包含引用类型的集合,您还可以指定页面大小。基于此,它计算NoOfPages
。GetListFromPage()
方法根据页索引返回集合的子集。请注意使用System.Linq.Enumerable
类提供的Skip()
和Take()
扩展方法。
CustomerController 和 OrderController 类的实现
有关这些类的详细实现,请参阅源代码。
关注点
如果您对Bootstrap
CSS感兴趣,可以访问他们的网站。此外,下面我还分享了一些您可能感兴趣的链接。
对其他设计模式感兴趣的读者可以使用以下链接获取更多详细信息。
结论
设计模式为软件开发中遇到的重复性问题提供了解决方案。`访问者`是一种流行的设计模式,我希望本文能让您深入了解这种模式的工作原理及其在实际场景中的应用。希望您喜欢阅读这篇文章。如果您有任何疑问或需要更多信息,可以发送电子邮件给我。谢谢。