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

使用 ASP.NET MVC、WCF 和 LINQ 进行 N 层开发

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.45/5 (31投票s)

2009年1月5日

CPOL

6分钟阅读

viewsIcon

249227

downloadIcon

11001

本文介绍了一个基于 ASP.NET MVC、WCF 和 LINQ to SQL 构建的演示性 Bug 跟踪应用程序。

引言

本文介绍了一种使用 **WCF**(Windows Communication Foundation,.NET Remoting 的替代品)和 **LINQ to SQL** 实现 ASP.NET MVC(Model View Controller)的 **N 层**架构的可能方法。本文不会深入解释 MVC、WCF 和 LINQ 的细节(这些技术已经有很多资料介绍了);相反,我将通过一个简单的 Bug 跟踪演示应用程序,展示在 N 层环境中实现这些功能的一种可能方式。

Bug 跟踪演示应用程序的要求

首先,我们应该说明演示应用程序的要求。正如下面所提到的,这些要求非常直接:

  • 我们希望应用程序能够生成当前已添加的 Bug 跟踪工单列表。
  • 我们希望用户能够将工单添加到系统中,包括添加工单日期、选择要分配工单的用户、添加工单日期,并让用户添加一些评论。
  • 在将新工单添加到系统之前,我们希望确保用户已提供有效的日期、选择了工单类型和用户,并为工单添加了评论。

架构概览

architecture.jpg

.NET 解决方案包含三个主要项目:

  • **BtDataLayer**:包含模型类和数据访问例程。
  • **BtWcfService**:包含表示层可以调用的服务。
  • **BugTracking**:包含表示层(ASP.NET MVC 应用程序)。

上面图中未提及的另一个项目是 **CommonLib**,它包含常用的例程(例如,每个模型类都继承的业务基类、对象克隆方法以及业务规则验证器引擎)。

Using the Code

