我的 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 实现之间的这种耦合?
EDIT 1
This answer相关并强调更新集合的问题。但是,建议的解决方案还使用当前的调度程序,我不希望它成为我的 ViewModel 的问题。
EDIT 2深入研究上面的问题,我找到了一个链接answer这确实回答了我的问题:在视图中创建一个 Action DependencyProperty,视图模型可以使用它来获取视图(无论是什么)来处理必要时的调度。
EDIT 3看来所提出的问题“没有实际意义”。但是,当我的 ViewModel 将 Observable Collection 作为要绑定到的视图的属性公开时(请参阅编辑 1),它仍然需要访问调度程序才能Add()
到收藏。例如:
应用程序.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();
}
}
}
主窗口.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()
以某种方式到视图?