在 WPF/Prism 中使用 RequestNavigate 切换视图后,ViewModel(可能还有视图)仍然处于活动状态
ViewModels (and maybe Views) still active after switching Views with RequestNavigate in WPF/Prism
我的大多数视图模型都在 WPF 项目上使用 Prism 的 EventAggregator 订阅了一个公共事件。基本上,语音命令会在视图上触发此事件,作为响应,视图会将包含其特定消息的另一个事件发布到文本到语音模块。
然而,当我实现这个时,我意识到当使用 RegionManager 的 RequestNavigate 切换到另一个视图时,之前的视图模型仍然以某种方式处于活动状态。当我为最近的视图触发公共事件时,它也会为前一个视图触发。
简化示例:
- 从视图 1 开始
- 触发常见事件
- 响应:来自视图 1 的消息
- 请求导航到视图 2
- 触发常见事件
- 响应:来自视图 2 的消息,然后来自视图 1 的消息
- 请求导航到视图 3
- 触发常见事件
- 响应:消息来自视图 3,然后是视图 2,然后是视图 1
- 等等
我在视图 1、视图 2 和视图 3 的公共事件上放置了一个断点,每次我从一个视图收到一条消息时,它的断点就会被命中。
我想要的很简单:我不希望以前的 ViewModel(也可能是 View)在我切换视图时仍然以某种方式处于活动状态。更好的做法是对它们进行垃圾回收,因为我也有一些奇怪的情况,通过再次导航到视图 1、视图 2 和视图 1,视图 1 的消息被发送了两次(它的断点也命中了两次),所以我什至不确定是否为 ViewModel 创建了多个引用,这可能会导致内存泄漏。
我试图通过创建另一个仅包含必需品的项目来重现此行为,所以这是代码。我将 Visual Studio 2017 与 .net Framework 4.5.2 和 Ninject.
一起使用
Shell.xaml
<Window x:Class="PrismTest.Shell"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:prsm="http://prismlibrary.com/"
mc:Ignorable="d">
<Grid>
<ContentControl Name="MainRegion" prsm:RegionManager.RegionName="MainRegion" />
</Grid>
</Window>
NinjectPrismBootstrapper.cs
public class NinjectPrismBootstrapper : NinjectBootstrapper
{
protected override void InitializeModules()
{
base.InitializeModules();
// Text to speech
Kernel.Bind<SpeechSynthesizer>().ToSelf().InSingletonScope();
Kernel.Bind<INarrator>().To<StandardNarrator>().InSingletonScope();
Kernel.Bind<INarratorEventManager>().To<NarratorEventManager>().InSingletonScope();
// View models
Kernel.Bind<MainPageViewModel>().ToSelf();
Kernel.Bind<SecondPageViewModel>().ToSelf();
// Views
Kernel.Bind<object>().To<MainPageView>().InTransientScope().Named(typeof(MainPageView).Name);
Kernel.Bind<object>().To<SecondPageView>().InTransientScope().Named(typeof(SecondPageView).Name);
Kernel.Bind<Shell>().ToSelf();
var narratorEventManager = Kernel.Get<INarratorEventManager>();
var regionManager = Kernel.Get<IRegionManager>();
regionManager.RegisterViewWithRegion("MainRegion", typeof(MainPageView));
}
protected override DependencyObject CreateShell()
{
return (Shell)Kernel.GetService(typeof(Shell));
}
protected override void InitializeShell()
{
base.InitializeShell();
Application.Current.MainWindow = (Shell)this.Shell;
Application.Current.MainWindow.Show();
}
}
MainPageView.xaml(我的起始页)
<UserControl x:Class="PrismTest.Views.MainPageView"
namespaces...>
<StackPanel>
<TextBlock Text="Main page"/>
<Button Content="Narrator speaks" Command="{Binding Path=NarratorSpeaksCommand}" />
<Button Content="Next page" Command="{Binding Path=GoToNextPageCommand}"/>
</StackPanel>
</UserControl>
MainPageView.xaml.cs
public partial class MainPageView : UserControl
{
public MainPageView(MainPageViewModel dataContext)
{
InitializeComponent();
this.DataContext = dataContext;
}
}
MainPageViewModel(MainPageView 的视图模型)
public class MainPageViewModel : BindableBase, IRegionMemberLifetime, INavigationAware
{
private readonly IEventAggregator _eventAggregator;
private readonly IRegionManager _regionManager;
public DelegateCommand GoToNextPageCommand { get; private set; }
public DelegateCommand NarratorSpeaksCommand { get; private set; }
public MainPageViewModel(IEventAggregator eventAggregator, IRegionManager regionManager)
{
_eventAggregator = eventAggregator;
_regionManager = regionManager;
ConfigureCommands();
//The original common event triggered by a vocal command is simulated in this project by simply clicking on a button
_eventAggregator.GetEvent<CommonEventToAllViews>().Subscribe(NarratorSpeaks);
}
private void ConfigureCommands()
{
GoToNextPageCommand = new DelegateCommand(GoToNextPage);
NarratorSpeaksCommand = new DelegateCommand(ClickPressed);
}
private void GoToNextPage()
{
_regionManager.RequestNavigate("MainRegion", new Uri("SecondPageView", UriKind.Relative));
}
private void ClickPressed()
{
_eventAggregator.GetEvent<CommonEventToAllViews>().Publish();
}
private void NarratorSpeaks()
{
_eventAggregator.GetEvent<NarratorSpeaksEvent>().Publish("Main page");
}
}
我不需要放置 SecondPageViewModel 和 SecondPageView 的代码,因为它们是完全相同的代码,除了 RequestNavigate 将用户发送回 MainPageView 并且它的 NarratorSpeaks 方法发送不同的字符串。
我尝试了什么:
1) 使MainPageViewModel和SecondPageViewModel继承IRegionMemberLifetime并设置KeepAlive为false
2) IsNavigationTarget方法继承INavigationAware并返回false
3) 将其添加到 INavigationAware 的 OnNavigatedFrom 方法中:
public void OnNavigatedFrom(NavigationContext navigationContext)
{
var region = _regionManager.Regions["MainRegion"];
var view = region.Views.Single(v => v.GetType().Name == "MainPageView");
region.Deactivate(view);
}
值得注意:即使没有停用部分,如果我在 var region = _regionManager.Regions["MainRegion"] 之后放置一个断点;并检查region.views,无论我如何切换视图,结果都只有一个。
没有任何效果,当我来回切换视图时,事件在以前的视图中不断被触发。
所以,我在这里很茫然。我不确定是我在 Ninject 中注册 Views 和 ViewModels 的方式触发了这个,还是其他什么,但如果有人有建议,我会很乐意接受。
谢谢!
我以前也遇到过类似的问题。您是否考虑过在导航时取消订阅事件?
我的大多数视图模型都在 WPF 项目上使用 Prism 的 EventAggregator 订阅了一个公共事件。基本上,语音命令会在视图上触发此事件,作为响应,视图会将包含其特定消息的另一个事件发布到文本到语音模块。 然而,当我实现这个时,我意识到当使用 RegionManager 的 RequestNavigate 切换到另一个视图时,之前的视图模型仍然以某种方式处于活动状态。当我为最近的视图触发公共事件时,它也会为前一个视图触发。
简化示例:
- 从视图 1 开始
- 触发常见事件
- 响应:来自视图 1 的消息
- 请求导航到视图 2
- 触发常见事件
- 响应:来自视图 2 的消息,然后来自视图 1 的消息
- 请求导航到视图 3
- 触发常见事件
- 响应:消息来自视图 3,然后是视图 2,然后是视图 1
- 等等
我在视图 1、视图 2 和视图 3 的公共事件上放置了一个断点,每次我从一个视图收到一条消息时,它的断点就会被命中。
我想要的很简单:我不希望以前的 ViewModel(也可能是 View)在我切换视图时仍然以某种方式处于活动状态。更好的做法是对它们进行垃圾回收,因为我也有一些奇怪的情况,通过再次导航到视图 1、视图 2 和视图 1,视图 1 的消息被发送了两次(它的断点也命中了两次),所以我什至不确定是否为 ViewModel 创建了多个引用,这可能会导致内存泄漏。
我试图通过创建另一个仅包含必需品的项目来重现此行为,所以这是代码。我将 Visual Studio 2017 与 .net Framework 4.5.2 和 Ninject.
一起使用Shell.xaml
<Window x:Class="PrismTest.Shell"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:prsm="http://prismlibrary.com/"
mc:Ignorable="d">
<Grid>
<ContentControl Name="MainRegion" prsm:RegionManager.RegionName="MainRegion" />
</Grid>
</Window>
NinjectPrismBootstrapper.cs
public class NinjectPrismBootstrapper : NinjectBootstrapper
{
protected override void InitializeModules()
{
base.InitializeModules();
// Text to speech
Kernel.Bind<SpeechSynthesizer>().ToSelf().InSingletonScope();
Kernel.Bind<INarrator>().To<StandardNarrator>().InSingletonScope();
Kernel.Bind<INarratorEventManager>().To<NarratorEventManager>().InSingletonScope();
// View models
Kernel.Bind<MainPageViewModel>().ToSelf();
Kernel.Bind<SecondPageViewModel>().ToSelf();
// Views
Kernel.Bind<object>().To<MainPageView>().InTransientScope().Named(typeof(MainPageView).Name);
Kernel.Bind<object>().To<SecondPageView>().InTransientScope().Named(typeof(SecondPageView).Name);
Kernel.Bind<Shell>().ToSelf();
var narratorEventManager = Kernel.Get<INarratorEventManager>();
var regionManager = Kernel.Get<IRegionManager>();
regionManager.RegisterViewWithRegion("MainRegion", typeof(MainPageView));
}
protected override DependencyObject CreateShell()
{
return (Shell)Kernel.GetService(typeof(Shell));
}
protected override void InitializeShell()
{
base.InitializeShell();
Application.Current.MainWindow = (Shell)this.Shell;
Application.Current.MainWindow.Show();
}
}
MainPageView.xaml(我的起始页)
<UserControl x:Class="PrismTest.Views.MainPageView"
namespaces...>
<StackPanel>
<TextBlock Text="Main page"/>
<Button Content="Narrator speaks" Command="{Binding Path=NarratorSpeaksCommand}" />
<Button Content="Next page" Command="{Binding Path=GoToNextPageCommand}"/>
</StackPanel>
</UserControl>
MainPageView.xaml.cs
public partial class MainPageView : UserControl
{
public MainPageView(MainPageViewModel dataContext)
{
InitializeComponent();
this.DataContext = dataContext;
}
}
MainPageViewModel(MainPageView 的视图模型)
public class MainPageViewModel : BindableBase, IRegionMemberLifetime, INavigationAware
{
private readonly IEventAggregator _eventAggregator;
private readonly IRegionManager _regionManager;
public DelegateCommand GoToNextPageCommand { get; private set; }
public DelegateCommand NarratorSpeaksCommand { get; private set; }
public MainPageViewModel(IEventAggregator eventAggregator, IRegionManager regionManager)
{
_eventAggregator = eventAggregator;
_regionManager = regionManager;
ConfigureCommands();
//The original common event triggered by a vocal command is simulated in this project by simply clicking on a button
_eventAggregator.GetEvent<CommonEventToAllViews>().Subscribe(NarratorSpeaks);
}
private void ConfigureCommands()
{
GoToNextPageCommand = new DelegateCommand(GoToNextPage);
NarratorSpeaksCommand = new DelegateCommand(ClickPressed);
}
private void GoToNextPage()
{
_regionManager.RequestNavigate("MainRegion", new Uri("SecondPageView", UriKind.Relative));
}
private void ClickPressed()
{
_eventAggregator.GetEvent<CommonEventToAllViews>().Publish();
}
private void NarratorSpeaks()
{
_eventAggregator.GetEvent<NarratorSpeaksEvent>().Publish("Main page");
}
}
我不需要放置 SecondPageViewModel 和 SecondPageView 的代码,因为它们是完全相同的代码,除了 RequestNavigate 将用户发送回 MainPageView 并且它的 NarratorSpeaks 方法发送不同的字符串。
我尝试了什么:
1) 使MainPageViewModel和SecondPageViewModel继承IRegionMemberLifetime并设置KeepAlive为false
2) IsNavigationTarget方法继承INavigationAware并返回false
3) 将其添加到 INavigationAware 的 OnNavigatedFrom 方法中:
public void OnNavigatedFrom(NavigationContext navigationContext)
{
var region = _regionManager.Regions["MainRegion"];
var view = region.Views.Single(v => v.GetType().Name == "MainPageView");
region.Deactivate(view);
}
值得注意:即使没有停用部分,如果我在 var region = _regionManager.Regions["MainRegion"] 之后放置一个断点;并检查region.views,无论我如何切换视图,结果都只有一个。
没有任何效果,当我来回切换视图时,事件在以前的视图中不断被触发。 所以,我在这里很茫然。我不确定是我在 Ninject 中注册 Views 和 ViewModels 的方式触发了这个,还是其他什么,但如果有人有建议,我会很乐意接受。
谢谢!
我以前也遇到过类似的问题。您是否考虑过在导航时取消订阅事件?