共用方式為


逐步解說:擴充本機資料庫快取以支援雙向同步處理

更新: 2008 年 7 月

在 Visual Studio 2008 中,[本機資料庫快取] 會設定 SQL Server Compact 3.5 資料庫,以及啟用 Microsoft Synchronization Services for ADO.NET 的部分類別集合。因為 Visual Studio 會產生部分類別,您可以撰寫程式碼以加入同步處理功能,同時仍然能夠檢視與變更 [設定資料同步處理] 對話方塊中的設定。如需部分類別的詳細資訊,請參閱 HOW TO:將類別分割成部分類別

根據預設,[設定資料同步處理] 對話方塊僅能讓您針對下載作業設定 Synchronization Services。這表示在您設定資料同步處理後,呼叫 Synchronize() 時只會將變更由伺服器下載至用戶端資料庫。擴充同步處理程式碼最常見的方法之一,是設定雙向 (Bidirectional) 同步處理。如此一來,您就能由用戶端將變更上載至伺服器。若要啟用雙向同步處理,我們建議您以下列方式擴充產生的程式碼:

  • 將同步處理方向設定為雙向。

  • 加入程式碼以處理同步處理衝突。

  • 由同步處理命令移除伺服器追蹤資料行。

必要條件

在開始這個逐步解說前,您必須先完成逐步解說:建立偶爾連接的應用程式。完成上述的逐步解說後,您會有包含 [本機資料庫快取] 與 Windows Form 應用程式的專案,即能由 Northwind Customers 資料表將變更下載至 SQL Server Compact 資料庫。現在您已準備就緒,可以載入此逐步解說方案並加入雙向功能。

注意事項:

您的電腦可能會在下列說明中,以不同名稱或位置顯示某些 Visual Studio 使用者介面項目。您所擁有的 Visual Studio 版本以及使用的設定會決定這些項目。如需詳細資訊,請參閱 Visual Studio 設定

若要開啟 OCSWalkthrough 方案

  1. 開啟 Visual Studio。

  2. 在 [檔案] 功能表上開啟現有的方案或專案,並找出 OCSWalkthrough 方案,也就是 OCSWalkthrough.sln 檔案。

設定同步處理方向

[設定資料同步處理] 對話方塊能讓您將 SyncDirection() 屬性設定為 DownloadOnly() 或 Snapshot()。若要啟用雙向同步處理,則針對要啟用將變更上載的功能的每個資料表,將 SyncDirection() 屬性設定為 Bidirectional()。

