Freigeben über


Post Mortem développement d’un Brokered Component dans la vraie vie !

Bonjour;

Depuis l’arrivée de Windows 8.1, nous avons la possibilité de créer des composants Brokered Component pour permettre une passerelle entre le monde ModernUI WinRT et le monde Desktop .NET

Il existe depuis peu les templates de projets associés au développement de Brokered Component, disponible ici :

https://visualstudiogallery.msdn.microsoft.com/527286e4-b06a-4234-adde-d313c9c3c23e

Pour plus d’informations, je vous suggère quelques articles, vidéos sur le sujet :

  1. Un tutorial en 3 parties : https://devhawk.net/2014/04/25/brokered-winrt-components-step-one/
  2. Une couche d’accès aux données : https://blogs.u2u.be/diederik/post/2014/04/25/Building-Enterprise-apps-using-Brokered-Windows-Runtime-Components.aspx
  3. La vidéo de présentation de la //Build 2014 : https://channel9.msdn.com/Events/Build/2014/2-515
  4. La documentation MSDN : https://msdn.microsoft.com/en-us/library/windows/apps/dn630195.aspx

Dans cet article, je vais m’attacher à faire un retour d’expérience de la création d’un composant complet “de la vraie vie”. Bref, un composant qui fait un peu plus que Hello World ou récupérer 10 lignes d’une base de données Sourire

Je vais partir du principe que vous connaissez déjà la composants Brokered WinRT et qu’au moins votre hello world fonctionne correctement.

A partir de là, quand on veut aller “plus loin” , on peut rencontrer quelques problèmes, et voici quelques remarques sur ce que j’ai pu observer.

Debugging

Premier point, et non des moindres, le debugging de votre composant WinRT Brokered.

Au runtime, aucun point d’arrêt dans votre composant n’est appelé lors votre exécution. En effet ce composant est hosté par un autre processus, appelé dllhost.exe. Il va falloir s’attacher à celui-ci.
Autre chose, le composant n’est chargé que lors de la première instanciation d’un objet de votre Brokered Component.

De mon coté, j’ai donc un constructeur simple, public, qui charge mon composant, et qui me permet de poser un point d’arrêt correctement.

Dans les faits :

  1. Mettre un point d’arrêt après l’instanciation de votre composant WinRT dans votre application ModernUI.

  2. Mettre un point d’arrêt dans votre composant WinRT sur la méthode à débuguer.

  3. Lancer l’application en debug et s’arrêter au point d’arrêt situé après l’instanciation de votre composant.

    image

  4. S’attacher au processus dllhost.exe de votre composant (oui oui, PENDANT l’exécution) : 

    image

  5. Débuguer votre composant WinRT, maintenant accessible Sourire

    image


Runtime Policy Helper

Et oui, le propre d’un composant Brokered component, c’est pouvoir réutiliser du code .NET existant. Souvent, ce code est déjà compilé, et parfois (souvent) dans une version du Framework 2.*

Pour activer la compatibilité descendante, la méthode la plus simple, c’est de rajouter dans votre fichier de configuration, le nœud de configuration suivant :

 1 <?xml version="1.0" encoding="utf-8" ?>
2 <configuration>
3   <startup useLegacyV2RuntimeActivationPolicy="true">
4     <supportedRuntime version="v4.0"/>
5   </startup>
6 </configuration>

Sauf que dans notre cas, impossible de rajouter un fichier de configuration à notre composant. Il va falloir réaliser cette configuration par le code.

Je me suis inspiré de deux articles sur le sujet :

  1. https://blogs.msdn.com/b/jomo_fisher/archive/2009/11/17/f-scripting-net-4-0-and-mixed-mode-assemblies.aspx
  2. https://reedcopsey.com/2011/09/15/setting-uselegacyv2runtimeactivationpolicy-at-runtime/

