Sébastien Pertus
Chapitre II : Synchronisation manuelle, personnalisation.
Après une introduction à Sync. Services for ADO.NET 2.0, nous allons construire aujourd'hui un ensemble de classes, sans passer par le designer, pour créer un système de synchronisation en nous appuyant sur Microsoft Synchronization Framework (MSF).
Par Sébastien Pertus publié le 13/01/2008 à 22:50, lu 9070 fois, 6 pages
 4 | Première solution
Pour cette première solution, je vous propose de créer deux ensembles de classes, un premier ensemble pour le coté client puis un second ensemble pour le coté serveur.
De plus, nous allons essayer de nous rapprocher de la "façon de faire" du Sync Designer VS 2008, c'est-à-dire, créer des classes héritant des classes de bases de synchronisation, il faut dire que ce sera nettement plus élégant.
Nous utiliserons aussi pour notre coté serveur du SqlSyncAdapterBuilder, qui nous permettra de configurer nos SyncAdapters facilement.
Première étape : Créer le fournisseur de synchronisation client, classe héritée de SqlCeClientSyncProvider.
Cette classe doit contenir la chaine de connexion à la source de donnée cliente, configurée dans notre fichier de configuration.
Note : SqlCeClientSyncProvider sait créer les tables à la volée, par contre il ne sait pas créer la base de donnée si celle-ci n'existe pas.

public class DemoClientSyncProvider : SqlCeClientSyncProvider

{

    public DemoClientSyncProvider()

    {

        this.ConnectionString = ConfigurationManager.ConnectionStrings["ClientConnexionString"];

    }

    public DemoClientSyncProvider(string connectionString)

    {

        this.ConnectionString = connectionString;

    }

}

Deuxième étape : Créer l'agent, classe héritée de SyncAgent.
Cette classe va contenir les SyncTables et le SyncGroup.
Dans notre exemple, deux SyncTables : Client et ClientType.
Nous lierons ces deux SyncTables dans un SyncGroup, pour préserver la clé externe, et créer un contexte Transactionnel de synchronisation.
Notez les options de création des SyncTables : Une est créée avec l'option de Direction BiDirectional, puisque nous voulons récupérer les enregistrements de façon incrémentale et l'autre avec l'option Snapshot, la table ClientType devant être récupérée entièrement, créant une sorte de "cache client".
Note : l'agent doit connaître le fournisseur serveur, nous ne l'avons pas encore créé, c'est pourquoi nous le mettons, pour l'instant, en commentaire.

public partial class DemoSyncAgent : SyncAgent

{

    private SyncGroup globalSyncGroup;

    private SyncTable clientSyncTable;

    private SyncTable clientTypeSyncTable;

 

    public DemoSyncAgent()

    {

        this.InitializeSyncProviders();

        this.InitializeSyncTables();

    }

 

    public SyncTable Client

    {

        get

        {

            return this.clientSyncTable;

        }

        set

        {

            this.Configuration.SyncTables.Remove(this.clientSyncTable);

            this.clientSyncTable = value;

            this.Configuration.SyncTables.Add(this.clientSyncTable);

        }

    }

 

    public SyncTable ClientType

    {

        get

        {

            return this.clientTypeSyncTable;

        }

        set

        {

            this.Configuration.SyncTables.Remove(this.clientTypeSyncTable);

            this.clientTypeSyncTable = value;

            this.Configuration.SyncTables.Add(this.clientTypeSyncTable);

        }

    }

 

    private void InitializeSyncProviders()

    {

        // this.RemoteProvider = new DemoServerSyncProvider();

        this.LocalProvider = new DemoClientSyncProvider();

    }

 

    private void InitializeSyncTables()

