共用方式為


HOW TO:處理資料衝突和錯誤

此主題示範如何處理 Sync Framework 中的資料衝突和錯誤。此主題中的範例將重點放在下列 Sync Framework 型別與事件:

如需有關如何執行範例程式碼的詳細資訊,請參閱 撰寫一般用戶端和伺服器同步處理工作中的<HOW-TO 主題中的範例應用程式>。

了解資料衝突和錯誤

在 Sync Framework 中,系統會在資料列層級偵測到衝突和錯誤。如果在同步處理之間一個以上的節點中變更資料列,就會產生衝突。同步處理期間發生的錯誤基本上與條件約束違規有關,例如重複的主索引鍵。應用程式應該做盡量能夠避免衝突的設計,因為偵測與解決衝突會增加額外的複雜性、處理程序以及網路流量。最常見之避免衝突的方法如下:只在一個節點中更新資料表 (通常是伺服器),或是對資料進行篩選,讓一個節點只更新一個特定的資料列。如需有關篩選的詳細資訊,請參閱 HOW TO:篩選資料列和資料行。在某些應用程式中,衝突是無法避免的。例如,在銷售人力應用程式中,兩名銷售人員可能會有共同的區域。這兩名銷售人員都可以更新相同的客戶和訂單。因此,Sync Framework 提供一組功能,可用來偵測並解決衝突。

當在超過一個以上的節點中對同步處理案例做變更時,就有可能發生資料衝突。很明顯地,衝突可能發生在雙向 (Bidirectional) 同步處理當中,不過也可能在只有下載或上傳的同步處理時發生。例如,假設伺服器中刪除了某個資料列,而用戶端卻更新了該資料列,當 Sync Framework 嘗試套用已上傳到伺服器的更新時,就會發生衝突。衝突永遠都在目前同步處理的伺服器與用戶端之間發生。請考量下列範例:

  1. 用戶端 A 和 B 與伺服器進行同步 (Synchronize)。

  2. 用戶端 A 更新了某個資料列,然後同步。這樣並不產生衝突,在伺服器中也會套用資料列。

  3. 用戶端 B 更新了同一個資料列,然後同步。因為在用戶端 A 中產生的更新,現在來自用戶端 B 的資料列就會與伺服器中的資料列產生衝突。

  4. 如果您想用以伺服器為主的方式解決此衝突,Sync Framework 可以從伺服器套用資料列至用戶端 B。如果您想用以用戶端 B 為主的方式解決此衝突,Sync Framework 可以從用戶端 B 套用資料列至伺服器。在稍後用戶端 A 與伺服器之間的同步處理中,用戶端 B 所產生的更新就會套用至用戶端 A。

衝突和錯誤的類型

Sync Framework 會偵測下列衝突類型:這些會在 ConflictType 列舉加以定義。

  • 當用戶端和伺服器都插入了具有相同主索引鍵的資料列時,會發生 ClientInsertServerInsert 衝突。此類型的衝突也稱為主索引鍵衝突 (collision)。

  • 當用戶端與伺服器變更同一個資料列時,會發生 ClientUpdateServerUpdate 衝突。這是最常見的衝突類型。

  • 當用戶端更新某個資料列,而伺服器卻刪除該資料列時,會發生 ClientUpdateServerDelete 衝突。

  • 當用戶端刪除了某個資料列,而伺服器卻更新該資料列時,會發生 ClientDeleteServerUpdate 衝突。

  • 當某個錯誤讓系統無法套用資料列時,就會發生 ErrorsOccurred 衝突。

衝突和錯誤偵測

如果在同步處理中無法套用某個資料列,通常是因為發生錯誤或資料衝突。在這兩種情況中,會根據是在同步處理的上傳或下載階段產生錯誤或衝突,而引發 DbServerSyncProvider ApplyChangeFailed 事件或 SqlCeClientSyncProvider ApplyChangeFailed 事件。如果引發了客戶端 ApplyChangeFailed 事件,Sync Framework 會為您選取所有衝突的資料列。然後再由您決定如何解決這些衝突。如果引發的是伺服器 ApplyChangeFailed 事件,則會使用您在 SyncAdapter 中定義的兩個命令,在每個資料表中選取衝突的資料列:

  • 您為 SelectConflictUpdatedRowsCommand 屬性所指定的查詢或預存程序,會從伺服器資料庫的基底資料表中選取衝突的資料列。如果插入、更新或刪除作業傳回的 @sync_row_count 值為 0,Sync Framework 就會執行此命令。這個值表示作業失敗。此命令選取有 ClientInsertServerInsertClientUpdateServerUpdateClientDeleteServerUpdate 衝突的資料列。

  • 您為 SelectConflictDeletedRowsCommand 指定的查詢或預存程序,會從伺服器資料庫的標記資料表中選取衝突的資料列。如果沒有在基底資料表中找到衝突的資料列,Sync Framework 會執行此命令。此命令選取有 ClientUpdateServerDelete 衝突的資料列。

來自各衝突資料列的資料都會儲存在 SyncConflict 集合中。此集合可能會成長而大到足以導致在下列情況中發生記憶不足的錯誤:

  • 存在大量的衝突資料列。建議在每個工作階段同步處理小量的資料列,或者只在一個節點上更新特定資料列,以限制衝突數量。

  • 衝突資料列包含使用大型資料型別的資料行。建議不要在同步處理的資料行集合中包含使用大型資料型別的資料行。如需詳細資訊,請參閱 HOW TO:篩選資料列和資料行

衝突和錯誤的解決方式

衝突和錯誤的解決方式應該是為了回應 DbServerSyncProvider ApplyChangeFailed 事件和 SqlCeClientSyncProvider ApplyChangeFailed 事件而處理。ApplyChangeFailedEventArgs 物件提供存取好幾個可以用來解決衝突的屬性:

  • Action 屬性設定為 ApplyAction 列舉型別的其中一個值,指定解決衝突的方式:

    • Continue:忽略衝突並繼續進行同步處理。

    • RetryApplyingRow:重試套用資料列。如果您沒有透過變更一個或兩個衝突的資料列的方式,解決造成衝突的原因,則重試會失敗,而且會再次引發事件。

    • RetryWithForceWrite:重試以邏輯來強制套用變更。SqlCeClientSyncProvider 內建支援此選項。若要在伺服器上使用此選項,請使用 @sync_force_write 參數,並在套用變更至伺服器資料庫的命令中加入支援。例如,針對 ClientUpdateServerDelete 衝突,@sync_force_write 設定為 1 時,您可以將更新變更為插入。如需範例程式碼,請參閱本主題稍後的「範例」一節。

  • 使用 Conflict 屬性,從用戶端與伺服器取得衝突類型,並檢視衝突的資料列。

  • 使用 Context 屬性,取得經過同步的變更之資料集。Conflict 屬性公開 (Expose) 的資料列是複本;因此,就算進行覆寫,也不會變更套用的資料列。使用 Context 屬性所公開的資料集,在應用程式需要時開發自訂的解決配置。如需範例程式碼,請參閱本主題稍後的「範例」一節。

SqlCeClientSyncProvider 也包括可以用來在用戶端解決衝突的 ConflictResolver 屬性。針對每種類型的衝突,您可以在 ResolveAction 列舉型別中設定一個值:

  • ClientWins:相當於設定 ContinueApplyAction

  • ServerWins:相當於設定 RetryWithForceWriteApplyAction

  • FireEvent:引發 ApplyChangeFailed 事件和預設值,然後處理該事件。

並非一定要為每種類型的衝突設定 ConflictResolver。您可以藉由處理 ApplyChangeFailed 事件,以類似在伺服器上處理的方式來解決衝突。不過,ConflictResolver 屬性並不能提供簡單地指定用戶端上的衝突解決選項的方法。

範例

下列程式碼範例顯示如何在 Sync Framework 範例資料庫的 Customer 資料表中,設定衝突的偵測與解決方式。在此範例中,同步處理命令並非使用 SqlSyncAdapterBuilder,而是手動建立的。您可以使用 SqlSyncAdapterBuilder 產生的命令來偵測與解決衝突,不過,尤其是在您要如何強制變更衝突的應用程式這方面,手動命令能夠提供更高的彈性。

API 的主要部分

本節提供的程式碼範例將指出用於偵測與解決衝突的 API 主要部分。下列查詢由伺服器資料庫的基底資料表中選取衝突的資料列。

SqlCommand customerUpdateConflicts = new SqlCommand();
customerUpdateConflicts.CommandText =
    "SELECT CustomerId, CustomerName, SalesPerson, CustomerType " +
    "FROM Sales.Customer " +
    "WHERE CustomerId = @CustomerId";
customerUpdateConflicts.Parameters.Add("@CustomerId", SqlDbType.UniqueIdentifier);
customerUpdateConflicts.Connection = serverConn;
customerSyncAdapter.SelectConflictUpdatedRowsCommand = customerUpdateConflicts;
Dim customerUpdateConflicts As New SqlCommand()
With customerUpdateConflicts
    .CommandText = _
        "SELECT CustomerId, CustomerName, SalesPerson, CustomerType " _
      & "FROM Sales.Customer " + "WHERE CustomerId = @CustomerId"
    .Parameters.Add("@CustomerId", SqlDbType.UniqueIdentifier)
    .Connection = serverConn
End With
customerSyncAdapter.SelectConflictUpdatedRowsCommand = customerUpdateConflicts

下列查詢由伺服器資料庫的標記資料表中選取衝突的資料列。

SqlCommand customerDeleteConflicts = new SqlCommand();
customerDeleteConflicts.CommandText =
    "SELECT CustomerId, CustomerName, SalesPerson, CustomerType " +
    "FROM Sales.Customer_Tombstone " +
    "WHERE CustomerId = @CustomerId";
customerDeleteConflicts.Parameters.Add("@CustomerId", SqlDbType.UniqueIdentifier);
customerDeleteConflicts.Connection = serverConn;
customerSyncAdapter.SelectConflictDeletedRowsCommand = customerDeleteConflicts;
Dim customerDeleteConflicts As New SqlCommand()
With customerDeleteConflicts
    .CommandText = _
        "SELECT CustomerId, CustomerName, SalesPerson, CustomerType " _
      & "FROM Sales.Customer_Tombstone " + "WHERE CustomerId = @CustomerId"
    .Parameters.Add("@CustomerId", SqlDbType.UniqueIdentifier)
    .Connection = serverConn
