【问题标题】:Any solutions to avoid ShowDialog() blocking a Messenger?任何避免 ShowDialog() 阻塞 Messenger 的解决方案?
【发布时间】:2012-09-16 18:28:12
【问题描述】:

我正在开发一个基于 mvvm light 工具包的项目。我有一个 MainView 和一个 DetailsView 及其 对应的视图模型。两个虚拟机都注册了NotificationMessage

// MainViewModel.cs and DetailsViewModel.cs
private void RegisterMessages()
{
    Messenger.Default.Register<NotificationMessage>(this, NotificationMessageHandler);
}

当收到“ShowDetails”消息时,MainViewModel 调用创建“DetailsView”的服务

// MainViewModel.cs
private void NotificationMessageHandler(NotificationMessage msg)
{
    if (msg.Notification == "ShowDetails")
    {
        _detailsService.ShowDetails(); // Does something like (new DetailsView).ShowDialog()
    }
}

DetailsView 使用ViewModelLocator 获取现有的DetailsViewModal 作为DataContext。

DetailsViewModel 也应该收到“ShowDetails”消息以更新其内部状态或请求一些数据。

// DetailsViewModel.cs
private void NotificationMessageHandler(NotificationMessage msg)
    {
        if (msg.Notification == "ShowDetails")
        {
            UpdateViewModel();
        }
    }

现在的问题: 因为我希望DetailsView 成为模态窗口,所以我在上面调用ShowDialog()。这似乎会阻止信使,直到 DetailsView 再次关闭。所以DetailsViewModal 在模态窗口关闭后收到消息。有没有办法解决这个问题?

如果我可以在MainViewModel 之前注册DetailsViewModal,我认为它会起作用。这将改变 MessageHandler 调用的顺序,并且 VM 更新发生在阻塞 ShowDialog() 之前。但是 MainViewModel 是首先创建和注册的,因为它就是它。 DetailsViewModel 是由 ViewModalLocator 在第一次需要时创建的,所以它总是输掉比赛。

【问题讨论】:

    标签: c# wpf modal-dialog mvvm-light


    【解决方案1】:

    很遗憾,我无法重现您的具体问题。我在 MainWindowView Loaded 事件处理程序中启动了一个单独的线程;一个除了不断发送特定消息之外什么都不做的线程。然后我在我的 SecondWindowView 上调用了 ShowDialog(),它的视图模型被注册来监听这个特定的消息。第二个窗口的视图模型中的消息处理程序重复执行。事实上,甚至在调用 ShowDailog() 之前就调用了处理程序,因为我的视图模型已经由 ViewModelLocator 在应用程序启动时创建。我需要查看更多代码才能更好地了解您的情况(即您的服务正在创建详细信息窗口,或者我可以编译以重现该问题的内容)。

    您可以为您的子窗口尝试以下方法。在应用程序的某处定义以下类:

    public class ShowChildWindowMessage : MessageBase { }
    public class HideChildWindowMessage : MessageBase { }
    public class DisplayDetailsMessage : MessageBase { }
    

    现在创建以下 ChildWindowVM 类并在 ViewModelLocator 中初始化它,就像初始化 MainWindowVM 一样:

    public class ChildWindowVM : ViewModelBase
    {
        private ViewModelBase m_currentContent;
        public ViewModelBase CurrentContent
        {
            get { return m_currentContent; }
            set
            {
                NotifySetProperty(ref m_currentContent, value, () => CurrentContent);
                if (m_currentContent != null)
                {
                    m_currentContent.Refresh();
                    Messenger.Default.Send(new ShowChildWindowMessage());
                }
            }
        }
    
        public ChildWindowVM()
        {
            Messenger.Default.Register<DisplayDetailsMessage>(this, OnDisplayDetails);
        }
    
        private void OnDisplayDetails(DisplayDetailsMessage msg)
        {
            CurrentContent = ViewModelLocator.DetailsViewModel; // or whatever view model you want to display
        }
    }
    

    Refresh() 方法将在 DetailsViewModel 类中定义,并且会在显示窗口之前处理您希望执行的任何初始化。请注意,当设置 CurrentContent 属性时,会向 MainWindowView 发出一条消息,以创建一个 ChildWindowView 实例来显示您的内容。

    MainWindowView 代码如下所示:

    public partial class MainWindowView : Window
    {
        private ChildWindowView m_childWindowView;
    
        public MainWindowView()
        {
            InitializeComponent();
            Closing += () => ViewModelLocator.CleanUp();      
    
            Messenger.Default.Register<ShowChildWindowMessage>(this, OnShowChildWindow);
            Messenger.Default.Register<HideChildWindowMessage>(this, OnHideChildWindow);
        }
    
        private void OnShowChildWindow(ShowChildWindowMessage msg)
        {
            m_childWindowView = new ChildWindowView();
            m_childWindowView.ShowDialog();
        }
    
        private void OnHideChildWindow(HideChildWindowMessage msg)
        {
            m_childWindowView.Close();
        }
    }
    

    最后一步是将 ChildWindowVM 类的 CurrentContent 属性绑定到您的 ChildWindowView 类。这是在您的 ChildWindowView 的 xaml 中完成的:

    <Window x:Class="Garmin.Cartography.AdminBucketTools.ChildWindowView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        DataContext="{Binding Path=ChildWindowVm, Source={StaticResource Locator}}">
    
    <Grid>
        <ContentPresenter Content="{Binding Path=CurrentContent}" />
    </Grid>
    

    现在您只需调用即可在应用程序的任何位置显示您的详细信息

    Messenger.Default.Send(new DisplayDetailsMessage());
    

    您可以通过调用以编程方式关闭窗口

    Messenger.Default.Send(new HideChildWindowMessage());
    

    您还可以从 MessageBase 派生任意数量的类,并在您的 ChildWindowVM 类中注册它们。在每个消息处理程序中,您可以通过将 CurrentContent 属性设置为适当的视图模型来指定要显示的内容。

    事实上,还有一件事。如果你真的想在你的子窗口中看到任何有用的东西,你需要在你的视图和视图模型之间指定模板绑定。这可以通过应用程序资源中的 xaml 来完成:

    <DataTemplate DataType="{x:Type viewmodels:DetailsViewModel}">
        <views:DetailsView />
    </DataTemplate>
    

    不要忘记定义命名空间(即“viewmodels”和“views”)。

    【讨论】:

    • 感谢您为帮助一个随机的陌生人付出了如此大的努力:)
    • 感谢您的广泛回答。您写道,VM 存在于非 UI 线程中。似乎很清楚,如果 Messenger 也这样做,我的问题就会消失。如何让 ViewModelLocator 在不同的线程上创建 VM?顺便说一句,我读到在将 VM 放入非 UI 线程时,我的 ObservableCollections 会遇到其他问题。
    • 对不起,我没有正确地描述 UI 线程。我的意思是说您的视图模型中的代码可以在单独的线程中运行。我已经更新了我的答案。此外,就 ObservableCollection 而言,如果您尝试从另一个线程更新绑定到您的视图的集合,您只会遇到问题。这必须在 UI 线程中完成,因此只需从您的其他线程中执行此操作:App.Current.Dispatcher.Invoke(new Action(() =&gt; { //update collection here }));
    • @bugged87 我也有类似的问题,你有什么建议吗?我的解决方案不使用 VML,所以不要认为我可以使用上述解决方案。 stackoverflow.com/questions/34347091/…
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-12-22
    • 2021-09-24
    • 1970-01-01
    • 1970-01-01
    • 2015-11-19
    • 1970-01-01
    相关资源
    最近更新 更多