通过使用ViewModel-first(绑定)方法进行奇怪的竞争条件

Den*_*sel 4 data-binding wpf command mvvm race-condition

我正在尝试一个简单的基于ViewModel的WPF应用程序和一些原始导航逻辑.该应用程序包含两个视图(屏幕).一个屏幕包含一个按钮"前进",另一个屏幕包含一个"前进"按钮.通过按下其中一个按钮,可以调用委托命令,从而使shell视图模型切换活动屏幕.屏幕1切换到屏幕2,而屏幕2切换到屏幕1.

这种方法的问题在于它引入了竞争条件.当单击足够快时,有可能执行相应的操作(前进/后退)两次,从而导致应用程序失败.有趣的是,屏幕已经更改,但UI不会立即反映状态变化.到目前为止,我从来没有经历过这种差距,我做了这个实验只是为了证明单线程(调度)WPF应用程序是自动线程安全的.

有人对这种奇怪的行为有解释吗?WPF绑定机制是否太慢,以便可以再次按下按钮,直到UI更新自身以表示新的屏幕状态?

根据开发mvvm应用程序的建议,我不知道如何解决这个问题.没有办法同步代码,因为只有一个线程.我希望你能帮助我,因为现在依靠WPF数据绑定和模板系统我感到非常不安全.

包含项目文件的Zip存档

屏幕1切换到屏幕2,而屏幕2切换到屏幕1.通过足够快地点击可以引入竞争条件(即前进/后退两次).

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>
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

屏幕只包含一个委托命令来启动动作类:

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
Run Code Online (Sandbox Code Playgroud)

Fab*_*Fab 5

没有奇怪的竞争条件

我已经运行了你的代码.有一个主题.主要的一个.

One thread = no race condition.
Run Code Online (Sandbox Code Playgroud)

为什么要证明以下内容?

我做了这个实验只是为了证明单线程(调度)WPF应用程序是自动线程安全的.

这是一个防弹事实.一个线程=线程安全(只要你不共享数据流程,但它不再是线程安全).

绑定和方法不支持连续调用

实际上,你的方法GoBack和GoForward不支持连续调用.他们应该一个接一个地叫.

此处的线程安全并不意味着您的方法不能连续调用两次.如果进程中有任何任务队列,则可以调用两次方法.

您可能想要证明的可能是以下内容:

点击被捕获并在线处理,点击之间没有任何任务排队,引发了属性更改事件,调度程序调用,绑定/显示刷新.

这显然是错的!

当您调用Dispatcher.BeginInvoke或Invoke时,它在内部使用任务队列.而且没有什么能阻止你排队两次来自两次类似点击的相同任务.

坦率地说,我无法重现你的错误.我认为它是捕获点击的同一个线程,它将它发送到您的代码然后在屏幕上显示.但是,由于点击事件的任务,显示刷新位于同一队列中,理论上可以在屏幕更改之前将两次点击排队.但是:

  • 我无法快速点击以击败我的CPU.
  • 我认为不需要SpinWait.
  • 我的配置可能会遗漏一些东西.

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

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

你可以使用:

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

2. A bool表示当前状态(前进或后退).

3. enum在代码中更清晰.

一台状态机...... 不!我在开玩笑.

注意:为什么使用堆栈来推送和弹出屏幕(顺便说一句)?并且...以防万一,你添加另一个线程: 堆栈弹出/推送不是线程安全的. 而是使用ConcurrentStack<T>