创建业务逻辑层 (C#)

作者 :斯科特·米切尔

下载 PDF

本教程介绍如何将业务规则集中到业务逻辑层(BLL)中,该层充当表示层与 DAL 之间的数据交换的中介。

介绍

在第一个教程中创建的数据访问层(DAL)将数据访问逻辑与表示逻辑完全分离。 但是,虽然 DAL 将数据访问详细信息与呈现层完全分开,但它不会强制实施可能应用的任何业务规则。 例如,对于我们的应用程序,我们可能希望禁止当CategoryID字段设置为 1 时,SupplierID表的ProductsDiscontinued字段被修改,或者我们可能要强制执行资历规则,禁止出现员工由在他们之后被雇用的人管理的情况。 另一种常见方案是授权,也许只有特定角色中的用户可以删除产品或更改 UnitPrice 值。

本教程介绍如何将这些业务规则集中到业务逻辑层(BLL)中,该层充当表示层和 DAL 之间数据交换的中介。 在实际应用程序中,BLL 应作为单独的类库项目实现;但是,对于这些教程,我们将实现 BLL 作为文件夹中 App_Code 的一系列类,以便简化项目结构。 图 1 说明了表示层、BLL 和 DAL 之间的体系结构关系。

BLL 将表示层与数据访问层分开并实施业务规则

图 1:BLL 将表示层与数据访问层分开并实施业务规则

步骤 1:创建 BLL 类

我们的 BLL 将由四个类组成,其中一个用于 DAL 中的每个 TableAdapter;其中每个 BLL 类都有用于在 DAL 中检索、插入、更新和删除相应 TableAdapter 的方法,并应用相应的业务规则。

为了更清晰地分隔 DAL 和 BLL 相关的类,让我们在App_Code文件夹中创建两个子文件夹, DALBLL 只需右键单击 App_Code 解决方案资源管理器中的文件夹,然后选择“新建文件夹”。 创建这两个文件夹后,将第一个教程 DAL 中创建的 Typed DataSet 移动到子文件夹中。

接下来,在子文件夹中创建四个 BLL BLL 类文件。 为此,请右键单击 BLL 子文件夹,选择“添加新项”,然后选择“类”模板。 将四个ProductsBLL类命名为 ,CategoriesBLLSuppliersBLL以及EmployeesBLL

向 App_Code 文件夹添加四个新类

图 2:向文件夹添加四个新类App_Code

接下来,让我们为每个类添加方法以简单地封装第一个教程中为 TableAdapters 定义的方法。 目前,这些方法将直接调用 DAL;稍后我们将返回以添加任何所需的业务逻辑。

注释

如果你使用的是 Visual Studio Standard Edition 或更高版本(即 ,你未 使用 Visual Web Developer),则可以选择使用 类设计器直观地设计类。 有关 Visual Studio 中此新功能的详细信息,请参阅 类设计器博客

对于类 ProductsBLL ,我们需要添加总共七种方法:

  • GetProducts() 返回所有产品
  • GetProductByProductID(productID) 返回具有指定产品 ID 的产品
  • GetProductsByCategoryID(categoryID) 返回指定类别中的所有产品
  • GetProductsBySupplier(supplierID) 返回指定供应商的所有产品
  • AddProduct(productName, supplierID, categoryID, quantityPerUnit, unitPrice, unitsInStock, unitsOnOrder, reorderLevel, discontinued) 使用传入的值将新产品插入数据库中;返回 ProductID 新插入的记录的值
  • UpdateProduct(productName, supplierID, categoryID, quantityPerUnit, unitPrice, unitsInStock, unitsOnOrder, reorderLevel, discontinued, productID) 使用传入值更新数据库中的现有产品;如果恰好更新了一行,则返回 true,否则返回 false
  • DeleteProduct(productID) 从数据库中删除指定的产品

ProductsBLL.cs

using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using NorthwindTableAdapters;

[System.ComponentModel.DataObject]
public class ProductsBLL
{
    private ProductsTableAdapter _productsAdapter = null;
    protected ProductsTableAdapter Adapter
    {
        get {
            if (_productsAdapter == null)
                _productsAdapter = new ProductsTableAdapter();

            return _productsAdapter;
        }
    }

    [System.ComponentModel.DataObjectMethodAttribute
        (System.ComponentModel.DataObjectMethodType.Select, true)]
    public Northwind.ProductsDataTable GetProducts()
    {
        return Adapter.GetProducts();
    }

    [System.ComponentModel.DataObjectMethodAttribute
        (System.ComponentModel.DataObjectMethodType.Select, false)]
    public Northwind.ProductsDataTable GetProductByProductID(int productID)
    {
        return Adapter.GetProductByProductID(productID);
    }

    [System.ComponentModel.DataObjectMethodAttribute
        (System.ComponentModel.DataObjectMethodType.Select, false)]
    public Northwind.ProductsDataTable GetProductsByCategoryID(int categoryID)
    {
        return Adapter.GetProductsByCategoryID(categoryID);
    }

    [System.ComponentModel.DataObjectMethodAttribute
        (System.ComponentModel.DataObjectMethodType.Select, false)]
    public Northwind.ProductsDataTable GetProductsBySupplierID(int supplierID)
    {
        return Adapter.GetProductsBySupplierID(supplierID);
    }
    [System.ComponentModel.DataObjectMethodAttribute
        (System.ComponentModel.DataObjectMethodType.Insert, true)]
    public bool AddProduct(string productName, int? supplierID, int? categoryID,
        string quantityPerUnit, decimal? unitPrice,  short? unitsInStock,
        short? unitsOnOrder, short? reorderLevel, bool discontinued)
    {
        // Create a new ProductRow instance
        Northwind.ProductsDataTable products = new Northwind.ProductsDataTable();
        Northwind.ProductsRow product = products.NewProductsRow();

        product.ProductName = productName;
        if (supplierID == null) product.SetSupplierIDNull();
          else product.SupplierID = supplierID.Value;
        if (categoryID == null) product.SetCategoryIDNull();
          else product.CategoryID = categoryID.Value;
        if (quantityPerUnit == null) product.SetQuantityPerUnitNull();
          else product.QuantityPerUnit = quantityPerUnit;
        if (unitPrice == null) product.SetUnitPriceNull();
          else product.UnitPrice = unitPrice.Value;
        if (unitsInStock == null) product.SetUnitsInStockNull();
          else product.UnitsInStock = unitsInStock.Value;
        if (unitsOnOrder == null) product.SetUnitsOnOrderNull();
          else product.UnitsOnOrder = unitsOnOrder.Value;
        if (reorderLevel == null) product.SetReorderLevelNull();
          else product.ReorderLevel = reorderLevel.Value;
        product.Discontinued = discontinued;

        // Add the new product
        products.AddProductsRow(product);
        int rowsAffected = Adapter.Update(products);

        // Return true if precisely one row was inserted,
        // otherwise false
        return rowsAffected == 1;
    }

    [System.ComponentModel.DataObjectMethodAttribute
        (System.ComponentModel.DataObjectMethodType.Update, true)]
    public bool UpdateProduct(string productName, int? supplierID, int? categoryID,
        string quantityPerUnit, decimal? unitPrice, short? unitsInStock,
        short? unitsOnOrder, short? reorderLevel, bool discontinued, int productID)
    {
        Northwind.ProductsDataTable products = Adapter.GetProductByProductID(productID);
        if (products.Count == 0)
            // no matching record found, return false
            return false;

        Northwind.ProductsRow product = products[0];

        product.ProductName = productName;
        if (supplierID == null) product.SetSupplierIDNull();
          else product.SupplierID = supplierID.Value;
        if (categoryID == null) product.SetCategoryIDNull();
          else product.CategoryID = categoryID.Value;
        if (quantityPerUnit == null) product.SetQuantityPerUnitNull();
          else product.QuantityPerUnit = quantityPerUnit;
        if (unitPrice == null) product.SetUnitPriceNull();
          else product.UnitPrice = unitPrice.Value;
        if (unitsInStock == null) product.SetUnitsInStockNull();
          else product.UnitsInStock = unitsInStock.Value;
        if (unitsOnOrder == null) product.SetUnitsOnOrderNull();
          else product.UnitsOnOrder = unitsOnOrder.Value;
        if (reorderLevel == null) product.SetReorderLevelNull();
          else product.ReorderLevel = reorderLevel.Value;
        product.Discontinued = discontinued;

        // Update the product record
        int rowsAffected = Adapter.Update(product);

        // Return true if precisely one row was updated,
        // otherwise false
        return rowsAffected == 1;
    }

    [System.ComponentModel.DataObjectMethodAttribute
        (System.ComponentModel.DataObjectMethodType.Delete, true)]
    public bool DeleteProduct(int productID)
    {
        int rowsAffected = Adapter.Delete(productID);

        // Return true if precisely one row was deleted,
        // otherwise false
        return rowsAffected == 1;
    }
}

简单返回数据的方法 GetProductsGetProductByProductIDGetProductsByCategoryIDGetProductBySuppliersID 非常直接,因为它们只是调用 DAL。 虽然在某些情况下,可能需要在此级别实施业务规则(例如基于当前登录用户或用户所属角色的授权规则),但我们只需将这些方法保留为空as-is。 对于这些方法,BLL 仅充当代理,表示层通过代理从数据访问层访问基础数据。

AddProductUpdateProduct方法都将各种产品字段的值作为参数,分别用来添加新产品或更新现有产品。 由于许多Product表的列可以接受NULL值(例如CategoryIDSupplierIDUnitPrice),因此,映射到这些列的AddProductUpdateProduct输入参数使用可为空的类型。 可空类型是 .NET 2.0 的新增功能,它提供了一种技术,用于指示值类型是否可以为 null。 在 C# 中,可以通过在类型后添加 ? 将值类型(如 int? x;)标记为可为 null 的类型。 有关详细信息,请参阅 可空类型部分中的C# 编程指南

这三种方法都会返回一个布尔值,这个值指示一行是否被插入、更新或删除,因为操作可能不会导致任何行受影响。 ** 例如,如果页面开发人员调用 DeleteProduct 并传入 ProductID 一个不存在的产品,那么发往数据库的 DELETE 语句将没有影响,因此 DeleteProduct 方法将返回 false

请注意,在添加新产品或更新现有产品时,我们会将新或修改的产品字段值作为标量列表而不是接受 ProductsRow 实例。 之所以选择此方法, ProductsRow 是因为该类派生自 ADO.NET DataRow 类,该类没有默认无参数构造函数。 若要创建新 ProductsRow 实例,必须先创建一个 ProductsDataTable 实例,然后调用其 NewProductRow() 方法(我们在其中 AddProduct执行)。 当我们转向使用 ObjectDataSource 插入和更新产品时,这种缺陷就会显现出来。 简言之,ObjectDataSource 将尝试创建输入参数的实例。 如果 BLL 方法需要实例,ObjectDataSource 将尝试创建一个 ProductsRow 实例,但由于缺少默认无参数构造函数,因此失败。 有关此问题的详细信息,请参阅以下两个 ASP.NET 论坛文章: 使用 Strongly-Typed 数据集更新 ObjectDataSources,以及 ObjectDataSource 问题以及 Strongly-Typed 数据集

接下来,代码AddProductUpdateProduct将创建一个ProductsRow实例,并使用刚刚传入的值填充该实例。 在为 DataRow 的 DataColumns 分配值时,可能会进行各种字段级验证检查。 因此,手动将传入的值放回 DataRow 有助于确保将数据传递到 BLL 方法的有效性。 遗憾的是,Visual Studio 生成的强类型 DataRow 类不使用可为 null 的类型。 相反,若要指示 DataRow 中的特定 DataColumn 应对应于数据库中的 NULL 值,我们必须使用 SetColumnNameNull() 方法。

我们首先使用 UpdateProduct 加载要更新的产品 GetProductByProductID(productID)。 虽然这似乎是一次不必要的数据库访问,但这种额外的数据库访问在探索乐观并发专题未来的教程中将证明其价值。 乐观并发是一种技术,可确保同时处理同一数据的两个用户不会意外覆盖彼此的更改。 提取整个记录还使在 BLL 中更容易创建更新方法,这些方法仅修改 DataRow 的部分列。 当我们探索该 SuppliersBLL 类时,我们将看到这样的示例。

最后,请注意,该 ProductsBLL 类具有应用于它的 DataObject 属性 (在 [System.ComponentModel.DataObject] 文件顶部附近的类语句前的语法),并且方法具有 DataObjectMethodAttribute 属性。 该 DataObject 特性将类标记为适合绑定到 ObjectDataSource 控件的对象,而 DataObjectMethodAttribute 指示方法的用途。 正如我们在将来的教程中看到的那样,ASP.NET 2.0 的 ObjectDataSource 可以轻松以声明方式访问类中的数据。 为了帮助筛选在 ObjectDataSource 向导中可以绑定的类,默认情况下,只有那些标记为 DataObjects 的类会在向导的下拉列表中显示。 添加这些属性可以更方便地在 ObjectDataSource 向导中使用,但即使没有这些属性,ProductsBLL 类仍然可以同样有效运作。

添加其他课程

完成 ProductsBLL 类后,我们仍需要添加用于处理类别、供应商和员工的类。 请花点时间使用上述示例中的概念创建以下类和方法:

  • CategoriesBLL.cs

    • GetCategories()
    • GetCategoryByCategoryID(categoryID)
  • SuppliersBLL.cs

    • GetSuppliers()
    • GetSupplierBySupplierID(supplierID)
    • GetSuppliersByCountry(country)
    • UpdateSupplierAddress(supplierID, address, city, country)
  • EmployeesBLL.cs

    • GetEmployees()
    • GetEmployeeByEmployeeID(employeeID)
    • GetEmployeesByManager(managerID)

值得注意的一种方法是 SuppliersBLLUpdateSupplierAddress 的方法。 此方法提供一个接口,用于仅更新供应商的地址信息。 在内部,此方法为指定的 SupplierDataRow(使用 supplierID)读取 GetSupplierBySupplierID 对象,设置其与地址相关的属性,然后调用 SupplierDataTableUpdate 方法。 方法 UpdateSupplierAddress 如下:

[System.ComponentModel.DataObjectMethodAttribute
    (System.ComponentModel.DataObjectMethodType.Update, true)]
public bool UpdateSupplierAddress
    (int supplierID, string address, string city, string country)
{
    Northwind.SuppliersDataTable suppliers =
        Adapter.GetSupplierBySupplierID(supplierID);
    if (suppliers.Count == 0)
        // no matching record found, return false
        return false;
    else
    {
        Northwind.SuppliersRow supplier = suppliers[0];

        if (address == null) supplier.SetAddressNull();
          else supplier.Address = address;
        if (city == null) supplier.SetCityNull();
          else supplier.City = city;
        if (country == null) supplier.SetCountryNull();
          else supplier.Country = country;

        // Update the supplier Address-related information
        int rowsAffected = Adapter.Update(supplier);

        // Return true if precisely one row was updated,
        // otherwise false
        return rowsAffected == 1;
    }
}

有关 BLL 类的完整实现,请参阅本文的下载。

步骤 2:通过 BLL 类访问类型化数据集

在第一个教程中,我们看到了以编程的方式直接使用类型化数据集的示例,但通过添加 BLL 类,表示层应该针对 BLL 进行操作。 在第一个教程的 AllProducts.aspx 示例中,ProductsTableAdapter 被用于将产品列表绑定到 GridView,代码如下所示:

ProductsTableAdapter productsAdapter = new ProductsTableAdapter();
GridView1.DataSource = productsAdapter.GetProducts();
GridView1.DataBind();

若要使用新的 BLL 类,只需将代码的第一行中ProductsTableAdapter对象替换为ProductBLL对象即可。

ProductsBLL productLogic = new ProductsBLL();
GridView1.DataSource = productLogic.GetProducts();
GridView1.DataBind();

可以通过使用 ObjectDataSource 以声明方式访问 BLL 类和类型化数据集。 我们将在以下教程中更详细地讨论 ObjectDataSource。

产品列表显示在 GridView 中

图 3:产品列表显示在 GridView 中(单击以查看全尺寸图像

步骤 3:向 DataRow 类添加 Field-Level 验证

字段级校验是针对业务对象属性值的检查,在插入或更新时进行。 产品的一些字段级验证规则包括:

  • 字段 ProductName 长度必须为 40 个字符或更少
  • 字段 QuantityPerUnit 长度必须为 20 个字符或更少
  • ProductIDProductNameDiscontinued字段是必填的,但所有其他字段都是可选的
  • UnitPriceUnitsInStockUnitsOnOrderReorderLevel字段必须大于或等于零

这些规则可以在数据库级别表示,并且应该在数据库级别表示。 ProductNameQuantityPerUnit字段的字符限制由Products表中的列nvarchar(40)nvarchar(20)的数据类型分别确定。 通过数据库表列是否允许 NULL 来表示字段是必需的还是可选的。 存在四个检查约束,确保只有大于或等于零的值才能进入UnitPriceUnitsInStockUnitsOnOrderReorderLevel列。

除了在数据库中强制执行这些规则之外,还应在 DataSet 级别强制执行这些规则。 事实上,对于每个 DataTable 的 DataColumns 集,字段长度以及值是必需还是可选的状态都已经被记录下来。 若要查看自动提供的现有字段级验证,请转到数据集设计器,从其中一个数据表中选择一个字段,然后转到“属性”窗口。 如图 4 所示,QuantityPerUnit 中的 ProductsDataTable DataColumn 的最大长度为 20 个字符,并且允许 NULL 值。 如果我们尝试将 ProductsDataRowQuantityPerUnit 属性设置为一个长于 20 个字符的字符串值,则会引发 ArgumentException

DataColumn 提供基础的 Field-Level 验证

图 4:DataColumn 提供基本 Field-Level 验证(单击以查看全尺寸图像

遗憾的是,我们无法通过“属性”窗口指定边界检查,例如值 UnitPrice 必须大于或等于零。 为了提供这种类型的字段级验证,我们需要为 DataTable 的 ColumnChanging 事件创建事件处理程序。 如 前面的教程中所述,可以使用分部类扩展由类型化数据集创建的 DataSet、DataTable 和 DataRow 对象。 使用此技术,我们可以为ColumnChanging类创建ProductsDataTable事件处理程序。 首先在 App_Code 文件夹中创建一个名为 ProductsDataTable.ColumnChanging.cs 的类。

将新类添加到 App_Code 文件夹

图 5:向文件夹添加新类 App_Code单击以查看全尺寸图像

接下来,为ColumnChanging事件创建一个事件处理程序,确保UnitPriceUnitsInStockUnitsOnOrderReorderLevel列值(如果不是NULL)大于或等于零。 如果任何此类列超过范围,则引发一个 ArgumentException

ProductsDataTable.ColumnChanging.cs

public partial class Northwind
{
    public partial class ProductsDataTable
    {
        public override void BeginInit()
         {
            this.ColumnChanging += ValidateColumn;
         }

         void ValidateColumn(object sender,
           DataColumnChangeEventArgs e)
         {
            if(e.Column.Equals(this.UnitPriceColumn))
            {
               if(!Convert.IsDBNull(e.ProposedValue) &&
                  (decimal)e.ProposedValue < 0)
               {
                  throw new ArgumentException(
                      "UnitPrice cannot be less than zero", "UnitPrice");
               }
            }
            else if (e.Column.Equals(this.UnitsInStockColumn) ||
                     e.Column.Equals(this.UnitsOnOrderColumn) ||
                     e.Column.Equals(this.ReorderLevelColumn))
            {
                if (!Convert.IsDBNull(e.ProposedValue) &&
                    (short)e.ProposedValue < 0)
                {
                    throw new ArgumentException(string.Format(
                        "{0} cannot be less than zero", e.Column.ColumnName),
                        e.Column.ColumnName);
                }
            }
         }
    }
}

步骤 4:将自定义业务规则添加到业务逻辑层(BLL)的类中

除了字段级验证之外,还可能存在涉及不同实体或概念的高级自定义业务规则,这些规则在单列级别不可表达,例如:

  • 如果产品已停用,则无法更新该产品UnitPrice
  • 员工居住国必须与其经理的居住国相同
  • 如果产品是供应商提供的唯一产品,则不能停止产品

BLL 类应包含检查,以确保遵守应用程序的业务规则。 可以将这些检查直接添加到所适用的方法。

假设我们的业务规则规定,如果产品是给定供应商中唯一的产品,则无法将其标记为停产。 也就是说,如果产品 X 是我们从供应商 Y 购买的唯一产品,则无法将 X 标记为已停产:但是,如果供应商 Y 向我们提供了三个产品 ABC,那么我们可以将任何和所有这些产品标记为已停用。 奇怪的业务规则,但业务规则和常识并不总是一致的!

UpdateProducts方法中强制执行此业务规则时,我们会首先检查Discontinued是否被设置为true,如果是,则调用GetProductsBySupplierID以确定购买自该产品供应商的产品数量。 如果只从该供应商购买一个产品,我们会抛出一个 ApplicationException

public bool UpdateProduct(string productName, int? supplierID, int? categoryID,
    string quantityPerUnit, decimal? unitPrice, short? unitsInStock,
    short? unitsOnOrder, short? reorderLevel, bool discontinued, int productID)
{
    Northwind.ProductsDataTable products = Adapter.GetProductByProductID(productID);
    if (products.Count == 0)
        // no matching record found, return false
        return false;

    Northwind.ProductsRow product = products[0];

    // Business rule check - cannot discontinue
    // a product that is supplied by only
    // one supplier
    if (discontinued)
    {
        // Get the products we buy from this supplier
        Northwind.ProductsDataTable productsBySupplier =
            Adapter.GetProductsBySupplierID(product.SupplierID);

        if (productsBySupplier.Count == 1)
            // this is the only product we buy from this supplier
            throw new ApplicationException(
                "You cannot mark a product as discontinued if it is the only
                  product purchased from a supplier");
    }

    product.ProductName = productName;
    if (supplierID == null) product.SetSupplierIDNull();
      else product.SupplierID = supplierID.Value;
    if (categoryID == null) product.SetCategoryIDNull();
      else product.CategoryID = categoryID.Value;
    if (quantityPerUnit == null) product.SetQuantityPerUnitNull();
      else product.QuantityPerUnit = quantityPerUnit;
    if (unitPrice == null) product.SetUnitPriceNull();
      else product.UnitPrice = unitPrice.Value;
    if (unitsInStock == null) product.SetUnitsInStockNull();
      else product.UnitsInStock = unitsInStock.Value;
    if (unitsOnOrder == null) product.SetUnitsOnOrderNull();
      else product.UnitsOnOrder = unitsOnOrder.Value;
    if (reorderLevel == null) product.SetReorderLevelNull();
      else product.ReorderLevel = reorderLevel.Value;
    product.Discontinued = discontinued;

    // Update the product record
    int rowsAffected = Adapter.Update(product);

    // Return true if precisely one row was updated,
    // otherwise false
    return rowsAffected == 1;
}

响应演示层中的验证错误

从表示层调用 BLL 时,我们可以决定是尝试处理任何可能引发的异常,还是让它们冒泡至 ASP.NET(这将引发 HttpApplicationError 事件)。 若要在以编程方式处理 BLL 时处理异常,可以使用 尝试...catch 块,如以下示例所示:

ProductsBLL productLogic = new ProductsBLL();

// Update information for ProductID 1
try
{
    // This will fail since we are attempting to use a
    // UnitPrice value less than 0.
    productLogic.UpdateProduct(
        "Scott s Tea", 1, 1, null, -14m, 10, null, null, false, 1);
}
catch (ArgumentException ae)
{
    Response.Write("There was a problem: " + ae.Message);
}

正如我们在将来的教程中看到的那样,在使用数据 Web 控件插入、更新或删除数据时,处理从 BLL 浮出水面的异常,可以直接在事件处理程序中处理,而无需在块中 try...catch 包装代码。

概要

精心构建的应用程序设计为不同的层,每个层都封装了特定角色。 在本文系列的第一篇教程中,我们使用类型化数据集创建了数据访问层;在本教程中,我们构建了一个业务逻辑层,作为应用程序文件夹中调用 DAL 的 App_Code 一系列类。 BLL 为应用程序实现字段级和业务级逻辑。 除了创建单独的 BLL,就像在本教程中所做的那样,另一个选项是使用分部类扩展 TableAdapters 的方法。 但是,使用此方法不允许我们替代现有方法,也不会像本文中采用的方法那样将 DAL 和 BLL 分开。

完成 DAL 和 BLL 后,我们可以开始构建演示界面层。 在下 一教程 中,我们将简要介绍数据访问主题,并定义一致的页面布局,以便在整个教程中使用。

快乐编程!

关于作者

斯科特·米切尔,七本 ASP/ASP.NET 书籍的作者和 4GuysFromRolla.com 的创始人,自1998年以来一直在与Microsoft Web 技术合作。 斯科特担任独立顾问、教练和作家。 他的最新书是 《Sams Teach Yourself ASP.NET 2.0 in 24 Hours》。 可以通过 mitchell@4GuysFromRolla.com 联系到他。

特别致谢

本教程系列由许多有用的审阅者审阅。 本教程的主要审阅者是莉兹·舒洛克、丹尼斯·帕特森、卡洛斯·桑托斯和希尔顿·吉塞诺。 有兴趣查看即将发布的 MSDN 文章? 如果是这样,请给我写信。mitchell@4GuysFromRolla.com