65.9K
CodeProject 正在变化。 阅读更多。
Home

在多个窗体和控件之间共享和继承 ImageLists

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (20投票s)

2006年4月7日

10分钟阅读

viewsIcon

125471

downloadIcon

1077

一个组件,允许 ImageLists 在多个窗体和控件之间全局继承和共享,并提供完整的设计时支持。

Sample screenshot

引言

如果您曾经想在多个窗体和控件上使用相同或两个 ImageList 组件,那么您会知道这只能在牺牲设计时支持的情况下实现。ImageList 组件被标记为 NotInheritable (在 sealed),所以您不能简单地继承一个 ImageList 组件并公开一个内部共享的 ImageList,然后就可以开始使用了。本文讨论了一个新的组件,它允许您在窗体和控件上使用共享/全局 ImageList,并提供完全支持可视化继承的设计时支持。

背景

如果您将一个包含图片的 ImageList 添加到一个基窗体,然后继承该基 Form,您会发现基窗体中的所有图片都会被序列化到新的继承窗体中,所以每个窗体现在都有自己的独立 ImageList,而不是使用基窗体中的相同图片。此外,如果您想将同一个 ImageListUserControlForm 共享,该怎么办?如果您需要使用该窗体的多个实例,那么每个窗体也将使用其自己的独立 ImageList。所有这些都会占用大量的内存。此外,在维护方面,您需要单独更新每个 ImageList。您还需要手动连接每个 ImageList,并手动设置代码中的所有图片索引。解决这些问题的唯一方法是创建一个通过共享方法或属性公开 ImageLists 的类。这允许您在任何地方使用相同的 ImageLists,但代价是牺牲设计时支持。

解决方案

介绍 SharedImageLists 组件

SharedImageLists 组件允许您非常轻松地在多个窗体和控件上公开您的共享 ImageList,并提供完整的运行时设计时支持。SharedImageLists 组件几乎所有的工作都在设计模式下处理 ImageLists,如下面的类图所示。

Class Diagram

SharedImageLists 组件只暴露两个方法,其中 NewImageList 方法对智能提示是隐藏的,因为它只需要在 InitailizeComponent 例程中调用。

在深入了解其工作原理之前,我将先解释如何使用 SharedImageLists 组件。使用该组件最快捷的方法是使用本文顶部下载文件中包含的模板项。只需将 Zip 文件复制到 Visual Studio 2005 Templates\ItemTemplates 文件夹(无需解压文件)。

现在,当您在解决方案资源管理器中右键单击并选择“添加新项”或“添加组件”时,只需从“我的模板”部分选择 SharedImageList 组件,如下图所示。

Add New Item Template Dialog

SharedImageLists 设计界面出现时,从工具箱中添加几个 ImageList 组件,添加一些图片,然后保存并编译。同时,请确保 ImageList 的修饰符属性设置为 Public

SharedImageLists design surface

现在到了有趣的部分。当您将 SharedImageLists1 组件添加到 FormUserControl 时,会自动弹出一个智能标记面板,如下图所示。

Smart Tag Panel

此智能标记面板包含一个下拉列表,其中列出了您在上面的 SharedImageLists 组件中添加的所有 ImageList。从列表中选择您要使用的共享 ImageList,然后单击其下方的“添加”链接。

此时,一个 ImageList 将被添加到组件托盘中。这是一个标准的 ImageList 组件,但它是由 SharedImageList 组件创建和控制的。所有共享的 ImageList 组件都有一个额外的图形手形图标,以象征它是共享的 ImageList

在属性浏览器窗口中,您还会注意到所有 ImageList 属性都已灰显。这是为了防止开发人员意外更改共享 ImageList 的属性,并防止代码生成器将此 ImageList 持久化到 Form 中。

Shared ImageList added

您现在可以将此共享 ImageList 绑定到控件,就像使用任何其他 ImageList 控件一样。您也可以继承此 Form,它将始终使用上面通过 SharedImageLists 设计器提供的相同的共享 ImageList

从下面的窗体可以看出,在运行时,所有控件都引用相同的 ImageList 组件,因为所有 ImageList 的句柄都相同。

ImageList at runtime

那么它是如何工作的?

SharedImageLists 组件

在表面上,它确实非常简单。SharedImageLists 组件会在 InitializeComponent 例程中为通过智能标记面板添加的每个共享 ImageList 创建一行代码。

列表 1 - SharedImageLists 组件在 InitializeComponent 中生成的代码

