« Prism 8 » : différence entre les versions
Aucun résumé des modifications |
(→Dialog) |
||
(131 versions intermédiaires par le même utilisateur non affichées) | |||
Ligne 2 : | Ligne 2 : | ||
= Links = | = Links = | ||
* [https://prismlibrary.com/docs/ Prism Library Documentation] | * [https://prismlibrary.com/docs/ Prism Library Documentation] | ||
* [https://github.com/PrismLibrary Prism Library on GitHub] | |||
= Description = | = Description = | ||
Fully open source version of Prism including MVVM | Fully open source version of Prism including | ||
* MVVM | |||
* Commanding: DelegateCommand | |||
* Messaging: EventAggregator | |||
* Navigation | |||
* Dialog services | |||
* Modularity | |||
* Dependency injection | |||
Prism 8 supports WPF, Xamarin Forms and UNO, but not Silverlight, Windows 8/8.1/WP8.1 or UWP. | Prism 8 supports WPF, Xamarin Forms and UNO, but not Silverlight, Windows 8/8.1/WP8.1 or UWP. | ||
= | = Getting Started = | ||
* [https://marketplace.visualstudio.com/items?itemName=BrianLagunas.PrismTemplatePack Prism Template Pack] | == [https://prismlibrary.com/docs/getting-started/NuGet-Packages.html Nuget packages] == | ||
* {{boxx|Prism.Core}} | |||
* {{boxx|Prism.Wpf}} | |||
* {{boxx|Prism.DryIoc}} {{boxx|Prism.Unity}} dependency injection containers | |||
{{info | Adding {{boxx|Prism.DryIoc}} will add {{boxx|Prism.Wpf}} and {{boxx|Prism.Core}}}} | |||
== [https://marketplace.visualstudio.com/items?itemName=BrianLagunas.PrismTemplatePack Prism Template Pack Visual Studio extension] == | |||
* Templates | |||
** Prism Blank App (.NET Core) - target .NET Core 3.1 | |||
** Prism Blank App (WPF) - target .NET Framework 4.7.2 | |||
** Prism Blank App (Uno plateform) | |||
* Snippets | |||
** {{boxx|propp}} property which notifies its changes | |||
** {{boxx|cmd}} {{boxx|cmdfull}} delegate command | |||
** {{boxx|cmdg}} {{boxx|cmgfull}} delegate command with parameter | |||
== App.xaml == | |||
* Shell: the main / root window | |||
<filebox fn='App.xaml'> | |||
<prism:PrismApplication x:Class="PrismFull.App" | |||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" | |||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" | |||
xmlns:prism="http://prismlibrary.com/" > | |||
</prism:PrismApplication> | |||
</filebox> | |||
<filebox fn='App.xaml.cs'> | |||
public partial class App | |||
{ | |||
protected override Window CreateShell() | |||
{ | |||
return Container.Resolve<MainWindow>(); | |||
} | |||
protected override void RegisterTypes(IContainerRegistry containerRegistry) | |||
{ } | |||
} | |||
</filebox> | |||
= ViewModels = | |||
<filebox fn='ViewModels/MyViewModel.cs'> | |||
public class MyViewModel : BindableBase | |||
{ | |||
private string myProperty; | |||
public string MyProperty | |||
{ | |||
get => myProperty; | |||
set => SetProperty(ref myProperty, value); | |||
} | |||
} | |||
</filebox> | |||
== [https://prismlibrary.com/docs/viewmodel-locator.html ViewModelLocator] == | |||
Used to automatically wire the {{boxx|DataContext}} of a view to an instance of a {{boxx|ViewModel}} using a standard naming convention. | |||
<filebox fn='MyView.xaml'> | |||
<!-- automatically wire the DataContext of MyView to an instance of MyApp.ViewModels.MyViewModel --> | |||
<UserControl x:Class="MyApp.Views.MyView" | |||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" | |||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" | |||
xmlns:viewModels="clr-namespace:MyApp.ViewModels" | |||
xmlns:prism="http://prismlibrary.com/" | |||
mc:Ignorable="d" | |||
d:DataContext="{d:DesignInstance viewModels:MyViewModel}" | |||
prism:ViewModelLocator.AutoWireViewModel="True"> | |||
</filebox> | |||
This convention assumes: | |||
* that {{boxx|ViewModels}} are in the same assembly as the view types | |||
* that {{boxx|ViewModels}} are in a {{boxx|.ViewModels}} child namespace | |||
* that {{boxx|Views}} are in a {{boxx|.Views}} child namespace | |||
* that {{boxx|ViewModel}} names correspond with {{boxx|View}} names and end with {{boxx|ViewModel}} | |||
=== Change the naming convention === | |||
<filebox fn='App.xaml.cs' collapsed> | |||
protected override void ConfigureViewModelLocator() | |||
{ | |||
base.ConfigureViewModelLocator(); | |||
ViewModelLocationProvider.SetDefaultViewTypeToViewModelTypeResolver((viewType) => | |||
{ | |||
var viewName = viewType.FullName; | |||
var viewAssemblyName = viewType.Assembly.FullName; | |||
var suffix = viewName.EndsWith("View") ? "Model" : "ViewModel"; | |||
var viewModelName = $"{viewName.Replace(".Views.", ".ViewModels.")}{suffix}, {viewAssemblyName}"; | |||
var assembly = viewType.Assembly; | |||
return Type.GetType(viewModelName, true); | |||
}); | |||
} | |||
</filebox> | |||
=== Custom ViewModel registration === | |||
Faster because it doesn't use reflexion. | |||
<filebox fn='ModuleAModule.cs' collapsed> | |||
public void RegisterTypes(IContainerRegistry containerRegistry) | |||
{ | |||
ViewModelLocationProvider.Register<ViewA, ViewAViewModel>(); | |||
ViewModelLocationProvider.Register<ViewA>(() => new ViewAViewModel()); | |||
} | |||
</filebox> | |||
= [https://prismlibrary.com/docs/commanding.html Commanding] = | |||
<filebox fn='MyViewModel.cs'> | |||
public ICommand DoCommand { get; } | |||
public DelegateCommand<string> DoWithParamCommand { get; } | |||
public MyViewModel() | |||
{ | |||
DoCommand = new DelegateCommand(Do, CanDo); | |||
DoWithParamCommand = new DelegateCommand<string>(DoWithParam, CanDoWithParam); | |||
} | |||
private void Do() { } | |||
private bool CanDo() => true; | |||
private void DoWithParam(string parameter) { } | |||
private bool CanDoWithParam(string parameter) => true; | |||
</filebox> | |||
<filebox fn='MyView.xaml'> | |||
<Button Command="{Binding DoCommand}" | |||
Content="Do" /> | |||
<Button Command="{Binding DoWithParamCommand}" | |||
CommandParameter="{Binding Content, RelativeSource={RelativeSource Self}}}" | |||
Content="Do" /> | |||
</filebox> | |||
{{info | Parameter can't be of value type (int, double, bool, etc). Use instead the equivalent nullable type.}} | |||
== RaiseCanExecuteChanged == | |||
<kode lang='cs'> | |||
private bool isEnabled; | |||
public bool IsEnabled | |||
{ | |||
get => isEnabled; | |||
set | |||
{ | |||
SetProperty(ref isEnabled, value); | |||
this.DoCommand.RaiseCanExecuteChanged(); | |||
} | |||
} | |||
</kode> | |||
== ObservesProperty == | |||
Whenever the value of the supplied property changes, the {{boxx|DelegateCommand}} will automatically call {{boxx|RaiseCanExecuteChanged}} to notify the UI of state changes. | |||
<kode lang='cs'> | |||
DoCommand = new DelegateCommand(Do, CanDo).ObservesProperty(() => IsEnabled); | |||
</kode> | |||
== ObservesCanExecute == | |||
If your {{boxx|CanExecute}} is the result of a simple {{boxx|Boolean}} property, you can eliminate the need to declare a {{boxx|CanExecute}} delegate, and use the {{boxx|ObservesCanExecute}} method instead. {{boxx|ObservesCanExecute}} will not only send notifications to the UI when the registered property value changes but it will also use that same property as the actual CanExecute delegate. | |||
<kode lang='cs'> | |||
DoCommand = new DelegateCommand(Do).ObservesCanExecute(() => IsEnabled); | |||
</kode> | |||
== Task-Based DelegateCommand == | |||
<kode lang='cs'> | |||
DoCommand = new DelegateCommand(DoAsync); | |||
async void DoAsync() | |||
{ | |||
await SomeAsyncMethod(); | |||
} | |||
</kode> | |||
== Composite command == | |||
Parent command to multiple child commands. | |||
<filebox fn='Core/Commands/IApplicationCommands.cs' collapsed> | |||
public interface IApplicationCommands | |||
{ | |||
CompositeCommand SaveAllCommand { get; } | |||
} | |||
</filebox> | |||
<filebox fn='Core/Commands/ApplicationCommands.cs' collapsed> | |||
public class ApplicationCommands : IApplicationCommands | |||
{ | |||
public CompositeCommand SaveAllCommand { get; } = new CompositeCommand(); | |||
} | |||
</filebox> | |||
<filebox fn='App.xaml.cs' collapsed> | |||
protected override void RegisterTypes(IContainerRegistry containerRegistry) | |||
{ | |||
containerRegistry.RegisterSingleton<IApplicationCommands, ApplicationCommands>(); | |||
} | |||
</filebox> | |||
<filebox fn='ViewModels/MainWindowViewModel.cs' collapsed> | |||
private IApplicationCommands applicationCommands; | |||
public IApplicationCommands ApplicationCommands | |||
{ | |||
get { return applicationCommands; } | |||
set { SetProperty(ref applicationCommands, value); } | |||
} | |||
public MainWindowViewModel(IApplicationCommands applicationCommands) | |||
{ | |||
ApplicationCommands = applicationCommands; | |||
} | |||
</filebox> | |||
<filebox fn='ModuleA/ViewModels/ViewAViewModel.cs' collapsed> | |||
public DelegateCommand SaveCommand { get; private set; } | |||
public TabViewModel(IApplicationCommands applicationCommands) | |||
{ | |||
SaveCommand = new DelegateCommand(Save).ObservesCanExecute(() => CanSave); | |||
applicationCommands.SaveAllCommand.RegisterCommand(SaveCommand); | |||
} | |||
</filebox> | |||
= [https://prismlibrary.com/docs/dependency-injection/index.html Dependency Injection] = | |||
<filebox fn='App.xaml.cs'> | |||
public partial class App | |||
{ | |||
protected override void RegisterTypes(IContainerRegistry containerRegistry) | |||
{ | |||
// register a singleton service | |||
containerRegistry.RegisterSingleton<IMyService, MyService>(); | |||
// registera transient service | |||
containerRegistry.Register<IMyService, MyService>(); | |||
} | |||
</filebox> | |||
{| class="wikitable wtp wtmono1" | |||
|- | |||
| singleton service || for a service which is used throughout the application and that retains its state. | |||
|- | |||
| transient service || create a new instance each time. | |||
|- | |||
| scoped service || no implementation because unlike a web application, desktop applications are dealing with a single user and not scoped user requests. | |||
|} | |||
= Region = | |||
* Region: placeholder for dynamic content where views will be injected. | |||
* Region manager: maintains a collection of all the regions of the application. Used for: | |||
** create and define regions | |||
** access to regions | |||
** region navigation | |||
** views composition | |||
<kode lang='xaml'> | |||
<ContentControl prism:RegionManager.RegionName="MyRegion" /> | |||
</kode> | |||
<filebox fn='App.xaml.cs'> | |||
protected override void OnInitialized() | |||
{ | |||
base.OnInitialized(); | |||
var regionManager = Container.Resolve<IRegionManager>(); | |||
regionManager.RegisterViewWithRegion("MyRegion", typeof(MyView)); | |||
} | |||
</filebox> | |||
== View discovery == | |||
Region looks for view type. | |||
<filebox fn='ModuleAModule.cs'> | |||
public void OnInitialized(IContainerProvider containerProvider) | |||
{ | |||
this.regionManager.RegisterViewWithRegion("MyRegion", typeof(ViewA)); | |||
} | |||
</filebox> | |||
== View injection == | |||
<filebox fn='ModuleAModule.cs'> | |||
public void OnInitialized(IContainerProvider containerProvider) | |||
{ | |||
var viewA = containerProvider.Resolve<ViewA>(); | |||
var contentRegion = this.regionManager.Regions["ContentRegion"]; | |||
contentRegion.Add(viewA); | |||
contentRegion.Add(anotherView); // add another view to the region | |||
contentRegion.Activate(anotherView); // tell the region to display this view | |||
contentRegion.Deactivate(anotherView); // tell the region not to display this view anymore | |||
} | |||
</filebox> | |||
== Region behavior == | |||
Allow to execute some code when a region is adapted. | |||
<filebox fn='MyRegionBehavior.cs'> | |||
public class DependentViewRegionBehavior : RegionBehavior | |||
{ | |||
protected override void OnAttach() | |||
{ } | |||
} | |||
</filebox> | |||
<filebox fn='App.xaml.cs'> | |||
protected override void ConfigureDefaultRegionBehaviors(IRegionBehaviorFactory regionBehaviors) | |||
{ | |||
base.ConfigureDefaultRegionBehaviors(regionBehaviors); | |||
regionBehaviors.AddIfMissing(nameof(MyRegionBehavior), typeof(MyRegionBehavior)); | |||
} | |||
</filebox> | |||
== Region adapter == | |||
* Adapts a view to a region. | |||
Prism provides 3 region adapters: | |||
* {{boxx|ContentControlRegionAdapter}} allow to inject a view in a {{boxx|ContentControl}} | |||
* {{boxx|ItemsControlRegionAdapter}} allow to inject a view in an {{boxx|ItemsControl}} | |||
* {{boxx|SelectorRegionAdapter}} allow to inject a view in a {{boxx|ComboBox}} {{boxx|ListBox}} {{boxx|Ribbon}} {{boxx|TabControl}} | |||
Other controls require a custom region adapter. | |||
<filebox fn='Core/Regions/StackPanelRegionAdapter.cs' collapsed> | |||
internal sealed class StackPanelRegionAdapter : RegionAdapterBase<StackPanel> | |||
{ | |||
public StackPanelRegionAdapter(RegionBehaviorFactory behaviorFactory) | |||
: base(behaviorFactory) | |||
{ } | |||
protected override void Adapt(IRegion region, StackPanel regionTarget) | |||
{ | |||
region.Views.CollectionChanged += (s, e) => | |||
{ | |||
if (e.Action == NotifyCollectionChangedAction.Add) | |||
{ | |||
foreach (FrameworkElement item in e.NewItems) | |||
{ | |||
regionTarget.Children.Add(item); | |||
} | |||
} | |||
else if (e.Action == NotifyCollectionChangedAction.Remove) | |||
{ | |||
foreach (FrameworkElement item in e.OldItems) | |||
{ | |||
regionTarget.Children.Remove(item); | |||
} | |||
} | |||
}; | |||
} | |||
protected override IRegion CreateRegion() => new Region(); | |||
} | |||
</filebox> | |||
<filebox fn='App.xaml.cs'> | |||
protected override void ConfigureRegionAdapterMappings(RegionAdapterMappings regionAdapterMappings) | |||
{ | |||
base.ConfigureRegionAdapterMappings(regionAdapterMappings); | |||
regionAdapterMappings.RegisterMapping<StackPanel>(Container.Resolve<StackPanelRegionAdapter>()); | |||
} | |||
</filebox> | |||
= Navigation = | |||
Move between views in a region based on url.<br> | |||
By default create a new instance a the view before to navigate to it or reuse the existing one (discriminate by view type) if it has already been created. | |||
<filebox fn='ModuleAModule.cs'> | |||
public void RegisterTypes(IContainerRegistry containerRegistry) | |||
{ | |||
// register the ViewA under the unique string key ViewA (type of the view by default) | |||
containerRegistry.RegisterForNavigation<ViewA>(); | |||
containerRegistry.RegisterForNavigation<ViewB>(); | |||
// associate a view-model while register | |||
containerRegistry.RegisterForNavigation<ViewA, ViewAViewModel>(); | |||
} | |||
</filebox> | |||
<filebox fn='ViewAViewModel.cs'> | |||
internal sealed class ViewAViewModel : BindableBase, INavigationAware | |||
{ | |||
public void OnNavigatedTo(NavigationContext navigationContext) | |||
{ | |||
var id = navigationContext.Parameters.GetValue<int>("id"); | |||
var key = navigationContext.Parameters.GetValue<string>("key"); | |||
} | |||
public void OnNavigatedFrom(NavigationContext navigationContext) | |||
{ } | |||
// true → reuse the existing instance | |||
// false → create a new instance | |||
public bool IsNavigationTarget(NavigationContext navigationContext) => true; | |||
} | |||
</filebox> | |||
<filebox fn='Views/MainWindow.xaml'> | |||
<Button Command="{Binding NavigateCommand}" | |||
CommandParameter="ViewA" | |||
Content="Navigate to A" /> | |||
<Button Command="{Binding NavigateCommand}" | |||
CommandParameter="ViewB " | |||
Content="Navigate to B" /> | |||
</filebox> | |||
<filebox fn='ViewModels/MainWindowViewModel.cs'> | |||
private readonly IRegionManager regionManager; | |||
public DelegateCommand<string> NavigateCommand { get; private set; } | |||
public MainWindowViewModel(IRegionManager regionManager) | |||
{ | |||
this.regionManager = regionManager; | |||
this.NavigateCommand = new DelegateCommand<string>(Navigate); | |||
} | |||
private void Navigate(string uri) | |||
{ | |||
var navigationParameters = new NavigationParameters(); | |||
navigationParameters.Add("key", "value"); | |||
this.regionManager.RequestNavigate("ContentRegion", $"{uri}?id=1", navigationParameters); | |||
} | |||
</filebox> | |||
== Confirm navigation request == | |||
<filebox fn='ViewAViewModel.cs'> | |||
internal sealed class ViewAViewModel : BindableBase, IConfirmNavigationRequest | |||
{ | |||
public void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallBack) | |||
{ | |||
var continueNavigation = true; | |||
continuationCallBack(continueNavigation); | |||
} | |||
} | |||
</filebox> | |||
== Navigation journal == | |||
<filebox fn='ViewAViewModel.cs'> | |||
internal sealed class ViewAViewModel : BindableBase, INavigationAware | |||
{ | |||
private IRegionNavigationJournal journal; | |||
public DelegateCommand GoBackCommand { get; set; } | |||
public ViewAViewModel() | |||
{ | |||
GoBackCommand = new DelegateCommand(GoBack); | |||
} | |||
public void OnNavigatedTo(NavigationContext navigationContext) | |||
{ | |||
journal = navigationContext.NavigationService.Journal; | |||
} | |||
private void GoBack() => journal.GoBack(); | |||
} | |||
</filebox> | |||
== [[Prism_navigation_with_tabcontrol|Navigation with TabControl]] == | |||
= [https://prismlibrary.com/docs/modules.html Modules] = | |||
A module represents a functional responsability of the application (independent feature). | |||
# Add → New Project → WPF UserControl Library | |||
# Remove AssemblyInfo.cs and UserControl1.xaml | |||
# Add {{boxx|Prism.Wpf}} Nuget package | |||
<filebox fn='ModuleAModule.cs'> | |||
public sealed class ModuleAModule : IModule | |||
{ | |||
public void OnInitialized(IContainerProvider containerProvider) | |||
{ } | |||
public void RegisterTypes(IContainerRegistry containerRegistry) | |||
{ } | |||
} | |||
</filebox> | |||
== Module catalog == | |||
Collection of modules that the application is going to load. | |||
=== Code base === | |||
Hard reference to the modules. | |||
<filebox fn='App.xaml.cs' collapsed> | |||
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog) | |||
{ | |||
moduleCatalog.AddModule<ModuleAModule>(); | |||
} | |||
</filebox> | |||
=== App.config === | |||
More loosely coupled approach. | |||
<filebox fn='App.xaml.cs' collapsed> | |||
protected override IModuleCatalog CreateModuleCatalog() | |||
=> new ConfigurationModuleCatalog(); | |||
</filebox> | |||
<filebox fn='App.config' lang='xml' collapsed> | |||
<configuration> | |||
<configSections> | |||
<section name="modules" | |||
type="Prism.Modularity.ModulesConfigurationSection, Prism.Wpf" /> | |||
</configSections> | |||
<modules> | |||
<module assemblyFile="ModuleA.dll" | |||
moduleType="ModuleA.ModileAModule, ModuleA, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" | |||
moduleName="ModuleAModule" | |||
startupLoaded="True" /> | |||
</modules> | |||
</configuration> | |||
</filebox> | |||
<kode lang='dos'> | |||
REM ModuleA post build event | |||
xcopy /y "$(TargetDir)*.*" "$(SolutionDir)$(SolutionName)\$(OutDir)" | |||
</kode> | |||
=== Directory === | |||
Define a location on disk where to find the modules. | |||
<filebox fn='App.xaml.cs' collapsed> | |||
protected override IModuleCatalog CreateModuleCatalog() | |||
=> new DirectoryModuleCatalog() { ModulePath = @".\Modules" }; | |||
</filebox> | |||
<kode lang='dos'> | |||
REM ModuleA post build event | |||
xcopy /y "$(TargetDir)$(TargetName)$(TargetExt)" "$(SolutionDir)$(SolutionName)\$(OutDir)Modules\" | |||
REM xcopy /y C:\PrismNetCore\ModuleA\bin\Debug\net5.0-windows\ModuleA.dll C:\PrismNetCore\PrismNetCore\bin\Debug\net5.0-windows\Modules\ | |||
</kode> | |||
=== XAML resource dictionary === | |||
= [https://prismlibrary.com/docs/event-aggregator.html Event Aggregator] = | |||
Loosely coupled event-based communication.<br> | |||
Event mechanism that enables communications between loosely coupled components in the application. | |||
<filebox fn='MyApp.Core/MyPayload.cs'> | |||
public sealed class MyPayload | |||
{ | |||
public string Prop1 { get; set; } | |||
public string Prop2 { get; set; } | |||
} | |||
</filebox> | |||
<filebox fn='MyApp.Core/MyEvent.cs'> | |||
public sealed class MyEvent : PubSubEvent<MyPayload> | |||
{ } | |||
</filebox> | |||
<filebox fn='MyApp/MyViewModel.cs'> | |||
internal sealed class MyViewModel | |||
{ | |||
IEventAggregator eventAggregator; | |||
public MyViewModel(IEventAggregator eventAggregator) | |||
{ | |||
this.eventAggregator = eventAggregator; | |||
} | |||
// publishing an event | |||
var payload = new MyPayload | |||
{ | |||
Prop1 = "value1", | |||
Prop2 = "value2", | |||
}; | |||
eventAggregator.GetEvent<MyEvent>().Publish(payload); | |||
// subscribing to events | |||
eventAggregator.GetEvent<MyEvent>().Subscribe(MyAction); | |||
void MyAction(MyPayload payload) | |||
{ } | |||
} | |||
</filebox> | |||
== Subscribing on the UI Thread == | |||
If the subscriber needs to update UI elements in response to events, subscribe on the UI thread. In WPF, only a UI thread can update UI elements. | |||
<kode lang='cs'> | |||
eventAggregator.GetEvent<MyEvent>().Subscribe(DisplayMessage, ThreadOption.UIThread); | |||
</kode> | |||
{| class="wikitable wtp wtmono1" | |||
|- | |||
| PublisherThread || use this setting to receive the event on the publishers' thread. Default value. | |||
|- | |||
| UIThread || use this setting to receive the event on the UI thread. | |||
|- | |||
| BackgroundThread || use this setting to asynchronously receive the event on a .NET Framework thread-pool thread. | |||
|} | |||
{{info | In order for {{boxx|PubSubEvent}} to publish to subscribers on the UI thread, the {{boxx|EventAggregator}} must initially be constructed on the UI thread.}} | |||
== Subscription Filtering == | |||
<kode lang='cs'> | |||
eventAggregator.GetEvent<MyEvent>().Subscribe(MyAction, ThreadOption.PublisherThread, false, x => x.Prop1 == "KeyMessage"); | |||
</kode> | |||
== Performance concern == | |||
<kode lang='cs'> | |||
var keepSubscriberReferenceAlive = true; | |||
var myEvent = eventAggregator.GetEvent<MyEvent>(); | |||
myEvent.Subscribe(MyAction, keepSubscriberReferenceAlive); | |||
myEvent.Unsubscribe(MyAction); | |||
SubscriptionToken token = myEvent.Subscribe(MyAction, keepSubscriberReferenceAlive); | |||
myEvent.Unsubscribe(token); | |||
</kode> | |||
{| class="wikitable wtp wtmono1" | |||
|+ keepSubscriberReferenceAlive | |||
|- | |||
| true || the event instance keeps a strong reference to the subscriber instance, thereby not allowing it to get garbage collected. | |||
|- | |||
| false || default value.<br>The event maintains a weak reference to the subscriber instance, thereby allowing the garbage collector to dispose the subscriber instance when there are no other references to it.<br>When the subscriber instance gets collected, the event is automatically unsubscribed. | |||
|} | |||
= Dialog = | |||
Popup window displayed over the current application. | |||
<filebox fn='ViewModels/ViewAViewModel.cs' collapsed> | |||
private readonly IDialogService dialogService; | |||
public DelegateCommand OpenDialogCommand { get; private set; } | |||
public ViewAViewModel(IDialogService dialogService) | |||
{ | |||
this.dialogService = dialogService; | |||
this.OpenDialogCommand = new DelegateCommand(OpenDialog); | |||
} | |||
private void OpenDialog() | |||
{ | |||
var dialogParameters = new DialogParameters(); | |||
dialogParameters.Add("message", "Dialog message !"); | |||
// MessageDialog: same name as the view | |||
this.dialogService.ShowDialog("MessageDialog", dialogParameters, (dialogResult) => | |||
{ | |||
var buttonResult = dialogResult.Result; | |||
var value = dialogResult.Parameters.GetValue<string>("key"); | |||
}); | |||
} | |||
</filebox> | |||
<filebox fn='Dialogs/MessageDialog.xaml' collapsed> | |||
<UserControl x:Class="ModuleA.Dialogs.MessageDialog" | |||
xmlns:mvvm="http://prismlibrary.com/" | |||
d:DataContext="{d:DesignInstance dialogs:MessageDialogViewModel}" | |||
mvvm:ViewModelLocator.AutoWireViewModel="True" | |||
mc:Ignorable="d"> | |||
<prism:Dialog.WindowStyle> | |||
<Style TargetType="Window"> | |||
<Setter Property="Height" Value="200" /> | |||
<Setter Property="Width" Value="400" /> | |||
<Setter Property="ResizeMode" Value="NoResize" /> | |||
<Setter Property="prism:Dialog.WindowStartupLocation" Value="CenterOwner" /> | |||
</Style> | |||
</prism:Dialog.WindowStyle> | |||
<DockPanel> | |||
<Button Command="{Binding CloseDialogCommand}" | |||
Content="Ok" | |||
DockPanel.Dock="Bottom" /> | |||
<TextBlock Text="{Binding Message}" /> | |||
</DockPanel> | |||
</UserControl> | |||
</filebox> | |||
<filebox fn='Dialogs/MessageDialogViewModel' collapsed> | |||
internal sealed class MessageDialogViewModel : BindableBase, IDialogAware | |||
{ | |||
public string Title => "Dialog message"; | |||
public event Action<IDialogResult> RequestClose; | |||
public ICommand CloseDialogCommand { get; } | |||
private string message; | |||
public string Message | |||
{ | |||
get => message; | |||
set => SetProperty(ref message, value); | |||
} | |||
public MessageDialogViewModel() | |||
{ | |||
CloseDialogCommand = new DelegateCommand(CloseDialog, CanCloseDialog); | |||
} | |||
public bool CanCloseDialog() => true; | |||
public void OnDialogClosed() | |||
{ } | |||
public void OnDialogOpened(IDialogParameters parameters) | |||
{ | |||
Message = parameters.GetValue<string>("message"); | |||
} | |||
private void CloseDialog() | |||
{ | |||
var dialogParameters = new DialogParameters | |||
{ | |||
{ "key", "value" } | |||
}; | |||
this.RequestClose?.Invoke(new DialogResult(ButtonResult.OK, dialogParameters)); | |||
} | |||
} | |||
</filebox> | |||
<filebox fn='ModuleAModule.cs' collapsed> | |||
public void RegisterTypes(IContainerRegistry containerRegistry) | |||
{ | |||
// register the MessageDialog under the unique string key MessageDialog (type of the dialog by default) | |||
containerRegistry.RegisterDialog<MessageDialog>(); | |||
} | |||
</filebox> |
Dernière version du 5 novembre 2021 à 17:59
Links
Description
Fully open source version of Prism including
- MVVM
- Commanding: DelegateCommand
- Messaging: EventAggregator
- Navigation
- Dialog services
- Modularity
- Dependency injection
Prism 8 supports WPF, Xamarin Forms and UNO, but not Silverlight, Windows 8/8.1/WP8.1 or UWP.
Getting Started
Nuget packages
- Prism.Core
- Prism.Wpf
- Prism.DryIoc Prism.Unity dependency injection containers
Adding Prism.DryIoc will add Prism.Wpf and Prism.Core |
Prism Template Pack Visual Studio extension
- Templates
- Prism Blank App (.NET Core) - target .NET Core 3.1
- Prism Blank App (WPF) - target .NET Framework 4.7.2
- Prism Blank App (Uno plateform)
- Snippets
- propp property which notifies its changes
- cmd cmdfull delegate command
- cmdg cmgfull delegate command with parameter
App.xaml
- Shell: the main / root window
App.xaml |
<prism:PrismApplication x:Class="PrismFull.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:prism="http://prismlibrary.com/" > </prism:PrismApplication> |
App.xaml.cs |
public partial class App { protected override Window CreateShell() { return Container.Resolve<MainWindow>(); } protected override void RegisterTypes(IContainerRegistry containerRegistry) { } } |
ViewModels
ViewModels/MyViewModel.cs |
public class MyViewModel : BindableBase { private string myProperty; public string MyProperty { get => myProperty; set => SetProperty(ref myProperty, value); } } |
ViewModelLocator
Used to automatically wire the DataContext of a view to an instance of a ViewModel using a standard naming convention.
MyView.xaml |
<!-- automatically wire the DataContext of MyView to an instance of MyApp.ViewModels.MyViewModel --> <UserControl x:Class="MyApp.Views.MyView" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:viewModels="clr-namespace:MyApp.ViewModels" xmlns:prism="http://prismlibrary.com/" mc:Ignorable="d" d:DataContext="{d:DesignInstance viewModels:MyViewModel}" prism:ViewModelLocator.AutoWireViewModel="True"> |
This convention assumes:
- that ViewModels are in the same assembly as the view types
- that ViewModels are in a .ViewModels child namespace
- that Views are in a .Views child namespace
- that ViewModel names correspond with View names and end with ViewModel
Change the naming convention
App.xaml.cs |
protected override void ConfigureViewModelLocator() { base.ConfigureViewModelLocator(); ViewModelLocationProvider.SetDefaultViewTypeToViewModelTypeResolver((viewType) => { var viewName = viewType.FullName; var viewAssemblyName = viewType.Assembly.FullName; var suffix = viewName.EndsWith("View") ? "Model" : "ViewModel"; var viewModelName = $"{viewName.Replace(".Views.", ".ViewModels.")}{suffix}, {viewAssemblyName}"; var assembly = viewType.Assembly; return Type.GetType(viewModelName, true); }); } |
Custom ViewModel registration
Faster because it doesn't use reflexion.
ModuleAModule.cs |
public void RegisterTypes(IContainerRegistry containerRegistry) { ViewModelLocationProvider.Register<ViewA, ViewAViewModel>(); ViewModelLocationProvider.Register<ViewA>(() => new ViewAViewModel()); } |
Commanding
MyViewModel.cs |
public ICommand DoCommand { get; } public DelegateCommand<string> DoWithParamCommand { get; } public MyViewModel() { DoCommand = new DelegateCommand(Do, CanDo); DoWithParamCommand = new DelegateCommand<string>(DoWithParam, CanDoWithParam); } private void Do() { } private bool CanDo() => true; private void DoWithParam(string parameter) { } private bool CanDoWithParam(string parameter) => true; |
MyView.xaml |
<Button Command="{Binding DoCommand}" Content="Do" /> <Button Command="{Binding DoWithParamCommand}" CommandParameter="{Binding Content, RelativeSource={RelativeSource Self}}}" Content="Do" /> |
Parameter can't be of value type (int, double, bool, etc). Use instead the equivalent nullable type. |
RaiseCanExecuteChanged
private bool isEnabled; public bool IsEnabled { get => isEnabled; set { SetProperty(ref isEnabled, value); this.DoCommand.RaiseCanExecuteChanged(); } } |
ObservesProperty
Whenever the value of the supplied property changes, the DelegateCommand will automatically call RaiseCanExecuteChanged to notify the UI of state changes.
DoCommand = new DelegateCommand(Do, CanDo).ObservesProperty(() => IsEnabled); |
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. ObservesCanExecute will not only send notifications to the UI when the registered property value changes but it will also use that same property as the actual CanExecute delegate.
DoCommand = new DelegateCommand(Do).ObservesCanExecute(() => IsEnabled); |
Task-Based DelegateCommand
DoCommand = new DelegateCommand(DoAsync); async void DoAsync() { await SomeAsyncMethod(); } |
Composite command
Parent command to multiple child commands.
Core/Commands/IApplicationCommands.cs |
public interface IApplicationCommands { CompositeCommand SaveAllCommand { get; } } |
Core/Commands/ApplicationCommands.cs |
public class ApplicationCommands : IApplicationCommands { public CompositeCommand SaveAllCommand { get; } = new CompositeCommand(); } |
App.xaml.cs |
protected override void RegisterTypes(IContainerRegistry containerRegistry) { containerRegistry.RegisterSingleton<IApplicationCommands, ApplicationCommands>(); } |
ViewModels/MainWindowViewModel.cs |
private IApplicationCommands applicationCommands; public IApplicationCommands ApplicationCommands { get { return applicationCommands; } set { SetProperty(ref applicationCommands, value); } } public MainWindowViewModel(IApplicationCommands applicationCommands) { ApplicationCommands = applicationCommands; } |
ModuleA/ViewModels/ViewAViewModel.cs |
public DelegateCommand SaveCommand { get; private set; } public TabViewModel(IApplicationCommands applicationCommands) { SaveCommand = new DelegateCommand(Save).ObservesCanExecute(() => CanSave); applicationCommands.SaveAllCommand.RegisterCommand(SaveCommand); } |
Dependency Injection
App.xaml.cs |
public partial class App { protected override void RegisterTypes(IContainerRegistry containerRegistry) { // register a singleton service containerRegistry.RegisterSingleton<IMyService, MyService>(); // registera transient service containerRegistry.Register<IMyService, MyService>(); } |
singleton service | for a service which is used throughout the application and that retains its state. |
transient service | create a new instance each time. |
scoped service | no implementation because unlike a web application, desktop applications are dealing with a single user and not scoped user requests. |
Region
- Region: placeholder for dynamic content where views will be injected.
- Region manager: maintains a collection of all the regions of the application. Used for:
- create and define regions
- access to regions
- region navigation
- views composition
<ContentControl prism:RegionManager.RegionName="MyRegion" /> |
App.xaml.cs |
protected override void OnInitialized() { base.OnInitialized(); var regionManager = Container.Resolve<IRegionManager>(); regionManager.RegisterViewWithRegion("MyRegion", typeof(MyView)); } |
View discovery
Region looks for view type.
ModuleAModule.cs |
public void OnInitialized(IContainerProvider containerProvider) { this.regionManager.RegisterViewWithRegion("MyRegion", typeof(ViewA)); } |
View injection
ModuleAModule.cs |
public void OnInitialized(IContainerProvider containerProvider) { var viewA = containerProvider.Resolve<ViewA>(); var contentRegion = this.regionManager.Regions["ContentRegion"]; contentRegion.Add(viewA); contentRegion.Add(anotherView); // add another view to the region contentRegion.Activate(anotherView); // tell the region to display this view contentRegion.Deactivate(anotherView); // tell the region not to display this view anymore } |
Region behavior
Allow to execute some code when a region is adapted.
MyRegionBehavior.cs |
public class DependentViewRegionBehavior : RegionBehavior { protected override void OnAttach() { } } |
App.xaml.cs |
protected override void ConfigureDefaultRegionBehaviors(IRegionBehaviorFactory regionBehaviors) { base.ConfigureDefaultRegionBehaviors(regionBehaviors); regionBehaviors.AddIfMissing(nameof(MyRegionBehavior), typeof(MyRegionBehavior)); } |
Region adapter
- Adapts a view to a region.
Prism provides 3 region adapters:
- ContentControlRegionAdapter allow to inject a view in a ContentControl
- ItemsControlRegionAdapter allow to inject a view in an ItemsControl
- SelectorRegionAdapter allow to inject a view in a ComboBox ListBox Ribbon TabControl
Other controls require a custom region adapter.
Core/Regions/StackPanelRegionAdapter.cs |
internal sealed class StackPanelRegionAdapter : RegionAdapterBase<StackPanel> { public StackPanelRegionAdapter(RegionBehaviorFactory behaviorFactory) : base(behaviorFactory) { } protected override void Adapt(IRegion region, StackPanel regionTarget) { region.Views.CollectionChanged += (s, e) => { if (e.Action == NotifyCollectionChangedAction.Add) { foreach (FrameworkElement item in e.NewItems) { regionTarget.Children.Add(item); } } else if (e.Action == NotifyCollectionChangedAction.Remove) { foreach (FrameworkElement item in e.OldItems) { regionTarget.Children.Remove(item); } } }; } protected override IRegion CreateRegion() => new Region(); } |
App.xaml.cs |
protected override void ConfigureRegionAdapterMappings(RegionAdapterMappings regionAdapterMappings) { base.ConfigureRegionAdapterMappings(regionAdapterMappings); regionAdapterMappings.RegisterMapping<StackPanel>(Container.Resolve<StackPanelRegionAdapter>()); } |
Move between views in a region based on url.
By default create a new instance a the view before to navigate to it or reuse the existing one (discriminate by view type) if it has already been created.
ModuleAModule.cs |
public void RegisterTypes(IContainerRegistry containerRegistry) { // register the ViewA under the unique string key ViewA (type of the view by default) containerRegistry.RegisterForNavigation<ViewA>(); containerRegistry.RegisterForNavigation<ViewB>(); // associate a view-model while register containerRegistry.RegisterForNavigation<ViewA, ViewAViewModel>(); } |
ViewAViewModel.cs |
internal sealed class ViewAViewModel : BindableBase, INavigationAware { public void OnNavigatedTo(NavigationContext navigationContext) { var id = navigationContext.Parameters.GetValue<int>("id"); var key = navigationContext.Parameters.GetValue<string>("key"); } public void OnNavigatedFrom(NavigationContext navigationContext) { } // true → reuse the existing instance // false → create a new instance public bool IsNavigationTarget(NavigationContext navigationContext) => true; } |
Views/MainWindow.xaml |
<Button Command="{Binding NavigateCommand}" CommandParameter="ViewA" Content="Navigate to A" /> <Button Command="{Binding NavigateCommand}" CommandParameter="ViewB " Content="Navigate to B" /> |
ViewModels/MainWindowViewModel.cs |
private readonly IRegionManager regionManager; public DelegateCommand<string> NavigateCommand { get; private set; } public MainWindowViewModel(IRegionManager regionManager) { this.regionManager = regionManager; this.NavigateCommand = new DelegateCommand<string>(Navigate); } private void Navigate(string uri) { var navigationParameters = new NavigationParameters(); navigationParameters.Add("key", "value"); this.regionManager.RequestNavigate("ContentRegion", $"{uri}?id=1", navigationParameters); } |
ViewAViewModel.cs |
internal sealed class ViewAViewModel : BindableBase, IConfirmNavigationRequest { public void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallBack) { var continueNavigation = true; continuationCallBack(continueNavigation); } } |
ViewAViewModel.cs |
internal sealed class ViewAViewModel : BindableBase, INavigationAware { private IRegionNavigationJournal journal; public DelegateCommand GoBackCommand { get; set; } public ViewAViewModel() { GoBackCommand = new DelegateCommand(GoBack); } public void OnNavigatedTo(NavigationContext navigationContext) { journal = navigationContext.NavigationService.Journal; } private void GoBack() => journal.GoBack(); } |
Modules
A module represents a functional responsability of the application (independent feature).
- Add → New Project → WPF UserControl Library
- Remove AssemblyInfo.cs and UserControl1.xaml
- Add Prism.Wpf Nuget package
ModuleAModule.cs |
public sealed class ModuleAModule : IModule { public void OnInitialized(IContainerProvider containerProvider) { } public void RegisterTypes(IContainerRegistry containerRegistry) { } } |
Module catalog
Collection of modules that the application is going to load.
Code base
Hard reference to the modules.
App.xaml.cs |
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog) { moduleCatalog.AddModule<ModuleAModule>(); } |
App.config
More loosely coupled approach.
App.xaml.cs |
protected override IModuleCatalog CreateModuleCatalog() => new ConfigurationModuleCatalog(); |
App.config |
<configuration> <configSections> <section name="modules" type="Prism.Modularity.ModulesConfigurationSection, Prism.Wpf" /> </configSections> <modules> <module assemblyFile="ModuleA.dll" moduleType="ModuleA.ModileAModule, ModuleA, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" moduleName="ModuleAModule" startupLoaded="True" /> </modules> </configuration> |
REM ModuleA post build event xcopy /y "$(TargetDir)*.*" "$(SolutionDir)$(SolutionName)\$(OutDir)" |
Directory
Define a location on disk where to find the modules.
App.xaml.cs |
protected override IModuleCatalog CreateModuleCatalog() => new DirectoryModuleCatalog() { ModulePath = @".\Modules" }; |
REM ModuleA post build event xcopy /y "$(TargetDir)$(TargetName)$(TargetExt)" "$(SolutionDir)$(SolutionName)\$(OutDir)Modules\" REM xcopy /y C:\PrismNetCore\ModuleA\bin\Debug\net5.0-windows\ModuleA.dll C:\PrismNetCore\PrismNetCore\bin\Debug\net5.0-windows\Modules\ |
XAML resource dictionary
Event Aggregator
Loosely coupled event-based communication.
Event mechanism that enables communications between loosely coupled components in the application.
MyApp.Core/MyPayload.cs |
public sealed class MyPayload { public string Prop1 { get; set; } public string Prop2 { get; set; } } |
MyApp.Core/MyEvent.cs |
public sealed class MyEvent : PubSubEvent<MyPayload> { } |
MyApp/MyViewModel.cs |
internal sealed class MyViewModel { IEventAggregator eventAggregator; public MyViewModel(IEventAggregator eventAggregator) { this.eventAggregator = eventAggregator; } // publishing an event var payload = new MyPayload { Prop1 = "value1", Prop2 = "value2", }; eventAggregator.GetEvent<MyEvent>().Publish(payload); // subscribing to events eventAggregator.GetEvent<MyEvent>().Subscribe(MyAction); void MyAction(MyPayload payload) { } } |
Subscribing on the UI Thread
If the subscriber needs to update UI elements in response to events, subscribe on the UI thread. In WPF, only a UI thread can update UI elements.
eventAggregator.GetEvent<MyEvent>().Subscribe(DisplayMessage, ThreadOption.UIThread); |
PublisherThread | use this setting to receive the event on the publishers' thread. Default value. |
UIThread | use this setting to receive the event on the UI thread. |
BackgroundThread | use this setting to asynchronously receive the event on a .NET Framework thread-pool thread. |
In order for PubSubEvent to publish to subscribers on the UI thread, the EventAggregator must initially be constructed on the UI thread. |
Subscription Filtering
eventAggregator.GetEvent<MyEvent>().Subscribe(MyAction, ThreadOption.PublisherThread, false, x => x.Prop1 == "KeyMessage"); |
Performance concern
var keepSubscriberReferenceAlive = true; var myEvent = eventAggregator.GetEvent<MyEvent>(); myEvent.Subscribe(MyAction, keepSubscriberReferenceAlive); myEvent.Unsubscribe(MyAction); SubscriptionToken token = myEvent.Subscribe(MyAction, keepSubscriberReferenceAlive); myEvent.Unsubscribe(token); |
true | the event instance keeps a strong reference to the subscriber instance, thereby not allowing it to get garbage collected. |
false | default value. The event maintains a weak reference to the subscriber instance, thereby allowing the garbage collector to dispose the subscriber instance when there are no other references to it. When the subscriber instance gets collected, the event is automatically unsubscribed. |
Dialog
Popup window displayed over the current application.
ViewModels/ViewAViewModel.cs |
private readonly IDialogService dialogService; public DelegateCommand OpenDialogCommand { get; private set; } public ViewAViewModel(IDialogService dialogService) { this.dialogService = dialogService; this.OpenDialogCommand = new DelegateCommand(OpenDialog); } private void OpenDialog() { var dialogParameters = new DialogParameters(); dialogParameters.Add("message", "Dialog message !"); // MessageDialog: same name as the view this.dialogService.ShowDialog("MessageDialog", dialogParameters, (dialogResult) => { var buttonResult = dialogResult.Result; var value = dialogResult.Parameters.GetValue<string>("key"); }); } |
Dialogs/MessageDialog.xaml |
<UserControl x:Class="ModuleA.Dialogs.MessageDialog" xmlns:mvvm="http://prismlibrary.com/" d:DataContext="{d:DesignInstance dialogs:MessageDialogViewModel}" mvvm:ViewModelLocator.AutoWireViewModel="True" mc:Ignorable="d"> <prism:Dialog.WindowStyle> <Style TargetType="Window"> <Setter Property="Height" Value="200" /> <Setter Property="Width" Value="400" /> <Setter Property="ResizeMode" Value="NoResize" /> <Setter Property="prism:Dialog.WindowStartupLocation" Value="CenterOwner" /> </Style> </prism:Dialog.WindowStyle> <DockPanel> <Button Command="{Binding CloseDialogCommand}" Content="Ok" DockPanel.Dock="Bottom" /> <TextBlock Text="{Binding Message}" /> </DockPanel> </UserControl> |
Dialogs/MessageDialogViewModel |
internal sealed class MessageDialogViewModel : BindableBase, IDialogAware { public string Title => "Dialog message"; public event Action<IDialogResult> RequestClose; public ICommand CloseDialogCommand { get; } private string message; public string Message { get => message; set => SetProperty(ref message, value); } public MessageDialogViewModel() { CloseDialogCommand = new DelegateCommand(CloseDialog, CanCloseDialog); } public bool CanCloseDialog() => true; public void OnDialogClosed() { } public void OnDialogOpened(IDialogParameters parameters) { Message = parameters.GetValue<string>("message"); } private void CloseDialog() { var dialogParameters = new DialogParameters { { "key", "value" } }; this.RequestClose?.Invoke(new DialogResult(ButtonResult.OK, dialogParameters)); } } |
ModuleAModule.cs |
public void RegisterTypes(IContainerRegistry containerRegistry) { // register the MessageDialog under the unique string key MessageDialog (type of the dialog by default) containerRegistry.RegisterDialog<MessageDialog>(); } |