我的C#应用程序有一个数据提供程序组件,它在自己的线程中异步更新。 ViewModel类都继承自实现INotifyPropertyChanged
的基类。为了让异步数据提供程序使用PropertyChanged事件更新View中的属性,我发现我的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实现之间的这种耦合?
编辑1
这个answer是相关的,并突出了更新集合的问题。但是,建议的解决方案也使用当前的调度程序,我不想将其视为我的ViewModel。
编辑2 深入研究上述问题,我发现了一个链接answer,可以回答我的问题:创建一个Action&lt;&gt;视图中的DependencyProperty可以用于获取视图(无论可能是什么)以在必要时处理调度。
编辑3
看来问题是问题&#34;是没有实际意义的&#34;。但是,当我的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()
委托给视图吗?
答案 0 :(得分:4)
(来自您的编辑)将更新发送到用户界面以通过操作进行展示不仅是hacky,而且完全没有必要。在VM中使用Dispatcher或SynchronizationContext完全没有任何好处。不要这样做。请。它毫无价值。
当绑定到实现INotifyPropertyChanged * 的对象时,绑定将自动处理UI线程上的调用更新。废话,你说?花一点时间创建一个小型原型来测试它。前进。我等一下...告诉你。
所以你的问题实际上没有实际意义 - 你根本不需要担心这个问题。
*对框架的这一更改是在3.5,iirc中引入的,因此如果您针对3进行构建,则不适用。
答案 1 :(得分:1)
您可以在Base(ViewModel)类中实现常规 PropertyChanged行为:
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事件。
我希望这种一般方法能够帮到你。
答案 2 :(得分:1)
此答案基于Will answer和Marcel B的评论,并被标记为社区维基答案。
在问题的简单应用程序中,公共SynchronizationContext
属性被添加到ViewModel类。这在必要时由View设置,并由ViewModel用于执行受保护的操作。在没有GUI线程的单元测试上下文中,可以模拟GUI线程,并使用SynchronizedContext代替真实线程。对于我的实际应用程序,其中一个视图没有任何特殊的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事件在代码隐藏中处理,以将GUI SynchronizationContext提供给ViewModel对象。 (在我的应用程序中,我没有代码行为,并且使用了绑定依赖项属性。)
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));
}
}
}
}
最后,Loaded事件处理程序绑定在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>
答案 3 :(得分:0)
如果使用接口,则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));