Architecturer des applications métier

Partie 5 - Modules et composition d'interface utilisateur

Cet article fait partie d'une série expliquant comment architecturer une application d'entreprise Silverlight centrée sur les données. Silverlight, lorsqu'elle est combinée avec les patterns les plus populaires aujourd'hui et certaines technologies-clés pour le génie logiciel, peut être une technologie très productive, simple, fiable et efficace pour construire ce genre d'applications, et en même temps, fournir une expérience utilisateur exceptionnelle.

Langage/Outil/Technologie : Silverlight 4.

Commentez ce tutoriel : Commentez Donner une note à l'article (0)

Article lu   fois.

Les deux auteur et traducteur

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

Traduction

Cet article est la traduction la plus fidèle possible de l'article de Manuel Felicio, Architecting Silverlight LOB applications (Part 5) - Modules and UI composition(1).

Architecturer des applications métier Silverlight - Modules et composition d'interface utilisateur

Dans cet article et le prochain, je vais parler du développement client et de comment créer une application Silverlight modulaire et composite. Dans cet article, je vais me concentrer sur la composition de l'interface utilisateur.

Lors de la construction d'applications métier Silverlight composites, vous devez être conscient que la modularité est une exigence importante. En tant que développeur, il devrait être plus facile pour vous de maintenir votre code et d'ajouter de nouvelles fonctionnalités à votre application sans avoir à changer le reste, de plus le fichier XAP de votre application ne grossit pas. Chaque module est un fichier XAP séparé, qui peut être téléchargé à la demande, selon l'intention de l'utilisateur d'utiliser une certaine fonctionnalité.

Ci-dessous est une architecture possible afin de réaliser ce dont nous avons besoin :

Image non disponible

Le dossier « Shell » contient le xap principal, que j'ai appelé MyApp. Ce xap est uniquement responsable de télécharger les autres et de leur fournir les moyens pour afficher leur contenu dans l'application principale. Le dossier « Modules » contient 4 modules essentiels pour le scénario d'entreprise que j'ai défini dans un précédent article. Le module « Orders » permet aux utilisateurs non-employés de passer de nouvelles commandes. Le module « Sales » permet aux employés de consulter les commandes en cours et de les traiter. Le module « Statistics » permet aux employés de visualiser des graphiques et des rapports de l'évolution des ventes. Les fonctionnalités pour le département des ressources humaines sont accomplies par le module « Administration », qui devrait également permettre aux utilisateurs de l'application d'être créés et/ou gérés. Les modules et le Shell sont compilés dans le dossier ClientBin de « MyApp.Web ».