End With
customerSyncAdapter.SelectConflictDeletedRowsCommand = customerDeleteConflicts

下列程式碼範例會建立套用更新至伺服器資料庫的預存程序。此程序已指定 UpdateCommand 屬性。預存程序也可用來在伺服器資料庫上套用插入及刪除。如需這些程序的範例,請參閱資料庫提供者的安裝指令碼 HOW-TO 主題

usp_CustomerApplyUpdate 更新程序會根據 @sync_force_write 參數的值以及要更新的資料列是否存在於伺服器資料庫,嘗試更新或插入作業。如果資料列不存在,程序會由更新轉變為插入作業。在此範例中,缺少的資料列是由更新/刪除衝突所造成的。

CREATE PROCEDURE usp_CustomerApplyUpdate ( 
    @sync_last_received_anchor binary(8), 
    @sync_client_id uniqueidentifier,
    @sync_force_write int,
    @sync_row_count int out,
    @CustomerId uniqueidentifier,
    @CustomerName nvarchar(100),
    @SalesPerson nvarchar(100),
    @CustomerType nvarchar(100))        
AS      
    -- Try to apply an update if the RetryWithForceWrite option
    -- was not specified for the sync adapter's update command.
    IF @sync_force_write = 0
    BEGIN   
        UPDATE Sales.Customer 
        SET CustomerName = @CustomerName, SalesPerson = @SalesPerson,
        CustomerType = @CustomerType, UpdateId = @sync_client_id
        WHERE CustomerId = @CustomerId
        AND (UpdateTimestamp <= @sync_last_received_anchor
        OR UpdateId = @sync_client_id)
    END
    ELSE
    -- Try to apply an update if the RetryWithForceWrite option
    -- was specified for the sync adapter's update command.
    BEGIN
        --If the row exists, update it.
        -- You might want to include code here to handle 
        -- possible error conditions.
        IF EXISTS (SELECT CustomerId FROM Sales.Customer
                   WHERE CustomerId = @CustomerId)
        BEGIN
            UPDATE Sales.Customer 
            SET CustomerName = @CustomerName, SalesPerson = @SalesPerson,
            CustomerType = @CustomerType, UpdateId = @sync_client_id
            WHERE CustomerId = @CustomerId          
        END
        
        -- The row does not exist, possibly due to a client-update/
        -- server-delete conflict. Change the update into an insert.
        ELSE
        BEGIN
            INSERT INTO Sales.Customer 
                   (CustomerId, CustomerName, SalesPerson,
                    CustomerType, UpdateId)
            VALUES (@CustomerId, @CustomerName, @SalesPerson,
                    @CustomerType, @sync_client_id)
        END
    END

    SET @sync_row_count = @@rowcount

下列程式碼範例為 SqlCeClientSyncProvider 設定衝突解決選項。如前所述,這些選項雖然不是必須的,但它們能夠提供簡單解決衝突的方法。在這個範例中,在更新/刪除衝突中,更新應該保持優先,而所有其他的衝突都應該引發用戶端 ApplyChangeFailed 事件。

this.ConflictResolver.ClientDeleteServerUpdateAction = ResolveAction.ServerWins;            
this.ConflictResolver.ClientUpdateServerDeleteAction = ResolveAction.ClientWins;
//If any of the following conflicts or errors occur, the ApplyChangeFailed
//event is raised.
this.ConflictResolver.ClientInsertServerInsertAction = ResolveAction.FireEvent;
this.ConflictResolver.ClientUpdateServerUpdateAction = ResolveAction.FireEvent;
this.ConflictResolver.StoreErrorAction = ResolveAction.FireEvent;

//Log information for the ApplyChangeFailed event and handle any
//ResolveAction.FireEvent cases.
this.ApplyChangeFailed +=new EventHandler<ApplyChangeFailedEventArgs>(SampleClientSyncProvider_ApplyChangeFailed);
Me.ConflictResolver.ClientDeleteServerUpdateAction = ResolveAction.ServerWins
Me.ConflictResolver.ClientUpdateServerDeleteAction = ResolveAction.ClientWins
'If any of the following conflicts or errors occur, the ApplyChangeFailed
'event is raised.
Me.ConflictResolver.ClientInsertServerInsertAction = ResolveAction.FireEvent
Me.ConflictResolver.ClientUpdateServerUpdateAction = ResolveAction.FireEvent
Me.ConflictResolver.StoreErrorAction = ResolveAction.FireEvent

'Log information for the ApplyChangeFailed event and handle any
'ResolveAction.FireEvent cases.
AddHandler Me.ApplyChangeFailed, AddressOf SampleClientSyncProvider_ApplyChangeFailed

針對用戶端更新/伺服器刪除衝突,更新會強制寫入伺服器中,如下列程式碼所示:針對用戶端更新/伺服器刪除衝突,會使用伺服器 ApplyChangeFailed 中的事件處理常式之 RetryWithForceWrite 選項,在伺服器上處理衝突。當您使用此選項,就表示在伺服器中呼叫更新預存程序時,@sync_force_write 的參數設定為 1。

if (e.Conflict.ConflictType == ConflictType.ClientUpdateServerDelete)
{

    //For client-update/server-delete conflicts, we force the client 
    //change to be applied at the server. The stored procedure specified for 
    //customerSyncAdapter.UpdateCommand accepts the @sync_force_write parameter
    //and includes logic to handle this case.
    Console.WriteLine(String.Empty);
    Console.WriteLine("***********************************");
    Console.WriteLine("A client update / server delete conflict was detected.");

    e.Action = ApplyAction.RetryWithForceWrite;
    
    Console.WriteLine("The client change was retried at the server with RetryWithForceWrite.");
    Console.WriteLine("***********************************"); 
    Console.WriteLine(String.Empty);
 
}
If e.Conflict.ConflictType = ConflictType.ClientUpdateServerDelete Then

    'For client-update/server-delete conflicts, we force the client 
    'change to be applied at the server. The stored procedure specified for 
    'customerSyncAdapter.UpdateCommand accepts the @sync_force_write parameter
    'and includes logic to handle this case.
    Console.WriteLine(String.Empty)
    Console.WriteLine("***********************************")
    Console.WriteLine("A client update / server delete conflict was detected.")

    e.Action = ApplyAction.RetryWithForceWrite

    Console.WriteLine("The client change was retried at the server with RetryWithForceWrite.")
    Console.WriteLine("***********************************")
    Console.WriteLine(String.Empty)
End If

下列程式碼範例記錄衝突資訊,並在用戶端的 ApplyChangeFailed 用戶事件處理常式中,強制寫入所有衝突的插入。

private void SampleClientSyncProvider_ApplyChangeFailed(object sender, ApplyChangeFailedEventArgs e)
{

    //Log event data from the client side.
    EventLogger.LogEvents(sender, e);

    //Force write any inserted server rows that are in conflict 
    //when they are downloaded.
    if (e.Conflict.ConflictType == ConflictType.ClientInsertServerInsert)
    {
        e.Action = ApplyAction.RetryWithForceWrite;
    }

    if (e.Conflict.ConflictType == ConflictType.ClientUpdateServerUpdate)
    {
        //Logic goes here.
    }

    if (e.Conflict.ConflictType == ConflictType.ErrorsOccurred)
    {
        //Logic goes here.
    }

}
Private Sub SampleClientSyncProvider_ApplyChangeFailed(ByVal sender As Object, ByVal e As ApplyChangeFailedEventArgs)

    'Log event data from the client side.
    EventLogger.LogEvents(sender, e)

    'Force write any inserted server rows that are in conflict 
    'when they are downloaded.
    If e.Conflict.ConflictType = ConflictType.ClientInsertServerInsert Then
        e.Action = ApplyAction.RetryWithForceWrite
    End If

    If e.Conflict.ConflictType = ConflictType.ClientUpdateServerUpdate Then
        'Logic goes here.
    End If

    If e.Conflict.ConflictType = ConflictType.ErrorsOccurred Then
        'Logic goes here.
    End If

End Sub 'SampleClientSyncProvider_ApplyChangeFailed

完整的程式碼範例

下列的完整程式碼範例包含先前所述的程式碼範例,以及其他的程式碼,可用來執行同步處理。同時請注意,範例提供應用程式使用者如何解決更新/刪除衝突的選擇。其中一個選項是結合了衝突資料列的資料行值的自訂解決配置。自訂解決配置的程式碼已包含在 SampleServerSyncProvider_ApplyChangeFailedSampleServerSyncProvider_ChangesApplied 事件處理常式中。此範例需要 Utility 類別,詳情請參閱資料庫提供者公用程式類別的 HOW-TO 主題

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlServerCe;
using Microsoft.Synchronization;
using Microsoft.Synchronization.Data;
using Microsoft.Synchronization.Data.Server;
using Microsoft.Synchronization.Data.SqlServerCe;

namespace Microsoft.Samples.Synchronization
{
    class Program
    {
        static void Main(string[] args)
        {

            //The SampleStats class handles information from the SyncStatistics
            //object that the Synchronize method returns.
            SampleStats sampleStats = new SampleStats();

            //Request a password for the client database, and delete
            //and re-create the database. The client synchronization
            //provider also enables you to create the client database 
            //if it does not exist.
            Utility.SetPassword_SqlCeClientSync();
            Utility.DeleteAndRecreateCompactDatabase(Utility.ConnStr_SqlCeClientSync, true);

            //Initial synchronization. Instantiate the SyncAgent
            //and call Synchronize.
            SampleSyncAgent sampleSyncAgent = new SampleSyncAgent();
            SyncStatistics syncStatistics = sampleSyncAgent.Synchronize();
            sampleStats.DisplayStats(syncStatistics, "initial");

            //Make a change at the client that fails when it is
            //applied at the server.
            Utility.MakeFailingChangeOnClient();

            //Make changes at the client and server that conflict
            //when they are synchronized.
            Utility.MakeConflictingChangesOnClientAndServer();

            //Subsequent synchronization.
            syncStatistics = sampleSyncAgent.Synchronize();
            sampleStats.DisplayStats(syncStatistics, "subsequent");

            //Return server data back to its original state.
            //Comment out this line if you want to view the
            //state of the data after all conflicts are resolved.
            Utility.CleanUpServer();

            //Exit.
            Console.Write("\nPress Enter to close the window.");
            Console.ReadLine();
        }
    }

