源码 https://github.com/gehongyan/AvaloniaTutorials/tree/main/AvaloniaPrism
Avalonia 中的 Prism
添加对 Prism 的引用
- 安装 Prism 库
dotnet add package Prism.Avalonia dotnet add package Prism.DryIoc.Avalonia - 在
App.xaml.cs中添加 Prism 的引用using Prism.Ioc; using Prism.DryIoc; - 在
App.xaml.cs中修改基类为PrismApplicationpublic partial class App : PrismApplication - 实现抽象方法
RegisterTypes/// <inheritdoc /> protected override void RegisterTypes(IContainerRegistry containerRegistry) { } /// <inheritdoc /> protected override AvaloniaObject CreateShell() { return Container.Resolve<MainWindow>(); } - 删除
OnFrameworkInitializationCompleted -
在
Initialize中初始化 Prismpublic override void Initialize() { AvaloniaXamlLoader.Load(this); base.Initialize(); // <-- 添加此行 }
视图与视图模型的绑定
添加视图 MainView 及其视图模型 MainViewModel,在 MainView 中添加
prism:ViewModelLocator.AutoWireViewModel="True",最后在 MainWindow 中引用该视图。
运行引用程序,可以发现,MainView.DataContext 被自动填充了 MainViewModel 的实例。
这是因为 Prism 会对设置了 AutoWireViewModel 为 True 的视图根据约定自动发现并绑定视图与视图模型。
不妨先看一下 Prism 绑定视图模型的源码:
它首先查看是否为该视图注册了映射,如果没有,则会回退到基于约定的方法。
GetViewModelForView
该方法尝试从 Dictionary<string, Func<object>> _factories 中寻找键为指定视图类型名称的键值对,其值为对应的视图模型的生成委托。
这是最短完成解析的路径,因为这需要我们手动注册视图与视图模型生成委托的映射关系。
Register 的其中一个重载中访问了 _factories 索引器的 set 方法,将视图类型名称与视图模型生成委托的键值对添加到 _factories 中。
因此,我们可以在 RegisterTypes 中手动指定视图与视图模型生成委托的绑定关系。
ViewModelLocationProvider.Register(typeof(MainView).FullName, () => new MainViewModel());
GetViewModelTypeForView
该方法尝试从 Dictionary<string, Type> _typeFactories 中寻找键为指定视图类型名称的键值对,其值为对应的视图模型类型。
Register 的另一个重载中访问了 _typeFactories 索引器的 set 方法,将视图类型名称与视图模型类型的键值对添加到 _typeFactories 中。
因此,我们可以在 RegisterTypes 中手动指定视图与视图模型类型的绑定关系。也存在另一个泛型重载,可以直接指定视图与视图模型类型的绑定关系。
ViewModelLocationProvider.Register<MainView, MainViewModel>();
ViewModelLocationProvider.Register(typeof(MainView).FullName, typeof(MainViewModel));
- 尝试从
Func<object, Type> _defaultViewToViewModelTypeResolver生成视图模型类型。
这是由各个平台各自实现的默认视图模型类型生成委托,目前仅有 MAUI 设置了该解析器。
- 尝试从
Func<Type, Type> _defaultViewTypeToViewModelTypeResolver生成视图模型类型。
这是最后的回退方案,它会根据视图类型名称生成视图模型类型名称,然后尝试从程序集中加载该视图模型类型。
该生成委托的默认逻辑为:
string viewName = viewType.FullName;
viewName = viewName.Replace(".Views.", ".ViewModels.");
string viewAssemblyName = viewType.GetTypeInfo().Assembly.FullName;
string suffix = viewName.EndsWith("View") ? "Model" : "ViewModel";
string viewModelName = string.Format(CultureInfo.InvariantCulture, "{0}{1}, {2}", viewName, suffix, viewAssemblyName);
return Type.GetType(viewModelName);
}
以 viewType 为 typepf(MainView) 为例:
– viewName 为 AvaloniaPrism.Views.MainView
– viewName 中的 .Views. 被替换为 .ViewModels.,得到 AvaloniaPrism.ViewModels.MainView
– viewAssemblyName 为 AvaloniaPrism, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
– 如果 viewName 以 View 结尾,则 suffix 为 Model,否则为 ViewModel
– viewModelName 为 AvaloniaPrism.ViewModels.MainViewModel, AvaloniaPrism, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
– 通过 Type.GetType(viewModelName) 获取到 MainViewModel 的类型
可以发现,要想实现自动转换,视图与视图模型之间需要遵循一定的约定:
– 视图模型位于与视图类型相同的程序集中
– 视图模型在一个名为 .ViewModels 的子命名空间中
– 视图位于一个名为 .Views 的子命名空间中
– 视图模型名称与视图名称相对应,并以 “ViewModel” 结尾
- 如果获取到了视图模型的类型,则通过
Activator.CreateInstance创建视图模型的实例,并通过setDataContextCallback
将其赋值给视图的DataContext属性。
依赖注入
Prism 中的依赖注入主要是由 DryIoc 完成的,Microsoft.Extensions.DependencyInjection 则是在付费商用许可中提供。
因此,此处仅介绍 Avalonia 中 DryIoc 的使用。
生命周期
Prism 与大多数依赖注入容器一样,支持以下三种生命周期:
– Transient:瞬态,每次请求都会创建一个新的实例
– Singleton:单例,整个应用程序中只有一个实例
– Scoped:作用域,在同一作用域中只有一个实例
- 单例
// 瞬态
containerRegistry.Register<IFooService, FooService>();
// 单例
containerRegistry.RegisterSingleton<IFooService, FooService>();
// 作用域
containerRegistry.RegisterScoped<IFooService, FooService>();
// 通过实例注册
containerRegistry.RegisterInstance<IFooService>(new FooService());
惰性解析
默认情况下,注册服务也会同步注册其相关的 Func<IService> 服务委托及 Lazy<IService> 惰性实例。
可以进一步将服务实例的解析推迟到第一次访问时,而不是在注册时立即解析。
但需要注意的是,如果服务是单例的,那惰性解析不一定会带来性能提升,因为单例服务在第一次解析时就会被创建,相反,惰性解析可能会带来额外的开销。
绑定
Prism 中提供了 BindableBase 类,它实现了 INotifyPropertyChanged 接口,可以方便地实现属性绑定。
public class MainViewModel : BindableBase
{
private string _title = "Hello World";
public string Title
{
get => _title;
set => SetProperty(ref _title, value);
}
}
命令
Prism 中的命令主要是通过 DelegateCommand 和 CompositeCommand 实现的。
DelegateCommand
DelegateCommand 是一个泛型类,它接受一个泛型参数,表示命令的参数类型,也可以不传入参数类型,即为无参命令。
public class MainViewModel : BindableBase
{
public DelegateCommand SayHelloCommand { get; }
public MainViewModel()
{
SayHelloCommand = new DelegateCommand(SayHello);
}
private void SayHello()
{
Title = "Hello Avalonia!";
}
}
如果需要控制命令是否可用,可以通过 CanExecute 方法返回一个布尔值来实现。
public class MainViewModel : BindableBase
{
private string _title = "Hello World";
public MainViewModel()
{
SayHelloCommand = new DelegateCommand(SayHello, CanSayHello); // <-- 修改此行
}
public string Title
{
get => _title;
set => SetProperty(ref _title, value);
}
public DelegateCommand SayHelloCommand { get; }
private void SayHello()
{
Title = "Hello Avalonia!";
}
private bool CanSayHello() // <-- 添加此方法
{
return !string.IsNullOrEmpty(Title);
}
}
如果 CanExecute 依赖其它可变值,可以通过 RaiseCanExecuteChanged 方法来主动通知命令重新计算是否可用。
public class MainViewModel : BindableBase
{
private string _title = "Hello World";
public MainViewModel()
{
SayHelloCommand = new DelegateCommand(SayHello, CanSayHello);
}
public string Title
{
get => _title;
set
{
SetProperty(ref _title, value);
SayHelloCommand.RaiseCanExecuteChanged(); // <-- 添加此行
}
}
public DelegateCommand SayHelloCommand { get; }
private void SayHello()
{
Title = "Hello Avalonia!";
}
private bool CanSayHello()
{
return !string.IsNullOrEmpty(Title);
}
}
也可以通过 ObservesCanExecute 来指定命令是否可执行所依赖的属性变更。
public class MainViewModel : BindableBase
{
private bool _canSayHello = true;
public MainViewModel()
{
SayHelloCommand = new DelegateCommand(SayHello)
.ObservesCanExecute(() => CanSayHello); // <-- 修改此行
}
public bool CanSayHello // 添加此属性
{
get => _canSayHello;
set => SetProperty(ref _canSayHello, value);
}
public DelegateCommand SayHelloCommand { get; }
private void SayHello()
{
Title = "Hello Avalonia!";
}
}
CompositeCommand
CompositeCommand 是一个命令集合,可以将多个命令组合成一个命令,当执行该命令时,会依次执行集合中的命令。
当集合中的所有命令的 CanExecute 方法返回 true 时,CompositeCommand 的 CanExecute 方法才会返回 true。
public class MainViewModel : BindableBase
{
public MainViewModel()
{
CompositeCommand = new CompositeCommand();
CompositeCommand.RegisterCommand(new DelegateCommand(SayHello));
CompositeCommand.RegisterCommand(new DelegateCommand(SayGoodbye));
}
public CompositeCommand CompositeCommand { get; }
private void SayHello()
{
Title = "Hello Avalonia!";
}
private void SayGoodbye()
{
Title = "Goodbye Avalonia!";
}
}
如果视图与视图模型可能会被销毁,则也需要考虑符合命令的取消注册。
SayHelloCompositeCommand.UnregisterCommand(new DelegateCommand(SayHello));
SayHelloCompositeCommand.UnregisterCommand(new DelegateCommand(SayGoodbye));
事件聚合器
Prism 中的事件聚合器是一个全局的事件总线,可以在不同的视图模型之间传递消息。
GetEvent 方法的泛型参数需要一个继承自 EventBase 的事件参数,框架提供了 PubSubEvent<TPayload> 可供直接使用。
PubSubEvent<TPayload> 是一个发布/订阅事件,可以通过 Publish 方法发布消息,通过 Subscribe 方法订阅消息。
如果需要操作 UI 元素,可以通过 ThreadOption.UIThread 来指定在 UI 线程上执行。
如果需要过滤指定的消息,可以通过 Subscribe 方法的的第四个参数 Predicate<TPayload> 来指定过滤条件。
public class EventsViewModel : BindableBase, IDisposable
{
private string _receivedMessage;
private readonly SubscriptionToken _subscriptionToken;
public EventsViewModel(IEventAggregator eventAggregator)
{
PublishEventCommand = new DelegateCommand(() =>
eventAggregator.GetEvent<PubSubEvent<string>>()
.Publish("Hello from Avalonia! It's {DateTime.Now} now."))
_subscriptionToken = eventAggregator.GetEvent<PubSubEvent<string>>()
.Subscribe(message => ReceivedMessage ="Received message: {message}", ThreadOption.UIThread);
}
public DelegateCommand PublishEventCommand { get; private set; }
public string ReceivedMessage
{
get => _receivedMessage;
private set => SetProperty(ref _receivedMessage, value);
}
/// <inheritdoc />
public void Dispose()
{
GC.SuppressFinalize(this);
_subscriptionToken.Dispose();
}
}
对话框服务
使用对话框服务前,需要在 App.xaml.cs 中注册对话框服务。
containerRegistry.RegisterDialog<MessageBoxView, MessageBoxViewModel>();
Prism 中提供了 IDialogService 接口服务,可以用于显示对话框。
dialogService.ShowDialog 的其中一个重载可以接收视图名称、对话框参数、回调方法。
DialogParameters parameters = new()
{
{ "title", "Dialog Title" },
{ "content", DialogContent }
};
dialogService.ShowDialog(nameof(MessageBoxView), parameters, result =>
{
DialogContent = result.Result == ButtonResult.OK
? "Dialog closed by OK"
: "Dialog closed by Cancel";
});
为 MessageBoxViewModel 实现 IDialogAware 接口,可以让感知作为对话框数据模型的参数与状态。
OnDialogOpened 方法,IDialogParameters parameters 参数上可以获取 ShowDialog 所传入的 parameters,可用于设置属性。
RequestClose 事件可以用于关闭对话框,可以传入一个 DialogResult 枚举值,表示对话框的结果,以供 ShowDialog 的回调方法使用。
区域导航
Prism 中的区域导航主要由 IRegionManager 接口提供,可以用于将指定的区域导航到指定的视图。
要使用区域导航,首先需要在 App.xaml.cs 中注册区域导航服务。
containerRegistry.RegisterForNavigation<ServicesView, ServicesViewModel>();
在视图中的 ContentControl 上添加 prism:RegionManager.RegionName 附加属性,指定区域名称。
<ContentControl prism:RegionManager.RegionName="MainRegion" />
在视图模型中,通过 IRegionManager.RequestNavigate 方法导航到指定的视图。
public class MainWindowViewModel : BindableBase
{
private readonly IRegionManager _regionManager;
public MainWindowViewModel(IRegionManager regionManager)
{
_regionManager = regionManager;
_regionManager.RequestNavigate("MainRegion", nameof(ServicesView));
}
}
即可让指定的区域导航至指定的视图。
INavigationAware 接口提供了参与导航的视图模型的相关信息。
OnNavigatedTo方法在视图模型导航到时调用,可以获取导航参数。OnNavigatedFrom方法在视图模型导航离开时调用。IsNavigationTarget方法用于指示当前视图模型是否可以处理导航请求。
在请求导航时可以传入参数:
NavigationParameters parameters = new()
{
{ "at", DateTime.Now }
};
regionManager.RequestNavigate(RegionNames.ContentRegion, nameof(Views.NavigationsView), parameters);
NavigationContext 上便可获取到传入的参数。
DateTime at = navigationContext.Parameters.GetValue<DateTime>("at");
IRegionNavigationService 上提供了 IRegionNavigationJournal Journal 属性,可以用于导航历史记录的管理。
CanGoBack 和 CanGoForward 属性,可以用于判断是否可以回退或前进。
GoBack 和 GoForward 方法,可以用于回退或前进。
如果需要让某些视图不被记录在导航历史记录中,例如启动页、登录页、对话框等中,可以为其实现 IJournalAware接口,并为 PersistInHistory
方法返回 false。
区域适配器
RegionManager.RegionName 所能附加的容器必须存在对应的区域适配器,否则 Prism 无法获知如何在容器中添加或删除视图。
内置的区域适配器有:
– ContentControlRegionAdapter
– ItemsControlRegionAdapter
而要在其它的容器上使用区域导航,需要自定义区域适配器。
public 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 is { Action: NotifyCollectionChangedAction.Add, NewItems: not null })
{
foreach (Control item in e.NewItems)
regionTarget.Children.Add(item);
}
if (e is { Action: NotifyCollectionChangedAction.Remove, OldItems: not null })
{
foreach (Control item in e.OldItems)
regionTarget.Children.Remove(item);
}
};
}
protected override IRegion CreateRegion() => new SingleActiveRegion();
}
然后在 App.xaml.cs 中注册该区域适配器。
protected override void ConfigureRegionAdapterMappings(RegionAdapterMappings regionAdapterMappings)
{
base.ConfigureRegionAdapterMappings(regionAdapterMappings);
regionAdapterMappings.RegisterMapping(typeof(StackPanel), Container.Resolve<StackPanelRegionAdapter>());
}
模块
模块是 Prism 中的一个概念,用于将应用程序分解为更小的功能单元,以便于管理和维护。
在模块的程序集内需要定义一个模块类,实现 IModule 接口。
public class ModuleAModule : IModule
{
public void RegisterTypes(IContainerRegistry containerRegistry)
{
}
public void OnInitialized(IContainerProvider containerProvider)
{
}
}
在模块的定义内,可以定于仅属于该模块的服务、视图、视图模型、导航等。这使得服务的定义不再混杂在主程序集中,而是更加清晰地分离开来。
containerRegistry.RegisterForNavigation<ModuleAView, ModuleAViewModel>();
containerRegistry.Register<IDelayService, DelayService>();
在主程序集中,需要在 App.xaml.cs 中注册模块。
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
{
base.ConfigureModuleCatalog(moduleCatalog);
moduleCatalog.AddModule<ModuleAModule>();
}
模块相关的视图、视图模型、服务等会在模块初始化时被注册到容器中,以便于在模块内外使用。
如果不想让主程序包含对模块的引用,而是在程序启动时扫描目录加载模块,可以使用 DirectoryModuleCatalog。
protected override IModuleCatalog CreateModuleCatalog()
{
const string modulePath = @".\Modules";
if (!Directory.Exists(modulePath))
Directory.CreateDirectory(modulePath);
return new DirectoryModuleCatalog { ModulePath = modulePath };
}
如果需要判断某个模块是否被加载,可以通过 IModuleManager 接口上提供的方法来判断。
if (moduleManager.ModuleExists("ModuleAModule")
&& moduleManager.IsModuleInitialized("ModuleAModule"))
regionManager.RequestNavigate(RegionNames.ContentRegion, "ModuleAView");、
Visits: 159