【问题标题】:How to properly implement/inherit a ComboBox.ObjectCollection class for design-time usage?如何正确实现/继承 ComboBox.ObjectCollection 类以供设计时使用?
【发布时间】:2017-03-28 11:46:36
【问题描述】:

场景


我继承了ComboBox 类来设计一种 ComboBox 控件,我可以在该控件上为其 ComboBox.Items 集合提供自定义类型 (MyItem)...

这是 ComboBox 的子类:

<DesignerCategory("UserControl")>
Public Class MyComboBox : Inherits ComboBox

    <Editor(GetType(CollectionEditor), GetType(UITypeEditor))>
    <DesignerSerializationVisibility(DesignerSerializationVisibility.Content)>
    Public Shadows ReadOnly Property Items As MyItemCollection

    <DebuggerStepThrough>
    Public Sub New()
        MyBase.DrawMode = DrawMode.OwnerDrawFixed
        Me.Items = New MyItemCollection(owner:=Me)
    End Sub

    Protected Overrides Sub OnDrawItem(ByVal e As DrawItemEventArgs)

        e.DrawBackground()
        e.DrawFocusRectangle()

        ' Check if it is an item from the Items collection.
        If (e.Index < 0) Then
            ' not an item, draw the text.
            Using brush As New SolidBrush(e.ForeColor)
                e.Graphics.DrawString(Me.Text, e.Font, brush, e.Bounds.Left, e.Bounds.Top)
            End Using

        Else
            ' Get the item to draw.
            Dim item As MyItem = Me.Items(e.Index)
            Using brush As New SolidBrush(e.ForeColor)
                e.Graphics.DrawString(item.Text, e.Font, brush, e.Bounds.Left, e.Bounds.Top)
            End Using

        End If

        MyBase.OnDrawItem(e)

    End Sub

End Class

这是ComboBox.ObjectCollection 的子类:

<ListBindable(False)>
<DefaultMember("Item")>
Public Class MyItemCollection : Inherits ObjectCollection

    Sub New(ByVal owner As MyComboBox)
        MyBase.New(owner)
    End Sub

    <Browsable(False)>
    <DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)>
    Default Public Overloads Property Item(ByVal index As Integer) As MyItem
        Get
            Return DirectCast(MyBase.Item(index), MyItem)
        End Get
        Set(ByVal value As MyItem)
            MyBase.Item(index) = value
        End Set
    End Property

    ' Original property:
    '
    '<Browsable(False)> 
    '<DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)>
    'Default Public Overrides Property Item(ByVal index As Integer) As Object
    '    Get
    '        Return MyBase.Item(index)
    '    End Get
    '    Set(value As Object)
    '        MyBase.Item(index) = value
    '    End Set
    'End Property

End Class

最后,这是我将用于我的控件的 Items 集合的自定义类型 MyItem

Public NotInheritable Class MyItem

    Public Property Text As String

    Public Sub New()
        Me.New(String.Empty)
    End Sub

    Public Sub New(ByVal text As String)
        Me.Text = text
    End Sub

End Class

所有这些在执行时都可以正常工作,我可以添加项目并且按预期工作,问题是它在设计时无法正常工作......

问题


当我尝试像这样在设计时添加项目时:

...乍一看似乎可行,因为我可以添加项目。

并且该记录似乎由 Visual Studio 在 Form 的 dsigner 类 (Form1.Designer.vb) 中正确自动生成:

Private Sub InitializeComponent()
    Me.MyComboBox1 = New MyComboBox()
    Dim MyItem1 As MyItem = New MyItem()
    MyItem1.Text = "Test"
    Me.MyComboBox1.Items.AddRange(New Object() {MyItem1})

    ' ...
End Sub

更重要的是,如果我尝试在执行时通过调用 Items.Count 属性来确定集合中当前项目的数量,它会给我预期值:

Public Class Form1 : Inherits Form

    Sub Form1_Shown(sender As Object, e As EventArgs) Handles MyBase.Shown

        MessageBox.Show(String.Format("There are '{0}' items in the collection.", 
                                      MyComboBox1.Items.Count))

    End Sub

