从单独的MVVM模块更新MainWindow属性

时间:2014-01-25 11:26:15

标签: c# wpf mvvm

我希望在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 Style”微调器。 要使用此处详述的微调器类:WPF Spinner via MVVM & Attached Properties 我已经能够通过在每个模块内容视图中添加AsyncNotifier.Trigger(详见上面的链接)来使用它,见下文。

enter image description here enter image description here

我的问题是: 如果我希望微调器覆盖整个应用程序窗口,那么我需要将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放在哪里?它应该在我的主项目中。

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

4 个答案:

答案 0 :(得分:3)

我会在这里看一下实现中介模式。这已经以EventAggregator

的形式出现在Prism中

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

  • 订户
    • 主窗口
  • 出版商
    • 第1单元
    • 第2单元
    • 第3单元

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

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

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

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

public class BusyUserInterfaceEvent : CompositeWpfEvent<bool>
{
}

(我想你可以直接使用CompositeWpfEvent<bool>这可能同样有效,但如果你决定包含更多使用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换成完全不同的窗口,并在应用程序生命周期的某个部分加载模块,只要它订阅这些事件类型,它仍然会忙/不忙关于从模块发布的消息。这是一个非常灵活的解决方案。

PRISMs实施还提供订阅过滤功能,以便您可以有选择地监听事件。

此外,由于您正在聚合事件,因此可以使用此方法进行许多操作以允许模块间通信。您唯一的挑战是确保每个人都知道消息类型,这就是为什么可能需要外部依赖来保存消息类型

答案 1 :(得分: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中,这个想法是一样的,只是不要在代码背后做。你可以创建一个单例(Singleton有一个静态实例)供微调器绑定,然后任何模块都可以搞乱单例属性。

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

这是一个实现接口的单例ViewModel。您的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接口或Catalog类吗?如果没有,请跳过此步骤,转到下一个想法。

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,但前提是您必须这样做,除非您有严格的设计规则,可以参考什么。

答案 2 :(得分:1)

您不应该仅为旋转器需要VM。我所做的是在我的主窗口视图中拥有微调器本身(动画,图标,等等),并让主窗口VM实现以下界面:

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

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

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

答案 3 :(得分:0)

如果你想创建一个微调器的更多“视觉”样式/界面,我建议你考虑创建你自己的子类微调器(如果你需要它后面的特殊功能),然后为它创建自己的自定义样式外观/行为。然后当你将微调器放在表单上时,只需使用你指定的类/样式的微调器,它们就能很好地发挥作用。

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