Prism 7

De Banane Atomic
Aller à la navigationAller à la recherche

Introduction

Prism est un framework qui permet de développer des applications composites.

Design de l'application

Prism Application Design.png
  • Shell: contient les regions
  • Regions: les vues sont injectées dans les regions au runtime
  • Modules: fonctionnalités majeures de l'application
  • Bootstrapper: initialise Prism


Install NuGet packages

Package Dependencies Content
Prism.Wpf Prism.Core CommonServiceLocator base packages
Prism.DryIoc DryIoc depency injection
Prism.Container.Extensions extension methods making the Prism Container easier to use with IServiceCollection IServiceProvider

Prism Template Pack

Ajoute des templates Prism dans les nouveaux projets Visual Studio Visual C# → Prism.
À la création d'un BlankApp, choix du conteneur IoC:

  • DryIoc
  • Autofac
  • Unity

Ajoute aussi des snippets: propp cmd cmdfull cmdg cmdgfull

Fichiers de base

App.xaml
<prism:PrismApplication x:Class="MyNamespace.App"
                        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                        xmlns:prism="http://prismlibrary.com/"
                        xmlns:local="clr-namespace:MyNamespace">
    <Application.Resources />
</prism:PrismApplication>
App.xaml.cs
// classe de type PrismApplication qui hérite de PrismApplicationBase
public partial class App
{
    protected override Window CreateShell()
    {
        return Container.Resolve<MainWindow>();
    }

    protected override void RegisterTypes(IContainerRegistry containerRegistry)
    {
        // RegisterServices available with Prism.Container.Extensions nuget package
        containerRegistry.RegisterServices(serviceCollection =>  serviceCollection.AddApplicationInsightsTelemetryWorkerService());
    }

    public override void Initialize()
    {
        base.Initialize();

        IRegionManager regionManager = ServiceLocator.Current.GetInstance<IRegionManager>();

        var viewA = Container.Resolve<ViewA>();
        IRegion region = regionManager.Regions["ContentRegion"];
        region.Add(viewA);
    }
Views/MainWindow.xaml
<Window x:Class="MyNamespace.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:prism="http://prismlibrary.com/"
        prism:ViewModelLocator.AutoWireViewModel="True"
        Title="{Binding Title}" Height="350" Width="525">
    <Grid>
        <ContentControl prism:RegionManager.RegionName="ContentRegion" />
    </Grid>
</Window>
ViewModels/MainWindowViewModel.cs
public class MainWindowViewModel : BindableBase
{
    private string _title = "Prism Application";
    public string Title
    {
        get => _title;
        set => SetProperty(ref _title, value);
    }

    public MainWindowViewModel()
    { }
}

Shell

Représente la fenêtre principale (MainWindow) qui contiendra le contenu de l'application.
Équivalent de la Master Page ASP.NET, représente le template de vue de l'application.
Le Shell contient des regions dans lesquelles les vues seront injectées.

Region

C'est un espace réservé (placeholder) pour du contenu dynamique. Permet de nommer un espace où placer une vue.
Une région n'a aucune connaissance de la vue qu'elle va contenir.
Une région n'est pas un Control mais une Attached Property qui s'applique à un Control.
Implémente IRegion.

Shell.xaml
<Window xmlns:prism="http://prismlibrary.com/">
    <DockPanel>
        <ContentControl prism:RegionManager.RegionName="ToolbarRegion" DoclkPanel.Dock="Top" />
        <ContentControl prism:RegionManager.RegionName="ContentRegion" />

Region Adapter

Permet d'adapter un UI Control pour qu'il devienne une région. Il faut un adapter pour chaque type de UI Control.

ContentControlRegionAdapter
ItemsControlRegionAdapter
SelectorRegionAdapter TabControl

Custom Region Adapter

StackPanelRegionAdapter.cs
class StackPanelRegionAdapter : RegionAdapterBase<StackPanel>
{
    public StackPanelRegionAdapter(IRegionBehaviorFactory regionBehaviorFactory) : base(regionBehaviorFactory)
    {

    }

    protected override void Adapt(IRegion region, StackPanel regionTarget)
    {
        region.Views.CollectionChanged += (sender, e) =>
        {
            if (e.Action == NotifyCollectionChangedAction.Add)
            {
                foreach (FrameworkElement element in e.NewItems)
                {
                    regionTarget.Children.Add(element);
                }
            }
        };
    }

