VB.NET Попытка изменить универсальный метод Invoke на универсальный метод BeginInvoke, возникли непредвиденные проблемы - PullRequest
3 голосов
/ 08 октября 2010

VB.NET 2010, .NET 4

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

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

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

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

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 поступают из события Elapsed System.Timers.Timer's. Его интервал составляет 500 мс, что должно быть более чем достаточно для завершения обновления пользовательского интерфейса (если это имеет значение). Что происходит?

Заранее спасибо за помощь!

Редактировать: Подробнее

Вот мой обработчик прошедшего времени System.Timer.Timer's:

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's 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) показывает, что они имеют ...

Ответы [ 2 ]

4 голосов
/ 08 октября 2010

Сложно использовать отладку, чтобы перехватить исключение, так как оно будет происходить при любом из нескольких 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

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

0 голосов
/ 12 ноября 2016

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

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

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

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

...