データの競合とエラーを処理する方法

このトピックでは、Sync Framework でデータの競合とエラーを処理する方法について説明します。このトピックの例では、次に示す Sync Framework の型とイベントを中心に説明します。

サンプル コードを実行する方法については、「一般的なクライアントとサーバーの同期タスクのプログラミング」の「操作方法に関するトピックのサンプル アプリケーション」を参照してください。

データの競合とエラーについて

Sync Framework では、競合とエラーが行レベルで検出されます。行は、同期中に複数のノードで変更された場合に競合します。同期中に発生するエラーでは、通常、主キーの重複などの制約違反が原因として挙げられます。アプリケーションは、可能な限り競合を回避するようにデザインする必要があります。これは、競合検出と解決により、複雑さが増し、処理とネットワーク トラフィックが増加するためです。競合を回避する最も一般的な方法としては、テーブルの更新を 1 つのノードのみ (通常はサーバー) で行うか、同じ行の更新が 1 つのノードしか行われないようにデータをフィルターで選択する方法があります。フィルター選択の詳細については、「行および列をフィルター選択する方法」を参照してください。アプリケーションによっては、競合を避けられない場合もあります。たとえば、営業部門のアプリケーションでは、2 人の営業担当者が 1 つの営業区域を共有し、両方の営業担当者が同じ顧客および注文のデータを更新する可能性があります。そのため、Sync Framework には、競合を検出および解決するために使用できる一式の機能が用意されています。

データの競合は、複数のノードでデータが変更される同期シナリオで発生する可能性があります。双方向同期で競合が発生することは明らかですが、ダウンロードのみの同期やアップロードのみの同期でも競合が発生することがあります。たとえば、ある行がサーバーで削除され、同じ行がクライアントで更新された場合は、Sync Framework が更新をサーバーにアップロードして適用しようとすると競合が発生します。競合は、常に、現在同期中のサーバーとクライアントとの間で発生します。次の例を考えてみましょう。

  1. クライアント A とクライアント B をサーバーと同期します。

  2. クライアント A で行が更新された後、クライアント A が同期します。競合は発生せず、サーバーでその行が適用されます。

  3. クライアント B で同じ行が更新された後、クライアント B が同期します。クライアント A で発生した更新により、クライアント B の行はサーバーの行と競合しています。

  4. サーバーを優先してこの競合を解決すると、Sync Framework では、サーバーの行をクライアント B に適用できます。クライアント B を優先して解決すると、Sync Framework では、クライアント B の行をサーバーに適用できます。その後のクライアント A とサーバー間の同期では、クライアント B の更新内容がクライアント A に適用されます。

競合とエラーの種類

Sync Framework では次の種類の競合が検出されます。これらは、ConflictType 列挙体で定義されています。

  • ClientInsertServerInsert 競合。クライアントとサーバーの両方で同じ主キーを持つ行が挿入されたときに発生します。この種類の競合は、主キーの衝突とも呼ばれます。

  • ClientUpdateServerUpdate 競合。クライアントとサーバーで同じ行が変更されたときに発生します。これは、最も一般的な種類の競合です。

  • ClientUpdateServerDelete 競合。クライアントで行が更新され、同じ行がサーバーで削除されたときに発生します。

  • ClientDeleteServerUpdate 競合。クライアントで行が更新され、同じ行がサーバーで更新されたときに発生します。

  • ErrorsOccurred 競合。エラーが原因で行を適用できないときに発生します。

競合とエラーの検出