End Class

...但是,这些都很重要,因为当我启动应用程序时,我没有看到在设计时添加的项目,我的意思是在设计时添加的项目在执行时没有绘制在控件中时间。

问题


我想做什么?我该如何解决这个问题?。

请注意,这可能不是绘图问题,因为我在执行时通过Items.Add()Items.AddRange() 方法添加的项目已正确添加和绘制。

我认为这可能是关于内容同步的某种问题......

我真的不知道发生了什么,因为通过 Visual Studio 的对象检查器,我确保为我的类、方法和属性指定所有属性类,与默认情况下为 ComboBox.Items 属性分配的相同还有ComboBox.ObjectCollection 类和ComboBox.ObjectCollection.Item 成员。

【问题讨论】:

  • 您知道您可以(几乎)将任何您想要的类型添加到标准 ListControls(CBO、ListBox)中,对吧?如果您在 MyItem 上覆盖 ToString(),则文本将显示在集合编辑器中。这些项目的问题是(可能)基础ObjectCollection 类不是强类型的。集合编辑器不会知道它确实需要创建一个 MyItem 类型而不是 Object
  • @Plutonix 感谢您的评论。是的,我知道您可以将任何类型添加到默认对象集合中,但是这样做,VS 设计器将向您显示对象集合 (&lt;Editor(GetType(ObjectSelectorEditor), GetType(UITypeEditor))&gt;) 的默认编辑器,您只能在该编辑器上添加“原始”对象,没有属性可以设置对象,而不是像我正在使用的特定编辑器 (&lt;Editor(GetType(CollectionEditor), GetType(UITypeEditor))&gt;),您可以在设计时设置对象的属性。
  • 集合编辑器已经创建了一个MyItem 对象,上面我展示了在通过集合编辑器添加项目后添加 VS 的自动生成代码。我不明白你的意思是什么区别应该覆盖 ToString 函数以将 MyItem 对象表示为文本,无论如何我做到了,但它并没有像预期的那样解决问题。
  • 遮蔽Items 属性不等同于覆盖虚拟属性。所有使用Items 属性的基本ComboBox 方法都将使用ComboBox 类中定义的方法。您可以使用反射设置基类支持字段,但您还需要考虑任何使支持字段为空的方法,因为Items Property 将重新创建支持字段,如果它是 Nothing。 RefreshItems 方法使支持字段为空,
  • 很抱歉没有尽快回复您。你误解了我的提议。您可以隐藏Items 属性,但您需要重新实现RefreshItems 方法的功能;它是可覆盖的。您将使用反射将 Combox 的 itemsCollection 字段设置为 MyItemCollection 的一个实例。您可以在构造函数和重新实现的 RefreshItems 方法中执行此操作。如果您需要这方面的帮助,我可以发布一份我认为应该可行的 hack 工作。

标签: .net vb.net winforms visual-studio windows-forms-designer


【解决方案1】:

这是我在 cmets 中提到的技术的一个示例。它高度依赖于使用反射来设置 ComboBox 类中的字段以及执行​​该类中的某些方法。因此,如果 MS 修改了这些项目,它可能会被破坏。

编辑 2:更正的代码

Imports System.ComponentModel
Imports System.ComponentModel.Design
Imports System.Drawing.Design
Imports System.Reflection