Me.LargeImageList = Me.SharedImageLists11.NewImageList( _
  Me.components, CType(Me.SharedImageLists11.GetSharedImageLists, _
  WindowsApplication2.SharedImageLists1).LargeImageList)

要理解这里发生了什么,我们需要看看 NewImageList 方法的作用。

列表 2 - 显示上面的 NewImageList 方法的作用

Public Function NewImageList(ByVal component As IContainer, _
       ByVal sharedImageList As ImageList) As ImageList

    If SharedImageListCodeDomSerializer.InDesignerMode Then
      Dim imageList As New ImageList

      'Add our imageList to the ImageLists collection

      'These ImageLists are then managed 

      'by the SharedImageListDesigner.

      Me.ImageLists.Add(imageList, sharedImageList)

      If component IsNot Nothing _
      AndAlso component.Components IsNot Nothing Then
        component.Add(imageList)
      End If

      Return imageList
    Else
      'Just return a reference to our Shared ImageList

      Return sharedImageList
    End If
End Function

在设计模式(在 IDE 中运行)下,我们创建一个新的 ImageList 组件,并将该组件定位到提供的 IContainerImageList 以及 sharedImageList 被添加到内部的 Dictionary 集合中并返回。SharedImageListDesigner 稍后在 ImageList 被定位时使用此 Dictionary 集合。

在设计模式下,我们不能简单地传递 sharedImageList 参数,因为在设计模式下,组件必须被定位,并且它们不能同时被定位在多个地方(即,同时在 UserControlForm 上)。在运行时,SharedImageList 组件只是返回传递的 sharedImageList 参数。

SharedImageListsDesigner

首先,我将展示一旦 ImageList 被添加到上面 NewImageList 方法的 Dictionary 中会发生什么。SharedImageLists 组件有一个 Designer Attribute,所以每当组件在设计模式下创建时,也会创建一个 SharedImageListDesigener 类的实例。

SharedImageListsDesignerOnComponentAdded 处理程序中捕获添加到容器中的共享 ImageList

列表 2 - OnComponentAdded 处理程序是设计器在定位共享 ImageList 时捕获它的地方。

Private Sub OnComponentAdded(ByVal sender As Object, _
            ByVal e As ComponentEventArgs)
    If TypeOf e.Component Is ImageList _
    AndAlso Me.ImageLists.ContainsKey(CType(e.Component, ImageList)) Then
      Dim targetImageList As ImageList = CType(e.Component, ImageList)
      Dim sharedImageList As ImageList = Me.ImageLists(targetImageList)
      If sharedImageList IsNot Nothing Then
        Me.InitializeImageList(targetImageList, sharedImageList)
      End If
    End If
End Sub

使用 OnComponentAdded 处理程序的原因是,在 NewImageList 方法中无法初始化 ImageList,因为继承的组件在调用 NewImageList 方法时没有有效的站点。继承的组件稍后由 IDesignerHost 定位。所以为了同时支持继承和非继承的组件,OnComponentAdded 处理程序用于捕获何时添加了我们的共享 ImageLists。从这里调用最关键的代码是在 InitalizeImageList 中,它就是 CopyImageList 例程。

列表 3 - 在设计模式下模拟共享 ImageList 进行本地复制,是通过 CopyImageList 方法完成的

Public Shared Sub CopyImageList(ByVal target As ImageList, _
       ByVal source As ImageList)
    If Not source.HandleCreated Then
      target.ImageStream = Nothing
      target.ColorDepth = source.ColorDepth
      target.ImageSize = source.ImageSize
      target.TransparentColor = source.TransparentColor
      Return
    End If
    If source.ImageStream Is Nothing Then Return
    Dim bf As New Binary Formatter
    Dim stream As New IO.MemoryStream
    bf.Serialize(stream, source.ImageStream)
    stream.Position = 0
    Dim ils As ImageListStreamer = _
        CType(bf.Deserialize(stream), ImageListStreamer)
    stream.Dispose()
    target.ImageStream = Nothing
    target.ImageStream = ils
End Sub

由于一个组件只能定位在一个地方,所以在设计模式下,我们需要模拟 ImageList 与运行时看到的相同的共享 ImageList。只要我们在设计时向最终开发者展示与运行时相同的图片集,那么我们在设计器中使用单独的 ImageList 并没有关系。在运行时使用相同的 ImageList 才是重要的。所以在设计模式下,每个共享 ImageList 实际上是真实共享 ImageList 的一个单独副本。

