Partie 5 - Modules et composition d'interface utilisateur▲
Dans cette partie et la prochaine, je vais parler du développement client et de comment créer une application Silverlight modulaire et composite. Dans cette partie, 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 :
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 quatre modules essentiels pour le scénario d'entreprise que j'ai défini dans une partie précédente. 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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
<
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 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.
2.
3.
4.
5.
6.
7.
<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 est 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, 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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
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
;
}
}
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 :
2.
3.
4.
5.
6.
7.
8.
9.
public
class
Messenger
{
public
static
T Get<
T>(
)
where
T :
EventBase
{
var
ev =
ServiceLocator.
Current.
GetInstance<
IEventAggregator>(
);
return
ev.
GetEvent<
T>(
);
}
}
Maintenant, nous pouvons faire publier des MenuOptionMessage par nos modules.
Initialisation du module « Order » :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
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 » :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
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 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.
2.
3.
4.
5.
6.
7.
8.
9.
public
enum
ViewPosition
{
Left,
Right,
Top,
Bottom,
Center,
Floating
}
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 :
2.
3.
4.
5.
6.
7.
8.
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 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 ».
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
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 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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
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 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 la prochaine partie je vais me concentrer sur les viewmodels et comment nous pouvons construire l'OrderView.