65.9K
CodeProject 正在变化。 阅读更多。
Home

客户发票系统/在ASP.NET MVC5和C#中使用访问者设计模式

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.48/5 (9投票s)

2015 年 6 月 7 日

CPOL

9分钟阅读

viewsIcon

72174

downloadIcon

3643

本文向您展示了如何在实际软件场景中应用访问者设计模式。本文还涵盖了ASP.NET MVC-5、实体框架和C#语言的各种功能。

Visitor Demo Screenshot

背景

最近我在这里发表了一篇关于`观察者`设计模式的文章。我的目的是展示`观察者`设计模式在实际场景中的应用。对此文章感兴趣的读者可以在这里阅读:实时股票行情仪表板(使用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`。下面给出了详细解释。

Class Diagram of Customer Invoicing System

图 1a. 客户发票系统的类图

从上面的类图可以看出,Invoice是一个概念类,负责定义发票的结构。它实现了InvoiceElement接口,并维护了所有发票组件的列表。本例中的发票包含三个关键的InvoiceElement,即HeaderElementOrderElementFooterElement。这些类都实现了InvoiceElement接口。InvoiceElement接口公开了一个接受IVisitor作为参数的accept()方法。所有具体的访问者类都必须实现此接口。此方法允许通过实现不同类型的访问者来扩展原始类(在本例中为Invoice)的功能,而无需修改这些类。因此,Invoice类通过将其扩展功能的实现委托给不同类型的访问者,从而明显地简化了其功能。

IVisitor接口公开了visit()方法,并且有几个重载版本,以便它可以接受不同类型的InvoiceElement作为其参数。因此,访问者的具体实现可以根据传递给它的特定类型的InvoiceElement进行操作并采取适当的行动。在本例中,我们有两个具体的访问者类,即InvoiceControllerSaveInvoiceInXML。我使用InvoiceController作为其中一个具体访问者,因为我想生成发票并将其呈现在HTML中。它具有所有合适的引用和基础结构,可以与ViewPartial views进行交互。我将在后面的部分详细介绍实现。SaveInvoiceInXML是一个负责以XML格式保存Invoice及其组件的访问者。

图 1b 显示了客户发票系统的另一个类图。这包含关键的控制器类 CustomerControllerOrderController 和业务层类以及其他辅助类。这些将在下面解释。

Class Diagram of Customer Invoicing System

图 1b. 客户发票系统的类图

CustomerController 是一个控制器类,负责与 Customer 视图的交互。它还通过使用通用自定义类 PagedList<T>PagedList<Customer> 列表实现分页。此类能够封装任何引用类型实体的列表,并公开实现分页功能所需的关键方法和属性。OrderController 类负责与 Order 视图的交互。它还通过使用 PagedList<Order> 实现分页。BONorthWindFacade 类充当门面,在控制器类和数据层之间提供统一接口。它公开了两个关键方法 GetCustomers()GetOrdersForTheCustomer(string customerID)。数据层已使用 Entity Framework 实现,NorthWindEntities 是一个关键类,它提供数据上下文并访问 CustomerOrderProductOrder_Details 等关键实体。

业务层

业务层由关键领域实体组成,例如CustomerOrderProductBONorthwindFacade是一个充当门面的类,为与数据层和控制器的交互提供统一接口。下一节将解释数据层,Invoice类、访问者类和其他功能特性的详细实现将在后面的章节中介绍。

数据层

数据层使用实体框架实现。NorthwindEntities是一个关键类,它充当数据上下文,并提供对所有关键实体(如CustomerOrderProductOrder_Details)的访问。实体图如图2所示。所有数据层类都使用Visual Studio自动生成。有关更多详细信息,请参阅源代码。

Class Diagram of Customer Invoicing System

图 2. 客户发票系统的 ER 图

访问者的实现

由于本文旨在解释访问者设计模式的应用,我将首先详细介绍InvoiceInvoiceControllerSaveInvoiceInXML这些类的实现。

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(),它反过来调用之前初始化的所有InvoiceElementaccept()方法。请注意lambda表达式的使用。有关HeaderElementOrderElementFooterElement实现细节的更多信息,请参阅源代码。

实现 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所示。

Sample Invoice

图 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格式。

Sample Invoice XML

图 4. 示例发票 XML

其他控制器和 PagedList<T> 类的实现

CustomerControllerOrderController 类负责显示 CustomerOrder 视图。这两个视图都实现了分页功能,这是通过自定义的 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>类的构造函数接受任何泛型列表,其中包含引用类型的集合,您还可以指定页面大小。基于此,它计算NoOfPagesGetListFromPage()方法根据页索引返回集合的子集。请注意使用System.Linq.Enumerable类提供的Skip()Take()扩展方法。

CustomerController 和 OrderController 类的实现

有关这些类的详细实现,请参阅源代码。

关注点

如果您对Bootstrap CSS感兴趣,可以访问他们的网站。此外,下面我还分享了一些您可能感兴趣的链接。

对其他设计模式感兴趣的读者可以使用以下链接获取更多详细信息。

结论

设计模式为软件开发中遇到的重复性问题提供了解决方案。`访问者`是一种流行的设计模式,我希望本文能让您深入了解这种模式的工作原理及其在实际场景中的应用。希望您喜欢阅读这篇文章。如果您有任何疑问或需要更多信息,可以发送电子邮件给我。谢谢。

© . All rights reserved.