    //Create a class that is derived from 
    //Microsoft.Synchronization.SyncAgent.
    public class SampleSyncAgent : SyncAgent
    {
        public SampleSyncAgent()
        {
            //Instantiate a client synchronization provider and specify it
            //as the local provider for this synchronization agent.
            this.LocalProvider = new SampleClientSyncProvider();

            //Instantiate a server synchronization provider and specify it
            //as the remote provider for this synchronization agent.
            this.RemoteProvider = new SampleServerSyncProvider();

            //Add the Customer table: specify a synchronization direction 
            //of Bidirectional.
            SyncTable customerSyncTable = new SyncTable("Customer");
            customerSyncTable.CreationOption = TableCreationOption.DropExistingOrCreateNewTable;
            customerSyncTable.SyncDirection = SyncDirection.Bidirectional;
            this.Configuration.SyncTables.Add(customerSyncTable);
        }
    }


    //Create a class that is derived from 
    //Microsoft.Synchronization.Server.DbServerSyncProvider.
    public class SampleServerSyncProvider : DbServerSyncProvider
    {
        public SampleServerSyncProvider()
        {
            //Create a connection to the sample server database.
            Utility util = new Utility();
            SqlConnection serverConn = new SqlConnection(Utility.ConnStr_DbServerSync);
            this.Connection = serverConn;

            //Create a command to retrieve a new anchor value from
            //the server. In this case, we use a timestamp value
            //that is retrieved and stored in the client database.
            //During each synchronization, the new anchor value and
            //the last anchor value from the previous synchronization
            //are used: the set of changes between these upper and
            //lower bounds is synchronized.
            //
            //SyncSession.SyncNewReceivedAnchor is a string constant; 
            //you could also use @sync_new_received_anchor directly in 
            //your queries.
            SqlCommand selectNewAnchorCommand = new SqlCommand();
            string newAnchorVariable = "@" + SyncSession.SyncNewReceivedAnchor;
            selectNewAnchorCommand.CommandText = "SELECT " + newAnchorVariable + " = min_active_rowversion() - 1";
            selectNewAnchorCommand.Parameters.Add(newAnchorVariable, SqlDbType.Timestamp);
            selectNewAnchorCommand.Parameters[newAnchorVariable].Direction = ParameterDirection.Output;
            selectNewAnchorCommand.Connection = serverConn;
            this.SelectNewAnchorCommand = selectNewAnchorCommand;


            //Create a SyncAdapter for the Customer table, and then define
            //the commands to synchronize changes:
            //* SelectConflictUpdatedRowsCommand SelectConflictDeletedRowsCommand
            //  are used to detect if there are conflicts on the server during
            //  synchronization.
            //* SelectIncrementalInsertsCommand, SelectIncrementalUpdatesCommand,
            //  and SelectIncrementalDeletesCommand are used to select changes
            //  from the server that the client provider then applies to the client.
            //* InsertCommand, UpdateCommand, and DeleteCommand are used to apply
            //  to the server the changes that the client provider has selected
            //  from the client.

            //Create the SyncAdapter.
            SyncAdapter customerSyncAdapter = new SyncAdapter("Customer");

            //This command is used if @sync_row_count returns
            //0 when changes are applied to the server.
            SqlCommand customerUpdateConflicts = new SqlCommand();
            customerUpdateConflicts.CommandText =
                "SELECT CustomerId, CustomerName, SalesPerson, CustomerType " +
                "FROM Sales.Customer " +
                "WHERE CustomerId = @CustomerId";
            customerUpdateConflicts.Parameters.Add("@CustomerId", SqlDbType.UniqueIdentifier);
            customerUpdateConflicts.Connection = serverConn;
            customerSyncAdapter.SelectConflictUpdatedRowsCommand = customerUpdateConflicts;

            //This command is used if the server provider cannot find
            //a row in the base table.
            SqlCommand customerDeleteConflicts = new SqlCommand();
            customerDeleteConflicts.CommandText =
                "SELECT CustomerId, CustomerName, SalesPerson, CustomerType " +
                "FROM Sales.Customer_Tombstone " +
                "WHERE CustomerId = @CustomerId";
            customerDeleteConflicts.Parameters.Add("@CustomerId", SqlDbType.UniqueIdentifier);
            customerDeleteConflicts.Connection = serverConn;
            customerSyncAdapter.SelectConflictDeletedRowsCommand = customerDeleteConflicts;

            //Select inserts from the server.
            SqlCommand customerIncrInserts = new SqlCommand();
            customerIncrInserts.CommandText =
                "SELECT CustomerId, CustomerName, SalesPerson, CustomerType " +
                "FROM Sales.Customer " +
                "WHERE (InsertTimestamp > @sync_last_received_anchor " +
                "AND InsertTimestamp <= @sync_new_received_anchor " +
                "AND InsertId <> @sync_client_id)";
            customerIncrInserts.Parameters.Add("@" + SyncSession.SyncLastReceivedAnchor, SqlDbType.Timestamp);
            customerIncrInserts.Parameters.Add("@" + SyncSession.SyncNewReceivedAnchor, SqlDbType.Timestamp);
            customerIncrInserts.Parameters.Add("@" + SyncSession.SyncClientId, SqlDbType.UniqueIdentifier);
            customerIncrInserts.Connection = serverConn;
            customerSyncAdapter.SelectIncrementalInsertsCommand = customerIncrInserts;

            //Apply inserts to the server.
            SqlCommand customerInserts = new SqlCommand();
            customerInserts.CommandType = CommandType.StoredProcedure;
            customerInserts.CommandText = "usp_CustomerApplyInsert";
            customerInserts.Parameters.Add("@" + SyncSession.SyncClientId, SqlDbType.UniqueIdentifier);
            customerInserts.Parameters.Add("@" + SyncSession.SyncForceWrite, SqlDbType.Bit); 
            customerInserts.Parameters.Add("@" + SyncSession.SyncRowCount, SqlDbType.Int).Direction = ParameterDirection.Output;
            customerInserts.Parameters.Add("@CustomerId", SqlDbType.UniqueIdentifier);
            customerInserts.Parameters.Add("@CustomerName", SqlDbType.NVarChar);
            customerInserts.Parameters.Add("@SalesPerson", SqlDbType.NVarChar);
            customerInserts.Parameters.Add("@CustomerType", SqlDbType.NVarChar);
            customerInserts.Connection = serverConn;
            customerSyncAdapter.InsertCommand = customerInserts;


            //Select updates from the server.
            SqlCommand customerIncrUpdates = new SqlCommand();
            customerIncrUpdates.CommandText =
                "SELECT CustomerId, CustomerName, SalesPerson, CustomerType " +
                "FROM Sales.Customer " +
                "WHERE (UpdateTimestamp > @sync_last_received_anchor " +
                "AND UpdateTimestamp <= @sync_new_received_anchor " +
                "AND UpdateId <> @sync_client_id " +
                "AND NOT (InsertTimestamp > @sync_last_received_anchor " +
                "AND InsertId <> @sync_client_id))";
            customerIncrUpdates.Parameters.Add("@" + SyncSession.SyncLastReceivedAnchor, SqlDbType.Timestamp);
            customerIncrUpdates.Parameters.Add("@" + SyncSession.SyncNewReceivedAnchor, SqlDbType.Timestamp);
            customerIncrUpdates.Parameters.Add("@" + SyncSession.SyncClientId, SqlDbType.UniqueIdentifier);
            customerIncrUpdates.Connection = serverConn;
            customerSyncAdapter.SelectIncrementalUpdatesCommand = customerIncrUpdates;

            //Apply updates to the server.
            SqlCommand customerUpdates = new SqlCommand();
            customerUpdates.CommandType = CommandType.StoredProcedure;
            customerUpdates.CommandText = "usp_CustomerApplyUpdate";
            customerUpdates.Parameters.Add("@" + SyncSession.SyncLastReceivedAnchor, SqlDbType.Timestamp);
            customerUpdates.Parameters.Add("@" + SyncSession.SyncClientId, SqlDbType.UniqueIdentifier);
            customerUpdates.Parameters.Add("@" + SyncSession.SyncForceWrite, SqlDbType.Bit);            
            customerUpdates.Parameters.Add("@" + SyncSession.SyncRowCount, SqlDbType.Int).Direction = ParameterDirection.Output;
            customerUpdates.Parameters.Add("@CustomerId", SqlDbType.UniqueIdentifier);
            customerUpdates.Parameters.Add("@CustomerName", SqlDbType.NVarChar);
            customerUpdates.Parameters.Add("@SalesPerson", SqlDbType.NVarChar);
            customerUpdates.Parameters.Add("@CustomerType", SqlDbType.NVarChar);
            customerUpdates.Connection = serverConn;
            customerSyncAdapter.UpdateCommand = customerUpdates;


            //Select deletes from the server.
            SqlCommand customerIncrDeletes = new SqlCommand();
            customerIncrDeletes.CommandText =
                "SELECT CustomerId, CustomerName, SalesPerson, CustomerType " +
                "FROM Sales.Customer_Tombstone " +
                "WHERE (@sync_initialized = 1 " +
                "AND DeleteTimestamp > @sync_last_received_anchor " +
                "AND DeleteTimestamp <= @sync_new_received_anchor " +
                "AND DeleteId <> @sync_client_id)";
            customerIncrDeletes.Parameters.Add("@" + SyncSession.SyncInitialized, SqlDbType.Bit);
            customerIncrDeletes.Parameters.Add("@" + SyncSession.SyncLastReceivedAnchor, SqlDbType.Timestamp);
            customerIncrDeletes.Parameters.Add("@" + SyncSession.SyncNewReceivedAnchor, SqlDbType.Timestamp);
            customerIncrDeletes.Parameters.Add("@" + SyncSession.SyncClientId, SqlDbType.UniqueIdentifier);
            customerIncrDeletes.Connection = serverConn;
            customerSyncAdapter.SelectIncrementalDeletesCommand = customerIncrDeletes;

            //Apply deletes to the server.
            SqlCommand customerDeletes = new SqlCommand();
            customerDeletes.CommandType = CommandType.StoredProcedure;
            customerDeletes.CommandText = "usp_CustomerApplyDelete";
            customerDeletes.Parameters.Add("@" + SyncSession.SyncLastReceivedAnchor, SqlDbType.Timestamp);
            customerDeletes.Parameters.Add("@" + SyncSession.SyncClientId, SqlDbType.UniqueIdentifier);
            customerDeletes.Parameters.Add("@" + SyncSession.SyncForceWrite, SqlDbType.Bit);           
            customerDeletes.Parameters.Add("@" + SyncSession.SyncRowCount, SqlDbType.Int).Direction = ParameterDirection.Output;
            customerDeletes.Parameters.Add("@CustomerId", SqlDbType.UniqueIdentifier);
            customerDeletes.Connection = serverConn;
            customerSyncAdapter.DeleteCommand = customerDeletes;


            //Add the SyncAdapter to the server synchronization provider.
            this.SyncAdapters.Add(customerSyncAdapter);


            //Handle the ApplyChangeFailed and ChangesApplied events. 
            //This allows us to respond to any conflicts that occur, and to 
            //make changes that are downloaded to the client during the same
            //session.
            this.ApplyChangeFailed +=new EventHandler<ApplyChangeFailedEventArgs>(SampleServerSyncProvider_ApplyChangeFailed);
            this.ChangesApplied +=new EventHandler<ChangesAppliedEventArgs>(SampleServerSyncProvider_ChangesApplied);
        }

