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
 
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.

 Commentaires (3) - Chapitre II : Synchronisation manuelle, personnalisation. 

Discussion démarée par fcastell le 04/02/2008 à 09:53 , 3 commentaire(s).

 Dernières Publications      

Exploiter les données CSV via Linq en toute simplicité
  A partir du requêteur dynamique fourni en exemple avec Visual Studio 2008, nous allons essayer de remplir les propriétés d'un ensemble d'objets à partir des données d'un fichier CSV. Nous enrichirons aussi le parseur de nos propres fonctions.
par Frédéric Mélantois posté le 17/05/2008 à 11:41, lu 138 fois, #0
Polymorphisme et contrats de données WCF
  WCF aborde les types polymorphes du point de vue de la sérialisation. En effet, la connaissance du type réel potentiel est rendue nécessaire dès la description du contrat de données. Une fois n'est pas coutume, j'ai réalisé l'exemple en VB.NET.
par Frédéric Colin posté le 14/05/2008 à 08:40, lu 296 fois, #2
A la découverte de BizTalk Server 2006 3/3
  Développer un assembleur pour BizTalk Server 2006 R2
par Kader Yildirim posté le 06/05/2008 à 13:20, lu 147 fois, #0
Chapitre III : Sync Services for ADO.NET et WCF
   Suite des deux premiers chapitres sur la synchronisation avec Sync Services for ADO.NET, voici un nouvel article impliquant WCF dans une synchronisation déconnectée.
Requêtes dynamiques sur les IEnumerable
  A partir d'un exemple fourni avec Visual Studio 2008, initialement prévu pour tout objet Queryable, nous allons présenter comment en ajoutant très peu de code rendre disponible aux IEnumerable un requêteur dynamique.
par Frédéric Mélantois posté le 24/04/2008 à 15:03, lu 842 fois, #0
Développer une visionneuse d'images avec WPF et WCF
  Au travers de cet article, nous allons découvrir comment mettre en place une visionneuse d'images, grâce aux technologies WPF et WCF.
par Thomas Lebrun posté le 22/04/2008 à 22:46, lu 1004 fois, #2
LINQRoleProvider
  L'objectif de cet article est d'implémenter un fournisseur de rôles ASP.NET personnalisé à l'aide de LINQ To Sql tout en faisant un tour d'horizons de la syntaxe des requêtes LINQ.
par Antoine Griffard posté le 13/04/2008 à 22:18, lu 491 fois, #4
WCF : Transfert de messages streamés et sécurisation personnalisée
  Je poursuis ma série d'articles sur WCF en vous présentant cette fois-ci le mode de communication Streamé. Histoire d'aller un petit peu plus loin, j'ai protégé le service de manière personnalisée et utilisé un binding très courant : BasicHttpBinding
par Frédéric Colin posté le 07/04/2008 à 08:12, lu 884 fois, #0

 Dernières Actualités      

Injection de code et API de profiling .NET
  Si vous êtes intéressés par la sécurité du Framework, par le reverse engineering et la manipulation/injection de code .NET et les packers, alors jetez un coup d’œil...
NDepend pour l'analyse statique de code .NET
  Pour ceux qui ne connaissent pas NDepend , il s’agit d’un outil d’analyse statique de code .NET qui permet de remonter des informations à toute une équipe de développement. NDepend aide à travailler sur...
Tags: Outils
Microsoft met à disposition son IoC Container Unity 1.0 en version finale
  Microsoft met à disposition la version 1.0 de son IoC container Unity, sur CodePlex sous la forme d'un Application Block des Enterprise Library. Si vous voulez en savoir plus sur le sujet en .NET je vous...
Tags: Application Block
Microsoft MVP (Most Valuable Professional) sur Tech Head Brothers
  Je voudrais féliciter les nouveaux Microsoft MVP (Most Valuable Professional) du mois d'Avril 2008 qui publient sur Tech Head Brothers! Sans les auteurs le site ne serait rien. Sébastien Pertus - MVP SQL...
Les Webcasts des Microsoft TechDays 2008 sont en ligne
  Si vous avez participé ou non au Microsoft TechDays 2008 vous avez certainement manqué certaines présentation qui vous intéressaient. Voilà enfin votre chance de pouvoir suivre ces présentations en ligne...
Rapide résumé de la conférence MIX08
  La conférence MIX08 s'est achevée la semaine dernière avec peu d'annonces de nouvelles technologies (DeepZoom) mais surtout des releases de produits (et c'est pas un mal...). Nous avons donc eu droit aux...