【问题标题】:Updating MainWindow Property from separate MVVM Module从单独的 MVVM 模块更新 MainWindow 属性
【发布时间】:2014-01-25 11:26:14
【问题描述】:

我希望在 WPF 应用程序中实现“JQuery”样式微调器。由于此微调器将由一些单独的 PRISM 模块触发,并且我希望微调器覆盖整个应用程序窗口,因此我需要访问 MainWindow 中的属性。

我在 MainWindow 中有一个属性,但我无法从其他模块中看到它。

我尝试过 Application.Current.MainWindow 但没有运气。

我也尝试过使用 Application.Current.Properties[] 但我不知道如何触发 OnPropertyChanged 事件。

请有人指点我正确的方向。

更新: 这里有一些屏幕截图和对我想要做的更好的描述。

好的,这是我的问题的一个示例。我有一个包含以下内容的应用程序:

  • WPFApp
  • WPFApp.Module1
  • WPFApp.Module2
  • WPFApp.Module3
  • WPFApp.Module4

WPFApp MainWindow 包含 2 个区域,一个菜单区域(在左侧)和一个内容区域。 每个模块包含 2 个视图:

  • 加载到 MainWindow 菜单区域的菜单视图
  • 加载到 MainWindow 内容区域的内容视图

在每个模块内容视图中,我想执行一个需要几秒钟的任务,并且在执行任务时,我想显示一个“Ajax 样式”微调器,它将覆盖整个应用程序窗口。 要使用此处详述的微调器类:WPF Spinner via MVVM & Attached Properties 我已经能够通过将 AsyncNotifier.Trigger(在上面的链接中详述)添加到每个模块内容视图来使用它,见下文。

我的问题是: 如果我希望微调器覆盖整个应用程序窗口,那么我需要将 AsyncNotifier.Trigger 添加到 MainWindow。我还需要从负责显示微调器的 MainWindow 中公开一个属性,并能够从每个模块中访问它。 关于如何做到这一点的任何想法?

更新:

好的,我想我可能会走得更远,但仍然有点卡在所有东西如何组合在一起。

我已经创建了我的接口并将其放入我的基础架构模块中,以便其他所有模块都可以访问它。

我正在使用以下代码从我的 AggregateModuleCatalog 类加载我的模块。

/// <summary>
/// this.Catalogs is a readonly collection of IModuleCatalog
/// Initializes the catalog, which may load and validate the modules.
/// </summary>
public void Initialize()
{
    foreach (var catalog in this.Catalogs)
    {
        catalog.Initialize();
    }
}

我的问题是我不确定应该将 SpinnerViewModel 放在哪里?它应该在我的主项目中吗?

另外,我应该在哪里使用构造函数注入传入单例?

