WPF MVVM une vraie Application - Prims

Dans le monde des Vraies Applications WPF MVVM, mon focus se porte sur le projet suivant :

CoreProject - WPF Real-Time Trading Application
By Alexy Shelest attention codeplex à disparu et Alexy Shelest est devenu chinois ;-) :)) :)) :))

J'ai dans l'idée d'étudier ce projet, ses contraintes et sa réalisation pour remplacer les patterns propriétaires par les patterns de Prims ... Est-ce possible ? C'est l'objet de cette série de posts.

Description du projet WPF Real-Time Trading Application

Comment s'articule ce projet ? Qu'elles sont les patterns spécifiques utilisées ?

View principale

Fenêtre principale :

WPF MVVM - Fenêtre principale
Elle est composée d'un UserControl :
\\WPFRealTime\Src\Presentation\BondModule\Views\RibbonView.xaml :

UserControl - RibbonView
Le premier bouton, qui n'a pas de contenu en mode desing est en fait un ToggleButton dont la propriété Content est Bindée sur la string OpenButtonContent du ViewModel. 

Et la propriété Command de ce bouton est bindée sur une commande du ViewModel :
Command="{Binding OpenModuleCommand}"

Les propriétés Command des deux autres boutons sont bindées sur des commandes du ViewModel :
Command="{Binding GetDataCommand}"
Et 
Command="{Binding PauseCommand}"

View du Module Bond

Lorsque l'on clique sur le bouton "Open Bond Module" on charge le module Bond dont la View est :
\\WPFRealTime\Src\Presentation\BondModule\Views\View.xaml :

View du Module Bond

ViewModel

Le contenu du ToggleButton ainsi que les commandes des boutons seront settés lors de la création du ViewModel :
\\WPFRealTime\Src\Presentation\BondModule\ViewModels\RibbonViewModel.cs

        public RibbonViewModel() : base("BondModule Ribbon", true, false)
        {
            StaticViewName = "BondModule Ribbon";
            GetDataCommand = new SimpleCommand<object>(o => _canGetData, GetData);
            OpenModuleCommand = new SimpleCommand<bool>(OpenModule);
            PauseCommand = new SimpleCommand<object>(o => _canGetData, _ => Mediator.GetInstance.Broadcast(Topic.BondModuleHang));
            OpenButtonContent = "Open Bond Module";
        }

A ce niveau nous avons déjà à faire à trois éléments importants et spécifiques du fonctionnement de ce projet :
Les ViewModels dérivent d'un BaseViewModel :
\\WPFRealTime\Src\Framework\WPF.RealTime.Infrastructure\BaseViewModel.cs

Les commandes bindées dans la vue sont des SimpleCommand :
\\WPFRealTime\Src\Framework\WPF.RealTime.Infrastructure\Commands\SimpleCommand.cs

La commande PauseCommand utilise le Mediator pour discuter avec le module lors de l'arrêt de la thread qui fait "vivre" les données :
\\WPFRealTime\Src\Framework\WPF.RealTime.Infrastructure\Messaging\Mediator.cs

Un dernier élément important qui n'est pas dans le constructeur du RibbonViewModel mais auquel il faut porter attention c'est le Bootstrapper :
\\WPFRealTime\Src\Shell\WPF.RealTime\Bootstrapper.cs

Comme dans MEF de Prims, WPF.RealTime\Bootstrapper.cs utilise l'objet AggregateCatalog ...

Bootstrapper utilisation du MEF de Prims

Tentons d'utiliser le Bootstrapper de MEF, je fais dériver le Bootstrapper de MefBootstrapper et je me sers de l'exemple bien connu :
\\Prism\Quickstarts\Modularity\Desktop\ModularityWithMef

Dans MEF de Prims il me faut :
App.xaml
App.xaml.cs
Et
Shell.xaml
Shell.xaml.cs
Donc dans le projet :
\\WPFRealTime\Src\Shell\WPF.RealTime
Je renomme donc MainWindow.xaml en Shell.xaml
Curieusement App.xaml.cs existe sous la forme App.cs dans le projet de référence WPF Real Time Trading Application, je le renomme en App.xaml.cs.
Je renomme également MainWindowViewModel.cs en ShellViewModel.cs.
Voilà on est plus proche du classique MEF de Prims, il ne reste plus qu'à implémenter le MefBootstrapper c'est à dire faire dériver le Bootstrapper de MefBootstrapper. Et à faire quelques autres petites modifications.

Modifications des sources

Dans : \\WPFRealTime\Src\Shell\WPF.RealTime\Shell.xaml.cs
using System;
using System.Windows;
using WPF.RealTime.Infrastructure.AttachedProperty;
using System.ComponentModel.Composition;
using Microsoft.Practices.Prism.Modularity;