        //Create a list to hold primary keys from the Customer
        //table. This list is used when we handle the ApplyChangeFailed 
        //and ChangesApplied events.
        private List<Guid> _updateConflictGuids = new List<Guid>();
        
        private void SampleServerSyncProvider_ApplyChangeFailed(object sender, ApplyChangeFailedEventArgs e)
        {

            //Log information for the ApplyChangeFailed event.
            EventLogger.LogEvents(sender, e);

            //Respond to four different types of conflicts:
            // * ClientDeleteServerUpdate
            // * ClientUpdateServerDelete
            // * ClientInsertServerInsert
            // * ClientUpdateServerUpdate
            //
            if (e.Conflict.ConflictType == ConflictType.ClientDeleteServerUpdate)
            {
                //With the commands we are using, the default is for the server 
                //change to win and be applied to the client. Here, we accept the 
                //default on the server side. We also set ConflictResolver.ServerWins 
                //for this conflict in the client provider. This ensures that the server
                //change is applied to the client during the download phase.
                Console.WriteLine(String.Empty);
                Console.WriteLine("***********************************");
                Console.WriteLine("A client delete / server update conflict was detected.");

                e.Action = ApplyAction.Continue;

                Console.WriteLine("The server change will be applied at the client.");
                Console.WriteLine("***********************************");
                Console.WriteLine(String.Empty);
            }

            if (e.Conflict.ConflictType == ConflictType.ClientUpdateServerDelete)
            {

                //For client-update/server-delete conflicts, we force the client 
                //change to be applied at the server. The stored procedure specified for 
                //customerSyncAdapter.UpdateCommand accepts the @sync_force_write parameter
                //and includes logic to handle this case.
                Console.WriteLine(String.Empty);
                Console.WriteLine("***********************************");
                Console.WriteLine("A client update / server delete conflict was detected.");

                e.Action = ApplyAction.RetryWithForceWrite;
                
                Console.WriteLine("The client change was retried at the server with RetryWithForceWrite.");
                Console.WriteLine("***********************************"); 
                Console.WriteLine(String.Empty);
             
            }

            if (e.Conflict.ConflictType == ConflictType.ClientInsertServerInsert)
            {
                //Similar to how we handled the client-delete/server-update conflict.
                //In this case, we set ConflictResolver.FireEvent and use RetryWithForceWrite
                //for this conflict in the client provider. This is equivalent to 
                //ConflictResolver.ServerWins, and ensures that the server
                //change is applied to the client during the download phase.
                Console.WriteLine(String.Empty);
                Console.WriteLine("***********************************");
                Console.WriteLine("A client insert / server insert conflict was detected.");

                e.Action = ApplyAction.Continue;

                Console.WriteLine("The server change will be applied at the client.");
                Console.WriteLine("***********************************");
                Console.WriteLine(String.Empty);
            }

            if (e.Conflict.ConflictType == ConflictType.ClientUpdateServerUpdate)
            {

                //For client-update/server-update conflicts, we want to
                //allow the user to specify the conflict resolution option.
                //
                //It is possible for the Conflict object from the
                //server to have more than one row. Because our custom
                //resolution code only works with one row at a time,
                //we only allow the user to select a resolution
                //option if the object contains a single row.
                if (e.Conflict.ServerChange.Rows.Count > 1)
                {
                    Console.WriteLine(String.Empty);
                    Console.WriteLine("***********************************");
                    Console.WriteLine("A client update / server update conflict was detected.");

                    e.Action = ApplyAction.Continue;

                    Console.WriteLine("The server change will be applied at the client.");
                    Console.WriteLine("***********************************");
                    Console.WriteLine(String.Empty);
                }
                else
                {
                    Console.WriteLine(String.Empty);
                    Console.WriteLine("***********************************");
                    Console.WriteLine("A client update / server update conflict was detected.");
                    Console.WriteLine("Conflicting rows are displayed below.");
                    Console.WriteLine("***********************************");

                    //Get the conflicting changes from the Conflict object
                    //and display them. The Conflict object holds a copy
                    //of the changes; updates to this object will not be 
                    //applied. To make changes, use the Context object,
                    //which is demonstrated in the next section of code
                    //under ' case "CU" '.
                    DataTable conflictingServerChange = e.Conflict.ServerChange;
                    DataTable conflictingClientChange = e.Conflict.ClientChange;
                    int serverColumnCount = conflictingServerChange.Columns.Count;
                    int clientColumnCount = conflictingClientChange.Columns.Count;
                    
                    Console.WriteLine(String.Empty);
                    Console.WriteLine("Server row: ");
                    Console.Write(" | ");

                    //Display the server row.
                    for (int i = 0; i < serverColumnCount; i++)
                    {
                        Console.Write(conflictingServerChange.Rows[0][i] + " | ");
                    }

                    Console.WriteLine(String.Empty);
                    Console.WriteLine(String.Empty);
                    Console.WriteLine("Client row: ");
                    Console.Write(" | ");

                    //Display the client row.
                    for (int i = 0; i < clientColumnCount; i++)
                    {
                        Console.Write(conflictingClientChange.Rows[0][i] + " | ");
                    }

                    Console.WriteLine(String.Empty);
                    Console.WriteLine(String.Empty);

                    //Ask for a conflict resolution option.
                    Console.WriteLine("Enter a resolution option for this conflict:");
                    Console.WriteLine("SE = server change wins");
                    Console.WriteLine("CL = client change wins");
                    Console.WriteLine("CU = custom resolution (combine rows)");

                    string conflictResolution = Console.ReadLine();
                    conflictResolution.ToUpper();

                    switch (conflictResolution)
                    {
                        case "SE":

                            //Again, this this is the default for the commands we are using:
                            //the server change is persisted and then downloaded to the client.
                            e.Action = ApplyAction.Continue;
                            Console.WriteLine(String.Empty);
                            Console.WriteLine("The server change will be applied at the client.");

                            break;

                        case "CL":

                            //Override the default by specifying that the client row
                            //should be applied at the server. The stored procedure specified  
                            //for customerSyncAdapter.UpdateCommand accepts the @sync_force_write 
                            //parameter and includes logic to handle this case.
                            e.Action = ApplyAction.RetryWithForceWrite;
                            Console.WriteLine(String.Empty);
                            Console.WriteLine("The client change was retried at the server with RetryWithForceWrite.");

                            break;

                        case "CU":

                            //Provide a custom resolution scheme that takes each conflicting
                            //column and applies the combined contents of the column to the 
                            //client and server. This is not necessarily a resolution scheme 
                            //that you would use in production. Instead, it is used to 
                            //demonstrate the various ways you can interact with conflicting 
                            //data during synchronization.
                            //
                            //Get the ID for the conflicting row from the client data table,
                            //and add it to a list of GUIDs. We update rows at the server
                            //based on this list.
                            Guid customerId = (Guid)conflictingClientChange.Rows[0]["CustomerId"];
                            _updateConflictGuids.Add(customerId);
                            
                            //Create a dictionary to hold the column ordinal and value for
                            //any columns that are in confict.
                            Dictionary<int, string> conflictingColumns = new Dictionary<int, string>();
                            string combinedColumnValue;

                            //Determine which columns are different at the client and server.
                            //We already looped through these columns once, but we wanted to
                            //keep this code separate from the display code above.
                            for (int i = 0; i < clientColumnCount; i++)
                            {
                                if (conflictingClientChange.Rows[0][i].ToString() != conflictingServerChange.Rows[0][i].ToString())
                                {
                                    //If we find a column that is different, combine the values from
                                    //the client and server, and write "| conflict |" between them.
                                    combinedColumnValue = conflictingClientChange.Rows[0][i] + "  | conflict |  " + 
                                        conflictingServerChange.Rows[0][i];
                                    conflictingColumns.Add(i, combinedColumnValue);
                                }
                            }

                            //Loop through the rows in the Context object, which exposes
                            //the set of changes that are uploaded from the client.
                            //Note: In the ApplyChangeFailed event for the client provider,  
                            //you have access to the set of changes that was downloaded from
                            //the server.
                            DataTable allClientChanges = e.Context.DataSet.Tables["Customer"];
                            int allClientRowCount = allClientChanges.Rows.Count;
                            int allClientColumnCount = allClientChanges.Columns.Count;

                            for (int i = 0; i < allClientRowCount; i++)
                            {
                                //Find the changed row with the GUID from the Conflict object.
                                if (allClientChanges.Rows[i].RowState == DataRowState.Modified &&
                                    (Guid)allClientChanges.Rows[i]["CustomerId"] == customerId)
                                {
                                    //Loop through the columns and check whether the column
                                    //is in the conflictingColumns dictionary. If it is,
                                    //update the value in the allClientChanges Context object.
                                    for (int j = 0; j < allClientColumnCount; j++)
                                    {
                                        if (conflictingColumns.ContainsKey(j))
                                        {
                                            allClientChanges.Rows[i][j] = conflictingColumns[j];
                                        }
                                    }
                                }
                            }

                            //Apply the changed row with its combined values to the server.
                            //This change will persist at the server, but it will not be 
                            //downloaded with the SelectIncrementalUpdate command that we use.
                            //It will not download the change because it checks for the UpdateId,
                            //which is still set to the client that made the upload.
                            //We use the ChangesApplied event to set the UpdateId for the
                            //change to a value that represents the server. This ensures
                            //that the change is applied at the client during the download
                            //phase of synchronization (see SampleServerSyncProvider_ChangesApplied).
                            e.Action = ApplyAction.RetryWithForceWrite;

                            Console.WriteLine(String.Empty);
                            Console.WriteLine("The custom change was retried at the server with RetryWithForceWrite.");

                            break;

                        default:
                            Console.WriteLine(String.Empty);
                            Console.WriteLine("Not a valid resolution option.");
                            
                            break;
                    }
                
                }

                Console.WriteLine(String.Empty);
            }
        }

