避免在多线程c#MVVM应用程序中从ViewModel对象调用BeginInvoke() [英] Avoid calling BeginInvoke() from ViewModel objects in multi-threaded c# MVVM application

查看:79
本文介绍了避免在多线程c#MVVM应用程序中从ViewModel对象调用BeginInvoke()的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我的C#应用​​程序具有一个数据提供程序组件,该组件在其自己的线程中异步更新. ViewModel类均从实现INotifyPropertyChanged的基类继承.为了让异步数据提供者使用PropertyChanged事件更新View中的属性,我发现由于只需从GUI线程中引发事件,我的ViewModel与视图就变得非常紧密了!

My C# application has a data provider component that updates asynchronously in its own thread. The ViewModel classes all inherit from a base class that implements INotifyPropertyChanged. In order for the asynchronous data provider to update properties in the View using the PropertyChanged event, I found my ViewModel became very closely coupled with the view due to the need to only raise the event from within the GUI thread!

#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实现之间的这种耦合?

Are there any strategies for eliminating this coupling between the ViewModel and the View implementation?

编辑1

答案与之相关,并突出了更新收藏集的问题.但是,建议的解决方案也使用了当前的调度程序,我不想成为我的ViewModel的关注对象.

This answer is related and highlights the issue of updating collections. However, the proposed solution also uses the current dispatcher, which I do not want to be a concern for my ViewModel.

编辑2 深入研究上面的问题,我发现了一个链接答案确实回答了我的问题:创建一个动作< > View模型中的DependencyProperty,View模型可用来获取View(无论可能是什么)以在必要时处理调度.

EDIT 2 Digging a bit deeper into the question above and I've found a link answer that does answer my question: create an Action<> DependencyProperty in the View that the View model can use to get the View (whatever that may be) to handle the dispatching where necessary.

编辑3 出现的问题是没有实际意义".但是,当我的ViewModel将Observable Collection作为要绑定的视图的属性公开时(请参见EDIT 1),它仍然需要访问Add()到集合的调度程序.例如:

EDIT 3 It appears the the question as asked "is moot". However, when my ViewModel exposes an Observable Collection as a property for the view to bind to (see EDIT 1), it still requires access to teh dispatcher to Add() to the collection. For example:

App.xaml.cs

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

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()委托给View?

So, how do I avoid that little call to BeginInvoke? Do I have to re-invent the wheel and create a ViewModel container for the list? Or can I delegate the Add() to the View somehow?

推荐答案

此答案基于Will的答案和来自Marcel B的评论,并被标记为社区Wiki答案.

This answer is based on Will's answer and the comment from Marcel B, and is marked as a community wiki answer.

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

In the simple application in the question, a public SynchronizationContext property is added to the ViewModel class. This is set by the View, where necessary, and used by the ViewModel to perform protected operations. In a unit test context that has no GUI thread, the GUI thread can be mocked and a SynchronizationContext for that used in place of the real one. For my actual application, where one of the Views does not have any special SynchronizationContext, it simply does not change the ViewModel's default ViewContext.

App.xaml.cs

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对象. (在我的应用程序中,我没有任何代码编写,并且使用了绑定的依赖项属性.)

In this example, the Window's Loaded event is handled in code-behind to supply the GUI SynchronizationContext to the ViewModel object. (In my application I have no code-behand and have used a bound dependency property.)

MainWindow.xaml.cs

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事件处理程序.

Finally, the Loaded event handler is bound in the XAML.

MainWindow.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>

这篇关于避免在多线程c#MVVM应用程序中从ViewModel对象调用BeginInvoke()的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