    {

        // Creation du SyncGroup.

        this.globalSyncGroup = new Microsoft.Synchronization.Data.SyncGroup("DemoGlobalSyncGroup");

 

        // Creation des SyncTables.

        this.clientSyncTable = new SyncTable();

        this.clientSyncTable.TableName = "Client";

        this.clientSyncTable.CreationOption = TableCreationOption.DropExistingOrCreateNewTable;

        this.clientSyncTable.SyncDirection = SyncDirection.Bidirectional;

        this.clientSyncTable.SyncGroup = globalSyncGroup;

        this.Configuration.SyncTables.Add(this.clientSyncTable);

 

        this.clientTypeSyncTable = new SyncTable();

        this.clientTypeSyncTable.SyncGroup = globalSyncGroup;

        this.clientTypeSyncTable.TableName = "ClientType";

        this.clientTypeSyncTable.CreationOption = TableCreationOption.DropExistingOrCreateNewTable;

        this.clientTypeSyncTable.SyncDirection = SyncDirection.Snapshot;

        this.Configuration.SyncTables.Add(this.clientTypeSyncTable);

    }

}

Nous avons terminé la partie Cliente, nous pouvons nous attaquer au gros morceau, la configuration du coté serveur.
Première étape : Créer une classe héritant de DbServerSyncProvider. Cette classe doit contenir deux SyncAdapters, respectivement associés à la table Client et la table ClientType.
Dans le constructeur, initialisation d'une connexion à notre base de données Sql Serveur 2005.

public partial class DemoServerSyncProvider : DbServerSyncProvider

{

    private SyncAdapter clientSyncAdapter;

    private SyncAdapter clientTypeSyncAdapter;

 

    public DemoServerSyncProvider()

    {

        string connectionString = ConfigurationManager.ConnectionStrings["ServeurConnexionString"].ConnectionString;

        this.InitializeConnection(connectionString);

    }

 

    public DemoServerSyncProvider(string connectionString)

    {

        this.InitializeConnection(connectionString);

    }

 

    public SyncAdapter ClientSyncAdapter

    {

        get

        {

            return this.clientSyncAdapter;

        }

        set

        {

            this.clientSyncAdapter = value;

        }

    }

 

    public SyncAdapter ClientTypeSyncAdapter

    {

        get

        {

            return this.clientTypeSyncAdapter;

        }

        set

        {

            this.clientTypeSyncAdapter = value;

        }

    }

 

    private void InitializeConnection(string connectionString)

    {

        this.Connection = new System.Data.SqlClient.SqlConnection(connectionString);

    }

}

Deuxième étape : Créer une commande chargée de récupérer une ancre.
Une ancre est une donnée, fournie par le serveur, déterminant, de façon unique, le moment de la synchronisation.
Cette ancre, va être "mémorisée" coté client, et sera utilisée pour la prochaine synchronisation.
Ces ancres, gérées par MSF, sont syntaxiquement connus :
  • @sync_last_received_anchor : Dernière ancre mémorisée sur le client, servant de borne inférieure.
  • @sync_new_received_anchor : Ancre servant de borne supérieure, générée lors de la synchronisation
Ces ancres peuvent être de deux types différents : Soit DateTime, soit Timestamp.
Note : Le type de cette ancre est déterminé par le type de colonne que vous avez choisi pour stocker les valeurs de temps de modification et de création d'une ligne de votre source de données. Dans notre cas (voir le schéma élaboré dans la première partie de l'article), nous avons créé des colonnes de type DateTime. Pour l'exemple, vous trouverez en commentaire la syntaxe pour récupérer une ancre de type timestamp.
Voici un schéma qui reprend le concept de création de ces ancres et la façon dont elles sont utilisées :
 
/content/61a8af9e-e912-4557-b7c5-17e0734e14d7/SchemaAncre.jpg
 
Nous n'avons pas besoin de gérer quoique ce soit ici, notre seule mission est de fournir au fournisseur de synchronisation, la façon de récupérer cette ancre.
Ceci est porté par une commande (DbCommand) nommé SelectNewAnchorCommand que l'on peut configurer de la façon suivante :

private void InitializeNewAnchorCommand()

