避免从多线程c#MVVM应用程序中的ViewModel对象调用BeginInvoke()

时间:2014-07-18 12:11:10

标签: c# wpf multithreading mvvm

我的C#应用​​程序有一个数据提供程序组件,它在自己的线程中异步更新。 ViewModel类都继承自实现INotifyPropertyChanged的基类。为了让异步数据提供程序使用PropertyChanged事件更新View中的属性,我发现我的ViewModel与视图紧密耦合,因为只需要从GUI线程中引发事件!

#region INotifyPropertyChanged

/// <summary>
/// Raised when a property on this object has a new value.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;

/// <summary>
/// Raises this object's PropertyChanged event.
/// </summary>
/// <param name="propertyName">The property that has a new value.</param>
protected void OnPropertyChanged(String propertyName)
{
    PropertyChangedEventHandler RaisePropertyChangedEvent = PropertyChanged;
    if (RaisePropertyChangedEvent!= null)
    {
        var propertyChangedEventArgs = new PropertyChangedEventArgs(propertyName);

        // This event has to be raised on the GUI thread!
        // How should I avoid the unpleasantly tight coupling with the View???
        Application.Current.Dispatcher.BeginInvoke(
            (Action)(() => RaisePropertyChangedEvent(this, propertyChangedEventArgs)));
    }
}

#endregion

是否有任何策略可以消除ViewModel和View实现之间的这种耦合?

编辑1

这个answer是相关的,并突出了更新集合的问题。但是,建议的解决方案也使用当前的调度程序,我不想将其视为我的ViewModel。

编辑2 深入研究上述问题,我发现了一个链接answer,可以回答我的问题:创建一个Action&lt;&gt;视图中的DependencyProperty可以用于获取视图(无论可能是什么)以在必要时处理调度。

编辑3 看来问题是问题&#34;是没有实际意义的&#34;。但是,当我的ViewModel将Observable Collection公开为要绑定的视图的属性时(请参阅编辑1),它仍然需要访问该集合的Add()调度程序。例如:

App.xaml.cs

using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;

namespace MultiThreadingGUI
{
    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : Application
    {
        public App()
        {
            Startup += new StartupEventHandler(App_Startup);
        }

        void App_Startup(object sender, StartupEventArgs e)
        {
            TestViewModel vm = new TestViewModel();
            MainWindow window = new MainWindow();
            window.DataContext = vm;
            vm.Start();

            window.Show();
        }
    }

    public class TestViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public ObservableCollection<String> ListFromElsewhere { get; private set; }
        public String TextFromElsewhere { get; private set; }

        private Task _testTask;

        internal void Start()
        {
            ListFromElsewhere = new ObservableCollection<string>();
            _testTask = new Task(new Action(()=>
            {
                int count = 0;
                while (true)
                {
                    TextFromElsewhere = Convert.ToString(count++);
                    PropertyChangedEventHandler RaisePropertyChanged = PropertyChanged;
                    if (null != RaisePropertyChanged)
                    {
                        RaisePropertyChanged(this, new PropertyChangedEventArgs("TextFromElsewhere"));
                    }

                    // This throws
                    //ListFromElsewhere.Add(TextFromElsewhere);

                    // This is needed
                    Application.Current.Dispatcher.BeginInvoke(
                        (Action)(() => ListFromElsewhere.Add(TextFromElsewhere)));

                    Thread.Sleep(1000);
                }
            }));
            _testTask.Start();
        }
    }
}

MainWindow.xaml

<Window x:Class="MultiThreadingGUI.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow"
        SizeToContent="WidthAndHeight">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Label Grid.Row="0" Grid.Column="0" Content="TextFromElsewhere:" />
        <Label Grid.Row="0" Grid.Column="1" Content="{Binding Path=TextFromElsewhere}" />
        <Label Grid.Row="1" Grid.Column="0" Content="ListFromElsewhere:" />
        <ListView x:Name="itemListView" Grid.Row="1" Grid.Column="1"
            ItemsSource="{Binding Path=ListFromElsewhere}">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <Label Content="{Binding}" />
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </Grid>
</Window>

那么,我如何避免对BeginInvoke的这个小调用呢?我是否必须重新发明轮子并为列表创建ViewModel容器?或者我可以以某种方式将Add()委托给视图吗?

4 个答案:

答案 0 :(得分:4)

  1. (来自您的编辑)将更新发送到用户界面以通过操作进行展示不仅是hacky,而且完全没有必要。在VM中使用Dispatcher或SynchronizationContext完全没有任何好处。不要这样做。请。它毫无价值。

  2. 当绑定到实现INotifyPropertyChanged * 的对象时,绑定将自动处理UI线程上的调用更新。废话,你说?花一点时间创建一个小型原型来测试它。前进。我等一下...告诉你。

  3. 所以你的问题实际上没有实际意义 - 你根本不需要担心这个问题。

    *对框架的这一更改是在3.5,iirc中引入的,因此如果您针对3进行构建,则不适用。

答案 1 :(得分:1)

您可以在Base(ViewModel)类中实现常规 PropertyChanged行为:

