방법: 데이터 충돌 및 오류 처리
이 항목에서는 Sync Framework의 데이터 충돌과 오류를 처리하는 방법에 대해 설명합니다. 이 항목의 예제에서는 다음과 같은 Sync Framework 형식 및 이벤트를 중점적으로 설명합니다.
DbServerSyncProvider ApplyChangeFailed 이벤트, SqlCeClientSyncProvider ApplyChangeFailed 이벤트 및 ApplyChangeFailedEventArgs
샘플 코드를 실행하는 방법에 대한 자세한 내용은 일반적인 클라이언트 및 서버 동기화 태스크 프로그래밍의 "방법 항목의 예제 응용 프로그램"을 참조하십시오.
데이터 충돌 및 오류 이해
Sync Framework에서 충돌과 오류는 행 수준에서 검색됩니다. 충돌하는 행은 동기화 간에 둘 이상의 노드에서 변경된 행입니다. 동기화 중의 오류는 일반적으로 중복 기본 키와 같은 제약 조건 위반과 관련되어 있습니다. 충돌을 검색하고 해결하려면 보다 정교한 기술과 처리 능력이 필요하고 네트워크 트래픽이 증가하므로 응용 프로그램은 가능한 한 충돌을 방지하도록 디자인되어야 합니다. 대개 한 노드(일반적으로 서버)에서만 테이블을 업데이트하거나, 특정 행이 한 노드에서만 업데이트되도록 데이터를 필터링하는 방식으로 충돌을 방지합니다. 필터링에 대한 자세한 내용은 방법: 행 및 열 필터링을 참조하십시오. 일부 응용 프로그램에서는 충돌이 불가피하게 발생합니다. 예를 들어 영업 응용 프로그램에서 두 영업 사원이 지역을 공유할 수 있으며 동일한 고객과 주문에 대한 데이터를 업데이트할 수 있습니다. 따라서 Sync Framework는 충돌을 검색 및 해결하는 데 사용할 수 있는 여러 가지 기능을 제공합니다.
데이터 충돌은 둘 이상의 노드에서 변경 작업이 수행되는 동기화 작업에서 발생할 수 있습니다. 즉, 충돌은 양방향 동기화에서도 발생할 수 있지만 다운로드 전용 및 업로드 전용 동기화에서도 발생할 수 있습니다. 예를 들어 서버에서 삭제한 행을 클라이언트에서 업데이트하는 경우 Sync Framework에서 서버로 업로드된 업데이트를 적용하려고 하면 충돌이 발생합니다. 충돌은 항상 현재 동기화 중인 서버와 클라이언트 간에 발생합니다. 다음 예제를 참조하십시오.
클라이언트 A와 클라이언트 B를 서버와 동기화합니다.
클라이언트 A에서 행이 업데이트된 후에 클라이언트 A가 동기화됩니다. 충돌이 발생하지 않으며 행이 서버에서 적용됩니다.
같은 행이 클라이언트 B에서 업데이트된 후에 클라이언트 B가 동기화됩니다. 그러면 클라이언트 A에서 수행된 업데이트로 인해 클라이언트 B의 행이 서버의 행과 충돌합니다.
이 충돌을 서버측에서 해결하는 경우 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에서 정의하는 두 명령으로 선택됩니다.
SelectConflictUpdatedRowsCommand 속성에 대해 사용자가 지정하는 쿼리 또는 저장 프로시저가 서버 데이터베이스의 기본 테이블에서 충돌하는 행을 선택합니다. Sync Framework는 삽입, 업데이트 또는 삭제 작업에서 @sync_row_count 값이 0으로 반환되면 이 명령을 실행합니다. 이 값은 작업이 실패했음을 나타냅니다. 이 명령은 ClientInsertServerInsert, ClientUpdateServerUpdate 및 ClientDeleteServerUpdate 충돌에 대한 행을 선택합니다.
SelectConflictDeletedRowsCommand에 대해 지정하는 쿼리 또는 저장 프로시저는 서버 데이터베이스의 삭제 표식 테이블에서 충돌하는 행을 선택합니다. Sync Framework는 충돌하는 행이 기본 테이블에 없는 경우 이 명령을 실행합니다. 이 명령은 ClientUpdateServerDelete 충돌에 대한 행을 선택합니다.
충돌하는 각 행의 데이터는 SyncConflict 컬렉션에 저장됩니다. 이 컬렉션은 다음과 같은 경우에 메모리 부족 오류를 발생시킬 정도로 커질 수 있습니다.
충돌하는 행 수가 많습니다. 각 세션에서 더 적은 수의 행을 동기화하는 것을 고려하거나 한 노드에서만 특정 행을 업데이트하여 충돌 횟수를 제한하십시오.
충돌하는 행에 큰 데이터 형식을 사용하는 열이 포함되어 있습니다. 동기화되는 열 집합에 큰 데이터 형식을 사용하는 열을 포함하지 않는 것을 고려하십시오. 자세한 내용은 방법: 행 및 열 필터링을 참조하십시오.
충돌 및 오류 해결
충돌 및 오류 해결은 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: Continue의 ApplyAction을 설정하는 것에 해당합니다.
ServerWins: RetryWithForceWrite의 ApplyAction을 설정하는 것에 해당합니다.
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
전체 코드 예제
다음의 전체 코드 예제에는 위에서 설명한 코드 예제와 동기화를 수행하는 데 필요한 추가 코드가 포함되어 있습니다. 예제를 통해 응용 프로그램 사용자는 업데이트/업데이트 충돌을 해결하는 방법을 선택할 수 있습니다. 이때 선택할 수 있는 옵션 중 하나가 충돌하는 행의 열 값을 합하는 사용자 지정 해결 체계입니다. 사용자 지정 해결 체계의 코드는 SampleServerSyncProvider_ApplyChangeFailed
및 SampleServerSyncProvider_ChangesApplied
이벤트 처리기에 포함되어 있습니다 이 예제를 사용하려면 데이터베이스 공급자용 유틸리티 클래스 방법 항목에 나와 있는 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