        private void SampleServerSyncProvider_ChangesApplied(object sender, ChangesAppliedEventArgs e)
        {
            //If _updateConflictGuids contains at least one GUID, update the UpdateId
            //column so that each change is downloaded to the client. For more
            //information, see SampleServerSyncProvider_ApplyChangeFailed.
            if (_updateConflictGuids.Count > 0)
            {
                SqlCommand updateTable = new SqlCommand();
                updateTable.Connection = (SqlConnection)e.Connection;
                updateTable.Transaction = (SqlTransaction)e.Transaction;
                updateTable.CommandText = String.Empty;

                for (int i = 0; i < _updateConflictGuids.Count; i++)
                {
                    updateTable.CommandText +=
                        " UPDATE Sales.Customer SET UpdateId = '00000000-0000-0000-0000-000000000000' " +
                        " WHERE CustomerId='" + _updateConflictGuids[i].ToString() + "'";
                }

                updateTable.ExecuteNonQuery();
            }
        }
    }

    //Create a class that is derived from 
    //Microsoft.Synchronization.Data.SqlServerCe.SqlCeClientSyncProvider.
    //You can just instantiate the provider directly and associate it
    //with the SyncAgent, but here we use this class to handle client 
    //provider events.
    public class SampleClientSyncProvider : SqlCeClientSyncProvider
    {

        public SampleClientSyncProvider()
        {
            //Specify a connection string for the sample client database.
            //By default, the client database is created if it does not
            //exist.
            Utility util = new Utility();
            this.ConnectionString = Utility.ConnStr_SqlCeClientSync;

            //Specify conflict resolution options for each type of
            //conflict or error that can occur. The client and server are
            //independent; therefore, these settings have no effect when changes 
            //are applied at the server. However, settings should agree with each
            //other. For example:
            // * We specify a value of ServerWins for client delete /
            //   server update. On the server side, by default our commands will 
            //   ignore the conflicting delete and download the update to the 
            //   client. ServerWins is equivalent to setting RetryWithForceWrite
            //   on the client.
            // * Conversely, we specify a value of ClientWins for client update /
            //   server delete. On the server side, we specify that our commands 
            //   should force write the update by turning it into an insert.
            this.ConflictResolver.ClientDeleteServerUpdateAction = ResolveAction.ServerWins;            
            this.ConflictResolver.ClientUpdateServerDeleteAction = ResolveAction.ClientWins;
            //If any of the following conflicts or errors occur, the ApplyChangeFailed
            //event is raised.
            this.ConflictResolver.ClientInsertServerInsertAction = ResolveAction.FireEvent;
            this.ConflictResolver.ClientUpdateServerUpdateAction = ResolveAction.FireEvent;
            this.ConflictResolver.StoreErrorAction = ResolveAction.FireEvent;

            //Log information for the ApplyChangeFailed event and handle any
            //ResolveAction.FireEvent cases.
            this.ApplyChangeFailed +=new EventHandler<ApplyChangeFailedEventArgs>(SampleClientSyncProvider_ApplyChangeFailed);

            //Use the following events to fix up schema on the client.
            //We use the CreatingSchema event to change the schema
            //by using the API. We use the SchemaCreated event 
            //to change the schema by using SQL.
            this.CreatingSchema += new EventHandler<CreatingSchemaEventArgs>(SampleClientSyncProvider_CreatingSchema);
            this.SchemaCreated += new EventHandler<SchemaCreatedEventArgs>(SampleClientSyncProvider_SchemaCreated);

        }

        private void SampleClientSyncProvider_ApplyChangeFailed(object sender, ApplyChangeFailedEventArgs e)
        {

            //Log event data from the client side.
            EventLogger.LogEvents(sender, e);

            //Force write any inserted server rows that are in conflict 
            //when they are downloaded.
            if (e.Conflict.ConflictType == ConflictType.ClientInsertServerInsert)
            {
                e.Action = ApplyAction.RetryWithForceWrite;
            }

            if (e.Conflict.ConflictType == ConflictType.ClientUpdateServerUpdate)
            {
                //Logic goes here.
            }

            if (e.Conflict.ConflictType == ConflictType.ErrorsOccurred)
            {
                //Logic goes here.
            }

        }

        private void SampleClientSyncProvider_CreatingSchema(object sender, CreatingSchemaEventArgs e)
        {
            
            //Set the RowGuid property because it is not copied
            //to the client by default. This is also a good time
            //to specify literal defaults with .Columns[ColName].DefaultValue,
            //but we will specify defaults like NEWID() by calling
            //ALTER TABLE after the table is created.
            e.Schema.Tables["Customer"].Columns["CustomerId"].RowGuid = true;
          
        }

        private void SampleClientSyncProvider_SchemaCreated(object sender, SchemaCreatedEventArgs e)
        {
            string tableName = e.Table.TableName;
            Utility util = new Utility();

            //Call ALTER TABLE on the client. This must be done
            //over the same connection and within the same
            //transaction that Sync Framework uses
            //to create the schema on the client.
            Utility.MakeSchemaChangesOnClient(e.Connection, e.Transaction, "Customer");
 
        }
    }

    //Handle the statistics that are returned by the SyncAgent.
    public class SampleStats
    {
        public void DisplayStats(SyncStatistics syncStatistics, string syncType)
        {
            Console.WriteLine(String.Empty);
            if (syncType == "initial")
            {
                Console.WriteLine("****** Initial Synchronization ******");
            }
            else if (syncType == "subsequent")
            {
                Console.WriteLine("***** Subsequent Synchronization ****");
            }

            Console.WriteLine("Start Time: " + syncStatistics.SyncStartTime);
            Console.WriteLine("Upload Changes Applied: " + syncStatistics.UploadChangesApplied);
            Console.WriteLine("Upload Changes Failed: " + syncStatistics.UploadChangesFailed);
            Console.WriteLine("Total Changes Uploaded: " + syncStatistics.TotalChangesUploaded);
            Console.WriteLine("Download Changes Applied: " + syncStatistics.DownloadChangesApplied);
            Console.WriteLine("Download Changes Failed: " + syncStatistics.DownloadChangesFailed);
            Console.WriteLine("Total Changes Downloaded: " + syncStatistics.TotalChangesDownloaded);
            Console.WriteLine("Complete Time: " + syncStatistics.SyncCompleteTime);
            Console.WriteLine(String.Empty);
        }
    }

    public class EventLogger
    {
        //Create client and server log files, and write to them
        //based on data from the ApplyChangeFailedEventArgs.
        public static void LogEvents(object sender, ApplyChangeFailedEventArgs e)
        {
            string logFile = String.Empty;
            string site = String.Empty;

            if (sender is SampleServerSyncProvider)
            {
                logFile = "ServerLogFile.txt";
                site = "server";
            }
            else if (sender is SampleClientSyncProvider)
            {
                logFile = "ClientLogFile.txt";
                site = "client";
            }

            StreamWriter streamWriter = File.AppendText(logFile);
            StringBuilder outputText = new StringBuilder();

            outputText.AppendLine("** CONFLICTING CHANGE OR ERROR AT " + site.ToUpper() + " **");
            outputText.AppendLine("Table for which error or conflict occurred: " + e.TableMetadata.TableName);
            outputText.AppendLine("Sync stage: " + e.Conflict.SyncStage);
            outputText.AppendLine("Conflict type: " + e.Conflict.ConflictType);

            //If it is a data conflict instead of an error, print out
            //the values of the rows at the client and server.
            if (e.Conflict.ConflictType != ConflictType.ErrorsOccurred && 
                e.Conflict.ConflictType != ConflictType.Unknown)
            {

                DataTable serverChange = e.Conflict.ServerChange;
                DataTable clientChange = e.Conflict.ClientChange;
                int serverRows = serverChange.Rows.Count;
                int clientRows = clientChange.Rows.Count;
                int serverColumns = serverChange.Columns.Count;
                int clientColumns = clientChange.Columns.Count;

                for (int i = 0; i < serverRows; i++)
                {
                    outputText.Append("Server row: ");
                    
                    for (int j = 0; j < serverColumns; j++)
                    {
                        outputText.Append(serverChange.Rows[i][j] + " | ");

                    }

                    outputText.AppendLine(String.Empty);
                }

                for (int i = 0; i < clientRows; i++)
                {
                    outputText.Append("Client row: ");
                    
                    for (int j = 0; j < clientColumns; j++)
                    {
                        outputText.Append(clientChange.Rows[i][j] + " | ");
                    }

                    outputText.AppendLine(String.Empty);
                }
            }

            if (e.Conflict.ConflictType == ConflictType.ErrorsOccurred)
            {
                outputText.AppendLine("Error message: " + e.Error.Message);
            }

            streamWriter.WriteLine(DateTime.Now.ToShortTimeString() + " | " + outputText.ToString());
            streamWriter.Flush();
            streamWriter.Dispose();
            
        }
    }
}
Imports System
Imports System.Collections
Imports System.Collections.Generic
Imports System.IO
Imports System.Text
Imports System.Data
Imports System.Data.SqlClient
Imports System.Data.SqlServerCe
Imports Microsoft.Synchronization
Imports Microsoft.Synchronization.Data
Imports Microsoft.Synchronization.Data.Server
Imports Microsoft.Synchronization.Data.SqlServerCe