{

    this.SelectNewAnchorCommand = new SqlCommand();

    this.SelectNewAnchorCommand.CommandText =

        "Select @" + SyncSesion.SyncNewReceivedAnchor + " = GETUTCDATE()";

    // Cas du TimeStamp

    // this.SelectNewAnchorCommand.CommandText = "Select @"

    //    + SyncSession.SyncNewReceivedAnchor + " = @@DBTS"; 

    // SQL Server 2005 SP2 et plus : Utiliser "min_active_rowversion() - 1"

 

    this.SelectNewAnchorCommand.CommandType = CommandType.Text;

    SqlParameter anchorParameter =

        new SqlParameter("@" + SyncSession.SyncNewReceivedAnchor, System.Data.SqlDbType.DateTime);

    // Cas du TimeStamp

    // SqlParameter anchorParameter = new SqlParameter("@sync_new_received_anchor",

    //                                              System.Data.SqlDbType.Timestamp);

    anchorParameter.Direction = System.Data.ParameterDirection.Output;

 

    this.SelectNewAnchorCommand.Parameters.Add(anchorParameter);

}

Troisième étape : Configurer nos SyncAdapters
C'est ici que nous allons utiliser notre SqlSyncAdapterBuilder. Celui-ci, une fois configuré, va générer notre SyncAdapter.
  1. Créer un SqlSyncAdapterBuilder, configuré pour une connexion serveur, et un type de synchronisation :

    this.clientSyncAdapter = new SyncAdapter();

    this.clientTypeSyncAdapter = new SyncAdapter();

     

    SqlSyncAdapterBuilder clientBuilder = new SqlSyncAdapterBuilder();

    clientBuilder.SyncDirection = SyncDirection.Bidirectional;

    clientBuilder.Connection = this.Connection as SqlConnection;

     

    SqlSyncAdapterBuilder clientTypeBuilder = new SqlSyncAdapterBuilder();

    clientTypeBuilder.SyncDirection = SyncDirection.Snapshot;

    clientTypeBuilder.Connection = this.Connection as SqlConnection;

  2. Paramétrer le Builder :
    • La liste des colonnes, qui serviront à créer la requête de sélection.
    • La table servant à stocker les identifiants des enregistrements à supprimer.
    • La liste des colonnes servant à "tracer" les changements survenus sur la table (et sur les lignes)

    // base table

    clientBuilder.TableName = "Client";

    clientBuilder.DataColumns.Add("ClientId");

    clientBuilder.DataColumns.Add("EmployeId");

    clientBuilder.DataColumns.Add("ClientTypeId");

    clientBuilder.DataColumns.Add("Prenom");

    clientBuilder.DataColumns.Add("Nom");

    clientBuilder.DataColumns.Add("Adresse");

    clientBuilder.DataColumns.Add("CreationDate");

    clientBuilder.DataColumns.Add("LastEditDate");

     

    // tombstone table

    clientBuilder.TombstoneTableName = "Client_Tombstone";

    clientBuilder.TombstoneDataColumns.Add("ClientId");

    clientBuilder.TombstoneDataColumns.Add("DeletionDate");

     

    // tracking\sync columns

    clientBuilder.CreationTrackingColumn = @"CreationDate";

    clientBuilder.UpdateTrackingColumn = @"LastEditDate";

    clientBuilder.DeletionTrackingColumn = @"DeletionDate";

     

     

    // base table

    clientTypeBuilder.TableName = "ClientType";

    clientTypeBuilder.DataColumns.Add("ClientTypeId");

    clientTypeBuilder.DataColumns.Add("Libelle");

     

    Note : la table ClientType, étant une table récupérée entièrement à chaque fois, inutile de lui fournir les informations non nécessaires, comme le table tombstone ou les colonnes de tracking (d'ailleurs tout ceci n'existe pas en base !)
  3. Demander à générer les SyncAdapters et les rajouter à la collection des SyncAdapters de notre fournisseur de synchronisation serveur :

    // Generation des syncadapters

    this.clientTypeSyncAdapter = clientBuilder.ToSyncAdapter();

    this.clientSyncAdapter = clientBuilder.ToSyncAdapter();

     

    this.SyncAdapters.Add(this.clientSyncAdapter);

    this.SyncAdapters.Add(this.clientTypeSyncAdapter);

