避免在多线程c# MVVM应用程序中从ViewModel对象调用BeginInvoke()
本文关键字:对象 ViewModel 调用 BeginInvoke 多线程 应用程序 MVVM | 更新日期: 2023-09-27 18:13:33
我的c#应用程序有一个数据提供程序组件,它在自己的线程中异步更新。ViewModel类都继承自实现INotifyPropertyChanged
的基类。为了让异步数据提供程序使用PropertyChanged事件更新视图中的属性,我发现由于只需要从GUI线程内引发事件,我的ViewModel与视图变得非常紧密地耦合在一起!
#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
这个答案是相关的,并且突出了更新集合的问题。然而,建议的解决方案也使用了当前调度程序,我不希望它成为我的ViewModel的关注点。
编辑2 深入挖掘一下上面的问题,我发现了一个链接答案,它确实回答了我的问题:在视图中创建一个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()
委托给视图吗?
-
(从你的编辑)发送更新到UI通过动作调度不仅是黑客,这是完全没有必要的。与在VM中使用Dispatcher或SynchronizationContext相比,这样做绝对没有好处。不要那样做。请。它一文不值。
-
当绑定到实现INotifyPropertyChanged的对象时,绑定将自动处理UI线程上的调用更新*。你说这是胡扯?花点时间创建一个小型原型进行测试。去做吧。我会等. ...告诉你。
所以你的问题实际上是没有意义的——你根本不需要担心这个。
*这个对框架的更改是在3.5,iirc中引入的,所以如果你是基于3构建的,则不适用
这个回答是基于Will的回答和Marcel B的评论,并被标记为社区wiki的回答。
在问题中的简单应用程序中,一个公共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; }
// 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事件是在后台代码中处理的,以向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));
}
}
}
}
最后,在XAML.
中绑定了Loaded事件处理程序。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>
你可以在你的Base(ViewModel) class中实现一个通用的 PropertyChanged Behavior:
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事件。
如果使用了一个接口,那么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));