【问题讨论】:

    标签: c# wpf mvvm


    【解决方案1】:

    我会考虑在这里实现中介者模式。这已经以EventAggregator的形式存在于Prism中

    基本上,听起来您想向任何订阅者发布某种类型的控制消息 - 在这种情况下,您有:

    • 订阅者
      • 主窗口
    • 出版商
      • 模块 1
      • 模块 2
      • 模块 3

    模块应该发布一条消息,表明他们想要在工作完成/模块加载等时“忙”MainWindow

    关键是他们不应该以任何方式知道MainWindow。这使解决方案保持解耦、MVVM 友好、可重用且易于维护。这种方法使用依赖注入,这通常是个好主意:)

    为了使其工作,模块需要依赖EventAggregator 服务(PRISM 中有一个IEventAggregator 接口,这是您的服务接口),然后应该向它发布消息

    首先你需要一个作为消息类的事件。显然,这需要发布者和订阅者都可以看到,因此可能需要放在外部引用中。这需要从 CompositeWpfEvent 继承,并且您应该为消息负载提供一个通用参数。

    public class BusyUserInterfaceEvent : CompositeWpfEvent<bool>
    {
    }
    

    (我想您可以直接使用 CompositeWpfEvent&lt;bool&gt; 来实现这一点,这可能同样有效,但如果您决定包含更多使用 bool 作为有效负载的事件类型,则不够具体)

    那么你需要从你的模块中发布上述类型的事件

    public class Module1
    {
        private IEventAggregator _eventAggregator;
    
        public Module1(IEventAggregator eventAggregator)
        {
            _eventAggregator = eventAggregator;
        }
    
        public DoSomeWork()
        {
            // Busy the UI by publishing the above event
            _eventAggregator.GetEvent<BusyUserInterfaceEvent>().Publish(true);
        }
    
        public FinishDoingSomeWork() 
        {
            // Unbusy the UI by publishing the above event with 'false'
            _eventAggregator.GetEvent<BusyUserInterfaceEvent>().Publish(false);
        }
    }
    

    MainWindow 还应该依赖于EventAggregator 服务,并且应该订阅任何特定类型的消息。如果要在 UI 线程上完成工作(如本例所示),您应该使用ThreadOption.UIThread 订阅

    public class MainWindow
    {
        private IEventAggregator _eventAggregator;
    
        public MainWindow(IEventAggregator eventAggregator)
        {
            _eventAggregator = eventAggregator;
    
            // Subscribe to any messages of the defined type on the UI thread
            // The BusyUserInterface method will handle the event
            _eventAggregator.GetEvent<BusyUserInterfaceEvent>().Subscribe(BusyUserInterface, ThreadOption.UIThread);
        }
    
        public BusyUserInterface(bool busy)
        {
            // Toggle the UI - pseudocode here!
            TickerActive = busy;
        }
    }
    

    虽然我之前在几个框架中成功使用过这种模式,但我承认我没有使用过 PRISM,所以上面的代码可能不太正确,但是,文档看起来很简洁:

    http://msdn.microsoft.com/en-us/library/ff921122.aspx

    使用这种模式可以解耦任何组件,因此很容易阻止MainWindow 订阅事件并在需要时将订阅者移动到父组件,而无需触及发布者的实现。

    这意味着您可以将 MainWindow 换成一个完全不同的窗口并在应用程序生命周期的某些部分加载您的模块,只要它订阅这些事件类型,它仍然会根据从模块发布消息。这是一个非常灵活的解决方案。

    PRISM 实现还提供订阅过滤,因此您可以有选择地监听事件。

    此外,由于您正在聚合事件,因此您可以做很多事情来允许模块间通信。您面临的唯一挑战是确保每个人都知道消息类型,这就是为什么可能需要外部依赖项来保存消息类型

    【讨论】:

    • 我在整个地方都使用这种模式来制作微调器 UI 元素,这与我使用的代码非常相似。但是,EventAggregator 不应该采用更全局的范围吗?
    • 不确定全局范围是什么意思,对我来说,将控制树顶部的元素与子元素分离是分离的组件间通信,这就是事件聚合器所做的。我不会认为这太限制范围 - 因为您正在创建自己的事件类型,您可以确定消息在哪个级别与组件交互。
    • @Charleh,工作就像一个魅力。谢谢人真的很感激。
    • @Charleh 我想我最初没有看到的是你有一个 IoC 模式,我没想到会在我的东西中使用它。我刚刚(在 Visual Basic 中)声明了一个 Module 和一个 Shared IEventAggregator 并且我在整个应用程序范围内使用它。你的方法更灵活;我很高兴在这件事上错了!
    【解决方案2】:

    我错过了什么吗?应该这么简单吗?

    App.xaml.cs

    public partial class App
    {
       public static Window MainWindow {get;set;}
    }
    

    MainWindow.xaml.cs

    public MainWindow()
    {
        App.MainWindow = this;
        InitializeComponent();
    }
    public void Spinner(bool show)
    {
        // Your code here
    }
    

    现在在 MVVM 中,想法是一样的,只是不要在后面的代码中这样做。您可以为微调器创建一个单例(单例有一个静态实例)来绑定,然后任何模块都可以与单例属性混淆。

    如果您喜欢依赖注入,请创建一个接口,创建一个实现该接口的对象,让 Spinner 绑定到该对象。然后在加载模块时将该对象作为接口注入到模块中。

    这是一个实现接口的单例 ViewModel。您的 Prism 项目只需要了解接口。但是加载 Prism 模块的代码应该知道接口和单例。然后在加载 Prism 模块时,将传入单例(随意使用最简单的注入方法:构造函数注入、方法注入或属性注入)。

    using System.ComponentModel;
    
    namespace ExampleCode
    {
        /// <summary>
        /// Interface for managing a spinner
        /// </summary>
        public interface IManageASpinner
        {
            bool IsVisible {get;set;}
            // Add any other properties or methods you might need.
        }
    
        /// <summary>
        /// A singleton spinner ViewModel for the main window spiner
        /// </summary>
        public class SpinnerViewModel : INotifyPropertyChanged, IManageASpinner
        {        #region Singleton and Constructor
            /// <summary>
            /// Singleton instance
            /// </summary>
            public static SpinnerViewModel Instance
            {
                get { return _Instance ?? (_Instance = new SpinnerViewModel()); }
            } private static SpinnerViewModel _Instance;
    
            /// <summary>
            /// Private constructor to prevent multiple instances
            /// </summary>
            private SpinnerViewModel()
            {
            }
            #endregion
    
            #region Properties
            /// <summary>
            /// Is the spinner visible or not? 
            /// Xaml Binding: {Binding Source={x:Static svm:SpinnerViewModel.Instance}, Path=IsVisible}
            /// </summary>
            public bool IsVisible
            {
                get { return _IsVisible; }
                set
                {
                    _IsVisible = value;
                    OnPropertyChanged("IsVisible");
                }
            } private bool _IsVisible;
    
            // Add any other properties you might want to include
            // such as IsSpinning, etc..
    
            #endregion
    
            #region INotifyPropertyChanged implementation
            public event PropertyChangedEventHandler PropertyChanged;
    
            public void OnPropertyChanged(string name)
            {
                var handler = PropertyChanged;
                if (handler != null)
                {
                    handler(this, new PropertyChangedEventArgs(name));
                }
            }
            #endregion
        }
    }
    

    希望你能从这里把这些点联系起来。

    2 月 12 日添加

    也许使用方法注入?我不确定你可以。你能编辑 IModuleCatalog 接口或目录类吗?如果没有,请跳过此内容并转到下一个想法。

    foreach (var catalog in this.Catalogs)
    {
        catalog.Initialize(SpinnerViewModel.Instance);
    }
    

    也许您的目录应该实现第二个接口并使用属性注入而不是构造函数注入。

    public interface IHaveASpinner
    {
       public IManageASpinner Spinner {get;set;}
    }
    

    现在,让所有实现 IModuleCatalog 的目录也实现 IHaveASpinner。

    然后让您的代码执行此操作:

    foreach (var catalog in this.Catalogs)
    {
        catalog.Initialize();
        var needSpinner = catalog as IHaveASpinner;
        if (needSpinner != null)
        {
            needSpinner.Spinner = SpinnerViewModel.Instance;
        }
    }
    

    我应该把 SpinnerViewModel 放在哪里?

    我会建议您的主要项目。这取决于您的设计。是否所有模块都已经引用了您的主项目?如果是这样,那么把它放在你的主项目中。如果没有,那么也许有一个项目是主项目并且所有模块都已经引用了?您甚至可以为您的微调器创建一个单独的项目/dll,但前提是您必须这样做,除非您有一个严格的设计,其中包含可以引用什么的规则。

    【讨论】:

    • in MVVM 你能举个例子说明我如何使用依赖注入来解决这个问题。
    • 好吧,我给你写了一个示例接口和实现该接口的单例 ViewModel。有关绑定到 Singleton 属性的信息,请参阅 ViewModel 中的 cmets。
    【解决方案3】:

    您不应该只为微调器需要一个 VM。我所做的是在我的主窗口视图中显示微调器本身(动画、图标等),并让主窗口 VM 实现以下接口:

    public interface IApplicationBusyIndicator
    {
        bool IsBusy { get; set; }
    }
    

    使用您最喜欢的 IoC 容器(我使用 Castle),我只是将 IApplicationBusyIndi​​cator 注入到其他 VM 中。每当其中一个启动一些长时间运行的任务时,它只会将 IsBusy 设置为 true(然后在完成时返回 false)。主窗口视图中的忙碌指示器将其 Visibility 属性绑定到主窗口 VM 的 IsBusy 属性(当然使用布尔到可见性的转换器)。

    如果在您的场景中,存在一些架构约束阻止其他模块中的虚拟机被注入主窗口虚拟机的实例,那么您可以使用 Prism 的事件聚合器发布消息说“应用程序忙”,并且让主窗口 VM 订阅事件,并显示忙碌指示符。

    【讨论】:

      【解决方案4】:

      如果您希望创建更多“视觉”样式/界面的微调器,我建议您考虑创建自己的子类微调器(如果您需要它背后的特殊功能),然后为其创建自己的自定义样式外观/行为。然后,当您将微调器放在表单上时,只需使用您指定的类/样式的微调器,它们应该可以很好地为您工作。

      Here is one of my own learning about styles, but simple label

      Another link where I helped someone walking through creating their own custom class/style which MIGHT be what you are trying to globally implement.

      And a link to template / style defaults to learn from

      【讨论】:

      • 谢谢。我已经设置了微调器类和样式,并将其放置在主窗口上,并将其绑定到 MainWindow ViewModel 中的属性,但我希望能够从不同的模块更新此属性。
      • 让我解释一下。 MainWindow 包含 2 个区域,一个菜单和内容。它也是加载微调器的地方,因此整个应用程序都被微调器覆盖。每个模块都包含一个菜单区域的视图以及一个内容区域的视图。我希望微调器由模块内容视图中的操作触发。问题是我不知道如何在 MainWindow 中设置一个属性,我可以从每个模块内容视图模型访问该属性以触发微调器加载。任何帮助将不胜感激。
      • @Nollaig,如果您可以打印屏幕/粘贴样本(即使您将其缩小一些)并添加到您的帖子以查看以更好地澄清,这可能会有所帮助......然后添加您在原始帖子中的评论/澄清...与此同时,我会尝试思考您在寻找什么。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多