Class Program

    Shared Sub Main(ByVal args() As String)

        'The SampleStats class handles information from the SyncStatistics
        'object that the Synchronize method returns.
        Dim sampleStats As New SampleStats()

        'Request a password for the client database, and delete
        'and re-create the database. The client synchronization
        'provider also enables you to create the client database 
        'if it does not exist.
        Utility.SetPassword_SqlCeClientSync()
        Utility.DeleteAndRecreateCompactDatabase(Utility.ConnStr_SqlCeClientSync, True)

        'Initial synchronization. Instantiate the SyncAgent
        'and call Synchronize.
        Dim sampleSyncAgent As New SampleSyncAgent()
        Dim syncStatistics As SyncStatistics = sampleSyncAgent.Synchronize()
        sampleStats.DisplayStats(syncStatistics, "initial")

        'Make a change at the client that fails when it is
        'applied at the server.
        Utility.MakeFailingChangeOnClient()

        'Make changes at the client and server that conflict
        'when they are synchronized.
        Utility.MakeConflictingChangesOnClientAndServer()

        'Subsequent synchronization.
        syncStatistics = sampleSyncAgent.Synchronize()
        sampleStats.DisplayStats(syncStatistics, "subsequent")

        'Return server data back to its original state.
        'Comment out this line if you want to view the
        'state of the data after all conflicts are resolved.
        Utility.CleanUpServer()

        'Exit.
        Console.Write(vbLf + "Press Enter to close the window.")
        Console.ReadLine()

    End Sub 'Main
End Class 'Program

'Create a class that is derived from 
'Microsoft.Synchronization.SyncAgent.

Public Class SampleSyncAgent
    Inherits SyncAgent

    Public Sub New()
        'Instantiate a client synchronization provider and specify it
        'as the local provider for this synchronization agent.
        Me.LocalProvider = New SampleClientSyncProvider()

        'Instantiate a server synchronization provider and specify it
        'as the remote provider for this synchronization agent.
        Me.RemoteProvider = New SampleServerSyncProvider()

        'Add the Customer table: specify a synchronization direction 
        'of Bidirectional.
        Dim customerSyncTable As New SyncTable("Customer")
        customerSyncTable.CreationOption = TableCreationOption.DropExistingOrCreateNewTable
        customerSyncTable.SyncDirection = SyncDirection.Bidirectional
        Me.Configuration.SyncTables.Add(customerSyncTable)

    End Sub 'New
End Class 'SampleSyncAgent


'Create a class that is derived from 
'Microsoft.Synchronization.Server.DbServerSyncProvider.