若要設定同步處理方向

  1. 以滑鼠右鍵按一下 [NorthwindCache.sync],然後按一下 [檢視程式碼。您第一次做這個動作時,Visual Studio 會在 [方案總管] 中的 [NorthwindCache.sync] 節點下建立一個 NorthwindCache 檔案。這個檔案中包含了一個 NorthwindCacheSyncAgent 部分類別,您可以視需要加入其他的類別。

  2. 在 NorthwindCache 類別檔案中加入程式碼,讓 NorthwindCacheSyncAgent.OnInitialized() 方法類似下列程式碼:

    partial void OnInitialized()
    {
        this.Customers.SyncDirection = 
        Microsoft.Synchronization.Data.SyncDirection.Bidirectional;
    }
    
    Private Sub OnInitialized()
        Me.Customers.SyncDirection = 
        Microsoft.Synchronization.Data.SyncDirection.Bidirectional
    End Sub
    
  3. 在 [程式碼編輯器] 中開啟 Form1。

  4. 在 Form1 檔案中,修改在 SynchronizeButton_Click 事件處理常式中的程式碼行,以包含上載和下載統計資料:

    MessageBox.Show("Changes downloaded: " +
        syncStats.TotalChangesDownloaded.ToString() + 
        Environment.NewLine +
        "Changes uploaded: " + 
        syncStats.TotalChangesUploaded.ToString());
    
    MessageBox.Show("Changes downloaded: " & _
        syncStats.TotalChangesDownloaded.ToString & _
        Environment.NewLine & _
        "Changes uploaded: " & _
        syncStats.TotalChangesUploaded.ToString)
    

測試應用程式

現在應用程式已設定為會在同步處理期間一併執行上載與下載。

若要測試應用程式

  1. 按 F5。

  2. 更新表單中的一項記錄,然後按一下 [儲存] 按鈕 (工具列上的磁碟圖示)。

  3. 按一下 [開始同步處理]。

  4. 隨即會出現一個訊息方塊,內含有關同步處理記錄的資訊。這些統計資料會顯示有一列已上載,也下載一列,雖然在伺服器上沒有變更。發生此額外下載的原因,是因為來自用戶端的變更在套用至伺服器後,會回應 (Echo) 至用戶端。如需詳細資訊,請參閱 HOW TO:使用自訂變更追蹤系統中的<判斷變更資料的用戶端>一節 (英文)。

  5. 按一下 [確定] 關閉訊息方塊,但讓應用程式繼續執行。

現在您將在用戶端和伺服器上同時變更同樣的記錄,在同步處理期間強迫產生衝突 (並行違規)。

若要測試應用程式並強迫產生衝突

  1. 更新表單中的一項記錄,然後按一下 [儲存] 按鈕。

  2. 在應用程式執行中,使用 [伺服器總管]/[資料庫總管] (或其他資料庫管理工具) 連接至伺服器資料庫。

  3. 為示範衝突解決方式的預設行為,在 [伺服器總管]/[資料庫總管] 中,對您在表單中所更新的同一筆記錄進行更新,但是變更為不同的值,然後認可變更 (離開修改過的資料列即可)。

  4. 返回表單並按一下 [開始同步處理]。

  5. 驗證在應用程式方格中與伺服器資料庫中的更新資料。請注意,您在伺服器上所做的更新,已覆寫用戶端上的更新。如需如何變更此衝突解決方法之行為的資訊,請參閱本主題的下一節「加入程式碼以處理同步處理衝突」。

加入程式碼以處理同步處理衝突

在 Synchronization Services 中,如果有一個資料列在同步處理的間隔中,同時在用戶端與伺服器上受到變更,就會出現衝突。Synchronization Services 提供了一組功能,可用於偵測與解決衝突。在本節中,您將加入基本的處理方式,解決同一個資料列同時在用戶端與伺服器上更新所造成的衝突。其他類型的衝突包括了同一個資料列在一個資料庫中被刪除,但在另一個資料庫中卻已更新,或有主索引鍵重複的資料列同時插入兩個資料庫的情況。如需偵測與解決衝突之方法的詳細資訊,請參閱 HOW TO:處理資料衝突與錯誤 (英文)。

注意事項:

範例程式碼提供了衝突處理的基本範例。您處理衝突的方式要根據您應用程式和商務邏輯的需求而定。

加入程式碼以處理伺服器 ApplyChangeFailed 事件與用戶端 ApplyChangeFailed 事件。當資料列由於衝突或錯誤而無法套用時,就會引發這些事件。處理這些事件的方法會檢查衝突類型,並指定將用戶端變更強制寫入伺服器資料庫,以解決用戶端更新 / 伺服器更新衝突。將更新套用至伺服器資料庫的同步處理命令,含有辨識何時應強制執行變更的邏輯。這項命令會包含在本主題下一節「由同步處理命令移除伺服器追蹤資料行」的程式碼中。

您為加入衝突處理所執行的步驟,會根據您使用的是 C# 或 Visual Basic 而有所不同。

若要加入衝突處理

  • 如果您使用 C#,請將下列程式碼加入至 NorthwindCache.cs 和 Form1.cs 中。在 NorthwindCache.cs 中,將下列程式碼加到 NorthwindCacheSyncAgent 類別的結尾之後:

    public partial class NorthwindCacheServerSyncProvider
    {
    
        partial void OnInitialized()
        {
            this.ApplyChangeFailed +=
                new System.EventHandler<Microsoft.Synchronization.Data.ApplyChangeFailedEventArgs>
                (NorthwindCacheServerSyncProvider_ApplyChangeFailed);
        }
    
        private void NorthwindCacheServerSyncProvider_ApplyChangeFailed(object sender,
            Microsoft.Synchronization.Data.ApplyChangeFailedEventArgs e)
        {
    
        if (e.Conflict.ConflictType ==
            Microsoft.Synchronization.Data.ConflictType.ClientUpdateServerUpdate)
            {
    
            // Resolve a client update / server update conflict by force writing
            // the client change to the server database.
            System.Windows.Forms.MessageBox.Show("A client update / server update conflict " +
                                                    "was detected at the server.");
            e.Action = Microsoft.Synchronization.Data.ApplyAction.RetryWithForceWrite;
    
            }
    
        }
    }
    
    public partial class NorthwindCacheClientSyncProvider
    {
    
        public void AddHandlers()
        {
            this.ApplyChangeFailed +=
                new System.EventHandler<Microsoft.Synchronization.Data.ApplyChangeFailedEventArgs>
                (NorthwindCacheClientSyncProvider_ApplyChangeFailed);
        }
    
        private void NorthwindCacheClientSyncProvider_ApplyChangeFailed(object sender,
            Microsoft.Synchronization.Data.ApplyChangeFailedEventArgs e)
        {
    
            if (e.Conflict.ConflictType ==
                Microsoft.Synchronization.Data.ConflictType.ClientUpdateServerUpdate)
            {
    
                // Resolve a client update / server update conflict by keeping the 
                // client change.
                e.Action = Microsoft.Synchronization.Data.ApplyAction.Continue;
    
            }
    
        }
    }
    

    在 Form1.cs 中修改 SynchronizeButton_Click 事件處理常式中的程式碼,以呼叫您在前一個步驟中加入至 NorthwindCache.cs 的 AddHandler 方法:

    NorthwindCacheSyncAgent syncAgent = new NorthwindCacheSyncAgent();
    
    NorthwindCacheClientSyncProvider clientSyncProvider =
        (NorthwindCacheClientSyncProvider)syncAgent.LocalProvider;
    clientSyncProvider.AddHandlers();
    
    Microsoft.Synchronization.Data.SyncStatistics syncStats = 
        syncAgent.Synchronize();
    
  • 如果您使用 Visual Basic,則將下列程式碼加到 NorthwindCache.vb 中 NorthwindCacheSyncAgent 類別的 End Class 陳述式之後。

    Partial Public Class NorthwindCacheServerSyncProvider
    
        Private Sub NorthwindCacheServerSyncProvider_ApplyChangeFailed( _
            ByVal sender As Object, ByVal e As  _
            Microsoft.Synchronization.Data.ApplyChangeFailedEventArgs) _
            Handles Me.ApplyChangeFailed
    
            If e.Conflict.ConflictType = _
                Microsoft.Synchronization.Data.ConflictType.ClientUpdateServerUpdate Then
    
                ' Resolve a client update / server update conflict by force writing
                ' the client change to the server database.
                MessageBox.Show("A client update / server update" & _
                    " conflict was detected at the server.")
                e.Action = Microsoft.Synchronization.Data.ApplyAction.RetryWithForceWrite
    
            End If
    
        End Sub
    
    End Class
    
    Partial Public Class NorthwindCacheClientSyncProvider
    
        Private Sub NorthwindCacheClientSyncProvider_ApplyChangeFailed( _
            ByVal sender As Object, ByVal e As  _
            Microsoft.Synchronization.Data.ApplyChangeFailedEventArgs) _
            Handles Me.ApplyChangeFailed
    
            If e.Conflict.ConflictType = _
                Microsoft.Synchronization.Data.ConflictType.ClientUpdateServerUpdate Then
    
                ' Resolve a client update / server update conflict by keeping the 
                ' client change.
                e.Action = Microsoft.Synchronization.Data.ApplyAction.Continue
    
            End If
    
        End Sub
    
    End Class
    

若要同步處理並檢視衝突解決方法

  1. 按 F5。

  2. 更新表單中的一項記錄,然後按一下 [儲存] 按鈕。

  3. 在 [伺服器總管]/[資料庫總管] 中,對您在表單中所更新的同一筆記錄進行更新,但是變更為不同的值,然後認可變更。

  4. 返回表單並按一下 [開始同步處理]。

  5. 驗證在應用程式方格中與伺服器資料庫中的更新資料。請注意,您在用戶端上所做的更新,已覆寫伺服器上的更新。

由同步處理命令移除伺服器追蹤資料行

建立 [本機資料庫快取] 時,用於追蹤伺服器資料庫中之變更的資料行會下載至用戶端 (在本逐步解說中,這些資料行指的是 CreationDate 和 LastEditDate)。若要支援雙向同步處理,並協助確保聚合用戶端與伺服器上的資料,請由套用變更至伺服器資料庫的 SQL 命令移除這些資料行。您也可以由選取伺服器上的變更以套用至用戶端的命令中,將這些資料行移除,但這不是必要的。由於用戶端資料庫中對某些結構描述變更的限制,因此無法刪除這些資料行。如需同步處理命令的詳細資訊,請參閱 HOW TO:指定快照、下載、上載與雙向同步處理 (英文)。

注意事項:

如果您使用 SQL Server 2008 變更追蹤,追蹤資料行將不會加到您的資料表中。在這樣的情況下,您不需要改變將變更套用至伺服器的命令。

下列程式碼會重新定義兩個設定為 Customers 資料表之 SyncAdapter 物件上的屬性,分別是 InsertCommand() 和 UpdateCommand() 屬性。這些由 [設定資料同步處理] 對話方塊產生的命令,包含對 CreationDate 和 LastEditDate 資料行的參考。在下列程式碼中,這些命令會在CustomersSyncAdapter 類別的 OnInitialized 方法中重新定義。因為對 CreationDate 和 LastEditDate 資料行沒有影響,因此 DeleteCommand() 屬性未重新定義。

每個 SQL 命令中的變數都會用於在 Synchronization Services、用戶端和伺服器間傳遞資料和中繼資料 (Metadata)。下列工作階段變數會用於下方的命令:

  • @sync\_row\_count:傳回受到伺服器上前一次執行的作業所影響的資料列數。在 SQL Server 資料庫中,@@rowcount 會提供此變數的值。

  • @sync\_force\_write:用於強制套用因為衝突或錯誤而套用失敗的變更。

  • @sync\_last\_received\_anchor:用於定義在工作階段中要同步處理的變更集。

如需工作階段變數的詳細資訊,請參閱 HOW TO:使用工作階段變數 (英文)。

若要由同步處理命令移除追蹤資料行

  • 在 NorthwindCache 類別 (NorthwindCache.vb 或 NorthwindCache.cs) 中,將下列程式碼加到 NorthwindCacheServerSyncProvider 類別的 End Class 陳述式之後。

    public partial class CustomersSyncAdapter
    {
    
        partial void OnInitialized()
        {
    
        // Redefine the insert command so that it does not insert values 
        // into the CreationDate and LastEditDate columns.
        System.Data.SqlClient.SqlCommand insertCommand = new _
            System.Data.SqlClient.SqlCommand();
    
        insertCommand.CommandText = "INSERT INTO dbo.Customers ([CustomerID], [CompanyName], " +
            "[ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], " +
            "[Country], [Phone], [Fax] )" +
            "VALUES (@CustomerID, @CompanyName, @ContactName, @ContactTitle, @Address, @City, " +
            "@Region, @PostalCode, @Country, @Phone, @Fax) SET @sync_row_count = @@rowcount";
        insertCommand.CommandType = System.Data.CommandType.Text;
        insertCommand.Parameters.Add("@CustomerID", System.Data.SqlDbType.NChar);
        insertCommand.Parameters.Add("@CompanyName", System.Data.SqlDbType.NVarChar);
        insertCommand.Parameters.Add("@ContactName", System.Data.SqlDbType.NVarChar);
        insertCommand.Parameters.Add("@ContactTitle", System.Data.SqlDbType.NVarChar);
        insertCommand.Parameters.Add("@Address", System.Data.SqlDbType.NVarChar);
        insertCommand.Parameters.Add("@City", System.Data.SqlDbType.NVarChar);
        insertCommand.Parameters.Add("@Region", System.Data.SqlDbType.NVarChar);
        insertCommand.Parameters.Add("@PostalCode", System.Data.SqlDbType.NVarChar);
        insertCommand.Parameters.Add("@Country", System.Data.SqlDbType.NVarChar);
        insertCommand.Parameters.Add("@Phone", System.Data.SqlDbType.NVarChar);
        insertCommand.Parameters.Add("@Fax", System.Data.SqlDbType.NVarChar);
        insertCommand.Parameters.Add("@sync_row_count", System.Data.SqlDbType.Int);
        insertCommand.Parameters["@sync_row_count"].Direction = 
            System.Data.ParameterDirection.Output;
    
        this.InsertCommand = insertCommand;
    
    
        // Redefine the update command so that it does not update values 
        // in the CreationDate and LastEditDate columns.
        System.Data.SqlClient.SqlCommand updateCommand = new System.Data.SqlClient.SqlCommand();
    
        updateCommand.CommandText = "UPDATE dbo.Customers SET [CompanyName] = @CompanyName, [ContactName] " +
            "= @ContactName, [ContactTitle] = @ContactTitle, [Address] = @Address, [City] " +
            "= @City, [Region] = @Region, [PostalCode] = @PostalCode, [Country] = @Country, " +
            "[Phone] = @Phone, [Fax] = @Fax " +
            "WHERE ([CustomerID] = @CustomerID) AND (@sync_force_write = 1 " +
            "OR ([LastEditDate] <= @sync_last_received_anchor)) SET @sync_row_count = @@rowcount";
        updateCommand.CommandType = System.Data.CommandType.Text;
        updateCommand.Parameters.Add("@CompanyName", System.Data.SqlDbType.NVarChar);
        updateCommand.Parameters.Add("@ContactName", System.Data.SqlDbType.NVarChar);
        updateCommand.Parameters.Add("@ContactTitle", System.Data.SqlDbType.NVarChar);
        updateCommand.Parameters.Add("@Address", System.Data.SqlDbType.NVarChar);
        updateCommand.Parameters.Add("@City", System.Data.SqlDbType.NVarChar);
        updateCommand.Parameters.Add("@Region", System.Data.SqlDbType.NVarChar);
        updateCommand.Parameters.Add("@PostalCode", System.Data.SqlDbType.NVarChar);
        updateCommand.Parameters.Add("@Country", System.Data.SqlDbType.NVarChar);
        updateCommand.Parameters.Add("@Phone", System.Data.SqlDbType.NVarChar);
        updateCommand.Parameters.Add("@Fax", System.Data.SqlDbType.NVarChar);
        updateCommand.Parameters.Add("@CustomerID", System.Data.SqlDbType.NChar);
        updateCommand.Parameters.Add("@sync_force_write", System.Data.SqlDbType.Bit);
        updateCommand.Parameters.Add("@sync_last_received_anchor", System.Data.SqlDbType.DateTime);
        updateCommand.Parameters.Add("@sync_row_count", System.Data.SqlDbType.Int);
        updateCommand.Parameters["@sync_row_count"].Direction = 
            System.Data.ParameterDirection.Output;
    
        this.UpdateCommand = updateCommand;
    
        }
    }
    
    Partial Public Class CustomersSyncAdapter
        Private Sub OnInitialized()
    
            ' Redefine the insert command so that it does not insert values 
            ' into the CreationDate and LastEditDate columns.
            Dim insertCommand As New System.Data.SqlClient.SqlCommand
            With insertCommand
                .CommandText = "INSERT INTO dbo.Customers ([CustomerID], [CompanyName], " & _
                    "[ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], " & _
                    "[Country], [Phone], [Fax] )" & _
                    "VALUES (@CustomerID, @CompanyName, @ContactName, @ContactTitle, @Address, @City, " & _
                    "@Region, @PostalCode, @Country, @Phone, @Fax) SET @sync_row_count = @@rowcount"
                .CommandType = System.Data.CommandType.Text
                .Parameters.Add("@CustomerID", System.Data.SqlDbType.NChar)
                .Parameters.Add("@CompanyName", System.Data.SqlDbType.NVarChar)
                .Parameters.Add("@ContactName", System.Data.SqlDbType.NVarChar)
                .Parameters.Add("@ContactTitle", System.Data.SqlDbType.NVarChar)
                .Parameters.Add("@Address", System.Data.SqlDbType.NVarChar)
                .Parameters.Add("@City", System.Data.SqlDbType.NVarChar)
                .Parameters.Add("@Region", System.Data.SqlDbType.NVarChar)
                .Parameters.Add("@PostalCode", System.Data.SqlDbType.NVarChar)
                .Parameters.Add("@Country", System.Data.SqlDbType.NVarChar)
                .Parameters.Add("@Phone", System.Data.SqlDbType.NVarChar)
                .Parameters.Add("@Fax", System.Data.SqlDbType.NVarChar)
                .Parameters.Add("@sync_row_count", System.Data.SqlDbType.Int)
                .Parameters("@sync_row_count").Direction = ParameterDirection.Output
            End With
    
            Me.InsertCommand = insertCommand
    
    
            ' Redefine the update command so that it does not update values 
            ' in the CreationDate and LastEditDate columns.
            Dim updateCommand As New System.Data.SqlClient.SqlCommand
            With updateCommand
                .CommandText = "UPDATE dbo.Customers SET [CompanyName] = @CompanyName, [ContactName] " & _
                    "= @ContactName, [ContactTitle] = @ContactTitle, [Address] = @Address, [City] " & _
                    "= @City, [Region] = @Region, [PostalCode] = @PostalCode, [Country] = @Country, " & _
                    "[Phone] = @Phone, [Fax] = @Fax " & _
                    "WHERE ([CustomerID] = @CustomerID) AND (@sync_force_write = 1 " & _
                    "OR ([LastEditDate] <= @sync_last_received_anchor)) SET @sync_row_count = @@rowcount"
                .CommandType = System.Data.CommandType.Text
                .Parameters.Add("@CompanyName", System.Data.SqlDbType.NVarChar)
                .Parameters.Add("@ContactName", System.Data.SqlDbType.NVarChar)
                .Parameters.Add("@ContactTitle", System.Data.SqlDbType.NVarChar)
                .Parameters.Add("@Address", System.Data.SqlDbType.NVarChar)
                .Parameters.Add("@City", System.Data.SqlDbType.NVarChar)
                .Parameters.Add("@Region", System.Data.SqlDbType.NVarChar)
                .Parameters.Add("@PostalCode", System.Data.SqlDbType.NVarChar)
                .Parameters.Add("@Country", System.Data.SqlDbType.NVarChar)
                .Parameters.Add("@Phone", System.Data.SqlDbType.NVarChar)
                .Parameters.Add("@Fax", System.Data.SqlDbType.NVarChar)
                .Parameters.Add("@CustomerID", System.Data.SqlDbType.NChar)
                .Parameters.Add("@sync_force_write", System.Data.SqlDbType.Bit)
                .Parameters.Add("@sync_last_received_anchor", System.Data.SqlDbType.DateTime)
                .Parameters.Add("@sync_row_count", System.Data.SqlDbType.Int)
                .Parameters("@sync_row_count").Direction = ParameterDirection.Output
            End With
    
            Me.UpdateCommand = updateCommand
    
        End Sub
    
    End Class
    

測試應用程式

若要同步處理並檢視追蹤資料行更新

  1. 按 F5。

  2. 變更 LastEditDate 資料行中的值,然後按一下 [儲存] 按鈕,更新表單中的一項記錄。

  3. 返回至表單並按一下 [開始同步處理]。

  4. 驗證在應用程式方格中與伺服器資料庫中的更新資料。請注意,伺服器上資料行中的值,已覆寫用戶端上的更新。更新程序如下所示:

    1. Synchronization Services 會判斷用戶端上有一個資料列受到變更。

    2. 在同步處理期間會上載這個資料列,並套用至伺服器資料庫中的資料表。然而,追蹤資料行並不包含在更新陳述式中。Synchronization Services 實際上對資料表執行的是「假更新」。

    3. 資料列接著會回應至用戶端,但由伺服器選取變更的命令有包含追蹤資料行。因此,在用戶端所做的變更會由伺服器上的值覆寫。

後續步驟

在本逐步解說中,您以基本衝突處理設定了雙向同步處理,並處理了在用戶端資料庫中使用伺服器追蹤資料行的可能問題。藉著使用部分類別,您還可以在其他重要的面向上擴充 [本機資料庫快取] 程式碼。例如,您可以重新定義由伺服器資料庫選取變更的 SQL 命令,讓資料在下載至用戶端時先篩選。我們建議您閱讀本文件中的 HOW TO 主題,了解您可以加入或變更同步處理程式碼的方式,以符合您應用程式的需求。如需詳細資訊,請參閱如何設計一般用戶端和伺服器同步處理工作的程式 (英文)。

請參閱

概念

偶爾連接的應用程式概觀

其他資源

如何設計一般用戶端和伺服器同步處理工作的程式 (英文)

協助您開發應用程式的工具 (同步處理服務) (英文)

變更記錄

日期

記錄

原因

2008 年 7 月

加入主題。

SP1 功能變更。