namespace WPF.RealTime.Shell
{
    /// <summary>
    /// Interaction logic for Shell.xaml
    /// </summary>
    [Export]
    public partial class Shell : Window
    {
        public Shell()
        {
            this.InitializeComponent();

            Left = Convert.ToDouble(GetValue(WindowProperties.LeftProperty));
            Top = Convert.ToDouble(GetValue(WindowProperties.TopProperty));
            Width = Convert.ToDouble(GetValue(WindowProperties.WidthProperty));
            Height = Convert.ToDouble(GetValue(WindowProperties.HeightProperty));

            this.DataContext = new ShellViewModel();
        }
    }
}

Explications des modifications

La directive [Export] permet au MEF de Prims de charger le module principal : Le Shell

Dans : \\WPFRealTime\Src\Shell\WPF.RealTime\Bootstrapper.cs
using System;
using System.Collections.Concurrent;
using System.Configuration;
using System.Linq;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;
using log4net;
using WPF.RealTime.Data;
using WPF.RealTime.Data.ResourceManager;
using WPF.RealTime.Infrastructure;
using WPF.RealTime.Infrastructure.Interfaces;
using WPF.RealTime.Infrastructure.Messaging;

// BRY_
using Microsoft.Practices.Prism.MefExtensions;
using Microsoft.Practices.Prism.Modularity;

