为什么会重复调用此事件处理程序方法?

Why is this event handler method being repeatedly called?

DataGridViewComboBoxColumn 控件可能很难使用。两天来我一直在与此代码的各种排列作斗争,所以我决定认输并寻求一些帮助。

最新的怪异化身是 ComboBox 事件处理程序,它会为单个用户操作触发越来越多的次数。奇怪的是,增长率恰好是之前计数的两倍(即 1, 2, 4, 8, 16, 32, 64 等)

首先,我将解释我要完成的工作并澄清一些术语。

我有一个Dictionary(Of Integer, String)。在我的域规则中,我将其称为 Key 属性 Channel 及其 Value 属性 Label。我将每个 KeyValuePair 映射到名为 Target 的第三个 String 值。 Dictionary(Of Integer, String) 项目是固定的——它们作为用户的视觉辅助存在,因此他可以轻松地 select 来自 List(Of String)Target

我已经决定使用 DataGridView 控件来提供此功能。我正在使用三列,如下所示:

请注意,已映射的 Target 列表项以几​​乎不可见的颜色显示,以阻止用户再次尝试使用它们。 (这就是事件处理程序问题的来源——当一个已经映射的 Target 被 selected 映射到不同的 Label .)

我在下面包含了我的完整代码库,但为了快速浏览,这里是重复的事件处理程序:

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

当我 select 一个已经映射的 Target 来自与以前不同的列表时(例如 selecting 两次 [=54] =]SCC 不会增加计数,但是 selecting 来自 SCC 然后 Scale 会。)

我为此尝试了很多很多可能的解决方案——太多无法在此列出,其中大部分我都不记得了——但none都成功了。

我能做些什么来限制处理器在每次 select 离子变化时只触发一次?


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

已修复。

我正在为每个 AddHandler/RemoveHandler 调用实例化一个新的事件处理程序对象。

当我删除实例并改用简单表达式时,ComboBoxes 开始正常运行。

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

现在按预期工作。