Почему этот метод обработчика событий вызывается неоднократно?

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

Это работает как ожидалось сейчас.

Другие вопросы по тегам