namespace WPF.RealTime.Shell
{
    public /*sealed*/ class Bootstrapper : MefBootstrapper
    {
        // Design time imports
        [ImportMany(typeof(IStaticViewModel))]
        public IEnumerable<IStaticViewModel> StaticViewModels;

        [ImportMany(typeof(IDynamicViewModel))]
        public IEnumerable<Lazy<IDynamicViewModel>> DynamicViewModels;

        [ImportMany(typeof(BaseServiceObserver))]
        public IEnumerable<Lazy<BaseServiceObserver>> ServiceObservers;

        // Run-time imports
        public IEnumerable<Lazy<IService>> Services;

        //public void Run()
        //{
        //    // BRY_ DiscoverParts();
        //    _log.Info("Parts discovered");

        //    Shell mainWindow = new Shell();
        //    InjectStaticViewModels(mainWindow);
        //    _log.Info("Static View Models injected");
        //    mainWindow.Show();

        //    Application.Current.MainWindow = mainWindow;

        //    InjectDynamicViewModels(_dm == "MULTI");
        //    _log.Info("Dynamic View Models injected");
        //    InjectServices();
        //    _log.Info("Services injected");
        //}

        private readonly ILog _log = LogManager.GetLogger(typeof(Bootstrapper));
        private CompositionContainer _container;
        private static readonly ConcurrentDictionary<string, Window> RunningWindows = new ConcurrentDictionary<string, Window>();
        private readonly string _sm = Convert.ToString(ConfigurationManager.AppSettings["SERVICE_MODE"]);
        private readonly string _dm = Convert.ToString(ConfigurationManager.AppSettings["DISPATCHER_MODE"]);
        private readonly string _sp = Convert.ToString(ConfigurationManager.AppSettings["SERVICES_PATH"]);
        private readonly string _mp = Convert.ToString(ConfigurationManager.AppSettings["MODULES_PATH"]);

        public Bootstrapper()
        {
            Mediator.GetInstance.RegisterInterest<Lazy<IDynamicViewModel>>(Topic.BootstrapperLoadViews, _createView, TaskType.LongRunning);
            Mediator.GetInstance.RegisterInterest<string>(Topic.BootstrapperUnloadView, UnloadView, TaskType.LongRunning);

        }

        protected override DependencyObject CreateShell()
        {
            return this.Container.GetExportedValue<Shell>();
        }

        protected override void InitializeShell()
        {
            base.InitializeShell();

            InjectStaticViewModels((Shell)this.Shell);
            _log.Info("Static View Models injected");
            InjectDynamicViewModels(_dm == "MULTI");
            _log.Info("Dynamic View Models injected");
            InjectServices();
            _log.Info("Services injected");

            Application.Current.MainWindow = (Shell)this.Shell;
            Application.Current.MainWindow.Show();
        }

        //private void DiscoverParts()
        protected override void ConfigureAggregateCatalog()
        {
            base.ConfigureAggregateCatalog();

            // Add Bootstrapper assembly to Catalogs
            this.AggregateCatalog.Catalogs.Add(new AssemblyCatalog(typeof(WPF.RealTime.Shell.Bootstrapper).Assembly));

            var catalog = new AggregateCatalog();
            catalog.Catalogs.Add(new DirectoryCatalog(_mp));
            catalog.Catalogs.Add(new DirectoryCatalog(_sp));

            // Create the CompositionContainer with the parts in the catalog
            _container = new CompositionContainer(catalog);

            this.AggregateCatalog.Catalogs.Add(catalog);

            // Fill the imports of this object
            try
            {
                _log.Info(String.Format("{0} Mode", _sm));
                _container.ComposeParts(this);
                Services = _container.GetExports<IService>(_sm);
            }
            catch (CompositionException compositionException)
            {
                throw new ApplicationException("Opps", compositionException);
            }
        }

        protected override void ConfigureContainer()
        {
            base.ConfigureContainer();
        }

        protected override IModuleCatalog CreateModuleCatalog()
        {
            // When using MEF, the existing Prism ModuleCatalog is still the place
            // to configure modules via configuration files.
            return new ConfigurationModuleCatalog();
        }

        private static void UnloadView(string key)
        {
            Window view;
            if (RunningWindows.TryGetValue(key, out view))
            {
                Action close = () => view.Close();
                view.Dispatcher.BeginInvoke(close);
                Window removed;
                RunningWindows.TryRemove(key, out removed);
                view.Dispatcher.BeginInvoke((Action)(() => Mediator.GetInstance.Unregister(removed.DataContext)));
                var heartbeat = new Heartbeat(removed.GetType().ToString(), String.Format("{0} View unloaded at: {1}", removed.GetType(), DateTime.UtcNow.ToLongTimeString()), DateTime.UtcNow, true);

                Mediator.GetInstance.Broadcast(Topic.ShellStateUpdated, heartbeat);
            }
        }

        private static void CreateView(Lazy<IDynamicViewModel> lazy)
        {
            var vm = lazy.Value;

            Mediator.GetInstance.Register(vm);
            Window view = (Window)((BaseViewModel)vm).ViewReference;
            RunningWindows.TryAdd(vm.DynamicViewName, view);
            var heartbeat = new Heartbeat(vm.GetType().ToString(), String.Format("{0} View loaded at: {1}", vm.GetType().ToString(), DateTime.UtcNow.ToLongTimeString()), DateTime.UtcNow, true);

            Mediator.GetInstance.Broadcast(Topic.ShellStateUpdated, heartbeat);
        }

        private readonly Action<Lazy<IDynamicViewModel>> _createView = ((t) =>
        {
            var vm = t.Value;

            Mediator.GetInstance.Register(vm);
            Window view = (Window)((BaseViewModel)vm).ViewReference;
            RunningWindows.TryAdd(vm.DynamicViewName, view);
            var heartbeat = new Heartbeat(vm.GetType().ToString(), String.Format("{0} View loaded at: {1}", vm.GetType().ToString(), DateTime.UtcNow.ToLongTimeString()), DateTime.UtcNow, true);

            Mediator.GetInstance.Broadcast(Topic.ShellStateUpdated, heartbeat);
            //view.Show();
            view.Closed += (sender, e) => view.Dispatcher.InvokeShutdown();
            Dispatcher.Run();
        });

        private void InjectDynamicViewModels(bool multiDispatchers)
        {
            foreach (var lazy in DynamicViewModels)
            {
                Lazy<IDynamicViewModel> localLazy = lazy;
                if (multiDispatchers)
                {
                    Mediator.GetInstance.Broadcast(Topic.BootstrapperLoadViews, localLazy); 
                }
                else
                {
                    CreateView(localLazy);
                }
                
            }
        }

        private void InjectServices()
        {
            foreach (var lazy in Services)
            {
                var service = lazy.Value;
                Mediator.GetInstance.Register(service);
                var heartbeat = new Heartbeat(service.GetType().ToString(), String.Format("{0} Service loaded at: {1}", service.GetType(), DateTime.UtcNow.ToLongTimeString()), DateTime.UtcNow, true);

                Mediator.GetInstance.Broadcast(Topic.ShellStateUpdated, heartbeat);
            }
            // Inject service observers
            foreach (var lazy in ServiceObservers)
            {
                lazy.Value.AddServicesToObserve(Services.Select(s => s.Value));
            }
        }

        private void InjectStaticViewModels(Shell mainWindow)
        {
            foreach (var vm in StaticViewModels)
            {
                Mediator.GetInstance.Register(vm);
                mainWindow.RibbonRegion.Items.Add(new TabItem { Content = ((BaseViewModel)vm).ViewReference, Header = vm.StaticViewName });
            }
        }
    }
}

Explications des modifications

J'ajoute les directives Using pour utiliser l'objet MefBootstrapper.
Le Run ne sert plus, il est remplacé par le Run standard de MEF dans App.xaml.cs ce qui était fait dans le Run du projet de référence est maintenant exécuté dans InitializeShell().
DiscoverParts() ressemble furieusement à ConfigureAggregateCatalog() que je réutilise, j'ajoute dans l'AggregateCatalog le chargement de mon Module pricipal Shell.Bootstrapper.

Et le tour est joué, cela fonctionne !

Conclusion sur le Bootstrapper

Pourquoi modifier ainsi le code ? Et bien je dirais simplement pour une plus grande standardisation avec les projets qui utilisent Prism et le MEF de Prism.
 

Aucun commentaire:

Enregistrer un commentaire

Pour plus d'interactivité, n'hésitez pas à laisser votre commentaire.