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

本文关键字:对象 ViewModel 调用 BeginInvoke 多线程 应用程序 MVVM | 更新日期: 2023-09-27 18:13:33

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

#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

这个答案是相关的,并且突出了更新集合的问题。然而,建议的解决方案也使用了当前调度程序,我不希望它成为我的ViewModel的关注点。

编辑2 深入挖掘一下上面的问题,我发现了一个链接答案,它确实回答了我的问题:在视图中创建一个Action<> DependencyProperty,视图模型可以使用它来获取视图(无论它可能是什么)来处理必要的调度。

编辑3 看来这个问题"没有实际意义"。然而,当我的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()委托给视图吗?

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

  1. (从你的编辑)发送更新到UI通过动作调度不仅是黑客,这是完全没有必要的。与在VM中使用Dispatcher或SynchronizationContext相比,这样做绝对没有好处。不要那样做。请。它一文不值。

  2. 当绑定到实现INotifyPropertyChanged的对象时,绑定将自动处理UI线程上的调用更新*。你说这是胡扯?花点时间创建一个小型原型进行测试。去做吧。我会等. ...告诉你。

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

*这个对框架的更改是在3.5,iirc中引入的,所以如果你是基于3构建的,则不适用

这个回答是基于Will的回答和Marcel B的评论,并被标记为社区wiki的回答。

在问题中的简单应用程序中,一个公共SynchronizationContext属性被添加到ViewModel类中。这是由视图在必要时设置的,并由ViewModel用于执行受保护的操作。在没有GUI线程的单元测试上下文中,可以模拟GUI线程,并用一个SynchronizationContext代替真实的线程。对于我的实际应用程序,其中一个视图没有任何特殊的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事件是在后台代码中处理的,以向ViewModel对象提供GUI SynchronizationContext。(在我的应用程序中,我没有现成的代码,并且使用了绑定依赖属性。)

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));
            }
        }
    }
}

最后,在XAML.

中绑定了Loaded事件处理程序。

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>

你可以在你的Base(ViewModel) class中实现一个通用的 PropertyChanged Behavior:

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事件。

如果使用了一个接口,那么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));