我正在试验一个简单的基于 ViewModel-first 的 WPF 应用程序和一些原始导航逻辑。该应用程序由两个 View (屏幕)组成。一个屏幕包含一个“前进”按钮,另一个屏幕包含一个“后退”按钮。通过按下其中一个按钮,会调用委托(delegate)命令,这反过来会导致 shell View 模型切换事件屏幕。屏幕 1 切换到屏幕 2,而屏幕 2 切换到屏幕 1。
这种方法的问题在于,它引入了竞争条件。当点击速度足够快时,相应的操作(前进/后退)可能会执行两次,从而导致应用程序失败。有趣的是,屏幕已经改变了,但 UI 并没有立即反射(reflect)状态的变化。到目前为止,我从未经历过这种差距,我做这个实验只是为了证明,单线程(分派(dispatch))WPF 应用程序是自动线程安全的。
有人对这种奇怪的行为有解释吗? WPF 绑定(bind)机制是否太慢,以至于可以再次按下按钮,直到 UI 更新自身以表示新的屏幕状态?
我不知道如何根据开发 mvvm 应用程序的建议来解决这个问题。没有办法同步代码,因为只有一个线程。我希望你能帮助我,因为现在我对依赖 WPF 数据绑定(bind)和模板系统感到非常不安全。
Zip archive containing the project files
MainWindow.xaml:
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApplication1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<DataTemplate DataType="{x:Type local:Screen1}">
<local:View1/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:Screen2}">
<local:View2/>
</DataTemplate>
</Window.Resources>
<Window.DataContext>
<local:ShellViewModel/>
</Window.DataContext>
<Grid>
<ContentControl Content="{Binding CurrentScreen}"/>
</Grid>
</Window>
ShellViewModel 包含“前进”和“后退”方法:
Public Class ShellViewModel
Inherits PropertyChangedBase
Private _currentScreen As Object
Public Property Screens As Stack(Of Object) = New Stack(Of Object)()
Public Sub New()
Me.Screens.Push(New Screen1(Me))
Me.GoForward()
End Sub
Property CurrentScreen As Object
Get
Return _currentScreen
End Get
Set(value)
_currentScreen = value
RaisePropertyChanged()
End Set
End Property
Public Sub GoBack()
Log("Going backward.")
If (Me.Screens.Count > 2) Then
Throw New InvalidOperationException("Race condition detected.")
End If
Log("Switching to Screen 1")
Me.Screens.Pop()
Me.CurrentScreen = Me.Screens.Peek()
End Sub
Public Sub GoForward()
Log("Going forward.")
If (Me.Screens.Count > 1) Then
Throw New InvalidOperationException("Race condition detected.")
End If
Log("Switching to Screen 2.")
Me.Screens.Push(New Screen2(Me))
Me.CurrentScreen = Me.Screens.Peek()
End Sub
End Class
屏幕仅包含一个委托(delegate)命令以启动操作的类:
Public Class Screen1
Inherits PropertyChangedBase
Private _clickCommand As ICommand
Private _shellViewModel As ShellViewModel
Public Sub New(parent As ShellViewModel)
_shellViewModel = parent
End Sub
Public ReadOnly Property ClickCommand As ICommand
Get
If _clickCommand Is Nothing Then
_clickCommand = New RelayCommand(AddressOf ExecuteClick, AddressOf CanExecute)
End If
Return _clickCommand
End Get
End Property
Private Function CanExecute(arg As Object) As Boolean
Return True
End Function
Private Sub ExecuteClick(obj As Object)
Threading.Thread.SpinWait(100000000)
_shellViewModel.GoForward()
End Sub
End Class
最佳答案
没有奇怪的比赛条件
我已经运行了你的代码。有一根线。主要的。
One thread = no race condition.
为什么要证明以下内容?
I made this experiment just to prove, that a single-threaded (dispatched) WPF application is automatically thread-safe.
这是一个防弹的事实。 一个线程 = 线程安全 (只要你不共享数据进程,但它不再是线程安全的)。
绑定(bind)和方法不支持连续调用
实际上,您的方法 GoBack 和 GoForward 不支持连续调用。他们应该一个接一个地被调用。
这里的线程安全并不意味着您的方法不能连续调用两次。如果进程中有任务队列,方法可以被调用两次。
您可能打算证明的内容可能如下:
Clicks are captured and processed in line, without any task queuing between the click, the property changed event raised, the dispatcher invocation, the binding / display refresh.
显然是错的!
当您调用 Dispatcher.BeginInvoke 或 Invoke 时,它在内部使用 任务队列 .没有什么能阻止你排队两次相同的任务来自 两次类似的点击 例如。
坦率地说,我无法重现您的错误。我认为这是同一线程捕获将其分派(dispatch)到您的代码然后将其显示在屏幕上的点击。然而,由于点击事件的任务,显示刷新在同一个队列中,理论上可以在屏幕更改之前将两次点击排入队列。然而 :
为什么不让你的方法支持连续调用?
GoBack 和 GoBackward 可以检查状态,如果当前状态无效,则不执行任何操作。
你可以使用:
1. 两个屏幕都从一开始就实例化。
2. 一个
bool
指示当前状态(前进或后退)。 3. 安
enum
使代码更清晰。 4. 状态机.. 不!我在开玩笑。
注意:为什么要使用 Stack 来推弹屏(顺便说一句)?和...
如果您添加另一个线程:
堆栈弹出/推送不是线程安全的。
而是使用
ConcurrentStack<T>
关于wpf - 使用 ViewModel-first(绑定(bind))方法的奇怪竞争条件,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/34885181/