【问题标题】:Avoid calling BeginInvoke() from ViewModel objects in multi-threaded c# MVVM application避免在多线程 c# MVVM 应用程序中从 ViewModel 对象调用 BeginInvoke()
【发布时间】:2015-07-03 09:06:18
【问题描述】:

我的 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

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

编辑 1

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

编辑 2 深入挖掘上面的问题,我找到了一个链接answer,它确实回答了我的问题:在视图中创建一个 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() 委托给视图吗?

【问题讨论】:

  • 您不必将PropertyChanged 事件分派到 UI 线程。
  • 我经常这样做,但我认为它更多是线程问题而不是 UI 问题。如果您不喜欢 ViewModel 中对 WPF 的依赖,可以使用静态 SynchronizationContext 属性,但必须从视图 (DispatcherSynchronizationContext) 设置它。然后您将一个依赖项切换为另一个。
  • 如果调用是尝试从后台线程更新 VM 的异步操作的结果,他会这样做。
  • @NovitchiS 许多属性都是如此,但不是集合。我将编辑我的问题。

标签: c# wpf multithreading mvvm


【解决方案1】:
  1. (来自您的编辑)将更新发送到 UI 以通过 Actions 进行调度不仅是 hacky,而且完全没有必要。与在 VM 中使用 Dispatcher 或 SynchronizationContext 相比,您绝对不会从中获得任何好处。不要那样做。请。一文不值。

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

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

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

【讨论】:

  • 你是对的。我最初的问题是没有实际意义的。我已经对其进行了编辑以显示它在可观察集合上失败的地方。也许我也需要编辑标题?
  • @MikeofSST:啊哈!是的,ObservableCollections 以线程为中心!那真不幸。第 1 点仍然有效。如果您真的担心,最佳实践是允许 ViewModel 类的用户分配(以 UI 为中心,有点)Dispatcher 或(以框架为中心,绝对是)SynchronizationContext 来发布更新。您将使用它来执行异步和同步工作,因此允许您类型的用户控制您正在处理的线程。消除虚拟机的顾虑。
【解决方案2】:

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

在问题中的简单应用程序中,将公共SynchronizationContext 属性添加到 ViewModel 类。这是由视图在必要时设置的,并由视图模型用于执行受保护的操作。在没有 GUI 线程的单元测试上下文中,可以模拟 GUI 线程并使用 SynchronizationContext 代替真实线程。对于我的实际应用,其中一个 View 没有任何特殊的 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】:

    您可以在 Base(ViewModel) 类中实现 general 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 事件。

    我希望这种通用方法对您有所帮助。

    【讨论】:

    • 如果我的“查看”是非 GUI 应用程序(例如正在记录事件的数据库),您知道会发生什么吗?
    • 是的,抱歉,我忘记将其添加到我的答案中。我的回答将逻辑从视图移动到基类,但仍与 GUI 视图耦合。
    【解决方案4】:

    如果使用接口,则 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));
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-06-24
      • 2021-11-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-06-29
      相关资源
      最近更新 更多