CopyImageList 例程中,由于一个 bug(参见 KB 814349),我们不能简单地使用 target.ImageStream = target.ImageStream,所以我们必须通过对 ImageStream 进行序列化/反序列化来复制流。

SharedImageListDesigner 中的大部分其他代码都用于确保我们的共享 ImageLists 的属性在属性浏览器中是只读的,这样 ImageListCodeDomSerializer 就不会序列化本地副本的 ImageList。这是通过实现 ITypeDescriptorFilterService 接口并修改 ImageList 的属性属性来实现的,为它们添加 ReadOnlyAttribute 和/或 DesignerSerializationVisibilityAttribute.Hidden 属性。下面的代码是执行所需过滤的代码。

列表 4 - 过滤 ImageList 属性属性,使共享 ImageLists 只读且从不本地序列化

Private Function FilterProperties(ByVal component As _
        System.ComponentModel.IComponent, _
        ByVal properties As System.Collections.IDictionary) _
        As Boolean Implements _
        ITypeDescriptorFilterService.FilterProperties
    Dim cache As Boolean = True
    If (Not Me._imageListFilterService Is Nothing) Then
      cache = _
        Me._imageListFilterService.FilterProperties(component, _
        properties)
    End If
    If TypeOf component Is ImageList Then
      If Utility.IsSharedImageList(Me.SharedImageLists, _
                                   component) Then
        Dim propsCopy() As PropertyDescriptor = _
          Utility.ToArray(Of PropertyDescriptor)(properties.Values)

        Dim pd As PropertyDescriptor
        Dim keys() As String = _
            Utility.ToArray(Of String)(properties.Keys)
        For i As Int32 = 0 To keys.Length - 1
          pd = DirectCast(properties(keys(i)), PropertyDescriptor)
          Select Case keys(i)
            Case "Modifiers"
              properties(keys(i)) = _
                TypeDescriptor.CreateProperty(pd.ComponentType, pd, _
                New Attribute() {ReadOnlyAttribute.Yes})
            Case "Images"
              properties(keys(i)) = _
                TypeDescriptor.CreateProperty(pd.ComponentType, pd, _
                New Attribute() {BrowsableAttribute.No, _
                ReadOnlyAttribute.Yes})
            Case "ImageStream"
              properties(keys(i)) = _
                TypeDescriptor.CreateProperty(pd.ComponentType, pd, _
                New Attribute() _
                {DesignerSerializationVisibilityAttribute.Hidden, _
                ReadOnlyAttribute.Yes})
            Case Else
              If pd.IsBrowsable AndAlso Not pd.DesignTimeOnly Then
                properties(keys(i)) = _
                  TypeDescriptor.CreateProperty(pd.ComponentType, pd, _
                  New Attribute() _
                  {DesignerSerializationVisibilityAttribute.Hidden, _
                  ReadOnlyAttribute.Yes})
              End If
          End Select
        Next
      End If
      cache = False
    Else
      cache = True
    End If
    Return cache
  End Function

CodeDomSerializers

这个项目中最具挑战性的部分之一是将下面这一行简单的代码写入 InitailizeComponent 例程。

列表 5 - CodeDomSerializer 需要输出的 CodeDom 语句

Me.LargeImageList = Me.SharedImageLists11.NewImageList( _
  Me.components, CType(Me.SharedImageLists11.GetSharedImageLists, _
  WindowsApplication2.SharedImageLists1).LargeImageList)

这造成最大问题的原因是,理想情况下,我希望 SharedImageListsCodeDomSerializer 完成所有的序列化工作,这样我就不必去覆盖 ImageList 自己的 CodeDomSerializer。不幸的是,CodeDomSerializers 的工作方式不是这样的。在多次尝试创建上面的 ImageList 赋值,并使代码顺序正确,并且默认 CodeDomSerialiser 不会创建冗余代码之后,我别无选择,只能找到一种方法来覆盖默认的 ImageList CodeComSerializer

但是,如何覆盖现有组件的默认 CodeDomSerializer 呢?我找到了 IDesignerSerializationProvider 接口,它似乎可以完成这项工作,所以每当需要序列化 ImageList 组件时,我的 CodeDomSerializer 就会被调用。经过一些文件调整后,我得到了这段代码来替换默认的 ImageList CodeDomSerializer

列表 5 - NewImageList CodeDom 方法语句