Le projet « Infrastructure » est un élément clé. Il définit les moyens de communiquer entre les modules (les modules ne référencent pas d'autres modules) et l'application principale. En d'autres termes, il s'agit d'un contrat commun partagé par les modules. Lorsqu'il est défini ou partiellement défini, le développement des modules peut commencer. Notez que le développement du module « Orders » ne dépend pas du développement du module « Sales » ou du module « Statistics », ce qui signifie que des personnes différentes peuvent travailler sur chacun d'eux.

MyApp.Data.Silverlight est un projet qui devrait posséder du code généré par WCF RIA Services, ainsi que les clients d'autres services qui sont utilisés par les modules.

La couche serveur correspond à l'architecture que j'ai définie dans la partie 1.

Donc, comment notre Shell télécharge-t-il les modules ? Nous avons juste besoin de spécifier un catalogue de modules et le donner au programme d'amorçage de Prism, qui est responsable de l'initialisation du framework de PRISM. Ci-dessous le catalogue de modules que j'ai défini pour ce scénario :

 
Sélectionnez
  1. <prism:ModuleCatalog xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
  2.     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
  3.     xmlns:sys="clr-namespace:System;assembly=mscorlib" 
  4.     xmlns:prism="clr-namespace:Microsoft.Practices.Composite.Modularity;assembly=Microsoft.Practices.Composite"> 
  5.  
  6.     <prism:ModuleInfo Ref="MyApp.Modules.Administration.xap" 
  7.                       ModuleName="Administration" 
  8.                       ModuleType="MyApp.Modules.Administration.Module, MyApp.Modules.Administration, Version=1.0.0.0" 
  9.                       InitializationMode="WhenAvailable"> 
  10.     </prism:ModuleInfo> 
  11.  
  12.     <prism:ModuleInfo Ref="MyApp.Modules.Statistics.xap" 
  13.                       ModuleName="Statistics" 
  14.                       ModuleType="MyApp.Modules.Statistics.Module, MyApp.Modules.Statistics, Version=1.0.0.0" 
  15.                       InitializationMode="WhenAvailable"> 
  16.     </prism:ModuleInfo> 
  17.  
  18.     <prism:ModuleInfo Ref="MyApp.Modules.Sales.xap" 
  19.                       ModuleName="Sales" 
  20.                       ModuleType="MyApp.Modules.Sales.Module, MyApp.Modules.Sales, Version=1.0.0.0" 
  21.                       InitializationMode="WhenAvailable"> 
  22.     </prism:ModuleInfo> 
  23.  
  24.     <prism:ModuleInfo Ref="MyApp.Modules.Orders.xap" 
  25.                       ModuleName="Orders" 
  26.                       ModuleType="MyApp.Modules.Orders.Module, MyApp.Modules.Orders, Version=1.0.0.0" 
  27.                       InitializationMode="WhenAvailable"> 
  28.     </prism:ModuleInfo> 
  29. </prism:ModuleCatalog> 

Notez les attributs InitializationMode. Ils nous permettent de spécifier si les modules doivent être téléchargés dès que possible ou s'ils doivent être téléchargés sur demande. Pour l'instant, je souhaite que mes modules soient téléchargés dès que possible, plus tard, je vais vous montrer des techniques efficaces pour les télécharger sur demande. Toutefois, cela n'affecte pas la façon dont je les construis.

Pour commencer, notre Shell peut avoir une barre supérieure qui affiche un message de type « hello », un menu sur la gauche et une région où les modules peuvent afficher leur contenu.

 
Sélectionnez
  1.     <Border BorderThickness="5" CornerRadius="10"> 
  2.         <tlk:DockPanel LastChildFill="True"> 
  3.             <shellWelcome:WelcomeBar tlk:DockPanel.Dock="Top" Margin="5"/> 
  4.             <shellMenu:Menu tlk:DockPanel.Dock="Left" VerticalAlignment="Stretch" Margin="5,0,5,5"/> 
  5.             <shellContainer:MainContainer Margin="0,0,5,5"/> 
  6.         </tlk:DockPanel> 
  7.     </Border> 

Le résultat visuel est quelque chose comme ceci :

Image non disponible

Je voudrais que le menu et le conteneur principal soient dynamiques, en d'autres termes, avoir son contenu défini dynamiquement par des modules au lieu d'être défini dans le contrôle lui-même.

Nous pourrions créer une interface comme IMenu, implémentée par le viewmodel de notre menu et l'utiliser par des modules pour afficher du contenu. Bien que cela semble être une approche découplée, car vos modules peuvent afficher leur contenu sans dépendre du contrôle du menu lui-même, il est effectivement couplé à l'objet qui implémente l'interface IMenu. Cela signifie que si vous souhaitez afficher vos options du menu dans d'autres endroits, comme une barre d'outils ou une barre de ruban, vous aurez besoin d'une autre interface pour interagir avec ces contrôles. Une solution à ce problème consiste à utiliser une technique de publication/souscription de messages s'appuyant sur un objet médiateur, l'EventAggregator de Prism. Maintenant, ce que vous faites est la publication d'un message disant « Je veux afficher cette option du menu » et quel que soit le contrôle qui est intéressé par l'affichage des options du menu, cela s'abonne au message et agit en conséquence. Voyons comment nous pouvons faire cela :

 
Sélectionnez
  1. public class MenuOptionMessage : CompositePresentationEvent<MenuOptionMessageArgs> { } 
  2.  
  3. public class MenuOptionMessageArgs 
  4. { 
  5.     public ICommand Command { get; set; } 
  6.     public string Text { get; set; } 
  7.     public string ImageSourceUri { get; set; } 
  8.  
  9.     public string Group { get; set; } 
  10. } 

MenuOptionMessage est un événement Prism dont la charge utile est un message MenuOptionMessageArgs. Donc, chaque fois que vous souhaitez publier un MenuOptionMessage vous créez un MenuOptionMessageArgs et dites à l'EventAggregator de le diffuser à tous les abonnés. L'EventAggregator peut être accédé par l'injection de dépendances ou le conteneur IoC. J'ai l'habitude de créer une classe utilitaire simple pour garder le code plus propre :

 
Sélectionnez
  1. public class Messenger 
  2. { 
  3.     public static T Get<T>() 
  4.         where T : EventBase 
  5.     { 
  6.         var ev = ServiceLocator.Current.GetInstance<IEventAggregator>(); 
  7.         return ev.GetEvent<T>(); 
  8.     } 
  9. } 

Maintenant, nous pouvons faire publier des MenuOptionMessage par nos modules.

Initialisation du module « Order » :

 
Sélectionnez
  1. Messenger.Get<MenuOptionMessage>().Publish(new MenuOptionMessageArgs 
  2. { 
  3.     Command = Commands.NewOrderCommand, 
  4.     Text = "Create New Order", 
  5.     Group = "Orders" 
  6. }); 
  7. Messenger.Get<MenuOptionMessage>().Publish(new MenuOptionMessageArgs 
  8. { 
  9.     Command = Commands.ViewOrdersCommand, 
  10.     Text = "View Orders", 
  11.     Group = "Orders" 
  12. }); 

Initialisation du module « Sales » :

 
Sélectionnez
  1. Messenger.Get<MenuOptionMessage>().Publish(new MenuOptionMessageArgs 
  2. { 
  3.     Command = Commands.ViewPendingOrdersCommand, 
  4.     Text = "Pending Orders", 
  5.     Group = "Sales" 
  6. }); 
  7. Messenger.Get<MenuOptionMessage>().Publish(new MenuOptionMessageArgs 
  8. { 
  9.     Command = Commands.ViewProcessedOrdersCommand, 
  10.     Text = "Processed Orders", 
  11.     Group = "Sales" 
  12. }); 

Le contrôle Menu affiche toutes les commandes groupées par leur « Group » et crée une extension pour chaque « Group ». Vous pouvez accéder au code source pour confirmer cela.

Maintenant, il nous reste à faire afficher des vues par nos commandes.

Prism a un concept de région, où vous définissez des régions au sein de votre Shell, leur donnez un nom et ensuite vos modules injectent des vues dans ces régions en spécifiant le nom. Je ne suis pas amateur de cette fonctionnalité, car elle crée un couplage étroit entre les vues et les régions. En outre cela signifie que votre logique écran fonctionne uniquement avec Prism. J'utilise d'habitude une approche différente, qui nous permet non seulement d'utiliser les régions Prism, mais également d'autres frameworks d'interfaces utilisateurs. En outre, il n'est pas difficile de construire votre propre framework d'interface utilisateur, si vous le simplifiez. Par exemple, au lieu de penser en termes de régions, pensez en termes de position de vues.

 
Sélectionnez
  1. public enum ViewPosition 
  2. { 
  3.     Left, 
  4.     Right, 
  5.     Top, 
  6.     Bottom, 
  7.     Center, 
  8.     Floating 
  9. } 

En utilisant la même technique que celle pour le menu, chaque fois que nous voulons afficher une vue, nous avons tout simplement à publier un message où nous spécifions la position et la vue et quel que soit celui qui est responsable d'afficher cette vue dans la position spécifiée devrait s'abonner à ce message et agir en conséquence. Les messages devraient être définis dans l'assembly Infastructure. Voyons comment le message est défini :

 
Sélectionnez
  1. public class CreateViewMessage : CompositePresentationEvent<CreateViewMessageArgs> { } 
  2.  
  3. public class CreateViewMessageArgs 
  4. { 
  5.     public Lazy<object> View { get; set; } 
  6.     public ViewPosition Position { get; set; } 
  7.     public string Title { get; set; } //optional 
  8. } 

Notez l'objet Lazy. Cela vous permet d'avoir votre vue créée par l'abonné au lieu d'être créée par l'émetteur. Cela vous permet d'utiliser différents threads lors de la publication / abonnement, bien que dans ce cas l'abonné devrait utiliser le thread de l''interface utilisateur.

Ci-dessous un exemple de comment publier un CreateViewMessage à partir du module « Order ».

 
Sélectionnez
  1. public class Commands 
  2. { 
  3.     public static ICommand NewOrderCommand = new ActionCommand(Commands.NewOrder); 
  4.     public static ICommand ViewOrdersCommand = new ActionCommand(Commands.ViewOrders); 
  5.  
  6.     private static void NewOrder() 
  7.     { 
  8.         Messenger.Get<CreateViewMessage>().Publish(new CreateViewMessageArgs 
  9.         { 
  10.             View = new Lazy<object>(() => ServiceLocator.Current.GetInstance<OrderView>()), 
  11.             Position = ViewPosition.Center, 
  12.             Title = "New Order" 
  13.         }); 
  14.     } 
  15.  
  16.     private static void ViewOrders() 
  17.     { 
  18.         //to be implemented 
  19.     } 
  20. } 

Notez l'OrderView qui est créé par le ServiceLocator pour permettre l'injection de dépendances, si nécessaire.

Le contrôle MainContainer est responsable de la souscription à ce message et d'afficher la vue dans un contrôle onglet.

 
Sélectionnez
  1. public partial class MainContainer : UserControl 
  2. { 
  3.     public MainContainer() 
  4.     { 
  5.         InitializeComponent(); 
  6.         if (DesignerProperties.IsInDesignTool) 
  7.             return; 
  8.  
  9.         Messenger.Get<CreateViewMessage>().Subscribe(this.HandleCreateView, ThreadOption.UIThread, true, this.CanHandleCreateView); 
  10.     } 
  11.  
  12.     private bool CanHandleCreateView(CreateViewMessageArgs args) 
  13.     { 
  14.         return args.Position == ViewPosition.Center; 
  15.     } 
  16.  
  17.     private void HandleCreateView(CreateViewMessageArgs args) 
  18.     { 
  19.         this.tabCtrl.Items.Add(new TabItem 
  20.         { 
  21.             Header = args.Title, Content = args.View.Value, 
  22.             VerticalContentAlignment = System.Windows.VerticalAlignment.Stretch, 
  23.             HorizontalContentAlignment = System.Windows.HorizontalAlignment.Stretch 
  24.         }); 
  25.     } 
  26. } 

Lorsque vous vous abonnez à un message, vous pouvez spécifier un prédicat qui dit au mécanisme de publication si vous souhaitez vraiment gérer ce message. Dans ce cas-ci, le conteneur principal ne veut gérer que des CreateViewMessage pour des vues qui doivent être affichées dans la position Center. Je pourrais avoir un autre composant s'abonnant au CreateViewMessage et afficher des vues à l'intérieur de fenêtres lorsque la position est Floating. Le paramètre ThreadOption est également important. Chaque message qui traite de l'interface utilisateur doit être souscrit dans le thread de l'interface utilisateur. Si vous souhaitez brancher un enregistreur afin de souscrire/consigner les messages, vous n'avez pas besoin de vous souscrire sur le thread de l'interface utilisateur. Dans ce cas, vous devez utiliser un thread d'arrière-plan.

Il s'agit d'une approche très simple pour faire de la composition d'interface utilisateur qui fonctionne et qui est très efficace.

Conclusion

Dans le prochain article je vais me concentrer sur les viewmodels et comment nous pouvons construire l'OrderView.

Liens

Pour terminer, je tenais à dire que tout le code est disponible sur http://code.google.com/p/mfelicio-wordpress sous trunk/2010/SL-MVVM-RIA.

Remerciements

Je tiens ici à remercier Manuel Felicio de m'avoir autorisé à traduire son article.

Je remercie tomlev pour sa relecture technique et ses propositions.

Je remercie également zoom61 pour sa relecture orthographique et ses propositions.


Le blog de l'auteur et l'article en anglais ne sont plus accessibles depuis quelques semaines.

  

Copyright © 2013 Manuel Felicio. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts. Droits de diffusion permanents accordés à Developpez LLC.