数据访问层(BtDataLayer)

  • 模型类(BugTracker.dbml
  • bugtrackrelations.jpg

    这些是 SQL Server Bugtracker 数据库(Zip 文件中包含数据库模板)中检索到的 LINQ TO SQL 类。由于此模型应该是可远程访问的,因此数据上下文的序列化模式应设置为 **单向**。

  • 数据访问代码(btDataManger.cs
  • 我们希望能够加载用户列表,以便我们可以选择一个用户来处理工单。

    public static List<user> GetUserList()
    {
       // Create the DataContext to retrieve the Users from.
       BugTrackerDataContext db = new BugTrackerDataContext();
    
       // Return a list of Users.
       return db.Users.ToList<user>();
    }

    我们希望能够加载 `tickettype` 列表,以便我们可以选择工单的类型(BUG、INFO 等)。

    public static List<tickettype> GetTicketTypeList()
    {
       // Create the DataContext to retrieve the Ticket Types from.
       BugTrackerDataContext db = new BugTrackerDataContext();
    
       // Return a list of TicketTypes.
       return db.TicketTypes.ToList<tickettype>();
    }

    我们希望能够检索我们的工单表中的所有当前工单。

    public static IEnumerable<ticket> GetTickets()
    {
       // Create the DataContainer to retrieve the tickets.
       BugTrackerDataContext db = new BugTrackerDataContext();
                
       // Return a List of Tickets
       return db.Tickets;
    }

    最后,我们希望能够持久化新添加的工单(对于演示应用程序,一次一个,但我们将方法设计为通用的,因此它可以接受许多添加或修改的对象)。

    public static bool PersistTickets(ref TicketList p_tickets)
    {
        // Only persist if any tickets to add or modify.
        if (p_tickets == null || p_tickets.Count == 0)
            return false;
    
        // Create the persistence datacontext
        BugTrackerDataContext db = new BugTrackerDataContext();
    
        // Persist any new or modified tickets.
        foreach (Ticket t in p_tickets)
        {
            if (t.TicketId == 0)
            {
                db.Tickets.InsertOnSubmit(t);
    
            }
            else
            {
                db.Tickets.Attach(t, t.IsDirty);
            }
        }
    
        try
        {
            db.SubmitChanges(ConflictMode.ContinueOnConflict);
    
            // Reset the isDirty flag.
            foreach (Ticket t in p_tickets)
            {
                t.IsDirty = false;
            }
    
        }
        catch (ChangeConflictException ex)
        {
            throw ex;
        }
    
        return true;
    }

服务层

我们的 WCF 启用的服务层的目的是使模型和数据访问例程(用于为模型提供数据)可供表示层使用。

我们的服务层主要包含两个例程:

  • 服务契约(包含 Web 应用程序可以调用的例程)。
  • [ServiceContract]
    public interface IBtService
    {
        [OperationContract()]
        IEnumerable<user> GetUserList();
    
        [OperationContract()]
        IEnumerable<tickettype> GetTicketTypeList();
    
        [OperationContract()]
        bool PersistTickets(ref TicketList p_tickets);
    
        [OperationContract()]
        IEnumerable<ticket> GetTickets();
    }
  • 服务实现,它实现了我们的契约并调用数据层。
  • public IEnumerable<user> GetUserList()
    {
        return BtDataManager.GetUserList();
    }
    
    public IEnumerable<tickettype> GetTicketTypeList()
    {
        return BtDataManager.GetTicketTypeList();
    }
    
    public bool PersistTickets(ref TicketList p_tickets)
    {
        return BtDataManager.PersistTickets(ref p_tickets);
    }
    
    public IEnumerable<ticket> GetTickets()
    {
        return BtDataManager.GetTickets();
    }

表示层

表示层包含我们的 ASP MVC 应用程序。如前所述,我不会深入介绍 MVC,因为网上已经有很多相关信息。但重要的是要了解 MVC 包含三个主要部分,即**模型**(代表我们的类、验证逻辑,并通过服务层远程访问)、**视图**(我们的网页)和**控制器**(基本上将模型“钩”到视图)。

视图和控制器

我们的演示应用程序托管以下支持 MVC 的网页:

Ticket.aspx

ticket.jpg

用户可以使用此页面将 Bug 工单添加到系统中。**HomeController** 负责呈现页面。与页面交互时,涉及两个操作:一个用于创建新工单(“GET”版本),另一个用于持久化新工单(当用户单击“提交”按钮时:POST 版本)。

[AcceptVerbs("GET")]
public ActionResult Ticket()
{
    this.LoadSelectionLists();

    // Get the userlist
    var userList = new SelectList(_userList, "UserId", "Username");
    ViewData["UserId"] = userList;

    // Get the tickettypelist
    var ticketTypeList = new SelectList 
    (_ticketTypeList, "TicketTypeId", "TicketTypeDescription");
    ViewData["TicketTypeId"] = ticketTypeList;

    return View();
}

当用户在索引页面上选择“创建工单”时,我们必须创建一个新页面,用户可以在其中输入工单详细信息。由于用户应该能够为工单选择用户和工单类型,因此这些数据将从服务加载并添加到表单的 ViewState 中。

private void LoadSelectionLists()
{
    // Get a proxy to the data service provider
    BtServiceRef.BtServiceClient proxy = new   
    BugTracking.BtServiceRef.BtServiceClient();

    // Get the userlist
    _userList = proxy.GetUserList();
    
    // Get the tickettypelist
    _ticketTypeList = proxy.GetTicketTypeList();
   
}

另一方面,当用户单击“提交”按钮时,将执行 POST 版本。我们将创建一个工单对象,将属性映射到表单字段属性,验证对象数据(有关详细验证信息,请参阅模型源代码,因为验证数据存储在模型类中),并通过服务将新添加的工单持久化到数据库。

[AcceptVerbs("POST")]
public ActionResult Ticket(FormCollection p_form)
{
    // Create placeholder for the ticket to add.
    var TicketToCreate = new Ticket();

    
    try
    {
        // Map the controls of the View to the PlaceHolder.
        this.UpdateModel(TicketToCreate, new[]  
        { "UserId", "TicketTypeId", "Comment", "GrantedToDate" });
        
        // Force Validation
        TicketToCreate.Validate();

        // First check if any validation Errors Exist on the newly 
           added ticket
        if (TicketToCreate.HasErrors)
        {
            
            // View Validation Errors

            foreach(DictionaryEntry entry in  
            TicketToCreate.ValidationErrors)
            {

                ViewData.ModelState.AddModelError(entry.Key.ToString
                (), entry.Value.ToString());
            }
            
            throw new InvalidOperationException();
        }
        else
        {
            // Create the WCF Remote service and let the service 
               persist the new ticket.
            BtServiceRef.BtServiceClient proxy = new 
            BugTracking.BtServiceRef.BtServiceClient();
            Ticket[] newTicketList = new Ticket[] { TicketToCreate };
            proxy.PersistTickets(ref newTicketList);
        }
       
    }
    catch
    {
        this.LoadSelectionLists();

        var userList = new SelectList(_userList, "UserId", "Username");
        ViewData["UserId"] = userList;

        var ticketTypeList = new SelectList  
        (_ticketTypeList, "TicketTypeId", "TicketTypeDescription");
        ViewData["TicketTypeId"] = ticketTypeList;


        // Show possible validation errors.
        return View(TicketToCreate);
    }
    
    // Return to main page.
    return RedirectToAction("Index");
}

ticket.aspx 表单上有两个组合框:一个用于用户选择,一个用于工单类型选择。如上面的代码所示,我们从服务加载用户和工单类型记录,并将它们添加为 ticket 页面的 ViewData。接下来,我们必须按如下方式绑定 ViewData(您还可以注意到我们如何执行绑定的目的是为了验证)。

dropdown.jpg

Index.aspx

在浏览器中加载应用程序时,索引页面是第一个显示的页面。我们可以获取数据库中当前工单的列表,并可以选择向系统中添加新工单。

indexpage.jpg

同样,**HomeController** 负责呈现页面,涉及的操作是:

public ActionResult Index()
{
    ViewData["Title"] = "Home Page";
    ViewData["Message"] = "Welcome to ASP.NET MVC Bugtracker !";

    // Create a proxy to the WCF service.
    BtServiceRef.BtServiceClient proxy = 
       new BugTracking.BtServiceRef.BtServiceClient();

    // Get all tickets from the DataLayer.
    return View(proxy.GetTickets());
}

由于我们将在该页面上显示当前工单,因此我们首先创建一个对我们服务的引用。此服务调用我们数据访问类中的 `GetTickets()` 方法,并将其作为视图的输入参数添加。接下来,为了确保我们的索引页面知道加载的工单列表,`BtDataLayer.Ticket` 类型应作为属性添加到派生的 **ViewPage** 中。

indexpage_classdef.jpg

最后,在我们的index.aspx 页面的 HTML 代码中,我们可以遍历加载的工单列表,并将每个工单放入数据行中。

tickets_in_table.jpg

您可能会注意到,我显示的是 `UserId` 和 `TickeTypeId` 而不是用户名和工单类型描述。如果您想显示用户名而不是 ID,您应该使用:`t.User.UserName`,但这会返回一个错误,因为用户未随工单一起加载。您可以通过修改数据访问方法并添加用户和工单类型的加载选项来更改这一点。在这种情况下,相关的用户和工单类型对象也将被加载。

结语

好了,这就是全部内容。本文展示了一种分离逻辑层并通过 WCF 和 LINQ-to-SQL 等新技术在边界之间传递数据的可能方法。最后,它展示了一种使用最新 ASP.NET MVC 技术实现的可能表示层。特别感谢 ASP.NET 网站 www.asp.net 的运行人员,以及 Beth Massi:http://blogs.msdn.com/bethmassi/,她以结构清晰、视图明确的方式解释了 LINQ-to-SQL 和通过 WCF 进行的 N 层开发。

© . All rights reserved.