    protected override IRegion CreateRegion()
    {
        return new AllActiveRegion();
    }
}
Bootstrapper.cs
protected override RegionAdapterMappings ConfigureRegionAdapterMappings()
{
    var mappings = base.ConfigureRegionAdapterMappings();
    mappings.RegisterMapping(typeof(StackPanel), Container.Resolve<StackPanelRegionAdapter>());
    return mappings;
}

RegionContext

Attached Property permettant de propager les données de la région à la vue.

Module

Package regroupant toutes les fonctionnalités d'une partie de l'application. Un module correspond à une fonctionnalité majeure de l'application.

Les modules sont dans des projets séparés de type Prism Module ou WPF User Control Library.
Modules/Test/TestModule.cs
public class TestModule : IModule
{
    public void OnInitialized(IContainerProvider containerProvider)
    {
        IRegionManager regionManager = ServiceLocator.Current.GetInstance<IRegionManager>();
        regionManager.RegisterViewWithRegion("ContentRegion", typeof(ViewA));
    }

    public void RegisterTypes(IContainerRegistry containerRegistry)
    { }

Ajout du module

Le projet principal (Shell) doit avoir une référence vers les modules.

App.xaml.cs
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
{
    moduleCatalog.AddModule(typeof(TestModule));

    // autre méthode d'ajout
    Type testModuleType = typeof(TestModule);
    moduleCatalog.AddModule(new ModuleInfo()
    {
        ModuleName = testModuleType.Name,
        ModuleType = testModuleType.AssemblyQualifiedName,
        InitializationMode = InitializationMode.WhenAvailable
    });

Module - OLD

Package regroupant toutes les fonctionnalités d'une partie de l'application. Un module correspond à une fonctionnalité majeure de l'application.

Les modules sont dans des projets séparés de type Prism Module ou WPF User Control Library.
Modules/Test/TestModule.cs
// Dans les cas où le module n'est pas chargé via le cas
// il peut définir lui même son ModuleName, son InitializationMode (par défaut WhenAvailable) et ses dépendances
[Module(ModuleName = "Test", OnDemand = true)]
[ModuleDependency("")]
public class TestModule : IModule
{
    public TestModule(IUnityContainer container, IRegionManager regionManager)
    {
        _container = container;
        _regionManager = regionManager;
    }

    public void Initialize()
    {
        _container.RegisterTypeForNavigation<ViewA>();
        _regionManager.RegisterViewWithRegion("ContentRegion", typeof(ViewA));

Chargement depuis le code

Le projet principal (Shell) doit avoir une référence vers les modules.

Bootstrapper.cs
protected override void ConfigureModuleCatalog()
{
    var moduleCatalog = (ModuleCatalog)ModuleCatalog;
    moduleCatalog.AddModule(typeof(TestModule));

    // autre méthode d'ajout
    Type testModuleType = typeof(TestModule);
    ModuleCatalog.AddModule(new ModuleInfo()
    {
        ModuleName = testModuleType.Name,
        ModuleType = testModuleType.AssemblyQualifiedName,
        InitializationMode = InitializationMode.WhenAvailable
    });

Chargement depuis un chemin

Un dossier Modules doit être présent au même niveau que MyApplication.exe. Ce dossier doit contenir les dll des modules (TestModule.dll).

Bootstrapper.cs
protected override IModuleCatalog CreateModuleCatalog()
{
    return new DirectoryModuleCatalog()
    {
        ModulePath = @".\Modules"
    };

Chargement depuis un fichier XAML

  1. Ajouter au projet principal un dictionnaire de ressources.
  2. Passer la Build Action à Resource
  3. Les dll des modules doivent être présents au même niveau que MyApplication.exe.
XamlCatalog.xaml
<Modularity:ModuleCatalog xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                          xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                          xmlns:Modularity="clr-namespace:Prism.Modularity;assembly=Prism.Wpf">

    <Modularity:ModuleInfo Ref="file://TestModule.dll"
                           ModuleName="Test"
                           ModuleType="TestModule.TestModuleModule, TestModule, Version=1.0.0.0" 
                           InitializationMode="WhenAvailable" />

</Modularity:ModuleCatalog>
Bootstrapper.cs
protected override IModuleCatalog CreateModuleCatalog()
{
    return Prism.Modularity.ModuleCatalog.CreateFromXaml(new Uri("/MyApplication;component/XamlCatalog.xaml", UriKind.Relative));
}

Chargement depuis un fichier App.config

  1. Ajouter au projet principal un Application Configuration File.
App.config
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="modules" type="Prism.Modularity.ModulesConfigurationSection, Prism.Wpf" />
  </configSections>

  <modules>
    <module assemblyFile="Modules/ModuleTest.dll"
            moduleType="ModuleTest.ModuleTestModule, ModuleTest, Version=1.0.0.0"
            moduleName="ModuleTest"
            startupLoaded="true" />
  </modules>
</configuration>
Bootstrapper.cs
protected override IModuleCatalog CreateModuleCatalog()
{
    return new ConfigurationModuleCatalog();
}

Chargement avec MEF

  1. Ajouter l'assembly System.ComponentModel.Composition au projet ModuleTest
  2. Prism.MefExtension ?

View

Pour créer une nouvelle vue, ajouter un UserControl dans un Module.

TestModule.cs
public class TestModule : IModule
{
    public void Initialize()
    {
        // enregistrer les types de vue
        _container.RegisterTypeForNavigation<ViewA>();

        // composition des vues

        // View Discovery
        _regionManager.RegisterViewWithRegion("ContentRegion", typeof(ViewA));

        // View Injection
        var viewA = _container.Resolve<ViewA>();
        IRegion region = _regionManager.Regions["ContentRegion"];
        region.Add(viewA);

        // remplacer une vue par une autre
        region.Deactivate(viewA);
        var viewB = _container.Resolve<ViewB>();
        region.Add(viewB);

ViewModel

BindableBase

Permet aux vue-modèles d'appeler RaisePropertyChanged.

Cs.svg
class ViewAViewModel : BindableBase
{
    private string myProperty;
    public string MyProperty
    {
        get => _myProperty;
        set => SetProperty(ref myProperty, value);
        set {
            myProperty = value;
            RaisePropertyChanged(nameof(MyProperty));
        }

ViewModelLocator

Associe à ViewA le VM ViewAViewModel.

Modules/A/Views/ViewA.xaml
<UserControl x:Class="A.Views.ViewA"
             prism:ViewModelLocator.AutoWireViewModel="True"
             d:DataContext="{d:DesignInstance local:ViewAViewModel}"> <!-- auto-completion during design -->

DelegateCommand

MyView.xaml
<Button Command="{Binding AddCmd}">Add</Button>
MyViewModel.cs
public ICommand AddCmd { get; private set; }

public MainWindowViewModel()
{
    AddCmd = new DelegateCommand(Add, CanAdd);
}

private void Add() {}
private bool CanAdd() { return true; }

Task-Based DelegateCommand

MyViewModel.cs
AddCmd = new DelegateCommand(async () => await AddAsync());

private async Task AddAsync()
{
    await SomeAsyncMethod();
}

AddCmd = new DelegateCommand(AddAsync);

private async void AddAsync()
{
    await SomeAsyncMethod();
}

Update CanExecute state

RaiseCanExecuteChanged

MyViewModel.cs
public ICommand AddCmd { get; private set; }

public MainWindowViewModel()
{
    AddCmd = new DelegateCommand(Add, CanAdd);
}

private void Add() {}
private bool CanAdd() => IsEnabled;

private bool _isEnabled;
public bool IsEnabled
{
    get => _isEnabled;
    set
    {
        SetProperty(ref _isEnabled, value);
        // when IsEnabled value has chanded, force the re-evaluation of CanAdd
        SubmitCommand.RaiseCanExecuteChanged();
    }
}

ObservesProperty

MyViewModel.cs
public ICommand AddCmd { get; private set; }

public MainWindowViewModel()
{
    AddCmd = new DelegateCommand(Add, CanAdd).ObservesProperty(() => IsEnabled);
    // whenever the value of the supplied property changes
    // the DelegateCommand will automatically call RaiseCanExecuteChanged to notify the UI of state changes
}

private void Add() {}
private bool CanAdd() => IsEnabled;

private bool _isEnabled;
public bool IsEnabled
{
    get => _isEnabled;
    set => SetProperty(ref _isEnabled, value);
}

ObservesCanExecute

If your CanExecute is the result of a simple Boolean property, you can eliminate the need to declare a CanExecute delegate, and use the ObservesCanExecute method instead.

MyViewModel.cs
public ICommand AddCmd { get; private set; }

public MainWindowViewModel()
{
    AddCmd = new DelegateCommand(Add).ObservesCanExecute(() => IsEnabled);
}

private void Add() {}

private bool _isEnabled;
public bool IsEnabled
{
    get => _isEnabled;
    set => SetProperty(ref _isEnabled, value);
}

Command with parameters

Parameters must be nullable
MyView.xaml
<Button Command="{Binding AddCmd}" CommandParameter="10">Add 10</Button>
MyViewModel.cs
public DelegateCommand<int?> AddCmd { get; private set; }

public MainWindowViewModel()
{
    AddCmd = new DelegateCommand<int?>(Add, CanAdd);
}

private void Add(int? parameter) {}
private bool CanAdd(int? parameter) { return true; }

Event Aggregator

Système de distribution de message (event bus) sans couplage entre le destinataire et l'expéditeur.

  • un objet envoie un message, sans savoir qui le recevra
  • un ou des objets s'inscrivent pour recevoir ces messages, sans savoir qui les envoie

Ce mécanisme permet:

  • d'appeler une méthode de la vue depuis le Vue-Modèle.
  • de communiquer entre Vue-Modèle.
Modules/A/ViewModels/View1ViewModel.cs
private readonly IEventAggregator eventAggregator;

public View1ViewModel(IEventAggregator eventAggregator)
{
    this.eventAggregator = eventAggregator;
}

private void MyAction()
{
    // publie l'évenement TestEvent avec "MyValue" comme payload
    eventAggregator.GetEvent<PubSubEvent<string>>().Publish("MyValue");
}
Modules/B/ViewModels/View2ViewModel.cs
private readonly IEventAggregator eventAggregator;

public View2ViewModel(IEventAggregator eventAggregator)
{
    this.eventAggregator = eventAggregator;
    var myEvent = this.eventAggregator.GetEvent<PubSubEvent<string>>();
    myEvent.Subscribe(MessageReceived);
    // recevoir l'évenement sur le thread UI pour pouvoir modifier l'UI
    myEvent.Subscribe(MessageReceived, ThreadOption.UIThread);
    // filter
    myEvent.Subscribe(MessageReceived, ThreadOption.PublisherThread, false,
        message => message.StartsWith("x"));
    // se désincrire
    myEvent.Unsubscribe(MessageReceived);
}

private void MessageReceived(string message)
{
    // reception des évenements TestEvent et de leur payload
}

Shared Services

Permet de faire de l'injection de dépendance (implémentation de l'inversion de contrôle)

Common/IDataService.cs
// interface commune
public interface IDataService
{
    IList<string> GetData();
}
Modules/Services/DataService.cs
// une implémentation du service
public class DataService : IDataService
{
    public IList<string> GetData()
    {
        return new List<string> { "un", "deux", "trois" };
    }
}
Modules/Services/ServicesModule.cs
public class ServicesModule : IModule
{
    public void RegisterTypes(IContainerRegistry containerRegistry)
    {
        // enregistrement de DataService comme implémentation de IDataService
        containerRegistry.Register<IDataService, DataService>();
    }
Modules/A/ViewModels/MyViewModel.cs
// injection de dépendance
private readonly IDataService dataService;
public MyViewModel(IDataService dataService)
{
    this.dataService = dataService;
}

Region Context

Permet aux vues de partager des données (context) avec ses vues filles.

Xaml.svg
<UserControl prism:RegionManager.RegionContext="{Binding SelectedElement.Id}">
Cs.svg
private void GetRegionContext()
{
    var id = (int)RegionContext.GetObservableContext(this).Value;
}

// suivre les changements dans le context
ObservableObject<object> viewRegionContext = RegionContext.GetObservableContext(this);
viewRegionContext.PropertyChanged += this.ViewRegionContext_OnPropertyChangedEvent;

private void ViewRegionContext_OnPropertyChangedEvent(object sender, PropertyChangedEventArgs args)
{
    if (args.PropertyName == "Value")
    {
        var context = (ObservableObject<object>)sender;
        int newValue = (int)context.Value;
    }
}