Private Function SerializeImageList( _
    ByVal manager As IDesignerSerializationManager, _
    ByVal sharedImageLists As SharedImageLists, _
    ByVal targetImageList As ImageList, _
    ByVal sharedImageList As ImageList) As CodeExpression

    Dim designer As SharedImageListsDesigner = _
        Utility.GetDesigner(sharedImageLists)
    If designer Is Nothing Then Return Nothing

    Dim sharedImageListName As String = _
    designer.GetSharedImageListName(sharedImageList)
    If String.IsNullOrEmpty(sharedImageListName) Then
        Return Nothing

    'The statement we need to construct

    'Me.SmallImageList1 = _

    '  SharedImageLists1.NewImageList(components, _

    '  CType(SharedImageLists1.GetSharedImageLists, _

    '  MySharedImageLists).ImageList1)


    Dim containerExp As CodeExpression = _
        MyBase.GetExpression(manager, sharedImageLists.Container)
    Dim sharedImageListsExp As CodeExpression = _
        MyBase.GetExpression(manager, sharedImageLists)    
    Dim targetImageListExp As CodeExpression = _
        MyBase.GetExpression(manager, targetImageList)      
    Dim sharedImageListExp As _
        New CodePropertyReferenceExpression(Nothing, _
        sharedImageListName)  
          
    If sharedImageListsExp Is Nothing Then
      sharedImageListsExp = _
      MyBase.SerializeToExpression(manager, _
             sharedImageLists)
    End If
    
    If containerExp Is Nothing Then
      containerExp = _
      MyBase.SerializeToExpression(manager, _
             sharedImageLists.Container)
    End If
    Dim getSharedImageListsMthd As _
        New CodeMethodInvokeExpression(sharedImageListsExp, _
        "GetSharedImageLists")
    Dim castTo As New CodeCastExpression(sharedImageLists.GetType, _
        getSharedImageListsMthd)
    sharedImageListExp.TargetObject = castTo

    Return New _
        CodeMethodInvokeExpression(sharedImageListsExp, _
           "NewImageList", containerExp, sharedImageListExp)
  End Function

我发现的下一个问题是,窗体和控件开始出现新命名的 WSOD(白色屏幕大死亡)。更糟糕的是,IDE 在粘贴共享 ImageList 时崩溃/消失了。

White Screen of Darn error message

经过大量调查,原来如果将 LargeImageList 变量设置为公共属性,它就能很好地工作。看起来 SerializerManager 在创建传递给我们的 Deserialize 方法的 CodeDom 集合时存在一个 bug 或限制。这导致默认的 CodeDomSerializer 尝试在 SharedImageLists 实例上查找一个不存在的成员。

为了解决这个问题,我们还重写了 Deserialize 方法,添加了一些额外的代码来确定是否提供了正确的 CodeExpression;如果不是,我们就用正确的表达式替换它,也就是说,用 CodePropertyExpression 替换 CodeFieldExpression,反之亦然。

列表 6 - 反序列化共享 ImageList 的例程

