避免在多线程c#MVVM应用程序中从ViewModel对象调用BeginInvoke()
我的C#应用程序有一个数据提供程序组件,它在自己的线程中异步更新。ViewModel类都继承自实现避免在多线程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
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线程上引发属性更改事件
我希望这一总体方针能对你有所帮助
*这个对框架的更改是在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));