TableAdapter 以使用 JOIN (C#)

更新 TableAdapter 以使用 JOIN (C#)

作者 :Scott Mitchell

下載 PDF

使用數據庫時,通常請求跨多個表分佈的數據。 若要從兩個不同的表中檢索數據,可以使用相關子查詢或 JOIN 操作。 在本教程中,我們將比較相關子查詢和 JOIN 語法,然後再瞭解如何創建在其主查詢中包含 JOIN 的 TableAdapter。

簡介

使用關係數據庫時,我們感興趣的數據通常分佈在多個表中。 例如,顯示產品信息時,我們可能需要列出每個產品對應的類別和供應商名稱。 該 Products 表具有 CategoryID 和 SupplierID 值,但實際類別和供應商名稱分別位於 Categories 表和 Suppliers 表中。

若要從另一個相關表檢索信息,可以使用 相關子查詢 或 JOINs。 相關子查詢是引用外部查詢中的列的嵌套 SELECT 查詢。 例如,在 “創建數據訪問層” 教程中,我們使用兩個相關子查詢在主查詢中 ProductsTableAdapter 返回每個產品的類別和供應商名稱。 一 JOIN 個 SQL 構造,用於合併兩個不同表中的相關行。 JOIN我們在查詢數據中使用了 SqlDataSource 控件教程,將類別信息與每個產品一起顯示。

我們在 TableAdapters 中使用 JOIN s 的原因是 TableAdapter 嚮導中存在自動生成相應和INSERTUPDATEDELETE語句的限制。 更具體地說,如果 TableAdapter 的主查詢包含任何 JOIN s,則 TableAdapter 無法爲表 InsertCommandUpdateCommandSQL 語句或存儲過程和DeleteCommand屬性自動創建臨時 SQL 語句或存儲過程。

在本教程中,我們將簡要比較和對比相關子查詢, JOIN 然後再探索如何創建包含其主查詢中的 TableAdapter JOIN 。

比較和對比相關子查詢和JOIN s

回想一下,DataSet 的第一個教程Northwind中創建的ProductsTableAdapter子查詢使用相關子查詢來收回每個產品的相應類別和供應商名稱。 下面 ProductsTableAdapter 顯示了主查詢。

SQL
SELECT ProductID, ProductName, SupplierID, CategoryID, 
       QuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnOrder, 
       ReorderLevel, Discontinued,
       (SELECT CategoryName FROM Categories WHERE Categories.CategoryID = 
            Products.CategoryID) as CategoryName, 
       (SELECT CompanyName FROM Suppliers WHERE Suppliers.SupplierID = 
            Products.SupplierID) as SupplierName
FROM Products

這兩個相關子查詢 - (SELECT CategoryName FROM Categories WHERE Categories.CategoryID = Products.CategoryID) - 和 (SELECT CompanyName FROM Suppliers WHERE Suppliers.SupplierID = Products.SupplierID) - 是 SELECT 返回每個產品的單個值的查詢,作爲外部 SELECT 語句列列表中的附加列。

或者,可以使用 a JOIN 返回每個產品的供應商和類別名稱。 以下查詢返回與上述查詢相同的輸出,但使用 JOIN 代替子查詢:

SQL
SELECT ProductID, ProductName, Products.SupplierID, Products.CategoryID, 
       QuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnOrder, 
       ReorderLevel, Discontinued,
       Categories.CategoryName, 
       Suppliers.CompanyName as SupplierName
FROM Products
    LEFT JOIN Categories ON
        Categories.CategoryID = Products.CategoryID
    LEFT JOIN Suppliers ON
        Suppliers.SupplierID = Products.SupplierID

根據一些條件,將 JOIN 一個表中的記錄與另一個表中的記錄合併。 例如,在上述查詢中,LEFT JOIN Categories ON Categories.CategoryID = Products.CategoryID指示SQL Server將每個產品記錄與其值與產品CategoryID值匹配的類別記錄CategoryID合併。 通過合併的結果,我們可以處理每個產品 (對應的類別字段,例如 CategoryName) 。

 備註

JOIN 從關係數據庫查詢數據時,通常使用 s。 如果你不熟悉JOIN語法或需要對其用法進行一些刷寫,我建議在 W3 Schools 學習 SQL 聯接教程。 同樣值得閱讀的是 JOINSQL 聯機叢書的基礎知識和子查詢基礎知識部分。

由於 JOIN s 和相關子查詢都可用於從其他表中檢索相關數據,因此許多開發人員會暫留頭,並想知道使用哪種方法。 我所談論的所有 SQL 大師都說了大致相同的事情,它並不重要,因爲SQL Server將產生大致相同的執行計劃。 然後,他們的建議是使用你和你的團隊最熟悉的技術。 值得指出的是,在提出這一建議後,這些專家立即表示他們對相關子查詢的偏好 JOIN 。

使用類型化數據集生成數據訪問層時,工具在使用子查詢時效果更好。 具體而言,TableAdapter 的嚮導不會自動生成相應的INSERTUPDATE查詢,如果DELETE主查詢包含任何 JOIN s,但會在使用相關子查詢時自動生成這些語句。

若要探索此缺點,請在文件夾中創建臨時類型化數據集 ~/App_Code/DAL 。 在 TableAdapter 配置嚮導中,選擇使用即席 SQL 語句並輸入以下 SELECT 查詢 (請參閱圖 1) :

SQL
SELECT ProductID, ProductName, Products.SupplierID, Products.CategoryID, 
       QuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnOrder, 
       ReorderLevel, Discontinued,
       Categories.CategoryName, 
       Suppliers.CompanyName as SupplierName
FROM Products
    LEFT JOIN Categories ON
        Categories.CategoryID = Products.CategoryID
    LEFT JOIN Suppliers ON
        Suppliers.SupplierID = Products.SupplierID

顯示 TableAdaptor 配置嚮導窗口的屏幕截圖,其中輸入了包含 JOIN 的查詢。

圖 1:輸入包含 JOIN (單擊以查看全尺寸圖像) 的主查詢

默認情況下,TableAdapter 將自動創建INSERTUPDATEDELETE基於主查詢的語句。 如果單擊“高級”按鈕,可以看到此功能已啓用。 儘管此設置,TableAdapter 將無法創建INSERTUPDATEDELETE語句,因爲主查詢包含 aJOIN

顯示“高級選項”窗口的屏幕截圖,其中選中了“生成插入”、“更新”和“刪除”語句複選框。

圖 2:輸入包含 s JOIN 的主查詢

單擊“完成”以完成嚮導。 此時,數據集設計器將包含一個 TableAdapter,其中包含一個 DataTable,其中包含查詢列列表中返回的每個字段的 SELECT 列。 這包括 CategoryName 和 SupplierName圖 3 所示。

DataTable 包含列列表中返回的每個字段的列

圖 3:DataTable 包含列列表中返回的每個字段的列

雖然 DataTable 具有相應的列,但 TableAdapter 缺少其值InsertCommandUpdateCommandDeleteCommand屬性。 若要確認這一點,請單擊設計器中的 TableAdapter,然後轉到屬性窗口。 你將看到 InsertCommand, UpdateCommand屬性 DeleteCommand 設置爲 (None) 。

InsertCommand、UpdateCommand 和 DeleteCommand 屬性設置爲 (無)

圖 4:InsertCommandUpdateCommand“和DeleteCommand”屬性“設置爲 (”無“) (單擊以查看全尺寸圖像)

若要解決此問題,可以通過屬性窗口手動提供 SQL 語句和DeleteCommand參數InsertCommandUpdateCommand以及屬性。 或者,我們可以首先將 TableAdapter 的主查詢配置爲 不包含 任何 JOIN 查詢。 這將允許自動生成和INSERTUPDATEDELETE語句。 完成嚮導後,我們可以從屬性窗口手動更新 TableAdapter sSelectCommand,使其包含JOIN語法。

雖然此方法有效,但使用即席 SQL 查詢時非常脆弱,因爲每當通過嚮導重新配置 TableAdapter 的主查詢時,自動生成INSERTUPDATE的和DELETE語句都重新創建。 這意味着,如果我們右鍵單擊 TableAdapter,從上下文菜單中選擇“配置”,然後再次完成嚮導,那麼我們稍後所做的所有自定義都將丟失。

TableAdapter 自動生成INSERTUPDATE的脆性以及DELETE語句幸運的是,僅限於臨時 SQL 語句。 如果 TableAdapter 使用存儲過程,則可以自定義SelectCommandInsertCommandUpdateCommandDeleteCommand存儲過程並重新運行 TableAdapter 配置嚮導,而無需擔心存儲過程將被修改。

在接下來的幾個步驟中,我們將創建一個 TableAdapter,該查詢最初使用省略任何 JOIN 項的主查詢,以便自動生成相應的插入、更新和刪除存儲過程。 然後,我們將更新 SelectCommand ,以便使用 JOIN 從相關表返回其他列的列。 最後,我們將創建相應的業務邏輯層類,並在 ASP.NET 網頁中使用 TableAdapter 進行演示。

步驟 1:使用簡化的主查詢創建 TableAdapter

在本教程中,我們將爲 Employees 數據集中的 NorthwindWithSprocs 表添加 TableAdapter 和強類型 DataTable。 該 Employees 表包含指定 ReportsTo 員工經理的字段 EmployeeID 。 例如,員工安妮·多茲沃思 ReportTo 的值爲 5,即 EmployeeID 史蒂文·布坎南。 因此,安妮向她的經理史蒂文報告。 除了報告每個員工 ReportsTo 的值之外,我們還可能想要檢索其經理的名稱。 這可以使用 a JOIN. 但是,在最初創建 TableAdapter 時使用 JOIN 阻止嚮導自動生成相應的插入、更新和刪除功能。 因此,我們將首先創建一個 TableAdapter,其主查詢不包含任何 JOIN 查詢。 然後,在步驟 2 中,我們將更新主查詢存儲過程,以通過 a JOIN檢索管理器的名稱。

首先打開文件夾中的NorthwindWithSprocs~/App_Code/DAL數據集。 右鍵單擊設計器,從上下文菜單中選擇“添加”選項,然後選擇 TableAdapter 菜單項。 這將啓動 TableAdapter 配置嚮導。 如圖 5 所示,讓嚮導創建新的存儲過程,然後單擊“下一步”。 有關從 TableAdapter 嚮導創建新存儲過程的刷新程序,請參閱 Typed DataSet s TableAdapters 教程的“創建新存儲過程 ”。

選擇“創建新存儲過程”選項

圖 5:選擇“創建新存儲過程”選項 (單擊以查看全尺寸圖像)

對 SELECT TableAdapter 的主查詢使用以下語句:

SQL
SELECT EmployeeID, LastName, FirstName, Title, HireDate, ReportsTo, Country
FROM Employees

由於此查詢不包含任何JOIN查詢,TableAdapter 嚮導將自動創建具有相應INSERTUPDATE、語句和語句的DELETE存儲過程,以及用於執行主查詢的存儲過程。

以下步驟允許我們命名 TableAdapter 的存儲過程。 使用名稱Employees_SelectEmployees_InsertEmployees_Update名稱和Employees_Delete圖 6 所示。

將 TableAdapter s 存儲過程命名爲

圖 6:將 TableAdapter s 存儲過程命名 (單擊以查看全尺寸圖像)

最後一步會提示我們命名 TableAdapter s 方法。 使用 Fill 並 GetEmployees 用作方法名稱。 此外,請確保保留 Create 方法以將更新直接發送到數據庫 (GenerateDBDirectMethods) 複選框。

將 TableAdapter s 方法命名爲 Fill 和 GetEmployees

圖 7:命名 TableAdapter 方法 Fill 並 GetEmployees (單擊以查看全尺寸圖像)

完成嚮導後,花點時間檢查數據庫中的存儲過程。 應會看到四個新項: Employees_Select、 Employees_Insert、 Employees_Update和 Employees_Delete。 接下來,檢查 EmployeesDataTable 並 EmployeesTableAdapter 剛剛創建。 DataTable 包含主查詢返回的每個字段的列。 單擊 TableAdapter,然後轉到屬性窗口。 你將在那裏看到InsertCommandUpdateCommand正確配置了和DeleteCommand屬性以調用相應的存儲過程。

TableAdapter 包括插入、更新和刪除功能

圖 8:TableAdapter 包括插入、更新和刪除功能, (單擊以查看全尺寸圖像)

通過自動創建的插入、更新和刪除存儲過程以及InsertCommandUpdateCommandDeleteCommand正確配置的屬性,我們已準備好自定義SelectCommand存儲過程以返回有關每個員工經理的其他信息。 具體而言,我們需要更新Employees_Select存儲過程以使用JOIN並返回管理器和FirstNameLastName值。 更新存儲過程後,我們需要更新 DataTable,使其包含這些附加列。 我們將在步驟 2 和 3 中處理這兩個任務。

步驟 2:自定義存儲過程以包含 aJOIN

首先轉到服務器資源管理器,向下鑽取到 Northwind 數據庫的“存儲過程”文件夾,然後打開 Employees_Select 存儲過程。 如果未看到此存儲過程,請右鍵單擊“存儲過程”文件夾,然後選擇“刷新”。 更新存儲過程,以便它使用 a LEFT JOIN 返回管理器的名字和姓氏:

SQL
SELECT Employees.EmployeeID, Employees.LastName, 
       Employees.FirstName, Employees.Title, 
       Employees.HireDate, Employees.ReportsTo, 
       Employees.Country,
       Manager.FirstName as ManagerFirstName, 
       Manager.LastName as ManagerLastName
FROM Employees
    LEFT JOIN Employees AS Manager ON
        Employees.ReportsTo = Manager.EmployeeID

更新 SELECT 語句後,轉到“文件”菜單並選擇“保存 Employees_Select”保存更改。 或者,可以單擊工具欄中的“保存”圖標或按 Ctrl+S。 保存更改後,右鍵單擊 Employees_Select 服務器資源管理器中的存儲過程,然後選擇“執行”。 這將運行存儲過程並在“輸出”窗口中顯示其結果, (請參閱圖 9) 。

存儲過程結果顯示在輸出窗口中

圖 9:存儲過程結果顯示在輸出窗口中 (單擊以查看全尺寸圖像)

步驟 3:更新 DataTable s 列

此時, Employees_Select 存儲過程返回 ManagerFirstName 和 ManagerLastName 值,但 EmployeesDataTable 缺少這些列。 可通過以下兩種方式之一將這些缺失列添加到 DataTable:

  • 手動 - 右鍵單擊數據集設計器中的 DataTable,然後從“添加”菜單中選擇“列”。 然後,可以命名列並相應地設置其屬性。
  • 自動 - TableAdapter 配置嚮導將更新 DataTable 的列,以反映存儲過程返回的 SelectCommand 字段。 使用即席 SQL 語句時,嚮導還將刪除InsertCommandSelectCommand現在包含 a JOIN的、UpdateCommand屬性和DeleteCommand屬性。 但是,使用存儲過程時,這些命令屬性保持不變。

我們已在前面的教程中手動添加 DataTable 列,包括 主記錄項目符號列表和詳細信息 DataList 和 上傳文件,我們將在下一教程中更詳細地查看此過程。 但是,對於本教程,讓我們通過 TableAdapter 配置嚮導使用自動方法。

首先右鍵單擊 EmployeesTableAdapter 上下文菜單中選擇“配置”。 此時會顯示 TableAdapter 配置嚮導,其中列出了用於選擇、插入、更新和刪除的存儲過程,以及返回值和參數 ((如果有任何) 的話)。 圖 10 顯示了此嚮導。 在這裏,我們可以看到 Employees_Select 存儲過程現在返回 ManagerFirstName 和 ManagerLastName 字段。

嚮導顯示Employees_Select存儲過程的更新列列表

圖 10:嚮導顯示存儲過程的更新列列表 Employees_Select (單擊以查看全尺寸圖像)

單擊“完成”完成嚮導。 返回到數據集設計器後,包括 EmployeesDataTable 兩個附加列: ManagerFirstName 和 ManagerLastName

EmployeesDataTable 包含兩個新列

圖 11: EmployeesDataTable 包含兩個新列 (單擊以查看全尺寸圖像)

爲了說明更新 Employees_Select 的存儲過程有效,並且 TableAdapter 的插入、更新和刪除功能仍然有效,讓我們創建一個網頁,允許用戶查看和刪除員工。 但是,在創建此類頁面之前,我們需要先在業務邏輯層中創建新類,以便與 NorthwindWithSprocs 數據集中的員工合作。 在步驟 4 中,我們將創建一個 EmployeesBLLWithSprocs 類。 在步驟 5 中,我們將從 ASP.NET 頁使用此類。

步驟 4:實現業務邏輯層

Create a new class file in the ~/App_Code/BLL folder named EmployeesBLLWithSprocs.cs. 此類模擬現有 EmployeesBLL 類的語義,只有這個新類提供較少的方法,並使用 NorthwindWithSprocs DataSet (而不是 Northwind DataSet) 。 將以下代碼添加到 EmployeesBLLWithSprocs 類。

C#
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 NorthwindWithSprocsTableAdapters;
[System.ComponentModel.DataObject]
public class EmployeesBLLWithSprocs
{
    private EmployeesTableAdapter _employeesAdapter = null;
    protected EmployeesTableAdapter Adapter
    {
        get
        {
            if (_employeesAdapter == null)
                _employeesAdapter = new EmployeesTableAdapter();
            return _employeesAdapter;
        }
    }
    [System.ComponentModel.DataObjectMethodAttribute
        (System.ComponentModel.DataObjectMethodType.Select, true)]
    public NorthwindWithSprocs.EmployeesDataTable GetEmployees()
    {
        return Adapter.GetEmployees();
    }
    [System.ComponentModel.DataObjectMethodAttribute
        (System.ComponentModel.DataObjectMethodType.Delete, true)]
    public bool DeleteEmployee(int employeeID)
    {
        int rowsAffected = Adapter.Delete(employeeID);
        // Return true if precisely one row was deleted, otherwise false
        return rowsAffected == 1;
    }
}

類 EmployeesBLLWithSprocs s Adapter 屬性返回數據集的EmployeesTableAdapter實例NorthwindWithSprocs。 類 GetEmployees 和 DeleteEmployee 方法使用此屬性。 該方法 GetEmployees 調用 EmployeesTableAdapter 相應的 GetEmployees 方法,該方法調用 Employees_Select 存儲過程並在其中 EmployeeDataTable填充其結果。 該方法DeleteEmployee同樣調用EmployeesTableAdapterDelete調用存儲過程的方法Employees_Delete

步驟 5:處理呈現層中的數據

完成 EmployeesBLLWithSprocs 課堂後,我們便可以通過 ASP.NET 頁處理員工數據。 JOINs.aspx打開文件夾中的頁面AdvancedDAL,將 GridView 從工具箱拖到設計器上,將其ID屬性設置爲 Employees。 Next, from the GridView s smart tag, bind the grid to a new ObjectDataSource control named EmployeesDataSource.

將 ObjectDataSource 配置爲使用 EmployeesBLLWithSprocs 類,並在 SELECT 和 DELETE 選項卡中,確保 GetEmployees 從下拉列表中選擇和 DeleteEmployee 方法。 單擊“完成”以完成 ObjectDataSource 的配置。

將 ObjectDataSource 配置爲使用 EmployeesBLLWithSprocs 類

圖 12:將 ObjectDataSource 配置爲使用 EmployeesBLLWithSprocs 類 (單擊以查看全尺寸圖像)

讓 ObjectDataSource 使用 GetEmployees 和 DeleteEmployee 方法

圖 13:讓 ObjectDataSource 使用 GetEmployees 和 DeleteEmployee 方法 (單擊以查看全尺寸圖像)

Visual Studio 將爲每個 EmployeesDataTable 列將 BoundField 添加到 GridView。 Remove all of these BoundFields except for TitleLastNameFirstNameManagerFirstName, and ManagerLastName and rename the HeaderText properties for the last four BoundFields to Last Name, First Name, Manager s First Name, and Manager s Last Name, respectively.

若要允許用戶從此頁面中刪除員工,我們需要執行兩項操作。 首先,通過從其智能標記中檢查“啓用刪除”選項,指示 GridView 提供刪除功能。 其次,將 ObjectDataSource 屬性從 ObjectDataSource OldValuesParameterFormatString 嚮導設置的值 (original_{0}) 更改爲其默認值 {0} () 。 進行這些更改後,GridView 和 ObjectDataSource 的聲明性標記應如下所示:

ASP.NET
<asp:GridView ID="Employees" runat="server" AutoGenerateColumns="False" 
    DataKeyNames="EmployeeID" DataSourceID="EmployeesDataSource">
    <Columns>
        <asp:CommandField ShowDeleteButton="True" />
        <asp:BoundField DataField="Title" 
            HeaderText="Title" 
            SortExpression="Title" />
        <asp:BoundField DataField="LastName" 
            HeaderText="Last Name" 
            SortExpression="LastName" />
        <asp:BoundField DataField="FirstName" 
            HeaderText="First Name" 
            SortExpression="FirstName" />
        <asp:BoundField DataField="ManagerFirstName" 
            HeaderText="Manager's First Name" 
            SortExpression="ManagerFirstName" />
        <asp:BoundField DataField="ManagerLastName" 
            HeaderText="Manager's Last Name" 
            SortExpression="ManagerLastName" />
    </Columns>
</asp:GridView>
<asp:ObjectDataSource ID="EmployeesDataSource" runat="server" 
    DeleteMethod="DeleteEmployee" OldValuesParameterFormatString="{0}" 
    SelectMethod="GetEmployees" TypeName="EmployeesBLLWithSprocs">
    <DeleteParameters>
        <asp:Parameter Name="employeeID" Type="Int32" />
    </DeleteParameters>
</asp:ObjectDataSource>

通過瀏覽器訪問頁面來測試頁面。 如圖 14 所示,頁面將列出每個員工及其經理的姓名, (假設他們有一個) 。

Employees_Select存儲過程中的 JOIN 返回管理器名稱

圖 14: JOIN 存儲過程中 Employees_Select 返回管理器的名稱 (單擊以查看全尺寸圖像)

單擊“刪除”按鈕將啓動刪除工作流,這最終導致存儲過程的執行 Employees_Delete 。 但是,由於外鍵約束衝突,存儲過程中的嘗試 DELETE 語句失敗, (請參閱圖 15) 。 具體而言,每個員工在 Orders 表中都有一個或多個記錄,導致刪除失敗。

刪除具有相應訂單的員工會導致外鍵約束衝突

圖 15:刪除具有相應訂單的員工會導致外鍵約束衝突 (單擊以查看全尺寸圖像)

若要允許刪除員工,可以:

我把這作爲讀者的練習。

總結

使用關係數據庫時,查詢通常會從多個相關表拉取其數據。 關聯子查詢並提供 JOIN 兩種不同的技術,用於從查詢中的相關表訪問數據。 在前面的教程中,我們最常使用相關子查詢,因爲 TableAdapter 無法自動生成INSERTUPDATEDELETE和語句用於涉及 JOIN s 的查詢。 雖然可以手動提供這些值,但當使用即席 SQL 語句時,當 TableAdapter 配置嚮導完成時,將覆蓋任何自定義項。

幸運的是,使用存儲過程創建的 TableAdapters 與使用即席 SQL 語句創建的表Adapters 不相同。 因此,創建一個 TableAdapter,其主查詢在使用存儲過程時使用 JOIN 表Adapter是可行的。 在本教程中,我們瞭解瞭如何創建此類 TableAdapter。 我們首先對 TableAdapter 的主查詢使用 JOIN-less SELECT 查詢,以便自動創建相應的插入、更新和刪除存儲過程。 完成 TableAdapter 的初始配置後,我們增強了 SelectCommand 存儲過程,以使用 JOIN 並重新運行 TableAdapter 配置嚮導來更新 EmployeesDataTable s 列。

重新運行 TableAdapter 配置嚮導會自動更新 EmployeesDataTable 列,以反映存儲過程返回 Employees_Select 的數據字段。 或者,我們可以手動將這些列添加到 DataTable。 下一教程將介紹如何手動將列添加到 DataTable。

快樂編程!

關於作者

斯科特·米切爾,七本 ASP/ASP.NET 書籍和 4GuysFromRolla.com 創始人,自1998年以來一直在與 Microsoft Web 技術合作。 斯科特擔任獨立顧問、教練和作家。 他的最新書是 山姆斯教自己在24小時內 ASP.NET 2.0。 他可以到達 [email protected] 或通過他的博客,可以在其中 http://ScottOnWriting.NET找到。

特別感謝

本教程系列由許多有用的審閱者審閱。 本教程的主要審閱者是希爾頓·吉塞諾、大衛·蘇魯和特蕾莎·墨菲。 有興趣查看即將發佈的 MSDN 文章? 如果是這樣,請把我扔一條線 [email protected]

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章