Public Overrides Function Deserialize(ByVal manager As _
       System.ComponentModel.Design.Serialization.
                    IDesignerSerializationManager, _
       ByVal codeObject As Object) As Object
    Dim instance As Object = Nothing

    For Each cc As CodeObject In CType(codeObject, _
                   CodeStatementCollection)
      Dim codeAssign As CodeAssignStatement = _
                     TryCast(cc, CodeAssignStatement)
      If codeAssign Is Nothing Then Continue For

      Dim methodI As CodeMethodInvokeExpression = _
          TryCast(codeAssign.Right, CodeMethodInvokeExpression)
      If methodI Is Nothing OrElse _
                 methodI.Parameters.Count <> 2 Then
          Continue For

      Dim sharedImageLists As Object = _
          MyBase.DeserializeExpression(manager, _
          MyBase.GetTargetComponentName(Nothing, _
                 methodI.Method.TargetObject, Nothing), _
                 methodI.Method.TargetObject)
      If sharedImageLists Is Nothing Then Continue For

      If Not GetType(SharedImageLists).IsAssignableFrom(
                     sharedImageLists.GetType) Then
          Continue For

      ' If we are here we can be pretty sure

      ' we have the correct statement.

      Dim name As String = Nothing
      Dim propRef As CodePropertyReferenceExpression = _
          TryCast(methodI.Parameters(1), _
          CodePropertyReferenceExpression)
      Dim fieldRef As CodeFieldReferenceExpression = Nothing
      Dim propInfo As PropertyInfo
      Dim fieldInfo As FieldInfo

      If propRef Is Nothing Then
        fieldRef = TryCast(methodI.Parameters(1), _
                   CodeFieldReferenceExpression)
      End If

      If propRef IsNot Nothing Then
        name = propRef.PropertyName
        ' Check that we actualy do have a property and not a field

        propInfo = sharedImageLists.GetType.GetProperty(name, _
                   BindingFlags.Instance Or BindingFlags.Public)
        If propInfo Is Nothing Then
          fieldInfo = sharedImageLists.GetType.GetField(name, _
                      BindingFlags.Instance Or BindingFlags.Public)
          If fieldInfo IsNot Nothing Then
            ' Change the property expression to a field expression

            methodI.Parameters(1) = _
                New CodeFieldReferenceExpression(propRef.TargetObject, name)
          End If
        End If
      ElseIf fieldRef IsNot Nothing Then
        ' Check that we actualy do have a field and not a property

        name = fieldRef.FieldName
        fieldInfo = sharedImageLists.GetType.GetField(name, _
                    BindingFlags.Instance Or BindingFlags.Public)
        If fieldInfo Is Nothing Then
          propInfo = sharedImageLists.GetType.GetProperty(name, _
                     BindingFlags.Instance Or BindingFlags.Public)
          If propInfo IsNot Nothing Then
            'change the field expression to a field expression

            methodI.Parameters(1) = New _
               CodePropertyReferenceExpression(fieldRef.TargetObject, name)
          End If
        End If
      End If

      If propRef IsNot Nothing OrElse fieldRef IsNot Nothing Then
        instance = CType(MyBase.Deserialize(manager, _
                   codeObject), ImageList)

        ' After the instance has been created we

        ' now need to make sure it's named correctly.

        If TypeOf instance Is ImageList Then
          If Not CType(instance, ImageList).Site.Name = name Then
            Dim obj As Object = manager.GetInstance(name)
            If obj Is Nothing Then
              CType(instance, ImageList).Site.Name = name
              manager.SetName(instance, name)
            End If
          End If
        End If
      End If
    Next

    If instance Is Nothing Then
      Dim ilSerializer As New ImageListCodeDomSerializer
      instance = ilSerializer.Deserialize(manager, codeObject)
    End If
    Return instance
  End Function

大部分代码只是遍历传递的 CodeDom 语句,以确定传递的语句是用于正常 ImageList 创建,还是包含 NewImageList 方法赋值。一旦我们有了 NewimageList 语句,就可以简单地直接调用 NewImageList 方法并返回创建的 ImageList 实例。如果找不到 NewImageList 语句,那么默认的 ImageListCodeDomSerializer 将正常工作,所以我们不会破坏任何标准的 ImageList 代码的反序列化。

最后一块拼图

上面的代码是成功的,我现在有了可以序列化和反序列化共享 ImageList 的工作代码,并且剪切/粘贴/撤销/重做也都能正常工作。除了还有一个问题需要解决。反序列化代码在第一次加载 Form/UserControl 时不起作用。为什么?嗯,问题在于,我用来加载自定义 ImageList CodeDomSerializer 的代码加载得太晚了。我使用了 TypeDescriptor.AddProvider 来在 SharedImageListsDesignerInitialize 重写中添加我的 IDesignerSerializationProvider 实现。问题是,到 SharedImageListsDesigner 加载时,已经太晚了,共享 ImageLists 已经被默认的 ImageList CodeDomSerializer 反序列化了,WSOD 再次出现。总之,我找到的唯一能尽早加载自定义 ImageListCodeDomSerializer 的方法是为 SharedImageListComponent 创建另一个 CodeDomSerializer。这一次,序列化/反序列化方法不做任何事情,只是调用默认的 CodeDomSerialiser。关键代码是在类的构造函数中。

列表 7 - 用于替换 ImageList CodeDom 序列化器的 SharedImageLists 构造函数。

Public Sub New()
    TypeDescriptor.AddAttributes(GetType(ImageList), New _
        Serialization.DesignerSerializerAttribute(_
        GetType(SharedImageListCodeDomSerializer), _
        GetType(Serialization.CodeDomSerializer)))
End Sub

所有代码所做的只是向 ImageListTypeDescriptor 添加一个额外的属性,以便我的 CodeDomSerializer 能够先于默认的 ImageList CodeDomSerializer 被找到。完成所有这些后,组件就可以使用了,并且在我到目前为止的所有测试中都运行良好。我认为 AddAttributes 代码有点像一个 hack,所以如果有人能提供更好的方法来实现这一点,请告诉我。希望您觉得这个组件很有用。

历史

  • 1.0 - 发布于 2005 年 3 月 1 日。
© . All rights reserved.