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

时间:2018-08-20 04:23:03

标签: vb.net winforms datagridview datagridviewcombobox

DataGridViewComboBoxColumn控件可能很难使用。我已经花了两天时间来与该代码的各种排列作斗争,所以我决定投入工作并寻求帮助。

最新的怪异形式是ComboBox事件处理程序,该事件处理程序为单个用户操作触发的次数越来越多。奇怪的是,增长率是前一个计数的精确翻倍(即1, 2, 4, 8, 16, 32, 64等)

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

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

我已经决定使用DataGridView控件来提供此功能。我正在使用三列,就像这样:

enter image description here

请注意,已映射的 Target 列表项以几​​乎不可见的颜色显示,以防止用户尝试再次使用它们。 (当选择了已映射的 Target 映射到其他 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

当我从与以前不同的列表中选择一个已映射的 Target 时,重复计数会成倍增加(例如,从 SCC 中选择两次不会增加计数,但是选择从 SCC 开始,然后从 Scale 开始。)

为此,我尝试了很多很多可能的解决方案-太多了,在这里列出,其中大多数我不记得了-但没有一个成功。

对于每次选择更改,我该如何限制处理程序仅触发一次?


  

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 个答案:

答案 0 :(得分: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

它现在按预期工作。