<DesignerCategory("UserControl")>
Public Class MyComboBox : Inherits ComboBox
    Private fiItemsCollection As FieldInfo
    Private piCMItem As PropertyInfo
    Private miNativeClear As MethodInfo
    Private miObjectCollectionAddRangeInternal As MethodInfo

    Private _Items As MyItemCollection

    <Editor(GetType(CollectionEditor), GetType(UITypeEditor))>
    <DesignerSerializationVisibility(DesignerSerializationVisibility.Content)>
    Public Shadows ReadOnly Property Items As MyItemCollection
        Get
        Return Me._Items
        End Get
    End Property

    <DebuggerStepThrough>
    Public Sub New()
        MyBase.New()
        MyBase.DrawMode = DrawMode.OwnerDrawFixed
        Me.fiItemsCollection = GetType(ComboBox).GetField("itemsCollection", BindingFlags.Instance Or BindingFlags.NonPublic)
        Me._Items = New MyItemCollection(Me)
        fiItemsCollection.SetValue(Me, Me._Items)
        Me.piCMItem = GetType(CurrencyManager).GetProperty("Item", BindingFlags.Instance Or BindingFlags.NonPublic)
        Me.miNativeClear = GetType(ComboBox).GetMethod("NativeClear", BindingFlags.Instance Or BindingFlags.NonPublic)
        Me.miObjectCollectionAddRangeInternal = GetType(ObjectCollection).GetMethod("AddRangeInternal", BindingFlags.Instance Or BindingFlags.NonPublic)
    End Sub

    Protected Overrides Sub RefreshItems()
        Dim selectedIndex As Integer = Me.SelectedIndex
        Dim itemsCollection As MyItemCollection = Me._Items
        Me._Items = Nothing

        Dim destination As Object() = Nothing
        If ((Not MyBase.DataManager Is Nothing) AndAlso (MyBase.DataManager.Count <> -1)) Then
            destination = New Object(MyBase.DataManager.Count - 1) {}
            Dim args(0 To 0) As Object
            For i As Int32 = 0 To destination.Length - 1
                args(0) = i
                destination(i) = piCMItem.GetValue(MyBase.DataManager, args)
            Next i
        ElseIf (Not itemsCollection Is Nothing) Then
            destination = New Object(itemsCollection.Count - 1) {}
            itemsCollection.CopyTo(destination, 0)
        End If

        Me.BeginUpdate()

        Try
            If MyBase.IsHandleCreated Then
                Me.miNativeClear.Invoke(Me, Nothing)
            End If
            If (Not destination Is Nothing) Then
                Me._Items = New MyItemCollection(Me)
                Me.miObjectCollectionAddRangeInternal.Invoke(Me._Items, New Object() {destination})
                Me.fiItemsCollection.SetValue(Me, Me._Items)
            End If
            If (Not MyBase.DataManager Is Nothing) Then
                Me.SelectedIndex = MyBase.DataManager.Position
            Else
                Me.SelectedIndex = selectedIndex
            End If
        Finally
            Me.EndUpdate()
        End Try
    End Sub

    Protected Overrides Sub OnDrawItem(ByVal e As DrawItemEventArgs)
        e.DrawBackground()
        e.DrawFocusRectangle()
        ' Check if it is an item from the Items collection.
        If (e.Index < 0) Then
            ' not an item, draw the text.
            Using brush As New SolidBrush(e.ForeColor)
                    e.Graphics.DrawString(Me.Text, e.Font, brush, e.Bounds.Left, e.Bounds.Top)
            End Using
        Else
            ' Get the item to draw.
            Dim item As MyItem = Me.Items(e.Index)
            Using brush As New SolidBrush(e.ForeColor)
                    e.Graphics.DrawString(item.Text, e.Font, brush, e.Bounds.Left, e.Bounds.Top)
            End Using
        End If
        MyBase.OnDrawItem(e)
    End Sub
End Class

【讨论】:

  • 谢谢!一个简单的问题:我在使用 DataSource 时会遇到问题吗? (因为调用 RefreshItems 的内部 OnDataSourceChanged 事件调用器)还是我不担心?
  • @ElektroStudios,我忘记使用 DataSource 进行测试,发现一个错误,我试图避免反射调用并使用公共方法 --> 错误。该错误已得到纠正,现在可以绑定到 List(Of MyItem) 了。如果项目不是 MyItem 类型,您将收到错误消息。此控件不应用作 ComboBox 的通用替代品。
  • 谢谢大家。我知道您在评论末尾所说的话,此控件的目的是提供一个带有图像列表的组合框,以便该控件只应接受特定的自定义类型的项目来显示图像,它是一个图像-ComboBox 的东西。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-07-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多