Procédure : gérer les conflits de données et les erreurs
Cette rubrique vous indique comment gérer les conflits de données et les erreurs dans Sync Framework. Les exemples de cette rubrique sont axés sur les types et les événements Sync Framework suivants :
Événements DbServerSyncProviderApplyChangeFailed, SqlCeClientSyncProviderApplyChangeFailed et ApplyChangeFailedEventArgs
Pour plus d'informations sur le mode d'exécution d'un exemple de code, consultez « Exemples d'application dans les rubriques de procédures » dans Programmation des tâches courantes de synchronisation client et serveur.
Présentation des conflits de données et des erreurs
Dans Sync Framework, les conflits et les erreurs sont détectés au niveau de la ligne. Une ligne est en conflit si elle a été modifiée sur plusieurs nœuds entre les synchronisations. Les erreurs qui se produisent lors de la synchronisation impliquent en règle générale une violation de contrainte, telle qu'une clé primaire en double. Les applications doivent être conçues pour éviter les conflits dans la mesure du possible, car la détection et la résolution des conflits conduisent à une plus grande complexité et génèrent un traitement supplémentaire ainsi qu'un trafic réseau accru. Pour éviter les conflits, les méthodes les plus courantes sont les suivantes : mise à jour d'une table sur un seul nœud (généralement le serveur) ou filtrage des données afin que seul un nœud mette à jour une ligne spécifique. Pour plus d'informations sur le filtrage, consultez Procédure : filtrer des lignes et des colonnes. Dans certaines applications, les conflits ne peuvent pas être évités. Par exemple, dans une application de force de vente, deux commerciaux peuvent partager un secteur de vente. Les deux commerciaux peuvent mettre à jour les données pour les mêmes client et commandes. Par conséquent, Sync Framework fournit un jeu des fonctionnalités qui permet de détecter et de résoudre des conflits.
Des conflits de données peuvent survenir dans les scénarios de synchronisation dans lesquels des modifications sont effectuées sur plusieurs nœuds. De toute évidence, des conflits peuvent se produire lors de la synchronisation bidirectionnelle, mais ils peuvent également se produire lors de la synchronisation par téléchargement ascendant uniquement et par téléchargement uniquement. Par exemple, si une ligne est détectée sur le serveur et que cette même ligne est mise à jour sur le client, un conflit se produit lorsque Sync Framework tente d'appliquer la mise à jour qui est téléchargée sur le serveur. Les conflits interviennent systématiquement entre le serveur et le client faisant l'objet d'une synchronisation. Prenons l'exemple suivant :
Le client A et le client B se synchronisent avec le serveur.
Une ligne est mise à jour sur le client A, puis le client A se synchronise. Aucun conflit ne se produit et la ligne est appliquée au niveau du serveur.
La même ligne est mise à jour sur le client B, puis le client B se synchronise. La ligne du client B est à présent en conflit avec la ligne du serveur en raison de la mise à jour qui a été initialisée sur le client A.
Si vous résolvez ce conflit en faveur du serveur, Sync Framework peut appliquer la ligne du serveur au client B. Inversement, si vous résolvez ce conflit en faveur du client B, Sync Framework peut appliquer la ligne du client B au serveur. Lors d'une synchronisation ultérieure entre le client A et le serveur, la mise à jour qui a été initialisée au niveau du client B est appliquée au client A.
Types de conflits et d'erreurs
Sync Framework détecte les types de conflits suivants. Ceux-ci sont définis dans l'énumération ConflictType :
Un conflit ClientInsertServerInsert se produit lorsque le client et le serveur insèrent tous deux une ligne qui possède la même clé primaire. Ce type de conflit est également dénommé « collision de clé primaire ».
Un conflit ClientUpdateServerUpdate se produit lorsque le client et le serveur modifient la même ligne. Il s'agit du type de conflit le plus courant.
Un conflit ClientUpdateServerDelete se produit lorsque le client met à jour une ligne et le serveur supprime la même ligne.
Un conflit ClientDeleteServerUpdate se produit lorsque le client supprime une ligne et le serveur met à jour la même ligne.
Un conflit ErrorsOccurred se produit lorsqu'une erreur empêche l'application d'une ligne.
Détection des conflits et des erreurs
S'il est impossible d'appliquer une ligne durant la synchronisation, ce problème est généralement dû à une erreur ou à un conflit de données. Dans les deux cas, l'événement DbServerSyncProviderApplyChangeFailed ou l'événement SqlCeClientSyncProviderApplyChangeFailed est déclenché, selon que l'erreur ou le conflit s'est produit durant la phase de synchronisation depuis le serveur ou vers le serveur. Si l'événement ApplyChangeFailed client est déclenché, Sync Framework sélectionne automatiquement toutes les lignes en conflit. Vous déterminez ensuite comment résoudre ces conflits. Si l'événement ApplyChangeFailed serveur est déclenché, les lignes en conflit sont sélectionnées à l'aide de deux commandes que vous définissez sur l'objet SyncAdapter de chaque table :
La requête ou la procédure stockée que vous spécifiez pour la propriété SelectConflictUpdatedRowsCommand sélectionne les lignes conflictuelles dans la table de base de la base de données serveur. Sync Framework exécute cette commande si une opération d'insertion, de mise à jour ou de suppression retourne une valeur @sync\_row\_count de 0. Cette valeur indique que l'opération a échoué. Cette commande sélectionne les lignes pour les conflits ClientInsertServerInsert, ClientUpdateServerUpdate et ClientDeleteServerUpdate.
La requête ou la procédure stockée que vous spécifiez pour la propriété SelectConflictDeletedRowsCommand sélectionne les lignes conflictuelles dans la table tombstone de la base de données du serveur. Sync Framework exécute cette commande si la ligne conflictuelle est introuvable dans la table de base. Cette commande sélectionne les lignes pour le conflit ClientUpdateServerDelete.
Les données de chaque ligne en conflit sont stockées dans une collection SyncConflict. Cette collection peut atteindre une taille assez importante pour provoquer une erreur de mémoire insuffisante dans les cas suivants :
Un grand nombre de lignes sont en conflit. Envisagez de synchroniser un plus petit nombre de lignes dans chaque session ou de limiter le nombre de conflits en mettant à jour une ligne particulière sur un seul nœud.
Les lignes en conflit contiennent des colonnes qui utilisent des types de données volumineux. Envisagez de ne pas inclure les colonnes qui utilisent des types de données volumineux dans l'ensemble de colonnes qui sont synchronisées. Pour plus d'informations, consultez Procédure : filtrer des lignes et des colonnes.
Résolution des conflits et des erreurs
La résolution des conflits et des erreurs doit être gérée en réponse aux événements DbServerSyncProviderApplyChangeFailed et SqlCeClientSyncProviderApplyChangeFailed. L'objet ApplyChangeFailedEventArgs permet d'accéder à plusieurs propriétés qui peuvent être utilisées lors de la résolution des conflits :
Spécifiez le mode de résolution du conflit en définissant la propriété Action à l'une des valeurs de l'énumération ApplyAction suivantes :
Continue : ignorer le conflit et poursuivre la synchronisation.
RetryApplyingRow : retenter d'appliquer la ligne. La nouvelle tentative échouera et l'événement sera déclenché de nouveau si vous ne résolvez pas le conflit en modifiant l'une des lignes conflictuelles ou les deux lignes conflictuelles.
RetryWithForceWrite : réessayer avec la logique pour forcer l'application de la modification. L'objet SqlCeClientSyncProvider intègre une prise en charge de cette option. Pour utiliser cette option sur le serveur, utilisez le paramètre @sync\_force\_write et ajoutez la prise en charge dans les commandes qui appliquent les modifications à la base de données du serveur. Par exemple, pour un conflit ClientUpdateServerDelete, vous pouvez modifier la mise à jour dans une insertion lorsque la valeur du paramètre @sync\_force\_write est 1. Pour obtenir un exemple de code, consultez la section « Exemples » ci-après dans cette rubrique.
Obtenez le type de conflit et affichez les lignes conflictuelles du client et du serveur à l'aide de la propriété Conflict.
Obtenez le groupe de données des modifications faisant l'objet d'une synchronisation à l'aide de la propriété Context. Les lignes exposées par la propriété Conflict sont des copies ; par conséquent, leur remplacement ne modifie pas les lignes qui sont appliquées. Utilisez le groupe de données exposé par la propriété Context pour développer des schémas de résolution personnalisés si ces derniers sont requis par l'application. Pour obtenir un exemple de code, consultez la section « Exemples » ci-après dans cette rubrique.
L'objet SqlCeClientSyncProvider inclut également une propriété ConflictResolver que vous pouvez utiliser pour résoudre les conflits sur le client. Pour chaque type de conflit, vous pouvez définir une valeur à partir de l'énumération ResolveAction :
ClientWins : équivaut à définir un objet ApplyAction de Continue.
ServerWins : équivaut à définir un objet ApplyAction de RetryWithForceWrite.
FireEvent : déclencher l'événement ApplyChangeFailed, la valeur par défaut, puis gérer l'événement.
Il n'est pas obligatoire de définir la propriété ConflictResolver pour chaque type de conflit. Vous pouvez résoudre les conflits comme vous le faites sur le serveur, en gérant l'événement ApplyChangeFailed. Toutefois, la propriété ConflictResolver permet de spécifier facilement les options de résolution de conflit sur le client.
Exemple
L'exemple de code suivant indique comment configurer la détection et la résolution de conflit pour la table Customer de l'exemple de base de données Sync Framework. Dans cet exemple, les commandes de synchronisation sont créées manuellement, et non à l'aide de l'objet SqlSyncAdapterBuilder. Vous pouvez utiliser la détection et la résolution de conflit avec les commandes qui sont générées par l'objet SqlSyncAdapterBuilder, mais les commandes manuelles sont plus flexibles, particulièrement en termes de choix de la méthode d'application des modifications conflictuelles.
Éléments clés de l'API
Cette section contient des exemples de code qui désignent les éléments clés de l'API qui sont utilisés pour la détection et la résolution de conflit. La requête suivante sélectionne les lignes conflictuelles dans la table de base de la base de données du serveur.
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
La requête suivante sélectionne les lignes conflictuelles dans la table tombstone de la base de données du serveur.
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
L'exemple de code suivant crée une procédure stockée qui applique les mises à jour à la base de données du serveur. Cette procédure est spécifiée pour la propriété UpdateCommand. Les procédures stockées peuvent également être utilisées pour appliquer des insertions et des suppressions à la base de données du serveur. Pour obtenir des exemples de ces procédures, consultez Scripts d'installation pour les rubriques de procédures sur le fournisseur de bases de données.
La procédure de mise à jour usp_CustomerApplyUpdate tente une opération de mise à jour ou d'insertion, en fonction de la valeur du paramètre @sync\_force\_write et selon que la ligne à mettre à jour existe ou non dans la base de données du serveur. Si la ligne n'existe pas, la procédure convertit la mise à jour en une opération d'insertion. Dans cet exemple, la ligne manquante est provoquée par un conflit de mise à jour/suppression.
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
L'exemple de code suivant définit les options de résolution de conflit pour SqlCeClientSyncProvider. Comme nous l'avons indiqué précédemment, ces options ne sont pas obligatoires, mais elles constituent une solution pratique pour la résolution des conflits. Dans cet exemple, les mises à jour doivent systématiquement prévaloir lors des conflits de mise à jour ou de suppression, et tous les autres conflits doivent déclencher l'événement ApplyChangeFailed client.
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
Pour les conflits de mise à jour du client/suppression du serveur, la mise à jour est écrite de manière forcée sur le serveur, comme l'indique l'exemple de code suivant. Le conflit de mise à jour du client/suppression du serveur est traité sur le serveur à l'aide de l'option RetryWithForceWrite du gestionnaire d'événements ApplyChangeFailed du serveur. Lorsque vous utilisez cette option, cela signifie que la valeur du paramètre @sync\_force\_write est définie à 1 lorsque la procédure stockée de mise à jour est appelée sur le serveur.
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
L'exemple de code suivant enregistre les informations de conflit et écrit de manière forcée les insertions conflictuelles dans le gestionnaire d'événements ApplyChangeFailed du client.
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
Exemple de code complet
L'exemple de code complet ci-dessous inclut les exemples de code décrits précédemment, ainsi que du code supplémentaire pour effectuer la synchronisation. Qui plus est, sachez que l'exemple offre aux utilisateurs de l'application la possibilité de choisir la méthode de résolution des conflits de mise à jour/suppression. L'une des options est un schéma de résolution personnalisé qui associe les valeurs de colonne des lignes conflictuelles. Le code du schéma de résolution personnalisé est contenu dans les gestionnaires d'événements SampleServerSyncProvider_ApplyChangeFailed et SampleServerSyncProvider_ChangesApplied. L'exemple requiert la classe Utility qui est disponible dans Classe d'utilitaire pour les rubriques de procédures sur le fournisseur de bases de données.
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
Voir aussi
Autres ressources
Programmation des tâches courantes de synchronisation client et serveur