Pour tester notre synchronisation, nous créons une instance de notre agent, puis appel à la méthode "Synchronize()".
Notez que nous vérifions l'existence de la base, et utilisons la flexibilité du moteur Sql Server CE pour créer à la volée la base de données.

private void btnSynchronize_Click(object sender, EventArgs e)

{

    if (!File.Exists(SqlCeDataBaseName))

    {

        string clientConnectionString =

            ConfigurationManager.ConnectionStrings["ClientConnectionString"].ConnectionString;

        SqlCeEngine sqlCeEngine = new SqlCeEngine(clientConnectionString);

        sqlCeEngine.CreateDatabase();

    }

 

    DemoSyncAgent agent = new DemoSyncAgent();

    agent.Synchronize();

 

}

Un petit tour en debug, pour vérifier ce que va générer notre Builder de SyncAdapters :
 
/content/61a8af9e-e912-4557-b7c5-17e0734e14d7/DebugSyncAdapter01.jpg
 
 
/content/61a8af9e-e912-4557-b7c5-17e0734e14d7/DebugSyncAdapter02.jpg
 
Notez que sur les captures d'écrans, nous nous attardons sur la génération de la commande permettant de récupérer les insertions incrémentales.
Nous avons bien la liste des colonnes configurées, ainsi que les deux paramètres servant à délimiter la plage de données à récupérer.
Maintenant que notre synchronisation fonctionne, nous pouvons essayer de la personnaliser.
Supposons que chaque employé embarque sur son portable l'application déconnectée, et que ceux-ci veuillent récupérer uniquement leurs clients attitrés.
Il faut bien faire la part des choses :
Que devons nous rajouter ? Sur le serveur, un paramètre supplémentaire, et un filtre dans les requêtes générées.
Qui envoie la valeur du paramètre ? Le client, via l'agent de synchronisation.

Coté serveur, notre Builder met à disposition deux propriétés, que nous pouvons renseigner :
  • FilterClause : Chaine de caractères représentant la clause. C'est une chaine de type SQL, se plaçant immédiatement après la clause WHERE.
  • FilterParameters : Collection des paramètres. Attention, nous sommes bien d'accord, ici on crée les paramètres (type et nom) MAIS nous n'affectons PAS leur valeur !

// Personnalisation

clientBuilder.FilterClause = "EmployeId = @EmployeId";

clientBuilder.FilterParameters.Add(new SqlParameter("@EmployeId", SqlDbType.UniqueIdentifier));

Coté Client, notre agent va contenir le paramètre et sa valeur
  • SyncAgent.Configuration.SyncParameters : Collection des paramètres, composés de paire clé-valeur
Notre code de synchronisation évolue (peu) et devient :

if (!File.Exists(SqlCeDataBaseName))

{

    string clientConnectionString =

        ConfigurationManager.ConnectionStrings["ClientConnectionString"].ConnectionString;

    SqlCeEngine sqlCeEngine = new SqlCeEngine(clientConnectionString);

    sqlCeEngine.CreateDatabase();

}

 

DemoSyncAgent agent = new DemoSyncAgent();

 

Guid empGuid = new Guid(ConfigurationManager.AppSettings["EmployeId"]);

agent.Configuration.SyncParameters.Add(

    new SyncParameter("@EmployeId", empGuid));

 

agent.Synchronize();

Voilà nous avons terminé notre première solution. Dans la prochaine partie nous allons voir comment faire tout ceci en se passant du Builder et en utilisant, non pas des commandes de type Text mais des commandes de type StoredProcedure.
 
» Démarrer une discussion
 
Discussion démarée par mbergeron le 29/12/2008 à 15:27, 1 commentaire(s).
Discussion démarée par fcastell le 04/02/2008 à 09:53, 3 commentaire(s).