A l’intégration, j’ai rajouté cette classe directement dans mon composant Brokered, en privé, en mode Nested :

  public sealed class SqlServerReplicationProvider : IReplicationProvider
    {
        private class RuntimePolicyHelper
        {
            private static bool legacyV2RuntimeEnabledSuccessfully;

            public static bool TryEnableLegacyV2Runtime()
            {
                if (legacyV2RuntimeEnabledSuccessfully)
                    return true;

                ICLRRuntimeInfo clrRuntimeInfo = (ICLRRuntimeInfo)RuntimeEnvironment.GetRuntimeInterfaceAsObject(Guid.Empty,
                typeof(ICLRRuntimeInfo).GUID);

                try
                {
                    clrRuntimeInfo.BindAsLegacyV2Runtime();
                    legacyV2RuntimeEnabledSuccessfully = true;
                }
                catch (COMException)
                {
                    legacyV2RuntimeEnabledSuccessfully = false;
                }

                return legacyV2RuntimeEnabledSuccessfully;
            }


            [ComImport]
            [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
            [Guid("BD39D1D2-BA2F-486A-89B0-B4B0CB466891")]
            private interface ICLRRuntimeInfo
            {
                void xGetVersionString();
                void xGetRuntimeDirectory();
                void xIsLoaded();
                void xIsLoadable();
                void xLoadErrorString();
                void xLoadLibrary();
                void xGetProcAddress();
                void xGetInterface();
                void xSetDefaultStartupFlags();
                void xGetDefaultStartupFlags();

                [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
                void BindAsLegacyV2Runtime();
            }
        }

Et j’appelle la méthode TryEnableLegacyV2Runtime dans le constructeur de mon composant, ce qui me va bien, si on reprend le point 1 au sujet du débugging Sourire

 1        public SqlServerReplicationProvider()
2         {
3             // Activer la compatibilité .NET 2.*
4             RuntimePolicyHelper.TryEnableLegacyV2Runtime();
5 
6         }

Classe abstraite / Interface

Bon, les classes abstraites, ça marche pas. Vos classes doivent être sealed pour être exposées en WinRT. Malheureusement les classes abstraites ne peuvent être sealed :

image

J’ai donc tout migré pour passer par une interface :

image

Enumération

Alors là, grosse déception au niveau des énumérations.

Les énumérations fonctionnent correctement avec des projets WinRT, mais ne fonctionnent pas dans le cas d’un Brokered Component..

Le simple fait de rajouter (sans même l’utiliser !) une énumération comme celle-ci, va générer une erreur :

 1     public enum SyncSecurityMode
2     {
3         Standard = 0,
4         Integrated = 1
5     }

Le pire c’est que l’erreur n’est remontée qu’au runtime, et que celle ci n’est pas du tout explicite. Vous rencontrez une simple BadImageFormatException :

 

image

Bref, si vous avez un soucis incompréhensible dans votre composant avec ce type d’erreur, cherchez du coté de vos énumérations Sourire

Pour remplacer votre énumération, vous pouvez :

  1. Passer par un simple entier (int) mais la lisibilité de votre code va pas mal en souffrir.
  2. Ecrire un composant non pas en C# mais directement en C++ WinMD. ça me semble complexe pour pas grand chose..
  3. Créer une solution de contournement utilisant un véritable objet (sealed donc) C’est cette solution que j’ai utilisée

J’ai pas mal cherché, j’ai fait pas mal de tests, je n’ai rien trouvé de très concluant pour palier ce problème. Du coup, je vous propose une solution, sujette à débat (si vous avez une idée, n’hésitez pas à commenter).

Pour faire simple :

  • J’intègre en mode privé l’énumération en nested class, manière de l’utiliser en interne
  • J’utilise des méthodes statiques me permettant d’utiliser la syntaxe :
 1 var ssm = SyncSecurityMode.Standard;
  • Je surcharge la méthode Equals, ce qui me permet de traiter la comparaison. Malheureusement surcharger les opérateurs == et != auraient été plus judicieux, mais la création de composants WinRT l’interdit.

    Mes comparaisons deviennent donc :

 1 if (this.PublisherSecurityMode.Equals(SyncSecurityMode.Standard))
  • Enfin, je surcharge la méthode ToString() pour pouvoir facilement afficher la valeur de l’énumération (encore une fois, pratique en mode Debug)

Voici l’extrait de la classe SyncSecurityMode que j’ai donc migré pour qu’elle fonctionne correctement :

  1    public sealed class SyncSecurityMode
 2     {
 3         private enum SyncSecurityModeEnum
 4         {
 5             Standard = 0,
 6             Integrated = 1
 7         }
 8         private SyncSecurityMode(SyncSecurityModeEnum value)
 9         {
10             this.syncSecurityModeEnum = value;
11         }
12         private SyncSecurityMode()
13         {
14         }
15         private SyncSecurityModeEnum syncSecurityModeEnum;
16 
17         public static SyncSecurityMode Standard { get { return new SyncSecurityMode(SyncSecurityModeEnum.Standard); } }
18     
19         public static SyncSecurityMode Integrated { get { return new SyncSecurityMode(SyncSecurityModeEnum.Integrated); } }
20 
21         public override bool Equals(object obj)
22         {
23             if (obj == null) return false;
24 
25             SyncSecurityMode other = (SyncSecurityMode)obj;
26             return this.syncSecurityModeEnum.Equals(other.syncSecurityModeEnum);
27         }
28 
29    }

IDisposable

Toutes vos classes WinRT se doivent d’être IDisposable. Sur l’ensemble de mes tests, ne pas implémenter IDisposable avait tendance à laisser le composant chargé en mémoire (surtout lors de la levé d’exceptions non managées par mon composant) dans dllhost.exe

J’utilise le pattern IDIsposable classique, où je différencie le moment de disposer mes ressources managées et non managées :

  1        /// <summary>
 2         /// Dispose pattern
 3         /// </summary>
 4         private bool disposed; // to detect redundant calls
 5 
 6         /// <summary>
 7         /// Closes and dispose.
 8         /// </summary>
 9         public void Close()
10         {
11             Dispose(true);
12             GC.SuppressFinalize(this);
13         }
14 
15         ~SqlServerReplicationProvider()
16         {
17             Dispose(false);
18         }
19         /// <summary>
20         /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
21         /// call Close() method
22         /// </summary>
23         public void Dispose()
24         {
25             Close();
26         }
27 
28         protected void Dispose(bool disposing)
29         {
30             if (!disposed)
31             {
32                 if (disposing)
33                 {
34                     // Dispose managed resources.
35                     if (subscriberConn != null && subscriberConn.IsOpen)
36                         subscriberConn.Disconnect();
37 
38                     if (publisherConn != null && publisherConn.IsOpen)
39                         publisherConn.Disconnect();
40                 }
41 
42                 // There are no unmanaged resources to release, but
43                 // if we add them, they need to be released here.
44             }
45             disposed = true;
46         }
47 

Plus d’informations sur le pattern IDisposable ici : https://msdn.microsoft.com/en-us/library/system.idisposable(v=vs.110).aspx

Legacy Objects : Gérer les évènements

Et oui, avant on avait plutôt l’habitude de léver des évènements dans nos objets. Ce qui pouvait poser quelques problèmes quand on est en asynchrone (revenir sur le thread UI etc … vous voyez suremment de quoi je parle Sourire)

Aujourd’hui, quand on parle pattern asynchrone, on pense IProgress<T> qui permet de lever une progression et ne pas avoir à gérer le problème de threading.

Pour présenter le problème que j’ai rencontré voici un exemple, simple, de la situation que j’ai dû résoudre :

  1    public class TimeSource : IDisposable
 2     {
 3         private readonly Timer timer;
 4 
 5         public event EventHandler<DateTime> OnTick;
 6         public event EventHandler<DateTime> Started;
 7         public event EventHandler<DateTime> Ended;
 8 
 9         private int tickCount = 0;
10         private int maxTick = 5;
11         private Double tickDuration = 1000;
12 
13 
14         public DateTime Value { get; set; }
15         public TimeSource()
16         {
17             this.timer = new Timer(tickDuration);
18             this.timer.Elapsed += TimerOnElapsed;
19        }
20 
21         public void Start()
22         {
23             this.timer.Start();
24             Value = DateTime.Now;
25 
26             if (Started != null)
27                 Started(this, Value);
28  
29         }
30 
31         public void Stop()
32         {
33             this.timer.Stop();
34             this.tickCount = 0;
35 
36             Value = DateTime.Now;
37 
38             if (Ended != null)
39                 Ended(this, Value);
40 
41         }
42 
43         private void TimerOnElapsed(object sender, ElapsedEventArgs elapsedEventArgs)
44         {
45             try
46             {
47                 Value = DateTime.Now;
48                 
49                 if (tickCount >= maxTick)
50                 {
51                     if (Ended != null)
52                         Ended(this, Value);
53                     
54                     this.timer.Stop();
55                     
56                     tickCount = 0;
57                 }
58                 else
59                 {
60                     if (OnTick != null)
61                         OnTick(this, Value);
62 
63                     tickCount++;
64 
65                 }
66             }
67             catch (Exception ex)
68             {
69                 Console.WriteLine(ex.Message);
70             }
71         }
72 
73         public void Dispose()
74         {
75             this.timer.Stop();
76             this.timer.Elapsed -= TimerOnElapsed;
77             this.timer.Dispose();
78         }
79 
80     }

 

Pour faire simple, l’objet TimeSource lève des évènements au fur et à mesure du déroulement de son métier. Lorsque le dernier évènement et levé, la tache est terminée.

Comment faire pour encapsuler cet objet (si on ne peut pas le modifier) pour en créer une Task, et pouvoir faire un await dessus ?

En utilisant l’objet TaskCompletionSource<T> qui va nous permettre d’attendre l’évènement de fin pour compléter la tache.

Les méthodes importantes de TaskCompletionSource<T> à implémenter :

  • SetResult<T> : Permet d’affecter le résultat et finir la tache
  • SetException<T> : Permet de gérer les exceptions et renvoyer l’exception au niveau de la tache
  • SetCanceled : Permet de gérer l’appel Cancel d’un jeton CancellationToken

Voici l’implémentation de ce pattern dans la classe TimeSourceAsync :

  1     public class TimeSourceAsync
 2     {
 3 
 4         private TimeSource innerTimeSource = new TimeSource();
 5 
 6         public async Task<DateTime> LaunchAsync(IProgress<DateTime> progress)
 7         {
 8            
 9             return await Task.Run<DateTime>(() =>
10             {
11                 var tcs = new TaskCompletionSource<DateTime>();
12 
13                 innerTimeSource.Started += (s, e) => progress.Report(e);
14                 innerTimeSource.OnTick += (s, e) => progress.Report(e);
15 
16                 innerTimeSource.Ended += (s, e) =>
17                     {
18                         tcs.SetResult(innerTimeSource.Value);
19                     };
20 
21                 innerTimeSource.Start();
22 
23                 return tcs.Task ;
24             });
25         }
26 
27     }

Evidemment pour aller plus loin que cet exemple trivial, n’hésitez pas aussi à implémenter le code permettant d’utiliser un jeton d’annulation (CancellationToken) qui appellera la méthode SetCancel() du TaskCompletationSource<T>

Dans mon cas simpliste, je n’ai pas utiliser un Try Catch, mais n’oubliez pas de l’implémenter et gérer l’exception avec la méthode SetException(Exception)

Task, AsyncInfo, IAsyncAction, IAsyncOperation, IProgress, CancellationToken …

Lors de la création de votre composant, vous voulez bien-sûr privilégier le mode asynchrone pour les taches lourdes.

Evidemment le premier réflexe est donc de créer une Task<T> , comme ceci par exemple :

  1         public async Task Sync()
 2         {
 3             return await Task.Run(() =>
 4             {
 5                 SyncStatistics statistics = new SyncStatistics();
 6                 MergeSynchronizationAgent syncAgent = null;
 7 
 8                 // bla bla bla 
 9 
10                 return;
11             });
12         }

Voir une tache qui renvoie un type, comme ceci :

  1         public async Task<SyncStatistics> Sync()
 2         {
 3             return await Task.Run<SyncStatistics>(() =>
 4             {
 5                 SyncStatistics statistics = new SyncStatistics();
 6                 MergeSynchronizationAgent syncAgent = null;
 7 
 8                 // bla bla bla 
 9 
10                 return statistics;
11             });
12         }

Et pour aller encore plus loin, créer une Task<T> , qui renvoie un type T, et qui gère un CancellationToken et un IProgress<T> , comme ceci :

  1         public async Task<SyncStatistics> Sync(CancellationToken cancellationToken, 
 2                                          IProgress<SyncStatistics> progress)
 3         {
 4             return await Task.Run<SyncStatistics>(() =>
 5             {
 6                 SyncStatistics statistics = new SyncStatistics();
 7                 MergeSynchronizationAgent syncAgent = null;
 8 
 9                 // Check if cancellation has occured
10                 if (cancellationToken.IsCancellationRequested)
11                     cancellationToken.ThrowIfCancellationRequested();
12                 
13                 // Report progress
14                 progress.Report(statistics);
15 
16                 // bla bla bla 
17 
18                 return statistics;
19             });
20         }

Sauf que ça, ça ne fonctionne pas pour un composant WinRT. L’erreur est explicite

image

Pour exposer une méthode en asynchrone, on doit renvoyer un interface, au minium, de type IAsyncAction ou IAsyncOperation.

Pour faire simple:

  • IAsyncAction : Permet de gérer un appel asynchrone
  • IAsyncOperation : Permet de gérer un appel asynchrone et renvoie un résultat
  • IAsyncActionWithProgress : Permet de gérer un appel asynchrone avec progression
  • IAsyncOperationWithProgress : Permet de gérer un appel asynchrone avec progression et qui renvoie un résultat

 

Pour plus d’informations sur le sujet, je vous suggère ces deux articles du Dieu en la matière Stephen Toub :

  1. https://blogs.msdn.com/b/windowsappdev/archive/2012/06/14/exposing-net-tasks-as-winrt-asynchronous-operations.aspx
  2. https://blogs.msdn.com/b/windowsappdev/archive/2012/04/24/diving-deep-with-winrt-and-await.aspx

 

Pour résumé, vous avez deux possibilités pour retourner IAsyncAction ou IAsyncOperation :

  1. Utiliser les méthodes d’extensions de Task<T> : AsAsyncOperation() ou AsAsyncAction()
  2. Utiliser l’objet AsyncInfo et la méthode statique Run()

 

Méthodes d’extension AsAsyncOperation() ou AsAsyncAction()

C’est de loin la méthode la plus simple. Elle est rapide à mettre en place, mais malheureusement ne prend pas en compte la progression (IProgress<T> ) ou l’annulation (CancellationToken)

Dans mon cas, j’ai tout d’abord créer une méthode privée qui renvoie une Task<T> , puis j’ai créé la méthode publique encapsulant l’appel en utilisant dans mon cas AsAsyncOperation() :

  1         public IAsyncOperation<SyncStatistics> SynchronizeAsync()
 2         {
 3             return InternalSync(CancellationToken.None, null).AsAsyncOperation();
 4 
 5         }
 6         private Task<SyncStatistics> InternalSync(CancellationToken cancellationToken, IProgress<int> progress)
 7         {
 8             return Task.Run<SyncStatistics>(() =>
 9             {
10                 SyncStatistics statistics = new SyncStatistics();
11                 MergeSynchronizationAgent syncAgent = null;
12 
13                 // Check if cancellation has occured
14                 if (cancellationToken.IsCancellationRequested)
15                     cancellationToken.ThrowIfCancellationRequested();
16 
17                 // Report progress
18                 if (progress != null)
19                     progress.Report(0);
20 
21                 // bla bla bla 
22 
23                 return statistics;
24             });
25         }

Comme on le remarque, on se passe de la progression et du jeton d’annulation

AsyncInfo.Run()

Ici plus de souplesse d’utilisation, mais un peu plus de complexité à la mise en œuvre (et encore que …)

L’objet AsyncInfo possède 4 méthodes statiques, permettant de renvoyer chacune des interfaces qui nous intéresse :

  1         public static class AsyncInfo
 2         {
 3             public static IAsyncOperationWithProgress<TResult, TProgress> Run<TResult, TProgress>(
 4                 Func<CancellationToken, IProgress<TProgress>, Task<TResult>> taskProvider);
 5             public static IAsyncActionWithProgress<TProgress> Run<TProgress>(
 6                 Func<CancellationToken, IProgress<TProgress>, Task> taskProvider);
 7             public static IAsyncOperation<TResult> Run<TResult>(
 8                 Func<CancellationToken, Task<TResult>> taskProvider);
 9             public static IAsyncAction Run(
10                 Func<CancellationToken, Task> taskProvider);
11         }
12     }

Dans notre cas, l’implémentation est assez simple, j’utilise la méthode IAsyncOperationWithProgress<T,T2> :

 1         public IAsyncOperationWithProgress<SyncStatistics, int> SynchronizeAsync()
2         {
3             return AsyncInfo.Run<SyncStatistics, int>((cancellationToken, progress) =>
4                      InternalSync(cancellationToken, progress));
5         }

A partir de là, nous avons un appel d’une méthode asynchrone gérant facilement le jeton d’annulation et la progression.

Implémentation et appel du code asynchrone du Brokered Component

L’appel coté code se fait de façon plutôt simple, tel que vous avez l’habitude de faire :

 1         private async void btnSynchronize_Click(object sender, RoutedEventArgs e)
2         {
3            // Constructeur de mon composant WinRT Brokered
4             syncProvider = GetSyncProvider();
5 
6             // Launch Sync
7             var syncCheck = await syncProvider.SynchronizeAsync();
8         }
9 

Le problème c’est que votre méthode ne prend PAS de paramètre CancellationToken ou IProgress<T> , alors que nous avons bien implémenter (grace à l’objet AsyncInfo) le code nécessaire pour faire fonctionner ce pattern.

Nous allons utiliser la méthode d’extension AsTask() qui va du coup nous renvoyer une Task tel qu’on a l’habitude de faire, et ainsi gérer le jeton d’annulation et la progression.

J’en ai déjà parlé en préambule mais si vous voulez apprendre comment fonctionne AsTask(), n’hésitez pas à regarder cet article de Monsieur Toub (Oui Monsieur, avec un M majuscule, lui il y a droit. Y’a des gens comme ça ….) : https://blogs.msdn.com/b/windowsappdev/archive/2012/04/24/diving-deep-with-winrt-and-await.aspx

Voici l’implémentation complète de mon appel :

  1         private async void btnSynchronize_Click(object sender, RoutedEventArgs e)
 2         {
 3             // Déclaration de ma progression
 4             Progress<int> progress = new Progress<int>((p) => lstEvents.Items.Add(String.Format("{0} % ", p)));
 5            
 6             // Jeton d'annulation
 7             CancellationTokenSource cancelTokenSource = new CancellationTokenSource();
 8             try
 9             {
10                 // Constructeur de mon composant WinRT Brokered
11                 syncProvider = GetSyncProvider();
12       
13                 // Launch Sync
14                 var syncCheck = await syncProvider.SynchronizeAsync().AsTask(cancelTokenSource.Token, progress);
15 
16             }
17             catch (Exception)
18             {
19                 cancelTokenSource.Cancel();
20                 throw;
21             }
22 
23         }

 

 

Voilà un premier retour d’expérience de la création d’un composant Brokered Component, dans la vraie vie.

Si vous voulez d’autre pointeurs sur la performance d’un Brokered Component, n’hésitez pas à consulter l’article MSDN sur le sujet : https://msdn.microsoft.com/en-us/library/windows/apps/dn630195.aspx

On y parle notamment de l’utilisation de struct et du renvoie d’array plutôt que de List<T> lorsque vous devez renvoyer “plus de 10 lignes” d’une base de données SQL Server par exemple.

Bon développement !

/Seb