同期中に行を適用できない場合は、通常、エラーまたはデータの競合が発生したことがその原因です。どちらの場合も、エラーまたは競合が同期のアップロード フェーズとダウンロード フェーズのどちらで発生したかに応じて、DbServerSyncProvider ApplyChangeFailed イベントまたは SqlCeClientSyncProvider ApplyChangeFailed イベントが発生します。クライアントの ApplyChangeFailed イベントが発生した場合、Sync Framework により、競合するすべての行が選択されます。その後、ユーザーは、こうした競合を解決する方法を決定します。サーバーの ApplyChangeFailed イベントが発生した場合は、各テーブルの SyncAdapter で定義した次の 2 つのコマンドを使用することで、競合する行が選択されます。

  • SelectConflictUpdatedRowsCommand プロパティに指定するクエリまたはストアド プロシージャによって、サーバー データベース内のベース テーブルから競合する行が選択されます。このコマンドは、挿入操作、更新操作、または削除操作によって @sync_row_count 値 0 が返された場合に Sync Framework によって実行されます。この値は、操作が失敗したことを示します。このコマンドでは、ClientInsertServerInsert 競合、ClientUpdateServerUpdate 競合、および ClientDeleteServerUpdate 競合の行が選択されます。

  • SelectConflictDeletedRowsCommand に指定するクエリまたはストアド プロシージャによって、サーバー データベース内の廃棄テーブルから競合する行が選択されます。このコマンドは、競合する行がベース テーブルに見つからなかった場合に Sync Framework によって実行されます。このコマンドでは、ClientUpdateServerDelete 競合の行が選択されます。

競合するそれぞれの行のデータは、SyncConflict コレクションに格納されます。このコレクションのサイズが大きくなり、次の状況でメモリ不足エラーの原因となる場合があります。

  • 競合する行の数が多い場合。各セッションで同期する行数を減らすか、または特定の行について更新が行われるノードを 1 つに限定して競合の数を減らしてください。

  • 競合する行に大きな値のデータ型を使用する列が含まれている場合。同期の対象となる列のセットに大きな値のデータ型を使用する列が含まれないようにしてください。詳細については、「行および列をフィルター選択する方法」を参照してください。

競合とエラーの解決

競合とエラーの解決は、DbServerSyncProvider ApplyChangeFailed イベントおよび SqlCeClientSyncProvider ApplyChangeFailed イベントに応答して処理する必要があります。ApplyChangeFailedEventArgs オブジェクトを使用すると、競合解決時に使用できるいくつかのプロパティにアクセスできます。

  • Action プロパティを ApplyAction 列挙体の次の値のいずれかに設定すると、競合解決の方法を指定できます。

    • Continue: 競合を無視して同期を続行します。

    • RetryApplyingRow: 行の適用を再試行します。競合する行の一方または両方を変更しても競合の原因に対処できない場合は、再試行が失敗し、イベントが再度発生します。

    • RetryWithForceWrite: 変更の適用を強制するロジックで再試行します。SqlCeClientSyncProvider には、このオプションのサポートが組み込まれています。このオプションをサーバー上で使用するには、サーバー データベースに変更を適用するコマンドで @sync_force_write パラメーターを使用してサポートを追加します。たとえば、ClientUpdateServerDelete 競合では、@sync_force_write を 1 に設定した場合、更新を挿入に変更できます。コード例については、この後の「例」を参照してください。

  • Conflict プロパティを使用すると、競合の種類を取得し、クライアントとサーバーの競合する行を表示できます。

  • Context プロパティを使用すると、同期中の変更のデータセットを取得できます。Conflict プロパティによって公開される行はコピーであるため、この行を上書きしても、適用される行は変更されません。アプリケーションで必要であれば、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 プロパティに指定されます。ストアド プロシージャを使用すると、サーバー データベースに挿入および削除も適用できます。このようなプロシージャの例については「データベース プロバイダーのセットアップ スクリプトに関するトピック」を参照してください。

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

完全なコード例

次の完全なコード例には、既に説明したコード例に加え、同期を実行するためのコードが含まれています。また、この例では、アプリケーションのユーザーが更新と更新の競合の解決方法を選択できることにも注意してください。解決方法の 1 つとして、競合する行の列の値を組み合わせるカスタムの解決方法があります。カスタムの解決方法のコードは、SampleServerSyncProvider_ApplyChangeFailed イベント ハンドラーと SampleServerSyncProvider_ChangesApplied イベント ハンドラーに含まれています。この例では、「データベース プロバイダーの Utility クラスに関するトピック」で説明されている Utility クラスが必要です。

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

参照

概念

一般的なクライアントとサーバーの同期タスクのプログラミング