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
.