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






4.93/5 (20投票s)
2006年4月7日
10分钟阅读

125471

1077
一个组件,允许 ImageLists 在多个窗体和控件之间全局继承和共享,并提供完整的设计时支持。
引言
如果您曾经想在多个窗体和控件上使用相同或两个 ImageList
组件,那么您会知道这只能在牺牲设计时支持的情况下实现。ImageList
组件被标记为 NotInheritable
(在 sealed
),所以您不能简单地继承一个 ImageList
组件并公开一个内部共享的 ImageList
,然后就可以开始使用了。本文讨论了一个新的组件,它允许您在窗体和控件上使用共享/全局 ImageList
,并提供完全支持可视化继承的设计时支持。
背景
如果您将一个包含图片的 ImageList
添加到一个基窗体,然后继承该基 Form
,您会发现基窗体中的所有图片都会被序列化到新的继承窗体中,所以每个窗体现在都有自己的独立 ImageList
,而不是使用基窗体中的相同图片。此外,如果您想将同一个 ImageList
与 UserControl
和 Form
共享,该怎么办?如果您需要使用该窗体的多个实例,那么每个窗体也将使用其自己的独立 ImageList
。所有这些都会占用大量的内存。此外,在维护方面,您需要单独更新每个 ImageList
。您还需要手动连接每个 ImageList
,并手动设置代码中的所有图片索引。解决这些问题的唯一方法是创建一个通过共享方法或属性公开 ImageList
s 的类。这允许您在任何地方使用相同的 ImageList
s,但代价是牺牲设计时支持。
解决方案
介绍 SharedImageLists 组件
SharedImageLists
组件允许您非常轻松地在多个窗体和控件上公开您的共享 ImageList
,并提供完整的运行时设计时支持。SharedImageLists
组件几乎所有的工作都在设计模式下处理 ImageList
s,如下面的类图所示。
SharedImageLists
组件只暴露两个方法,其中 NewImageList
方法对智能提示是隐藏的,因为它只需要在 InitailizeComponent
例程中调用。
在深入了解其工作原理之前,我将先解释如何使用 SharedImageLists
组件。使用该组件最快捷的方法是使用本文顶部下载文件中包含的模板项。只需将 Zip 文件复制到 Visual Studio 2005 Templates\ItemTemplates 文件夹(无需解压文件)。
现在,当您在解决方案资源管理器中右键单击并选择“添加新项”或“添加组件”时,只需从“我的模板”部分选择 SharedImageList
组件,如下图所示。
当 SharedImageLists
设计界面出现时,从工具箱中添加几个 ImageList
组件,添加一些图片,然后保存并编译。同时,请确保 ImageList
的修饰符属性设置为 Public
。
现在到了有趣的部分。当您将 SharedImageLists1
组件添加到 Form
或 UserControl
时,会自动弹出一个智能标记面板,如下图所示。
此智能标记面板包含一个下拉列表,其中列出了您在上面的 SharedImageLists
组件中添加的所有 ImageList
。从列表中选择您要使用的共享 ImageList
,然后单击其下方的“添加”链接。
此时,一个 ImageList
将被添加到组件托盘中。这是一个标准的 ImageList
组件,但它是由 SharedImageList
组件创建和控制的。所有共享的 ImageList
组件都有一个额外的图形手形图标,以象征它是共享的 ImageList
。
在属性浏览器窗口中,您还会注意到所有 ImageList
属性都已灰显。这是为了防止开发人员意外更改共享 ImageList
的属性,并防止代码生成器将此 ImageList
持久化到 Form
中。
您现在可以将此共享 ImageList
绑定到控件,就像使用任何其他 ImageList
控件一样。您也可以继承此 Form
,它将始终使用上面通过 SharedImageLists
设计器提供的相同的共享 ImageList
。
从下面的窗体可以看出,在运行时,所有控件都引用相同的 ImageList
组件,因为所有 ImageList
的句柄都相同。
那么它是如何工作的?
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
组件,并将该组件定位到提供的 IContainer
。ImageList
以及 sharedImageList
被添加到内部的 Dictionary
集合中并返回。SharedImageListDesigner
稍后在 ImageList
被定位时使用此 Dictionary
集合。
在设计模式下,我们不能简单地传递 sharedImageList
参数,因为在设计模式下,组件必须被定位,并且它们不能同时被定位在多个地方(即,同时在 UserControl
和 Form
上)。在运行时,SharedImageList
组件只是返回传递的 sharedImageList
参数。
SharedImageListsDesigner
首先,我将展示一旦 ImageList
被添加到上面 NewImageList
方法的 Dictionary
中会发生什么。SharedImageLists
组件有一个 Designer Attribute,所以每当组件在设计模式下创建时,也会创建一个 SharedImageListDesigener
类的实例。
SharedImageListsDesigner
在 OnComponentAdded
处理程序中捕获添加到容器中的共享 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
处理程序用于捕获何时添加了我们的共享 ImageList
s。从这里调用最关键的代码是在 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
中的大部分其他代码都用于确保我们的共享 ImageList
s 的属性在属性浏览器中是只读的,这样 ImageList
的 CodeDomSerializer
就不会序列化本地副本的 ImageList
。这是通过实现 ITypeDescriptorFilterService
接口并修改 ImageList
的属性属性来实现的,为它们添加 ReadOnlyAttribute
和/或 DesignerSerializationVisibilityAttribute.Hidden
属性。下面的代码是执行所需过滤的代码。
列表 4 - 过滤 ImageList
属性属性,使共享 ImageList
s 只读且从不本地序列化
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
。不幸的是,CodeDomSerializer
s 的工作方式不是这样的。在多次尝试创建上面的 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
时崩溃/消失了。
经过大量调查,原来如果将 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
来在 SharedImageListsDesigner
的 Initialize
重写中添加我的 IDesignerSerializationProvider
实现。问题是,到 SharedImageListsDesigner
加载时,已经太晚了,共享 ImageList
s 已经被默认的 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
所有代码所做的只是向 ImageList
的 TypeDescriptor
添加一个额外的属性,以便我的 CodeDomSerializer
能够先于默认的 ImageList CodeDomSerializer
被找到。完成所有这些后,组件就可以使用了,并且在我到目前为止的所有测试中都运行良好。我认为 AddAttributes
代码有点像一个 hack,所以如果有人能提供更好的方法来实现这一点,请告诉我。希望您觉得这个组件很有用。
历史
- 1.0 - 发布于 2005 年 3 月 1 日。