我想知道在MVVM中使模型线程安全需要做些什么。假设我有以下类,它被实例化为单例:
public class RunningTotal: INotifyPropertyChange
{
private int _total;
public int Total
{
get { return _total; }
set
{
_total = value;
PropertyChanged("Total");
}
}
...etc...
}
我的视图模型通过属性公开它:
public RunningTotal RunningTotal { get; }
我的视图中有一个文本块,即{Binding Path=RunningTotal.Total}
。
我的应用有一个后台线程,定期更新Total的值。假设没有其他任何更新Total,我应该做什么(如果有的话)使所有这些线程安全?
现在,如果我想做类似但使用Dictionary<>
或ObservableCollection<>
类型的属性,该怎么办?哪些成员(添加,删除,清除,索引器)是线程安全的?我应该使用ConcurrentDictionary吗?
答案 0 :(得分:8)
我的应用有一个后台线程,定期更新Total的值。假设没有其他任何更新Total,我应该做什么(如果有的话)使所有这些线程安全?
对于标量属性,您不需要做任何特殊的事情; PropertyChanged
事件会自动封送到UI线程。
现在,如果我想做类似的事情,但使用Dictionary&lt;&gt;,或ObservableCollection&lt;&gt;?类型的属性,该怎么办?哪些成员(添加,删除,清除,索引器)是线程安全的?我应该使用ConcurrentDictionary吗?
不,这不是线程安全的。如果您从后台线程更改ObservableCollection<T>
的内容,它将会中断。您需要在UI线程上执行此操作。一种简单的方法是使用一个在UI线程上引发事件的集合,如here所描述的那样。
至于Dictionary<TKey, TValue>
,它的内容发生变化时不会发出通知,因此无论如何都不会通知用户界面。
答案 1 :(得分:2)
假设有两个线程正在更新Total
,并且您希望在_total
方法中将所有更改记录到PropertyChanged
。现在存在PropertyChanged
可能错过值的竞争条件。当线程在调用set_Total
的过程中阻塞时会发生这种情况。它会更新_total
,但仍会调用PropertyChanged。在此期间,另一个线程将_total
更新为另一个值:
thread1: _total = 4;
thread2: _total = 5;
thread2: PropertyChanged("Total");
thread1: PropertyChanged("Total");
现在,永远不会使用值4来调用PropertyChanged
。
您可以通过将值传递给PropertyChanged
方法或使用setter中的锁来解决此问题。
由于您说您有一个更新此属性的线程,因此不存在竞争条件。只有当多个线程(或进程)同时更新同一个东西时才会这样。
答案 2 :(得分:1)
模型应该以线程安全的方式编写,就像任何代码一样;由您决定是使用锁,并发容器还是其他任何东西来执行此操作。这些模型只是库代码,它(几乎)不应该知道它的功能将被MVVM应用程序使用。
但是,VM必须在UI线程中工作。这意味着通常他们不能依赖模型中的事件进入UI线程,因此如果订阅的事件不在UI线程中,他们必须编组调用或将它们存储在任务队列中。因此,VM应该以特定的方式关注线程安全,而不仅仅是模型需要。
反过来,View代码通常可以对所有线程问题感到高兴:它在专用UI线程中获取所有消息/调用/事件/任何内容,并在UI线程中进行自己的调用
特别针对您的情况,您的代码不是模型而是VM,对吗?在这种情况下,您必须在UI线程中触发事件,否则View将不满意。
答案 3 :(得分:1)
这个问题提供了ObservableCollection
的线程编组版本How do you correctly update a databound datagridview from a background thread
但是,您仍然需要担心线程之间的争用,这需要您在更新资源时锁定资源,或使用Interlocked.Increment之类的东西。
如果一个线程正在更新,另一个线程正在读取,则存在在更新中途完成读取的可能性(例如,Int64正在被修改。前半部分(32位)已在一个更新中线程,在更新后半部分之前,从第二个线程读取值。读取完全错误的值)
这可能是也可能不是问题,具体取决于您的应用程序将要执行的操作。如果错误的值将在GUI上闪烁1秒,那么它可能不是什么大问题,并且可以忽略锁的性能损失。如果您的程序将根据该值执行操作,那么您可能需要将其锁定。
答案 4 :(得分:0)
一个简单的答案是您需要通过UI Thread的Dispatcher在UI Thread中安排属性更新。这会将更新操作放入队列中,不会使应用程序崩溃。
private void handler(object sender, EventArgs e)
{
Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, (ThreadStart)delegate { updates(); });
}
private void updates() { /* real updates go here */ }
答案 5 :(得分:0)
有些事情更详细,它实际上是如此简单......当你在视图中实例化你的viewmodel时,只需将调度程序传递给ctor。
ServerOperationViewmodel ViewModel;
public pgeServerOperations()
{
InitializeComponent();
ViewModel = new ServerOperationViewmodel(Dispatcher);
}
然后在您的View模型中:
Dispatcher UIDispatcher;
public ServerOperationViewmodel(Dispatcher uiDisp)
{
UIDispatcher = uiDisp;
}
并像普通的UI调度程序一样使用它。
UIDispatcher.Invoke(() =>
{
.......
});
我承认我仍然是MVVM的新手,但我不认为这打破了MVVM的座右铭。