Почему этот метод обработчика событий вызывается неоднократно?
DataGridViewComboBoxColumn
с управлением может быть сложно работать. Я боролся с различными вариантами кода в течение двух долгих дней, поэтому я решил добавить полотенце и обратиться за помощью.
Последнее воплощение странностей - это ComboBox
обработчик событий, который запускает все большее число раз для одного пользовательского действия. Как ни странно, скорость увеличения является точным удвоением числа предшествующих (т.е. 1, 2, 4, 8, 16, 32, 64
так далее.)
Для начала я объясню, чего я пытаюсь достичь, а также поясню некоторые термины.
у меня есть Dictionary(Of Integer, String)
, В моих правилах домена я звоню Key
Канал собственности и его Value
Метка собственности. Я картирую каждый KeyValuePair
на треть String
Значение называется Target. Dictionary(Of Integer, String)
элементы являются фиксированными - они существуют в качестве визуального пособия для пользователя, поэтому он может легко выбрать цель из List(Of String)
,
Я остановился на DataGridView
контроль, чтобы обеспечить эту функциональность. Я использую три столбца, вот так:
Обратите внимание, что уже сопоставленные элементы списка целей отображаются почти невидимым цветом, чтобы пользователь не пытался использовать их снова. (Именно здесь возникает проблема с обработчиком событий - когда уже назначенная цель выбрана для сопоставления с другой меткой.)
Я включил мою полную кодовую базу ниже, но для быстрого просмотра вот обработчик событий, который повторяется:
Private Sub ComboBox_SelectionChangeCommitted(Sender As ComboBox, e As EventArgs)
' '
' Look for other labels that have already been mapped to this target '
' '
If Me.OtherTargetCells.Any(Function(Cell) Cell.FormattedValue = Sender.Text) Then
If Me.IsInteractiveChange Then
MsgBox("Target [] is already mapped to Label []. If you want to map Target [] to Label [], you must first set Label [] to [Not mapped].", MsgBoxStyle.Exclamation, Me.DataGridView.FindForm.Text)
Me.IsInteractiveChange = False
Sender.SelectedIndex = 0
Me.IsInteractiveChange = True
End If
End If
End Sub
И вот как я все это провожу:
Public Sub New()
Task.Run(Sub()
Dim oHandler As DataGridViewEditingControlShowingEventHandler
While Me.DataGridView Is Nothing
End While
oHandler = New DataGridViewEditingControlShowingEventHandler(AddressOf DataGridView_EditingControlShowing)
RemoveHandler Me.DataGridView.EditingControlShowing, oHandler
AddHandler Me.DataGridView.EditingControlShowing, oHandler
End Sub)
End Sub
Private Sub DataGridView_EditingControlShowing(Sender As DataGridView, e As DataGridViewEditingControlShowingEventArgs)
Dim oComboBox As ComboBox
If TypeOf e.Control Is ComboBox Then
oComboBox = e.Control
oComboBox.DrawMode = DrawMode.OwnerDrawFixed
RemoveHandler oComboBox.DrawItem, New DrawItemEventHandler(AddressOf ComboBox_DrawItem)
AddHandler oComboBox.DrawItem, New DrawItemEventHandler(AddressOf ComboBox_DrawItem)
RemoveHandler oComboBox.SelectionChangeCommitted, New EventHandler(AddressOf ComboBox_SelectionChangeCommitted)
AddHandler oComboBox.SelectionChangeCommitted, New EventHandler(AddressOf ComboBox_SelectionChangeCommitted)
End If
End Sub
Счетчик повторений умножается, когда я выбираю уже сопоставленную цель из списка, отличного от предыдущего (например, выбор дважды из SCC не увеличивает счет, а выбор из SCC, а затем масштабирование).
Я перепробовал много, много, много возможных решений для этого - слишком много, чтобы перечислить здесь и большинство из которых я просто не помню - но ни одно из них не имело успеха.
Что я могу сделать, чтобы заставить обработчик запускаться только один раз для каждого изменения выбора?
Mapping.TargetsColumn.vb
Namespace Mapping
Public Class TargetsColumn
Inherits DataGridViewComboBoxColumn
Public Sub New()
Task.Run(Sub()
Dim oHandler As DataGridViewEditingControlShowingEventHandler
While Me.DataGridView Is Nothing
End While
oHandler = New DataGridViewEditingControlShowingEventHandler(AddressOf DataGridView_EditingControlShowing)
RemoveHandler Me.DataGridView.EditingControlShowing, oHandler
AddHandler Me.DataGridView.EditingControlShowing, oHandler
End Sub)
End Sub
Private Sub DataGridView_EditingControlShowing(Sender As DataGridView, e As DataGridViewEditingControlShowingEventArgs)
Dim oComboBox As ComboBox
If TypeOf e.Control Is ComboBox Then
oComboBox = e.Control
oComboBox.DrawMode = DrawMode.OwnerDrawFixed
RemoveHandler oComboBox.DrawItem, New DrawItemEventHandler(AddressOf ComboBox_DrawItem)
AddHandler oComboBox.DrawItem, New DrawItemEventHandler(AddressOf ComboBox_DrawItem)
RemoveHandler oComboBox.SelectionChangeCommitted, New EventHandler(AddressOf ComboBox_SelectionChangeCommitted)
AddHandler oComboBox.SelectionChangeCommitted, New EventHandler(AddressOf ComboBox_SelectionChangeCommitted)
End If
End Sub
Private Sub ComboBox_DrawItem(Sender As ComboBox, e As DrawItemEventArgs)
Dim sThisTarget As String
Dim oForeColor As Color
Dim _
iSeparatorBottom,
iSeparatorRight,
iSeparatorLeft As Integer
Dim _
oSeparatorStart,
oSeparatorStop As Point
sThisTarget = DirectCast(Me.Items(e.Index), Target).Value
iSeparatorBottom = e.Bounds.Bottom - 2
iSeparatorRight = e.Bounds.Right
iSeparatorLeft = e.Bounds.Left
e.DrawBackground()
If e.Index = 0 Then
oSeparatorStart = New Point(iSeparatorLeft, iSeparatorBottom)
oSeparatorStop = New Point(iSeparatorRight, iSeparatorBottom)
oForeColor = SystemColors.HotTrack
e.Graphics.FillRectangle(SystemBrushes.Control, e.Bounds)
e.Graphics.DrawLine(SystemPens.ControlDark, oSeparatorStart, oSeparatorStop)
Else
If Me.OtherTargets.Contains(sThisTarget) Then
oForeColor = SystemColors.ControlLight
Else
oForeColor = e.ForeColor
End If
End If
Using oBrush As New SolidBrush(oForeColor)
e.Graphics.DrawString(sThisTarget, e.Font, oBrush, e.Bounds)
End Using
If e.State.HasFlag(DrawItemState.Focus) Then e.DrawFocusRectangle()
Me.DataGridView.FindForm.Text = sThisTarget
End Sub
Private Sub ComboBox_SelectionChangeCommitted(Sender As ComboBox, e As EventArgs)
' '
' Look for other labels that have already been mapped to this target '
' '
If Me.OtherTargetCells.Any(Function(Cell) Cell.FormattedValue = Sender.Text) Then
If Me.IsInteractiveChange Then
MsgBox("Target [] is already mapped to Label []. If you want to map Target [] to Label [], you must first set Label [] to [Not mapped].", MsgBoxStyle.Exclamation, Me.DataGridView.FindForm.Text)
Me.IsInteractiveChange = False
Sender.SelectedIndex = 0
Me.IsInteractiveChange = True
End If
End If
End Sub
Private ReadOnly Property OtherTargets As List(Of String)
Get
Return Me.OtherTargetCells.Select(Function(Cell) DirectCast(Cell.FormattedValue, String)).ToList
End Get
End Property
Private ReadOnly Property CurrentTargetCell As DataGridViewCell
Get
Return Me.AllTargetCells(Me.DataGridView.CurrentRow.Index)
End Get
End Property
Private ReadOnly Property AllTargetCells As List(Of DataGridViewCell)
Get
Dim oAllCells As IEnumerable(Of DataGridViewCell)
Dim oRows As IEnumerable(Of DataGridViewRow)
oRows = Me.DataGridView.Rows.Cast(Of DataGridViewRow)
oAllCells = oRows.SelectMany(Function(Row) Row.Cells.Cast(Of DataGridViewCell))
Return oAllCells.Where(Function(Cell) TypeOf Cell Is DataGridViewComboBoxCell).ToList
End Get
End Property
Private ReadOnly Property OtherTargetCells As List(Of DataGridViewCell)
Get
Return Me.AllTargetCells.Where(Function(Cell) Cell.RowIndex <> Me.RowIndex).ToList
End Get
End Property
Private ReadOnly Property RowIndex As Integer
Get
Return Me.DataGridView.CurrentRow.Index
End Get
End Property
Private IsInteractiveChange As Boolean = True
Private ReadOnly ComboBoxes As New Dictionary(Of Integer, ComboBox)
End Class
End Namespace
Form1.vb
Public Class Form1
Inherits Form
Public Sub New()
Dim oColTargets As Mapping.TargetsColumn
Dim oTargets As IEnumerable(Of String)
Dim oQuery As Func(Of Target, Boolean)
Dim sChannel As String
Dim oTarget As Target
Dim oMaps As Dictionary(Of Integer, String)
Dim oMap As Map
Dim _
oColChannels,
oColLabels As DataGridViewTextBoxColumn
Me.InitializeComponent()
Me.Targets.Add(New Target("Not mapped"))
sChannel = String.Empty
oQuery = Function(Target) Target.Value = sChannel
'oTargets = Reader.Client.Create.Call(Function(Service As Reader.IService) Service.GetChannelTargets)'
oTargets = New List(Of String) From {"Scale", "SCC", "CO", "O2"}
oTargets.ToList.ForEach(Sub(Target)
Me.Targets.Add(New Target(Target))
End Sub)
'oMaps = Reader.Client.Create.Call(Function(Service As Reader.IService) Service.GetChannelMaps)'
oMaps = New Dictionary(Of Integer, String) From {{3, "Test"}, {7, "SCC"}, {8, "Scale"}, {9, "CO"}, {10, "O2"}}
oMaps.ToList.ForEach(Sub(Map)
sChannel = Map.Value
If Me.Targets.Any(oQuery) Then
oTarget = Me.Targets.Single(oQuery)
Else
oTarget = Me.Targets.First
End If
oMap = New Map With {
.Channel = Map.Key,
.Label = Map.Value,
.Target = oTarget
}
Me.Maps.Add(oMap)
End Sub)
oColChannels = New DataGridViewTextBoxColumn With {
.DataPropertyName = NameOf(Map.Channel),
.AutoSizeMode = DataGridViewAutoSizeColumnMode.ColumnHeader,
.HeaderText = NameOf(Map.Channel),
.ReadOnly = True,
.Name = NameOf(oColChannels)
}
oColLabels = New DataGridViewTextBoxColumn With {
.DataPropertyName = NameOf(Map.Label),
.AutoSizeMode = DataGridViewAutoSizeColumnMode.ColumnHeader,
.HeaderText = NameOf(Map.Label),
.ReadOnly = True,
.Name = NameOf(oColLabels)
}
oColTargets = New Mapping.TargetsColumn With {
.DataPropertyName = NameOf(Map.Target),
.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill,
.DisplayMember = NameOf(Target.Value),
.ValueMember = NameOf(Target.Self),
.HeaderText = NameOf(Map.Target),
.DataSource = Me.Targets,
.Name = NameOf(oColTargets)
}
dgvMapping.AutoGenerateColumns = False
dgvMapping.Columns.AddRange({oColChannels, oColLabels, oColTargets})
For Each oColumn As DataGridViewColumn In dgvMapping.Columns
oColumn.HeaderCell.Style.Alignment = DataGridViewContentAlignment.MiddleCenter
If oColumn.Index = 0 Then
oColumn.DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleCenter
End If
Next
dgvMapping.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize
dgvMapping.DataSource = New BindingList(Of Map)(Me.Maps)
If dgvMapping.RowCount = 0 Then
dgvMapping.Height = 150
Else
dgvMapping.Height = ((dgvMapping.RowCount + 0) * dgvMapping.Rows(0).Height) + dgvMapping.ColumnHeadersHeight
End If
End Sub
Private Sub Form1_FormClosing(Sender As Form1, e As FormClosingEventArgs) Handles Me.FormClosing
Dim oPolicy As Target = Me.Maps.First.Target
Dim sName As String = Me.Maps.First.Channel
End Sub
Private Sub _dgvMapping_DataError(Sender As DataGridView, e As DataGridViewDataErrorEventArgs) Handles dgvMapping.DataError
MsgBox(e.Exception.Message, MsgBoxStyle.Critical, Me.Text)
End Sub
Private Targets As New BindingList(Of Target)
Private Maps As New List(Of Map)
End Class
Public Class Map
Public Property Channel As Integer
Public Property Label As String
Public Property Target As Target
End Class
Public Class Target
Public Sub New(Target As String)
Me.Value = Target
End Sub
Public ReadOnly Property Self As Target
Get
Return Me
End Get
End Property
Public ReadOnly Property Value As String
End Class
Form1.Designer.vb
<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()>
Partial Class Form1
Inherits System.Windows.Forms.Form
'Form overrides dispose to clean up the component list.'
<System.Diagnostics.DebuggerNonUserCode()>
Protected Overrides Sub Dispose(ByVal disposing As Boolean)
Try
If disposing AndAlso components IsNot Nothing Then
components.Dispose()
End If
Finally
MyBase.Dispose(disposing)
End Try
End Sub
'Required by the Windows Form Designer'
Private components As System.ComponentModel.IContainer
'NOTE: The following procedure is required by the Windows Form Designer'
'It can be modified using the Windows Form Designer.'
'Do not modify it using the code editor.'
<System.Diagnostics.DebuggerStepThrough()>
Private Sub InitializeComponent()
Me.dgvMapping = New System.Windows.Forms.DataGridView()
CType(Me.dgvMapping, System.ComponentModel.ISupportInitialize).BeginInit()
Me.SuspendLayout()
' '
'dgvMapping'
' '
Me.dgvMapping.AllowUserToAddRows = False
Me.dgvMapping.AllowUserToDeleteRows = False
Me.dgvMapping.AllowUserToOrderColumns = True
Me.dgvMapping.AllowUserToResizeColumns = False
Me.dgvMapping.AllowUserToResizeRows = False
Me.dgvMapping.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize
Me.dgvMapping.EditMode = System.Windows.Forms.DataGridViewEditMode.EditOnEnter
Me.dgvMapping.Location = New System.Drawing.Point(12, 12)
Me.dgvMapping.Name = "dgvMapping"
Me.dgvMapping.RowHeadersVisible = False
Me.dgvMapping.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.FullRowSelect
Me.dgvMapping.Size = New System.Drawing.Size(250, 150)
Me.dgvMapping.TabIndex = 0
' '
'Form1'
' '
Me.AutoScaleDimensions = New System.Drawing.SizeF(6.0!, 13.0!)
Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font
Me.ClientSize = New System.Drawing.Size(800, 450)
Me.Controls.Add(Me.dgvMapping)
Me.Font = New System.Drawing.Font("Segoe UI", 8.0!, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, CType(0, Byte))
Me.Name = "Form1"
Me.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen
Me.Text = "Form1"
CType(Me.dgvMapping, System.ComponentModel.ISupportInitialize).EndInit()
Me.ResumeLayout(False)
End Sub
Friend WithEvents dgvMapping As DataGridView
End Class
1 ответ
Исправлена.
Я создавал новый объект-обработчик событий для каждого AddHandler
/RemoveHandler
вызов.
Когда я удалил экземпляры и вместо них использовал простые выражения, ComboBox
Я начал вести себя правильно.
Public Sub New()
Task.Run(Sub()
While Me.DataGridView Is Nothing
End While
RemoveHandler Me.DataGridView.EditingControlShowing, AddressOf DataGridView_EditingControlShowing
AddHandler Me.DataGridView.EditingControlShowing, AddressOf DataGridView_EditingControlShowing
End Sub)
End Sub
Private Sub DataGridView_EditingControlShowing(Sender As Object, e As DataGridViewEditingControlShowingEventArgs)
Dim oComboBox As ComboBox
If TypeOf e.Control Is ComboBox Then
oComboBox = e.Control
oComboBox.DrawMode = DrawMode.OwnerDrawFixed
RemoveHandler oComboBox.DrawItem, AddressOf ComboBox_DrawItem
AddHandler oComboBox.DrawItem, AddressOf ComboBox_DrawItem
RemoveHandler oComboBox.SelectionChangeCommitted, AddressOf ComboBox_SelectionChangeCommitted
AddHandler oComboBox.SelectionChangeCommitted, AddressOf ComboBox_SelectionChangeCommitted
End If
End Sub
Я должен был расслабиться Sender
типы параметров для Object
в методах обработчика событий, но это не имело серьезных последствий.
Private Sub DataGridView_EditingControlShowing(Sender As Object, e As DataGridViewEditingControlShowingEventArgs)
End Sub
Private Sub ComboBox_DrawItem(Sender As Object, e As DrawItemEventArgs)
End Sub
Private Sub ComboBox_SelectionChangeCommitted(Sender As Object, e As EventArgs)
End Sub
Для чего это стоит: я вообще предпочитаю ограничивать Sender
параметр для вызывающего типа, для более эффективного кодирования, но это не было возможно в этом случае. Тем не менее, единственным воздействием была необходимость бросить Sender
в одном месте в одном теле метода:
Dim oQuery = Function(Cell) Cell.FormattedValue = DirectCast(Sender, ComboBox).Text
Это работает как ожидалось сейчас.