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

避免在多线程c#MVVM应用程序中从ViewModel对象调用BeginInvoke(),c#,wpf,multithreading,mvvm,C#,Wpf,Multithreading,Mvvm,我的C#应用程序有一个数据提供程序组件,它在自己的线程中异步更新。ViewModel类都继承自实现INotifyPropertyChanged的基类。为了让异步数据提供程序使用PropertyChanged事件更新视图中的属性,我发现我的ViewModel与视图的耦合非常紧密,因为只需要从GUI线程中引发事件 #region INotifyPropertyChanged /// <summary> /// Raised when a property on this object

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


那么,我该如何避免这一小小的开始觉醒的呼吁呢?我是否必须重新发明控制盘并为列表创建ViewModel容器?或者我可以以某种方式将
Add()
委托给视图吗?

您可以在基本(ViewModel)类中实现常规属性更改行为:

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

我希望这一总体方针能对你有所帮助

  • (从您的编辑)通过操作向UI发送更新以进行发布不仅是黑客行为,而且完全没有必要。与在VM中使用Dispatcher或SynchronizationContext相比,您绝对不会从中受益。不要那样做。请它一文不值

  • 当绑定到实现INotifyPropertyChanged*的对象时,绑定将自动处理UI线程上的调用更新。。胡说,你说?花一分钟时间创建一个小型原型来测试它。着手我等。。。告诉你了

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


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

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

    在问题中的简单应用程序中,一个public
    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; }
    
            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();
            }
        }
    }
    
    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();
            }
        }
    }
    
    使用系统;
    使用System.Collections.ObjectModel;
    使用系统组件模型;
    使用系统线程;
    使用System.Threading.Tasks;
    使用System.Windows;
    名称空间多线程GUI
    {
    /// 
    ///App.xaml的交互逻辑
    /// 
    公共部分类应用程序:应用程序
    {
    公共应用程序()
    {
    Startup+=新的StartupEventHandler(应用程序启动);
    }
    无效应用程序启动(对象发送器、StartupEventArgs e)
    {
    TestViewModel vm=新的TestViewModel();
    主窗口=新的主窗口();
    window.DataContext=vm;
    vm.Start();
    window.Show();
    }
    }
    公共类TestViewModel:INotifyPropertyChanged
    {
    公共事件属性更改事件处理程序属性更改;
    公共ObservableCollection ListFromOthere{get;private set;}
    公共字符串textfromothere{get;private set;}
    //提供ViewModel封送操作的机制
    //视图线程上的工作线程。将设置GUI上下文
    //在主窗口加载事件处理程序期间,当
    //线程上下文和此类的实例都可用。
    公共同步上下文视图上下文{get;set;}
    公共TestViewModel()
    {
    //提供基于当前线程的默认上下文
    //如果视图需要其他视图,则可以对其进行更改。
    //碰巧在这个简单的例子中,当前的上下文
    //是GUI上下文,但在一个完整的应用程序中
    //情况未必如此。
    ViewContext=SynchronizationContext.Current;
    }
    内部无效开始()
    {
    ListFrom别处=新的ObservableCollection();
    任务testTask=新任务(新操作(()=>
    {
    整数计数=0;
    while(true)
    {
    textfromeverywhere=Convert.ToString(count++);
    //这是由框架在正确的线程上封送的。
    PropertyChangedEventHandler RaisePropertyChanged=PropertyChanged;
    if(null!=RaisePropertyChanged)
    {
    RaiseProperty已更改(此,
    新属性ChangedEventArgs(“其他地方的文本”);
    }
    //可观察集合(除其他外)以线程为中心,
    //因此,请使用视图提供的SynchronizationContext来
    //执行添加操作。
    ViewContext.Post(
    (param)=>listfromotherbes.Add((String)param),textfromotherbes);
    睡眠(1000);
    }
    }));
    _testTask.Start();
    }
    }
    }
    
    在本例中,窗口的已加载事件在代码隐藏中处理,以向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));
                }
            }
        }
    }
    
    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)); } } } }
    <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>
    
    interface ISynchronizationContext
    {
        System.Threading.SynchronizationContext ViewContext { get; set; }
    } 
    (this.DataContext as ISynchronizationContext).ViewContext  = 
    (SynchronizationContext)Dispatcher.Invoke
    (new Func<SynchronizationContext>(() => SynchronizationContext.Current));