【问题标题】:ViewModels (and maybe Views) still active after switching Views with RequestNavigate in WPF/Prism在 WPF/Prism 中使用 RequestNavigate 切换视图后,视图模型(可能还有视图)仍然处于活动状态
【发布时间】:2017-09-05 11:20:01
【问题描述】:

我的大多数视图模型在 WPF 项目中使用 Prism 的 EventAggregator 订阅公共事件。基本上,语音命令会在视图上触发此事件,并且作为响应,视图会将包含其特定消息的另一个事件发布到文本到语音模块。 然而,当我实现这个时,我意识到当使用 RegionManager 的 RequestNavigate 切换到另一个视图时,之前的视图模型仍然处于某种活动状态。当我为最近的视图触发公共事件时,它也会为上一个视图触发。

简化示例:

  1. 从视图 1 开始
  2. 触发普通事件
  3. 响应:来自视图 1 的消息
  4. 请求导航到视图 2
  5. 触发普通事件
  6. 响应:来自视图 2 的消息,然后来自视图 1 的消息
  7. 请求导航到视图 3
  8. 触发普通事件
  9. 响应:消息来自视图 3,然后是视图 2,然后是视图 1

我在视图 1、视图 2 和视图 3 的公共事件上放置了一个断点,每次我从一个视图中得到消息时,都会命中它的断点。

我想要的很简单:我不希望以前的 ViewModel(也可能是 View)在切换视图时仍然以某种方式处于活动状态。更好的是它们被垃圾收集,因为我也有一些奇怪的情况,通过再次导航到视图 1、视图 2 和视图 1,视图 1 的消息被发送了两次(并且它的断点也命中了两次),所以我什至不确定是否为 ViewModel 创建了多个引用,这可能会导致内存泄漏。

我试图通过创建另一个仅包含基本要素的项目来重现此行为,所以这里是代码。我正在使用带有 .net 框架 4.5.2 和 Ninject 的 Visual Studio 2017。

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) 继承INavigationAware并在IsNavigationTarget方法中返回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 的方式触发了这个,或者其他什么,但如果有人有建议,我很乐意接受。

谢谢!

【问题讨论】:

    标签: c# wpf mvvm ninject prism


    【解决方案1】:

    过去我也遇到过类似的问题。您是否考虑过在导航时取消订阅事件?

    【讨论】:

    • 是的,实际上在我的原始项目中,我使用取消订阅作为临时措施,以防止语音合成器堆积消息而只说当前的消息。但是,让我担心的是,当我执行类似的操作时,再次转到视图 1 > 视图 2 > 视图 1,来自视图 1 的消息重复出现,这让我认为可能正在创建多个 ViewModel 引用而不是由垃圾处理收集器,最终导致内存泄漏。切换视图后,我尝试等待 500 万,但仍在处理相同的消息。
    • 我真的不知道垃圾收集器处理未使用的引用需要多少时间,而且由于我是 WPF 和 Prism 的初学者,我想确保我没有这样做有事吗。这实际上是未使用的 ViewModel 的正常行为吗?
    • @Bob_ZombX:GC 不是确定性的,它可以在系统空闲或内存压力下运行。只要您不持有对视图的引用,就应该收集它。考虑到 INavigationAware 允许重用您的 View 实例。
    • 好的,所以基本上,我只需要使用 Unsubscribe 来获取当前视图的消息,我不应该担心未使用的 ViewModel,因为在某些时候,即使它看起来有点长,会被收起来吗?
    • @Bob_ZombX:如果他们在完整的收藏中幸存下来,您可以尝试:GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-12-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-11-02
    • 2017-08-25
    相关资源
    最近更新 更多