Dans ce poste et le suivant, je vais métaler sur le développement client et comment créer une application Silverlight modulaire et composite. Dans ce post, je vais me concentrer sur la composition UI.
Lors de la construction composite Silverlight LOB applications, vous devez savoir que la modularité est un critère important. En tant que développeur, il devrait être plus facile pour vous de mantenir votre code et d’ajouter de nouvelles fonctionnalités à votre application sans avoir à changer le reste, plus le fichier de votre application XAP grossisse. Chaque module est un fichier séparé XAP, qui peuvent être téléchargés à la demande, selon l’intention de l’utilisateur à utiliser un certain type de fonctionnalité.
Ci-dessous est une architecture possible pour réaliser ce dont nous avons besoin :

Le dossier contient les main xap, que j’ai appelé MyApp. Ce xap est le seul responsable pour le téléchargement d’autres et fournir des moyens pour eux pour afficher son contenu dans l’application principale. Le dossier contient 4 modules essentiels pour le scénario que j’ai défini dans un précédent post. Le module de commandes permet aux utilisateurs non-salarié à remplir de nouvelles commandes. Le module de vente permet aux employés de consulter les commandes en cours et de les traiter. Le module de statistiques permet aux employés d’afficher des graphiques et des rapports de l’évolution des ventes. Caractéristiques pour le département des ressources humaines sont accomplies par le module d’administration, qui devrait également permettre aux utilisateurs de l’application à créer et / ou gérées. Les modules et les Shell sont complié dans le dossier ClientBin de « MyApp.Web ».
Le projet d’infrastructure est un élément clé. Il définit les moyens de communiquer entre les modules et entre l’application principale. En d’autres termes, il s’agit d’un contrat commun partagé par les modules. Quand elle est définie ou partiellement défini, le développement de module peut commencer. Notez que le développement du module de commandes ne dépend pas de la mise au point du module de vente ou le module de statistiques, ce qui signifie que des personnes différentes peuvent travailler sur chacun d’eux.
MyApp.Data.Silverlight est un projet qui possède du WCF RIA Services généré (le code), ainsi que les clients d’autres services qui sont utilisés par les modules.
La couche Serveur correspond à l’architecture que j’ai défini dans le premier post.
Alors, comment notre Shell télécharge les modules ? Nous avons juste besoin de spécifier un catalogue de module et le donner à l’amorçage PRISM, qui est responsable de l’initialisation cadre du PRISM. Ci-dessous le catalogue de module que j’ai défini pour ce scénario :
<prism:ModuleCatalog xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:prism="clr-namespace:Microsoft.Practices.Composite.Modularity;assembly=Microsoft.Practices.Composite">
<prism:ModuleInfo Ref="MyApp.Modules.Administration.xap"
ModuleName="Administration"
ModuleType="MyApp.Modules.Administration.Module, MyApp.Modules.Administration, Version=1.0.0.0"
InitializationMode="WhenAvailable">
</prism:ModuleInfo>
<prism:ModuleInfo Ref="MyApp.Modules.Statistics.xap"
ModuleName="Statistics"
ModuleType="MyApp.Modules.Statistics.Module, MyApp.Modules.Statistics, Version=1.0.0.0"
InitializationMode="WhenAvailable">
</prism:ModuleInfo>
<prism:ModuleInfo Ref="MyApp.Modules.Sales.xap"
ModuleName="Sales"
ModuleType="MyApp.Modules.Sales.Module, MyApp.Modules.Sales, Version=1.0.0.0"
InitializationMode="WhenAvailable">
</prism:ModuleInfo>
<prism:ModuleInfo Ref="MyApp.Modules.Orders.xap"
ModuleName="Orders"
ModuleType="MyApp.Modules.Orders.Module, MyApp.Modules.Orders, Version=1.0.0.0"
InitializationMode="WhenAvailable">
</prism:ModuleInfo>
</prism:ModuleCatalog>
Notez les attributs de InitializationMode. Ils nous permetent de spécifier les conditions dans lesquels les modules doivent être téléchargés sur demande. Pour l’instant, je veux que mes modules soit à télécharger 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 vais les construire.
Pour commencer, notre Shell peut avoir une barre supérieure qui affiche un message « bonjour », un menu sur la gauche et une région où les modules peuvent afficher son contenu.
<Border BorderThickness="5" CornerRadius="10">
<tlk:DockPanel LastChildFill="True">
<shellWelcome:WelcomeBar tlk:DockPanel.Dock="Top" Margin="5"/>
<shellMenu:Menu tlk:DockPanel.Dock="Left" VerticalAlignment="Stretch" Margin="5,0,5,5"/>
<shellContainer:MainContainer Margin="0,0,5,5"/>
</tlk:DockPanel>
</Border>
Le résultat visuel donne quelque chose comme ceci:

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 », mis en œuvre par notre ViewModel Menu et l’utiliser par des modules pour afficher le contenu. Bien que cela semble être une aproche découplée, car vos modules peut afficher son contenu sans en fonction de la commande de menu lui-même, il est effectivement associée à l’objet qui implémente l’interface « imenu ». Cela signifie que si vous souhaitez afficher vos options de 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 les contrôles. Une solution à ce problème consiste à utiliser une technique de publisher/subscriber en s’appuyant sur un objet médiateur, qui est EventAggregator PRISM. Maintenant, ce que vous faites est la publication d’un message disant « Je veux afficher cette option de menu. Voyons comment nous pouvons faire ce qui suit:
public class MenuOptionMessage : CompositePresentationEvent<MenuOptionMessageArgs> { }
public class MenuOptionMessageArgs
{
public ICommand Command { get; set; }
public string Text { get; set; }
public string ImageSourceUri { get; set; }
public string Group { get; set; }
}
Le MenuOptionMessage PRISM est un événement dont la charge utile est un message MenuOptionMessageArgs. Donc, chaque fois que vous souhaitez publier un MenuOptionMessage vous créez un MenuOptionMessageArgs et dite au EventAggregator de le diffuser à tous les abonnés. Le EventAggregator peut être consulté par l’injection de dépendance ou le conteneur IoC. J’ai l’habitude de créer une classe utilitaire simple pour garder le code plus propre:
public class Messenger
{
public static T Get<T>()
where T : EventBase
{
var ev = ServiceLocator.Current.GetInstance<IEventAggregator>();
return ev.GetEvent<T>();
}
}
Maintenant, nous pouvons avoir nos modules publié MenuOptionMessages.
initialisation du module « Order » :
Messenger.Get<MenuOptionMessage>().Publish(new MenuOptionMessageArgs
{
Command = Commands.NewOrderCommand,
Text = "Create New Order",
Group = "Orders"
});
Messenger.Get<MenuOptionMessage>().Publish(new MenuOptionMessageArgs
{
Command = Commands.ViewOrdersCommand,
Text = "View Orders",
Group = "Orders"
});
Initialisation du module « Sales » :
Messenger.Get<MenuOptionMessage>().Publish(new MenuOptionMessageArgs
{
Command = Commands.ViewPendingOrdersCommand,
Text = "Pending Orders",
Group = "Sales"
});
Messenger.Get<MenuOptionMessage>().Publish(new MenuOptionMessageArgs
{
Command = Commands.ViewProcessedOrdersCommand,
Text = "Processed Orders",
Group = "Sales"
});
Le contrôle Menu affiche toutes les commandes groupées par son Groupe et crée une extension pour chaque groupe. Vous pouvez accéder au code source pour le confirmer.
Maintenant, ce que nous devons faire est d’avoir nos commandes d’affichage des vues.
PRISM a un concept de région, où vous de définir des régions au sein de votre Shell, leur donner un nom et ensuite vos modules injecter les vues dans ces régions en spécifiant le nom. Je ne suis pas fan de cette fonctionnalité, car elle crée un couplage étroit entre les vues et les régions. Outre cela signifie que votre logique écran fonctionne uniquement avec PRISM. J’utilise généralement une approche différente, qui nous permet d’utiliser également les régions PRISM, mais d’autres cadres de UI ainsi. En outre, il n’est pas difficile de construire votre propre cadre UI, si vous restez simple. Par exemple, au lieu de penser sur les régions, pensez à voir les view positions.
public enum ViewPosition
{
Left,
Right,
Top,
Bottom,
Center,
Floating
}
En utilisant la même technique que nous avons fait pour le menu, chaque fois que nous voulons afficher une vue, nous n’avons tout simplement de publier un message où nous spécifier la position et la vue et celui qui est responsable d’afficher cet vue dans la position spécifiée doit souscrire le message et agir en conséquence. Les Messages devraient être définis dans l’assembly Infastructure. Voyons comment le message est défini:
public class CreateViewMessage : CompositePresentationEvent<CreateViewMessageArgs> { }
public class CreateViewMessageArgs
{
public Lazy<object> View { get; set; }
public ViewPosition Position { get; set; }
public string Title { get; set; } //optional
}
Notez le Lazy objcet. Cela vous permet d’avoir votre vue créés par l’abonné au lieu d’être créé par l’éditeur. Cela vous permet d’utiliser différents threads lors de la publication / abonnement, mais dans ce cas l’abonné doit utiliser le thread d’interface utilisateur.
Ci-dessous un exemple de la façon de publier un CreateViewMessage à partir du module de commande.
public class Commands
{
public static ICommand NewOrderCommand = new ActionCommand(Commands.NewOrder);
public static ICommand ViewOrdersCommand = new ActionCommand(Commands.ViewOrders);
private static void NewOrder()
{
Messenger.Get<CreateViewMessage>().Publish(new CreateViewMessageArgs
{
View = new Lazy<object>(() => ServiceLocator.Current.GetInstance<OrderView>()),
Position = ViewPosition.Center,
Title = "New Order"
});
}
private static void ViewOrders()
{
//to be implemented
}
}
Notez le OrderView qui doit être créé par le ServiceLocator pour permettre l’injection de dépendance, si nécessaire.
Le contrôle MainContainer est responsable de ce message et de son inscription pour afficher la vue intérieur d’un contrôle onglet.
public partial class MainContainer : UserControl
{
public MainContainer()
{
InitializeComponent();
if (DesignerProperties.IsInDesignTool)
return;
Messenger.Get<CreateViewMessage>().Subscribe(this.HandleCreateView, ThreadOption.UIThread, true, this.CanHandleCreateView);
}
private bool CanHandleCreateView(CreateViewMessageArgs args)
{
return args.Position == ViewPosition.Center;
}
private void HandleCreateView(CreateViewMessageArgs args)
{
this.tabCtrl.Items.Add(new TabItem
{
Header = args.Title, Content = args.View.Value,
VerticalContentAlignment = System.Windows.VerticalAlignment.Stretch,
HorizontalContentAlignment = System.Windows.HorizontalAlignment.Stretch
});
}
}
Lorsque vous vous abonnez un message, vous pouvez spécifier un prédicat qui dit défini le mécanisme de publier que vous voulez vraiment pour traiter ce message. Dans ce cas, le conteneur principal vise seulement à des CreateViewMessages qui ont des vues à afficher dans la position centrale. J’aurais pu souscrire un autre composant du CreateViewMessage et afficher des vues à l’intérieur de fenêtres lorsque la situation est variable. Le paramètre ThreadOption est également important. Chaque message qui traite de l’UI doit être souscrit dans le thread d’interface utilisateur. Si vous voulez brancher un enregistreur de souscription/log messages vous n’avez pas besoin de vous inscrire sur le thread d’interface utilisateur. Dans ce cas, vous devez utiliser un thread d’arrière.
Il s’agit d’une aproche très simples sur la façon de faire la composition d’UI qui fonctionne et qui est très efficace.
Suite : Dans le prochain post je vais me concentrer sur ViewModels et comment nous pouvons construire le OrderView.
PS : cet article est une adaptation en français d’une suite d’articles par Manuel Felício
Kamel DJELLAL
Chef de projet
EDIS CONSULTING – GROUPE UBISIDE
http://www.ubiside.fr/