Public Class SampleServerSyncProvider
    Inherits DbServerSyncProvider

    Public Sub New()
        'Create a connection to the sample server database.
        Dim util As New Utility()
        Dim serverConn As New SqlConnection(Utility.ConnStr_DbServerSync)
        Me.Connection = serverConn

        'Create a command to retrieve a new anchor value from
        'the server. In this case, we use a timestamp value
        'that is retrieved and stored in the client database.
        'During each synchronization, the new anchor value and
        'the last anchor value from the previous synchronization
        'are used: the set of changes between these upper and
        'lower bounds is synchronized.
        '
        'SyncSession.SyncNewReceivedAnchor is a string constant; 
        'you could also use @sync_new_received_anchor directly in 
        'your queries.
        Dim selectNewAnchorCommand As New SqlCommand()
        Dim newAnchorVariable As String = "@" + SyncSession.SyncNewReceivedAnchor
        With selectNewAnchorCommand
            .CommandText = "SELECT " + newAnchorVariable + " = min_active_rowversion() - 1"
            .Parameters.Add(newAnchorVariable, SqlDbType.Timestamp)
            .Parameters(newAnchorVariable).Direction = ParameterDirection.Output
            .Connection = serverConn
        End With
        Me.SelectNewAnchorCommand = selectNewAnchorCommand


        'Create a SyncAdapter for the Customer table, and then define
        'the commands to synchronize changes:
        '* SelectConflictUpdatedRowsCommand SelectConflictDeletedRowsCommand
        '  are used to detect if there are conflicts on the server during
        '  synchronization.
        '* SelectIncrementalInsertsCommand, SelectIncrementalUpdatesCommand,
        '  and SelectIncrementalDeletesCommand are used to select changes
        '  from the server that the client provider then applies to the client.
        '* InsertCommand, UpdateCommand, and DeleteCommand are used to apply
        '  to the server the changes that the client provider has selected
        '  from the client.
        'Create the SyncAdapter.
        Dim customerSyncAdapter As New SyncAdapter("Customer")

        'This command is used if @sync_row_count returns
        '0 when changes are applied to the server.
        Dim customerUpdateConflicts As New SqlCommand()
        With customerUpdateConflicts
            .CommandText = _
                "SELECT CustomerId, CustomerName, SalesPerson, CustomerType " _
              & "FROM Sales.Customer " + "WHERE CustomerId = @CustomerId"
            .Parameters.Add("@CustomerId", SqlDbType.UniqueIdentifier)
            .Connection = serverConn
        End With
        customerSyncAdapter.SelectConflictUpdatedRowsCommand = customerUpdateConflicts

        'This command is used if the server provider cannot find
        'a row in the base table.
        Dim customerDeleteConflicts As New SqlCommand()
        With customerDeleteConflicts
            .CommandText = _
                "SELECT CustomerId, CustomerName, SalesPerson, CustomerType " _
              & "FROM Sales.Customer_Tombstone " + "WHERE CustomerId = @CustomerId"
            .Parameters.Add("@CustomerId", SqlDbType.UniqueIdentifier)
            .Connection = serverConn
        End With
        customerSyncAdapter.SelectConflictDeletedRowsCommand = customerDeleteConflicts

        'Select inserts from the server.
        Dim customerIncrInserts As New SqlCommand()
        With customerIncrInserts
            .CommandText = _
                "SELECT CustomerId, CustomerName, SalesPerson, CustomerType " _
              & "FROM Sales.Customer " _
              & "WHERE (InsertTimestamp > @sync_last_received_anchor " _
              & "AND InsertTimestamp <= @sync_new_received_anchor " _
              & "AND InsertId <> @sync_client_id)"
            .Parameters.Add("@" + SyncSession.SyncLastReceivedAnchor, SqlDbType.Timestamp)
            .Parameters.Add("@" + SyncSession.SyncNewReceivedAnchor, SqlDbType.Timestamp)
            .Parameters.Add("@" + SyncSession.SyncClientId, SqlDbType.UniqueIdentifier)
            .Connection = serverConn
        End With
        customerSyncAdapter.SelectIncrementalInsertsCommand = customerIncrInserts

        'Apply inserts to the server.
        Dim customerInserts As New SqlCommand()
        customerInserts.CommandType = CommandType.StoredProcedure
        customerInserts.CommandText = "usp_CustomerApplyInsert"
        customerInserts.Parameters.Add("@" + SyncSession.SyncClientId, SqlDbType.UniqueIdentifier)
        customerInserts.Parameters.Add("@" + SyncSession.SyncForceWrite, SqlDbType.Bit)
        customerInserts.Parameters.Add("@" + SyncSession.SyncRowCount, SqlDbType.Int).Direction = ParameterDirection.Output
        customerInserts.Parameters.Add("@CustomerId", SqlDbType.UniqueIdentifier)
        customerInserts.Parameters.Add("@CustomerName", SqlDbType.NVarChar)
        customerInserts.Parameters.Add("@SalesPerson", SqlDbType.NVarChar)
        customerInserts.Parameters.Add("@CustomerType", SqlDbType.NVarChar)
        customerInserts.Connection = serverConn
        customerSyncAdapter.InsertCommand = customerInserts


        'Select updates from the server.
        Dim customerIncrUpdates As New SqlCommand()
        With customerIncrUpdates
            .CommandText = _
                "SELECT CustomerId, CustomerName, SalesPerson, CustomerType " _
              & "FROM Sales.Customer " _
              & "WHERE (UpdateTimestamp > @sync_last_received_anchor " _
              & "AND UpdateTimestamp <= @sync_new_received_anchor " _
              & "AND UpdateId <> @sync_client_id " _
              & "AND NOT (InsertTimestamp > @sync_last_received_anchor " _
              & "AND InsertId <> @sync_client_id))"
            .Parameters.Add("@" + SyncSession.SyncLastReceivedAnchor, SqlDbType.Timestamp)
            .Parameters.Add("@" + SyncSession.SyncNewReceivedAnchor, SqlDbType.Timestamp)
            .Parameters.Add("@" + SyncSession.SyncClientId, SqlDbType.UniqueIdentifier)
            .Connection = serverConn
        End With
        customerSyncAdapter.SelectIncrementalUpdatesCommand = customerIncrUpdates

        'Apply updates to the server.
        Dim customerUpdates As New SqlCommand()
        customerUpdates.CommandType = CommandType.StoredProcedure
        customerUpdates.CommandText = "usp_CustomerApplyUpdate"
        customerUpdates.Parameters.Add("@" + SyncSession.SyncLastReceivedAnchor, SqlDbType.Timestamp)
        customerUpdates.Parameters.Add("@" + SyncSession.SyncClientId, SqlDbType.UniqueIdentifier)
        customerUpdates.Parameters.Add("@" + SyncSession.SyncForceWrite, SqlDbType.Bit)
        customerUpdates.Parameters.Add("@" + SyncSession.SyncRowCount, SqlDbType.Int).Direction = ParameterDirection.Output
        customerUpdates.Parameters.Add("@CustomerId", SqlDbType.UniqueIdentifier)
        customerUpdates.Parameters.Add("@CustomerName", SqlDbType.NVarChar)
        customerUpdates.Parameters.Add("@SalesPerson", SqlDbType.NVarChar)
        customerUpdates.Parameters.Add("@CustomerType", SqlDbType.NVarChar)
        customerUpdates.Connection = serverConn
        customerSyncAdapter.UpdateCommand = customerUpdates


        'Select deletes from the server.
        Dim customerIncrDeletes As New SqlCommand()
        With customerIncrDeletes
            .CommandText = _
                "SELECT CustomerId, CustomerName, SalesPerson, CustomerType " _
              & "FROM Sales.Customer_Tombstone " _
              & "WHERE (@sync_initialized = 1 " _
              & "AND DeleteTimestamp > @sync_last_received_anchor " _
              & "AND DeleteTimestamp <= @sync_new_received_anchor " _
              & "AND DeleteId <> @sync_client_id)"
            .Parameters.Add("@" + SyncSession.SyncInitialized, SqlDbType.Bit)
            .Parameters.Add("@" + SyncSession.SyncLastReceivedAnchor, SqlDbType.Timestamp)
            .Parameters.Add("@" + SyncSession.SyncNewReceivedAnchor, SqlDbType.Timestamp)
            .Parameters.Add("@" + SyncSession.SyncClientId, SqlDbType.UniqueIdentifier)
            .Connection = serverConn
        End With
        customerSyncAdapter.SelectIncrementalDeletesCommand = customerIncrDeletes

        'Apply deletes to the server.
        Dim customerDeletes As New SqlCommand()
        customerDeletes.CommandType = CommandType.StoredProcedure
        customerDeletes.CommandText = "usp_CustomerApplyDelete"
        customerDeletes.Parameters.Add("@" + SyncSession.SyncLastReceivedAnchor, SqlDbType.Timestamp)
        customerDeletes.Parameters.Add("@" + SyncSession.SyncClientId, SqlDbType.UniqueIdentifier)
        customerDeletes.Parameters.Add("@" + SyncSession.SyncForceWrite, SqlDbType.Bit)
        customerDeletes.Parameters.Add("@" + SyncSession.SyncRowCount, SqlDbType.Int).Direction = ParameterDirection.Output
        customerDeletes.Parameters.Add("@CustomerId", SqlDbType.UniqueIdentifier)
        customerDeletes.Connection = serverConn
        customerSyncAdapter.DeleteCommand = customerDeletes


        'Add the SyncAdapter to the server synchronization provider.
        Me.SyncAdapters.Add(customerSyncAdapter)


        'Handle the ApplyChangeFailed and ChangesApplied events. 
        'This allows us to respond to any conflicts that occur, and to 
        'make changes that are downloaded to the client during the same
        'session.
        AddHandler Me.ApplyChangeFailed, AddressOf SampleServerSyncProvider_ApplyChangeFailed
        AddHandler Me.ChangesApplied, AddressOf SampleServerSyncProvider_ChangesApplied

    End Sub 'New

    'Create a list to hold primary keys from the Customer
    'table. This list is used when we handle the ApplyChangeFailed 
    'and ChangesApplied events.
    Private _updateConflictGuids As ArrayList = New ArrayList

    Private Sub SampleServerSyncProvider_ApplyChangeFailed(ByVal sender As Object, ByVal e As ApplyChangeFailedEventArgs)

        'Log information for the ApplyChangeFailed event.
        EventLogger.LogEvents(sender, e)

        'Respond to four different types of conflicts:
        ' * ClientDeleteServerUpdate
        ' * ClientUpdateServerDelete
        ' * ClientInsertServerInsert
        ' * ClientUpdateServerUpdate
        '
        If e.Conflict.ConflictType = ConflictType.ClientDeleteServerUpdate Then
            'With the commands we are using, the default is for the server 
            'change to win and be applied to the client. Here, we accept the 
            'default on the server side. We also set ConflictResolver.ServerWins 
            'for this conflict in the client provider. This ensures that the server
            'change is applied to the client during the download phase.
            Console.WriteLine(String.Empty)
            Console.WriteLine("***********************************")
            Console.WriteLine("A client delete / server update conflict was detected.")

            e.Action = ApplyAction.Continue

            Console.WriteLine("The server change will be applied at the client.")
            Console.WriteLine("***********************************")
            Console.WriteLine(String.Empty)
        End If

        If e.Conflict.ConflictType = ConflictType.ClientUpdateServerDelete Then

            'For client-update/server-delete conflicts, we force the client 
            'change to be applied at the server. The stored procedure specified for 
            'customerSyncAdapter.UpdateCommand accepts the @sync_force_write parameter
            'and includes logic to handle this case.
            Console.WriteLine(String.Empty)
            Console.WriteLine("***********************************")
            Console.WriteLine("A client update / server delete conflict was detected.")

            e.Action = ApplyAction.RetryWithForceWrite

            Console.WriteLine("The client change was retried at the server with RetryWithForceWrite.")
            Console.WriteLine("***********************************")
            Console.WriteLine(String.Empty)
        End If

        If e.Conflict.ConflictType = ConflictType.ClientInsertServerInsert Then
            'Similar to how we handled the client-delete/server-update conflict.
            'In this case, we set ConflictResolver.FireEvent and use RetryWithForceWrite
            'for this conflict in the client provider. This is equivalent to 
            'ConflictResolver.ServerWins, and ensures that the server
            'change is applied to the client during the download phase.
            Console.WriteLine(String.Empty)
            Console.WriteLine("***********************************")
            Console.WriteLine("A client insert / server insert conflict was detected.")

            e.Action = ApplyAction.Continue

            Console.WriteLine("The server change will be applied at the client.")
            Console.WriteLine("***********************************")
            Console.WriteLine(String.Empty)
        End If

        If e.Conflict.ConflictType = ConflictType.ClientUpdateServerUpdate Then

            'For client-update/server-update conflicts, we want to
            'allow the user to specify the conflict resolution option.
            '
            'It is possible for the Conflict object from the
            'server to have more than one row. Because our custom
            'resolution code only works with one row at a time,
            'we only allow the user to select a resolution
            'option if the object contains a single row.
            If e.Conflict.ServerChange.Rows.Count > 1 Then
                Console.WriteLine(String.Empty)
                Console.WriteLine("***********************************")
                Console.WriteLine("A client update / server update conflict was detected.")

                e.Action = ApplyAction.Continue

                Console.WriteLine("The server change will be applied at the client.")
                Console.WriteLine("***********************************")
                Console.WriteLine(String.Empty)
            Else
                Console.WriteLine(String.Empty)
                Console.WriteLine("***********************************")
                Console.WriteLine("A client update / server update conflict was detected.")
                Console.WriteLine("Conflicting rows are displayed below.")
                Console.WriteLine("***********************************")

                'Get the conflicting changes from the Conflict object
                'and display them. The Conflict object holds a copy
                'of the changes; updates to this object will not be 
                'applied. To make changes, use the Context object,
                'which is demonstrated in the next section of code
                'under ' case "CU" '.
                Dim conflictingServerChange As DataTable = e.Conflict.ServerChange
                Dim conflictingClientChange As DataTable = e.Conflict.ClientChange
                Dim serverColumnCount As Integer = conflictingServerChange.Columns.Count
                Dim clientColumnCount As Integer = conflictingClientChange.Columns.Count

                Console.WriteLine(String.Empty)
                Console.WriteLine("Server row: ")
                Console.Write(" | ")

                'Display the server row.
                Dim i As Integer
                For i = 0 To serverColumnCount - 1
                    Console.Write(conflictingServerChange.Rows(0)(i).ToString() & " | ")
                Next i

                Console.WriteLine(String.Empty)
                Console.WriteLine(String.Empty)
                Console.WriteLine("Client row: ")
                Console.Write(" | ")

                'Display the client row.
                For i = 0 To clientColumnCount - 1
                    Console.Write(conflictingClientChange.Rows(0)(i).ToString() & " | ")
                Next i

                Console.WriteLine(String.Empty)
                Console.WriteLine(String.Empty)

                'Ask for a conflict resolution option.
                Console.WriteLine("Enter a resolution option for this conflict:")
                Console.WriteLine("SE = server change wins")
                Console.WriteLine("CL = client change wins")
                Console.WriteLine("CU = custom resolution (combine rows)")

                Dim conflictResolution As String = Console.ReadLine()
                conflictResolution.ToUpper()

                Select Case conflictResolution
                    Case "SE"

                        'Again, this this is the default for the commands we are using:
                        'the server change is persisted and then downloaded to the client.
                        e.Action = ApplyAction.Continue
                        Console.WriteLine(String.Empty)
                        Console.WriteLine("The server change will be applied at the client.")


                    Case "CL"

                        'Override the default by specifying that the client row
                        'should be applied at the server. The stored procedure specified  
                        'for customerSyncAdapter.UpdateCommand accepts the @sync_force_write 
                        'parameter and includes logic to handle this case.
                        e.Action = ApplyAction.RetryWithForceWrite
                        Console.WriteLine(String.Empty)
                        Console.WriteLine("The client change was retried at the server with RetryWithForceWrite.")


                    Case "CU"

                        'Provide a custom resolution scheme that takes each conflicting
                        'column and applies the combined contents of the column to the 
                        'client and server. This is not necessarily a resolution scheme 
                        'that you would use in production. Instead, it is used to 
                        'demonstrate the various ways you can interact with conflicting 
                        'data during synchronization.
                        '
                        'Get the ID for the conflicting row from the client data table,
                        'and add it to a list of GUIDs. We update rows at the server
                        'based on this list.
                        Dim customerId As Guid = CType(conflictingClientChange.Rows(0)("CustomerId"), Guid)
                        _updateConflictGuids.Add(customerId)

                        'Create a hashtable to hold the column ordinal and value for
                        'any columns that are in confict.
                        Dim conflictingColumns As Hashtable = New Hashtable()
                        Dim combinedColumnValue As String

                        'Determine which columns are different at the client and server.
                        'We already looped through these columns once, but we wanted to
                        'keep this code separate from the display code above.
                        For i = 0 To clientColumnCount - 1
                            If conflictingClientChange.Rows(0)(i).ToString() <> conflictingServerChange.Rows(0)(i).ToString() Then
                                'If we find a column that is different, combine the values from
                                'the client and server, and write "| conflict |" between them.
                                combinedColumnValue = conflictingClientChange.Rows(0)(i).ToString() _
                                & "  | conflict |  " & conflictingServerChange.Rows(0)(i).ToString()
                                conflictingColumns.Add(i, combinedColumnValue)
                            End If
                        Next i

                        'Loop through the rows in the Context object, which exposes
                        'the set of changes that are uploaded from the client.
                        'Note: In the ApplyChangeFailed event for the client provider,  
                        'you have access to the set of changes that was downloaded from
                        'the server.
                        Dim allClientChanges As DataTable = e.Context.DataSet.Tables("Customer")
                        Dim allClientRowCount As Integer = allClientChanges.Rows.Count
                        Dim allClientColumnCount As Integer = allClientChanges.Columns.Count

                        For i = 0 To allClientRowCount - 1
                            'Find the changed row with the GUID from the Conflict object.
                            If allClientChanges.Rows(i).RowState = DataRowState.Modified AndAlso CType(allClientChanges.Rows(i)("CustomerId"), Guid) = customerId Then
                                'Loop through the columns and check whether the column
                                'is in the conflictingColumns hashtable. If it is,
                                'update the value in the allClientChanges Context object.
                                Dim j As Integer
                                For j = 0 To allClientColumnCount - 1
                                    If conflictingColumns.ContainsKey(j) Then
                                        allClientChanges.Rows(i)(j) = conflictingColumns(j)
                                    End If
                                Next j
                            End If
                        Next i

                        'Apply the changed row with its combined values to the server.
                        'This change will persist at the server, but it will not be 
                        'downloaded with the SelectIncrementalUpdate command that we use.
                        'It will not download the change because it checks for the UpdateId,
                        'which is still set to the client that made the upload.
                        'We use the ChangesApplied event to set the UpdateId for the
                        'change to a value that represents the server. This ensures
                        'that the change is applied at the client during the download
                        'phase of synchronization (see SampleServerSyncProvider_ChangesApplied).
                        e.Action = ApplyAction.RetryWithForceWrite

                        Console.WriteLine(String.Empty)
                        Console.WriteLine("The custom change was retried at the server with RetryWithForceWrite.")


                    Case Else
                        Console.WriteLine(String.Empty)
                        Console.WriteLine("Not a valid resolution option.")
                End Select
            End If


            Console.WriteLine(String.Empty)
        End If

    End Sub 'SampleServerSyncProvider_ApplyChangeFailed


    Private Sub SampleServerSyncProvider_ChangesApplied(ByVal sender As Object, ByVal e As ChangesAppliedEventArgs)
        'If _updateConflictGuids contains at least one GUID, update the UpdateId
        'column so that each change is downloaded to the client. For more
        'information, see SampleServerSyncProvider_ApplyChangeFailed.
        If _updateConflictGuids.Count > 0 Then
            Dim updateTable As New SqlCommand()
            updateTable.Connection = CType(e.Connection, SqlConnection)
            updateTable.Transaction = CType(e.Transaction, SqlTransaction)
            updateTable.CommandText = String.Empty

            Dim i As Integer
            For i = 0 To _updateConflictGuids.Count - 1
                updateTable.CommandText += _
                    " UPDATE Sales.Customer SET UpdateId = '00000000-0000-0000-0000-000000000000' " _
                    + " WHERE CustomerId='" + _updateConflictGuids(i).ToString() + "'"
            Next i

            updateTable.ExecuteNonQuery()
        End If

    End Sub 'SampleServerSyncProvider_ChangesApplied
