wpf - 使用 ViewModel-first(绑定(bind))方法的奇怪竞争条件

标签 wpf mvvm data-binding command race-condition

我正在试验一个简单的基于 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

Screen 1 switches to Screen 2, whereas Screen 2 switches to Screen 1. By clicking fast enough it is possible to introduce a race condition (that is going forward/backward twice).

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)到您的代码然后将其显示在屏幕上的点击。然而,由于点击事件的任务,显示刷新在同一个队列中,理论上可以在屏幕更改之前将两次点击排入队列。然而 :
  • 我的点击速度不够快,无法击败我的 CPU。
  • 我认为不需要 SpinWait。
  • 我的配置中可能会遗漏一些东西。

  • 为什么不让你的方法支持连续调用?

    GoBack 和 GoBackward 可以检查状态,如果当前状态无效,则不执行任何操作。

    你可以使用:

    1. 两个屏幕都从一开始就实例化。

    2. 一个bool指示当前状态(前进或后退)。

    3. 安enum使代码更清晰。

    4. 状态机.. 不!我在开玩笑。

    注意:为什么要使用 Stack 来推弹屏(顺便说一句)?和...
    如果您添加另一个线程:
    堆栈弹出/推送不是线程安全的。
    而是使用 ConcurrentStack<T>

    关于wpf - 使用 ViewModel-first(绑定(bind))方法的奇怪竞争条件,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/34885181/

    相关文章:

    wpf - 当我离开它时,为什么我的 DataGridComboBoxColumn 会清除它的值?

    c# - 如何实现属性更改通知

    c# - 主细节 MVVM WPF 不工作

    c# - 使 WPF Tabcontrol 标题垂直堆叠

    c# - ReactiveUI、WPF 和验证

    silverlight-4.0 - MVVM 和导航服务

    wpf mvvm .. 访问 View 模型中的 View 元素

    wpf - 棘手的 WPF 绑定(bind)

    javascript - 如何呈现数据属性?

    c# - XAML Designer - 默认缩放?