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

Evi*_*Pie 4 c# wpf multithreading mvvm

我的 C# 应用程序有一个数据提供程序组件,它在自己的线程中异步更新。ViewModel 类都继承自一个实现INotifyPropertyChanged. 为了让异步数据提供者使用 PropertyChanged 事件更新视图中的属性,我发现我的 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
Run Code Online (Sandbox Code Playgroud)

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

编辑 1

这个答案 是相关的,并强调了更新集合的问题。但是,建议的解决方案还使用当前的调度程序,我不希望它成为我的 ViewModel 的问题。

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

编辑 3 似乎所问的问题“没有实际意义”。但是,当我的 ViewModel 公开一个 Observable 集合作为要绑定到的视图的属性时(请参阅编辑 1),它仍然需要访问Add()该集合的调度程序。例如:

应用程序.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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

主窗口.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>
Run Code Online (Sandbox Code Playgroud)

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

小智 5

  1. (来自您的编辑)将更新发送到 UI 以通过操作进行调度不仅是hacky,而且完全没有必要。与在 VM 中使用 Dispatcher 或 SynchronizationContext 相比,您绝对不会从中受益。不要那样做。请。它毫无价值。

  2. 当绑定到实现 INotifyPropertyChanged 的​​对象时,绑定将自动处理 UI 线程上的调用更新* . 胡说八道,你说?花一点时间创建一个小原型来测试它。前进。我会等待。... 跟你说了。

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

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