End Class 'SampleServerSyncProvider

'Create a class that is derived from 
'Microsoft.Synchronization.Data.SqlServerCe.SqlCeClientSyncProvider.
'You can just instantiate the provider directly and associate it
'with the SyncAgent, but here we use this class to handle client 
'provider events.
Public Class SampleClientSyncProvider
    Inherits SqlCeClientSyncProvider


    Public Sub New()
        'Specify a connection string for the sample client database.
        'By default, the client database is created if it does not
        'exist.
        Dim util As New Utility()
        Me.ConnectionString = Utility.ConnStr_SqlCeClientSync

        'Specify conflict resolution options for each type of
        'conflict or error that can occur. The client and server are
        'independent; therefore, these settings have no effect when changes 
        'are applied at the server. However, settings should agree with each
        'other. For example:
        ' * We specify a value of ServerWins for client delete /
        '   server update. On the server side, by default our commands will 
        '   ignore the conflicting delete and download the update to the 
        '   client. ServerWins is equivalent to setting RetryWithForceWrite
        '   on the client.
        ' * Conversely, we specify a value of ClientWins for client update /
        '   server delete. On the server side, we specify that our commands 
        '   should force write the update by turning it into an insert.
        Me.ConflictResolver.ClientDeleteServerUpdateAction = ResolveAction.ServerWins
        Me.ConflictResolver.ClientUpdateServerDeleteAction = ResolveAction.ClientWins
        'If any of the following conflicts or errors occur, the ApplyChangeFailed
        'event is raised.
        Me.ConflictResolver.ClientInsertServerInsertAction = ResolveAction.FireEvent
        Me.ConflictResolver.ClientUpdateServerUpdateAction = ResolveAction.FireEvent
        Me.ConflictResolver.StoreErrorAction = ResolveAction.FireEvent

        'Log information for the ApplyChangeFailed event and handle any
        'ResolveAction.FireEvent cases.
        AddHandler Me.ApplyChangeFailed, AddressOf SampleClientSyncProvider_ApplyChangeFailed

        'Use the following events to fix up schema on the client.
        'We use the CreatingSchema event to change the schema
        'by using the API. We use the SchemaCreated event 
        'to change the schema by using SQL.
        AddHandler Me.CreatingSchema, AddressOf SampleClientSyncProvider_CreatingSchema
        AddHandler Me.SchemaCreated, AddressOf SampleClientSyncProvider_SchemaCreated

    End Sub 'New


    Private Sub SampleClientSyncProvider_ApplyChangeFailed(ByVal sender As Object, ByVal e As ApplyChangeFailedEventArgs)

        'Log event data from the client side.
        EventLogger.LogEvents(sender, e)

        'Force write any inserted server rows that are in conflict 
        'when they are downloaded.
        If e.Conflict.ConflictType = ConflictType.ClientInsertServerInsert Then
            e.Action = ApplyAction.RetryWithForceWrite
        End If

        If e.Conflict.ConflictType = ConflictType.ClientUpdateServerUpdate Then
            'Logic goes here.
        End If

        If e.Conflict.ConflictType = ConflictType.ErrorsOccurred Then
            'Logic goes here.
        End If

    End Sub 'SampleClientSyncProvider_ApplyChangeFailed

    Private Sub SampleClientSyncProvider_CreatingSchema(ByVal sender As Object, ByVal e As CreatingSchemaEventArgs)

        'Set the RowGuid property because it is not copied
        'to the client by default. This is also a good time
        'to specify literal defaults with .Columns[ColName].DefaultValue,
        'but we will specify defaults like NEWID() by calling
        'ALTER TABLE after the table is created.
        e.Schema.Tables("Customer").Columns("CustomerId").RowGuid = True

    End Sub 'SampleClientSyncProvider_CreatingSchema


    Private Sub SampleClientSyncProvider_SchemaCreated(ByVal sender As Object, ByVal e As SchemaCreatedEventArgs)
        Dim tableName As String = e.Table.TableName
        Dim util As New Utility()

        'Call ALTER TABLE on the client. This must be done
        'over the same connection and within the same
        'transaction that Sync Framework uses
        'to create the schema on the client.
        Utility.MakeSchemaChangesOnClient(e.Connection, e.Transaction, "Customer")

    End Sub 'SampleClientSyncProvider_SchemaCreated 
End Class 'SampleClientSyncProvider

'Handle the statistics that are returned by the SyncAgent.
Public Class SampleStats

    Public Sub DisplayStats(ByVal syncStatistics As SyncStatistics, ByVal syncType As String)
        Console.WriteLine(String.Empty)
        If syncType = "initial" Then
            Console.WriteLine("****** Initial Synchronization ******")
        ElseIf syncType = "subsequent" Then
            Console.WriteLine("***** Subsequent Synchronization ****")
        End If

        Console.WriteLine("Start Time: " & syncStatistics.SyncStartTime)
        Console.WriteLine("Upload Changes Applied: " & syncStatistics.UploadChangesApplied)
        Console.WriteLine("Upload Changes Failed: " & syncStatistics.UploadChangesFailed)
        Console.WriteLine("Total Changes Uploaded: " & syncStatistics.TotalChangesUploaded)
        Console.WriteLine("Download Changes Applied: " & syncStatistics.DownloadChangesApplied)
        Console.WriteLine("Download Changes Failed: " & syncStatistics.DownloadChangesFailed)
        Console.WriteLine("Total Changes Downloaded: " & syncStatistics.TotalChangesDownloaded)
        Console.WriteLine("Complete Time: " & syncStatistics.SyncCompleteTime)
        Console.WriteLine(String.Empty)

    End Sub 'DisplayStats
End Class 'SampleStats


Public Class EventLogger

    'Create client and server log files, and write to them
    'based on data from the ApplyChangeFailedEventArgs.
    Public Shared Sub LogEvents(ByVal sender As Object, ByVal e As ApplyChangeFailedEventArgs)
        Dim logFile As String = String.Empty
        Dim site As String = String.Empty

        If TypeOf sender Is SampleServerSyncProvider Then
            logFile = "ServerLogFile.txt"
            site = "server"
        ElseIf TypeOf sender Is SampleClientSyncProvider Then
            logFile = "ClientLogFile.txt"
            site = "client"
        End If

        Dim streamWriter As StreamWriter = File.AppendText(logFile)
        Dim outputText As New StringBuilder()

        outputText.AppendLine("** CONFLICTING CHANGE OR ERROR AT " & site.ToUpper() & " **")
        outputText.AppendLine("Table for which error or conflict occurred: " & e.TableMetadata.TableName)
        outputText.AppendLine("Sync stage: " & e.Conflict.SyncStage.ToString())
        outputText.AppendLine("Conflict type: " & e.Conflict.ConflictType.ToString())

        'If it is a data conflict instead of an error, print out
        'the values of the rows at the client and server.
        If e.Conflict.ConflictType <> ConflictType.ErrorsOccurred AndAlso e.Conflict.ConflictType <> ConflictType.Unknown Then

            Dim serverChange As DataTable = e.Conflict.ServerChange
            Dim clientChange As DataTable = e.Conflict.ClientChange
            Dim serverRows As Integer = serverChange.Rows.Count
            Dim clientRows As Integer = clientChange.Rows.Count
            Dim serverColumns As Integer = serverChange.Columns.Count
            Dim clientColumns As Integer = clientChange.Columns.Count

            Dim i As Integer
            For i = 0 To serverRows - 1
                outputText.Append("Server row: ")

                Dim j As Integer
                For j = 0 To serverColumns - 1
                    outputText.Append(serverChange.Rows(i)(j).ToString() & " | ")
                Next j

                outputText.AppendLine(String.Empty)
            Next i

            For i = 0 To clientRows - 1
                outputText.Append("Client row: ")

                Dim j As Integer
                For j = 0 To clientColumns - 1
                    outputText.Append(clientChange.Rows(i)(j).ToString() & " | ")
                Next j

                outputText.AppendLine(String.Empty)
            Next i
        End If

        If e.Conflict.ConflictType = ConflictType.ErrorsOccurred Then
            outputText.AppendLine("Error message: " + e.Error.Message)
        End If

        streamWriter.WriteLine(DateTime.Now.ToShortTimeString() & " | " + outputText.ToString())
        streamWriter.Flush()
        streamWriter.Dispose()

    End Sub 'LogEvents 
End Class 'EventLogger

請參閱

概念

撰寫一般用戶端和伺服器同步處理工作