private void RaisePropertyChanged(string propertyName)
        {
            if (Application.Current == null || Application.Current.Dispatcher.CheckAccess())
            {
                RaisePropertyChangedUnsafe(propertyName);
            }
            else
            {
                Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.DataBind,
                    new ThreadStart(() => RaisePropertyChangedUnsafe(propertyName)));
            }
        }

 private void RaisePropertyChangingUnsafe(string propertyName)
        {
            PropertyChangingEventHandler handler = PropertyChanging;
            if (handler != null)
            {
                handler(this, new PropertyChangingEventArgs(propertyName));
            }
        }

此代码将检查对主GUI调度程序的访问,并将在当前或GUI线程上引发Property Changed事件。

我希望这种一般方法能够帮到你。

答案 2 :(得分:1)

此答案基于Will answer和Marcel B的评论,并被标记为社区维基答案。

在问题的简单应用程序中,公共SynchronizationContext属性被添加到ViewModel类。这在必要时由View设置,并由ViewModel用于执行受保护的操作。在没有GUI线程的单元测试上下文中,可以模拟GUI线程,并使用SynchronizedContext代替真实线程。对于我的实际应用程序,其中一个视图没有任何特殊的SynchronizationContext,它只是不会更改ViewModel的默认ViewContext。

App.xaml.cs

using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;

namespace MultiThreadingGUI
{
    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : Application
    {
        public App()
        {
            Startup += new StartupEventHandler(App_Startup);
        }

        void App_Startup(object sender, StartupEventArgs e)
        {
            TestViewModel vm = new TestViewModel();
            MainWindow window = new MainWindow();
            window.DataContext = vm;
            vm.Start();

            window.Show();
        }
    }

    public class TestViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public ObservableCollection<String> ListFromElsewhere { get; private set; }
        public String TextFromElsewhere { get; private set; }

        // Provides a mechanism for the ViewModel to marshal operations from
        // worker threads on the View's thread.  The GUI context will be set
        // during the MainWindow's Loaded event handler, when both the GUI
        // thread context and an instance of this class are both available.
        public SynchronizationContext ViewContext { get; set; }

        public TestViewModel()
        {
            // Provide a default context based on the current thread that
            // can be changed by the View, should it required a different one.
            // It just happens that in this simple example the Current context
            // is the GUI context, but in a complete application that may
            // not necessarily be the case.
            ViewContext = SynchronizationContext.Current;
        }

        internal void Start()
        {
            ListFromElsewhere = new ObservableCollection<string>();
            Task testTask = new Task(new Action(()=>
            {
                int count = 0;
                while (true)
                {
                    TextFromElsewhere = Convert.ToString(count++);

                    // This is Marshalled on the correct thread by the framework.
                    PropertyChangedEventHandler RaisePropertyChanged = PropertyChanged;
                    if (null != RaisePropertyChanged)
                    {
                        RaisePropertyChanged(this, 
                            new PropertyChangedEventArgs("TextFromElsewhere"));
                    }

                    // ObservableCollections (amongst other things) are thread-centric,
                    // so use the SynchronizationContext supplied by the View to
                    // perform the Add operation.
                    ViewContext.Post(
                        (param) => ListFromElsewhere.Add((String)param), TextFromElsewhere);

                    Thread.Sleep(1000);
                }
            }));
            _testTask.Start();
        }
    }
}

在此示例中,Window-Loaded事件在代码隐藏中处理,以将GUI SynchronizationContext提供给ViewModel对象。 (在我的应用程序中,我没有代码行为,并且使用了绑定依赖项属性。)

MainWindow.xaml.cs

using System;
using System.Threading;
using System.Windows;

namespace MultiThreadingGUI
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            // The ViewModel object that needs to marshal some actions is
            // attached as the DataContext by the time of the loaded event.
            TestViewModel vmTest = (this.DataContext as TestViewModel);
            if (null != vmTest)
            {
                // Set the ViewModel's reference SynchronizationContext to
                // the View's current context.
                vmTest.ViewContext = (SynchronizationContext)Dispatcher.Invoke
                    (new Func<SynchronizationContext>(() => SynchronizationContext.Current));
            }
        }
    }
}

最后,Loaded事件处理程序绑定在XAML中。

MainWindow.xaml

<Window x:Class="MultiThreadingGUI.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow"
        SizeToContent="WidthAndHeight"
        Loaded="Window_Loaded"
        >
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Label Grid.Row="0" Grid.Column="0" Content="TextFromElsewhere:" />
        <Label Grid.Row="0" Grid.Column="1" Content="{Binding Path=TextFromElsewhere}" />
        <Label Grid.Row="1" Grid.Column="0" Content="ListFromElsewhere:" />
        <ListView x:Name="itemListView" Grid.Row="1" Grid.Column="1"
            ItemsSource="{Binding Path=ListFromElsewhere}">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <Label Content="{Binding}" />
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </Grid>
</Window>

答案 3 :(得分:0)

如果使用接口,则MainWindow.xaml.cs将失去TestViewModel依赖性。

interface ISynchronizationContext
{
    System.Threading.SynchronizationContext ViewContext { get; set; }
} 
(this.DataContext as ISynchronizationContext).ViewContext  = 
(SynchronizationContext)Dispatcher.Invoke
(new Func<SynchronizationContext>(() => SynchronizationContext.Current));