VB.NET. Попытка изменить общий метод Invoke на общий метод BeginInvoke с неожиданными проблемами.

VB.NET 2010,.NET 4

Здравствуйте,

Я использую довольно гладкий общий метод вызова для обновления пользовательского интерфейса из фоновых потоков. Я забыл, откуда я его скопировал (преобразовал его в VB.NET с С#), но вот он:

Public Sub InvokeControl(Of T As Control)(ByVal Control As t, ByVal Action As Action(Of t))
    If Control.InvokeRequired Then
        Try
            Control.Invoke(New Action(Of T, Action(Of T))(AddressOf InvokeControl), New Object() {Control, Action})
        Catch ex As Exception
        End Try
    Else
        Action(Control)
    End If
End Sub

Теперь я хочу изменить это, чтобы создать функцию, которая возвращает Nothing, если не требуется вызов (или было отправлено исключение) или IAsyncResult, возвращенный из BeginInvoke, если требуется invoke. Вот что у меня есть:

Public Function InvokeControl(Of T As Control)(ByVal Control As t, ByVal Action As Action(Of t)) As IAsyncResult
    If Control.InvokeRequired Then
        Try
            Return Control.BeginInvoke(New Action(Of T, Action(Of T))(AddressOf InvokeControl), New Object() {Control, Action})
        Catch ex As Exception
            Return Nothing
        End Try
    Else
        Action(Control)
        Return Nothing
    End If
End Function

Я хотел сделать это прежде всего, чтобы избежать блокировки. Проблема в том, что теперь я получаю ошибки при выполнении таких вызовов:

InvokeControl(SomeTextBox, Sub(x) x.Text = "Some text")

Это отлично работало с исходным методом Invoke (а не BeginInvoke). Теперь я получаю исключение "Объект ссылка не установлен на экземпляр объекта". Если я поставлю часы на SomeTextBox, он говорит

SomeTextBox {Text = (Text) threw an exception of type Microsoft.VisualStudio.Debugger.Runtime.CrossThreadMessagingException.}

Может быть, что такие вызовы InvokeControl происходят из события System.Timers.Timer Elapsed. Его интервал составляет 500 мс, что должно быть более чем достаточно для завершения обновления пользовательского интерфейса (если это имеет значение). Что происходит?

Заранее благодарим за помощь!

Изменить: Подробнее

Вот мой обработчик System.Timer.Timer Elapsed:

Private Sub MasterTimer_Elapsed(ByVal sender As Object, ByVal e As System.Timers.ElapsedEventArgs) Handles MasterTimer.Elapsed
    MasterTimer.Enabled = False
    If Not MasterTimer.Interval = My.Settings.TimingMasterTimerInterval Then
        MasterTimer.Interval = My.Settings.TimingMasterTimerInterval
        NewEventLogEntry("The master timer interval has been changed to " & MasterTimer.Interval.ToString & " milliseconds.")
    End If
    InvokeControl(TimerPictureBox, Sub(x) x.Toggle(True))
    ReadFromDevices()
    UpdateIndicators()
    'This block is not executing when the error is thrown
    If Mode > RunMode.NotRunning Then
        UpdateProcessTime()
        UpdateRemainingTime()
        UpdateStatusTime()
    End If
    'This block is not executing when the error is thrown
    If Mode = RunMode.Running Then
        CheckMillerCurrent()
        CheckTolerances()
    End If

    MasterTimer.Enabled = True
End Sub

Private Sub ReadFromDevices()
    For Each dev As Device In Devices
        Try
            If dev.GetType.Equals(GetType(Miller)) Then
                Dim devAsMiller As Miller = CType(dev, Miller)
                With devAsMiller
                    If .PowerOn.Enabled Then .PowerOn.Read()
                    If .CurrentRead.Enabled Then .CurrentRead.Read()
                    If .VoltageRead.Enabled Then .VoltageRead.Read()
                    If .Trigger.Enabled Then .Trigger.Read()
                    If .Shutter.Enabled Then .Shutter.Read()
                End With
            ElseIf dev.GetType.Equals(GetType(SubstrateBiasVoltage)) Then
                Dim devAsSubstrateBiasVoltage As SubstrateBiasVoltage = CType(dev, SubstrateBiasVoltage)
                With devAsSubstrateBiasVoltage
                    If .LambdaCurrentRead.Enabled Then .LambdaCurrentRead.Read()
                    If .LambdaVoltageRead.Enabled Then .LambdaVoltageRead.Read()
                    If .BiasResistor.Enabled Then .BiasResistor.Read()
                    If .Pinnacle.Enabled Then .Pinnacle.Read()
                End With
            Else
                If dev.Enabled Then dev.Read()
            End If
        Catch ex As Exception
            NewEventLogEntry("An error occurred while trying to read from a device.", ex, EventLogItem.Types.Warning)
        End Try
    Next
End Sub

Private Sub UpdateIndicators()
    Dim ObjLock As New Object
    SyncLock ObjLock
        With Devices
            InvokeControl(EmergencyStopPictureBox, Sub(x As DigitalPictureBox) x.Toggle(Mode > RunMode.NotRunning))

            InvokeControl(MillerCurrentIndicator, Sub(x) x.Text = .Miller1.CurrentRead.GetParsedValue.ToString)
            InvokeControl(MillerVoltageIndicator, Sub(x) x.Text = .Miller1.VoltageRead.GetParsedValue.ToString)
            With .SubstrateBiasVoltage
                InvokeControl(LambdaVoltageIndicator, Sub(x) x.Text = .LambdaVoltageRead.GetParsedValue.ToString)
                InvokeControl(LambdaCurrentIndicator, Sub(x) x.Text = .LambdaCurrentRead.GetParsedValue.ToString)
                InvokeControl(PinnacleVoltageIndicator, Sub(x) x.Text = .Pinnacle.GetParsedValue.ToString)
                InvokeControl(PinnacleCurrentIndicator, Sub(x) x.Text = .Pinnacle.ReadCurrent.ToString)
            End With
            InvokeControl(HeaterPowerIndicator, Sub(x) x.Text = .HeaterPower.GetParsedValue.ToString)
            InvokeControl(ConvectronIndicator, Sub(x) x.Text = .Convectron.GetParsedValue.ToString)
            If .Baratron.GetParsedValue > 200 Then
                InvokeControl(BaratronIndicator, Sub(x) x.Text = "OFF")
            Else
                InvokeControl(BaratronIndicator, Sub(x) x.Text = .Baratron.GetParsedValue.ToString)
            End If
            If .Ion.GetParsedValue > 0.01 Then
                InvokeControl(IonIndicator, Sub(x) x.Text = "OFF")
            Else
                InvokeControl(IonIndicator, Sub(x) x.Text = .Ion.GetParsedValue.ToString)
            End If
            InvokeControl(ArgonFlowRateIndicator, Sub(x) x.Text = .ArgonFlowRate.GetParsedValue.ToString)
            InvokeControl(NitrogenFlowRateIndicator, Sub(x) x.Text = .NitrogenFlowRate.GetParsedValue.ToString)
            InvokeControl(GateValvePositionIndicator, Sub(x) x.Text = .GateValvePosition.GetParsedValue.ToString)

            InvokeControl(RoughingPumpPowerOnIndicator, Sub(x As PowerButton) x.IsOn = .RoughingPumpPowerOn.Value = Power.On)

            ToggleImageList(.Miller1.CurrentRead.ImageList, .Miller1.CurrentRead.GetParsedValue > My.Settings.MinimumMillerCurrent)
            ToggleImageList(.Miller1.Trigger.ImageList, .Miller1.Trigger.GetParsedValue = Power.On)
            ToggleImageList(.HeaterPower.ImageList, .HeaterPower.Value > 0)
            With .SubstrateBiasVoltage
                ToggleImageList(.LambdaVoltageRead.ImageList, .LambdaVoltageRead.GetParsedValue > 0 And .BiasResistor.GetParsedValue = BiasResistor.Lambda)
                ToggleImageList(.Pinnacle.ImageList, .Pinnacle.GetParsedValue > 10 And .BiasResistor.GetParsedValue = BiasResistor.Pinnacle)
            End With
            ToggleImageList(.ArgonValveOpen.ImageList, .ArgonValveOpen.Value = Valve.Open)
            ToggleImageList(.NitrogenValveOpen.ImageList, .NitrogenValveOpen.Value = Valve.Open)
            ToggleImageList(.RoughingPumpValveOpen.ImageList, .RoughingPumpValveOpen.Value = Valve.Open)
            ToggleImageList(.SlowPumpDownValve.ImageList, .SlowPumpDownValve.Value = Valve.Open)
            ToggleImageList(.RotationPowerOn.ImageList, .RotationPowerOn.Value = Power.On)
            ToggleImageList(.WaterMonitor1.ImageList, .WaterMonitor1.Value = Power.On And .WaterMonitor2.Value = Power.On)
            ToggleImageList(.GateValvePosition.ImageList, .GateValvePosition.SetValue > 0)
        End With
    End SyncLock
End Sub

Private Sub ToggleImageList(ByRef ImageList As ImageList, ByVal IsOn As Boolean)
    For Each img As OnOffPictureBox In ImageList
        SafeInvokeControl(img, Sub(x As OnOffPictureBox) x.Toggle(IsOn))
    Next
End Sub

Надеюсь, что не TMI, но, надеюсь, это поможет определить, что происходит не так.

Кроме того, с часами в одном из текстовых полей и некоторых точек останова, я обнаружил, что ошибка как-то волшебно выбрасывается после ReadFromDevices, но до UpdateIndicators. Под этим я подразумеваю, что точка останова в самом конце ReadFromDevices показывает, что текстовые поля не выбрасывали ошибку, но точка останова в начале UpdateIndicators (до того, как были сделаны вызовы InvokeControl) показывает, что они имеют...

Ответы

Ответ 1

Трудно использовать отладку, чтобы поймать исключение, поскольку оно будет происходить на любом из нескольких вызовов PostMessage на насос сообщений пользовательского интерфейса (вызванный вызовами InvokeControl и BeginInvoke). Visual Studio будет нелегко нарушить исключение. Вероятно, поэтому кажется, что исключение "магически" бросается.

Ваша проблема заключается не в вашей реализации InvokeControl, а в методе UpdateIndicators. Это связано с использованием оператора With и асинхронных вызовов потоков пользовательского интерфейса, например:

With Devices
    ...
    InvokeControl(MillerCurrentIndicator, Sub(x) x.Text = .Miller1.CurrentRead.GetParsedValue.ToString)
    ... 
 End With

Поскольку код Sub(x) выполняется в потоке пользовательского интерфейса, отправляя сообщение в потоке пользовательского интерфейса, весьма вероятно, что код вызова текущего потока будет завершен до того, как поток пользовательского интерфейса будет выполнен.

Проблема заключается в базовой реализации инструкции Visual Basic With. По сути, компилятор создает анонимную локальную переменную для оператора With, который устанавливается в Nothing в инструкции End With.

В качестве примера, если у вас есть этот код:

Dim p As New Person
With p
    .Name = "James"
    .Age = 40
End With

Компилятор Visual Basic превращает это в:

Dim p As New Person
Dim VB$t_ref$L0 As Person = p
VB$t_ref$L0.Name = "James"
VB$t_ref$L0.Age = 40
VB$t_ref$L0 = Nothing

Итак, в вашем случае, когда выполняется код потока пользовательского интерфейса, эта анонимная локальная переменная теперь Nothing, и вы получаете исключение "Объект ссылка не установлена ​​в экземпляр объекта".

Ваш код по существу эквивалентен этому:

Dim VB$t_ref$L0 = Devices
Dim action = new Action(Sub(x) x.Text = VB$t_ref$L0.Miller1.CurrentRead.GetParsedValue.ToString);
VB$t_ref$L0 = Nothing
action(MillerCurrentIndicator);

К тому времени, когда действие вызывается, переменная VB$t_ref$L0 уже установлена ​​на Nothing и whammo!

Ответ заключается не в использовании оператора With. Они плохие.


Это должен быть ваш ответ, но в вашем коде есть и другие проблемы, хотя вы тоже должны смотреть.

В коде SyncLock используется переменная локального блокирования, которая по существу делает бесполезную блокировку. Поэтому не делайте этого:

Private Sub UpdateIndicators()
    Dim ObjLock As New Object
    SyncLock ObjLock
    With Devices
        ...
    End With
    End SyncLock
End Sub

Сделайте это вместо:

Private ObjLock As New Object
Private Sub UpdateIndicators()
    SyncLock ObjLock
    With Devices
        ...
    End With
    End SyncLock
End Sub

Все вызовы InvokeControl в методе UpdateIndicators затрудняют отладку вашего кода. Сокращение всех этих вызовов до одного звонка должно помочь вам безболезненно. Вместо этого попробуйте:

Private Sub UpdateIndicators()
    SyncLock ObjLock
        InvokeControl(Me, AddressOf UpdateIndicators)
    End SyncLock
End Sub

Private Sub UpdateIndicators(ByVal form As ControlInvokeForm)
    With Devices
        EmergencyStopPictureBox.Toggle(Mode > RunMode.NotRunning)
        MillerCurrentIndicator.Text = .Miller1.CurrentRead.GetParsedValue.ToString
        ...
        ToggleImageList(.GateValvePosition.ImageList, .GateValvePosition.SetValue > 0)
    End With
End Sub

Очевидно, вам нужно удалить код With Devices, чтобы они работали.

Существует ряд проблем со следующим типом кода:

If .Ion.GetParsedValue > 0.01 Then
    InvokeControl(IonIndicator, Sub(x) x.Text = "OFF")
Else
    InvokeControl(IonIndicator, Sub(x) x.Text = .Ion.GetParsedValue.ToString)
End If

Значение .Ion.GetParsedValue может быть изменено между оцениваемым условием и выполняемым оператором Else. Это усугубляется, потому что условие в операторе If оценивается в текущем потоке, но оператор Else выполняется в потоке пользовательского интерфейса, поэтому задержка может быть большой. Кроме того, если класс .Ion. не является потокобезопасным, вы подвергаете себя потенциальным ошибкам.

Сделайте это вместо:

Dim parsedIonValue = .Ion.GetParsedValue
If parsedIonValue > 0.01 Then
    InvokeControl(IonIndicator, Sub(x) x.Text = "OFF")
Else
    InvokeControl(IonIndicator, Sub(x) x.Text = parsedIonValue.ToString)
End If

(Это также избавляет вас от проблемы With.)

Используйте AutoReset = True на MasterTimer для автоматического вызова Enabled = False, когда событие срабатывает, чтобы избежать (удаленной) возможности условий гонки.

Ваш код также не кажется правильным в том, что вы используете With Devices в методе UpdateIndicators, но у вас есть цикл For Each в методе ReadFromDevices. Devices тогда выглядит как коллекция, но код в UpdateIndicators использует объект Devices, как если бы это был объект Device. И он вызывает .SubstrateBiasVoltage в объекте Devices. Поэтому я не уверен точно, что делает объект Devices.

В методе ToggleImageList, который вы передаете параметр ImageList, передается ByRef, но вы не меняете ссылку на ImageList. Лучше передать его в ByVal, чтобы избежать возможных ошибок.

Кроме того, вместо этого:

If dev.GetType.Equals(GetType(Miller)) Then
    Dim devAsMiller As Miller = CType(dev, Miller)
    With devAsMiller

Было бы проще сделать это:

Dim devAsMiller = TryCast(dev, Miller)
If devAsMiller IsNot Nothing Then
    With devAsMiller

Надеюсь, это не похоже, что я погружаю загрузку! Надеюсь, это поможет.

Ответ 2

Enigmativity ясно решила всю вашу проблему, но только для поддержки использования оператора With: (То есть я определенно не говорю, что это правильное решение для всей вашей программы, просто подчеркивая, как только вы знаете, что проблема решение проблемы With.)

Так же, как мы привыкли к переменным цикла for, объявление локальной переменной для ваших выражений . позволит избежать проблемы:

With Devices
    ...
    Dim miller1 = .Miller1
    InvokeControl(MillerCurrentIndicator, Sub(x) x.Text = miller1.CurrentRead.GetParsedValue.ToString)
    ... 
End With

Конечно, это часто бывает только то, что вы хотите, когда свойство возвращает "активный" объект, который будет обновляться, когда он вызывается целевым потоком. Если это не так, вам нужно отказаться от инструкции With.