Introduction
If you have ever wanted to use the same or two ImageList
components on multiple forms and controls, then you'll will know it can only be achieved with a loss of design time support. The ImageList
component is marked as NotInheritable
(sealed
), so you cannot simply inherit an ImageList
component and expose an internal shared ImageList
, and away you go. This article discusses a new component that allows you to use shared/global ImageList
s on forms and controls, with full design time support that supports visual inheritance.
Background
If you add an ImageList
filled with images to a base form, and then inherited that base Form
, you will notice that all the images from the base form end up being serialized into the new inherited Form
, so every form now has its own separate ImageList
rather than using the same images from the base form. There's also the problem of what if you want to share the same ImageList
with both UserControl
s and Form
s? If you need to use several instances of the same form, then each form will also be using its own separate ImageList
. All of this is a bit of a memory hog. Plus, when it comes to maintenance, you will need to update each ImageList
separately. You also have to hook up each ImageList
manually, and also set all the image indexes manually in code. The only way around these problems is to create a class that exposes the ImageList
s through shared methods or properties. This allows you to use the same ImageList
s anywhere, but at the cost of design time support.
The Solution
Introducing the SharedImageLists Component
The SharedImageLists
component allows you to very easily expose your shared ImageList
s on multiple forms and controls with the added bonus of full design-time support. Almost all of the work of the SharedImageLists
component deals with working with the ImageList
s in design mode, as can be seen from the class diagram below.
The SharedImageLists
component exposes just two methods, and one of those, the NewImageList
method, is hidden from intellisence since it only needs to be called in the InitailizeComponent
routines.
I'll start by explaining how to use the SharedImageLists
component before we get into the nitty gritty of how it all works. The quickest way to use the component is to use the provided template item that is included in the downloadable file at the top of this article. Simply copy the Zip file to the Visual Studio 2005 Templates\ItemTemplates folder (no need to unzip the files).
Now when you click the Add NewItem or Add Component from the context menu in the Solution Explorer, simply select the SharedImageList
component from the My Templates section as below:
When the SharedImageLists
design surface presents itself, add a couple of ImageList
components from the toolbox, add some images, then save and compile. Also make sure that the ImageList
modifier properties are set to Public
.
Now comes the fun part. When you add the SharedImageLists1
component to a Form
or UserControl
, a SmartTag panel will automatically pop up as shown below:
This SmartTag panel contains a dropdown list with all the ImageList
s you added to the SharedImageLists
component above. Select from this list the shared ImageList
you want to use, and click on the Add link below it.
An ImageList
will then be added to the component tray. This ImageList
is a standard ImageList
component, except that it's created and controlled by the SharedImageList
component. All shared ImageList
components have an extra graphic hand icon to symbolize that it's a shared ImageList
.
In the property browser window, you will also notice that all the ImageList
properties are grayed out. This is to stop the developer accidentally changing the properties of the shared ImageList
, and to prevent the code generator from persisting this ImageList
into the Form
.
You can now bind this shared ImageList
to controls as you would any other ImageList
control. You can also inherit this Form
, and it will always use the same shared ImageList
supplied through the SharedImageLists
designer above.
As you can see from the Form
s below, at runtime, all the controls are referencing the same ImageList
component because all the ImageList
handles are the same.
So how does it work?
The SharedImageLists component
Well, on the one level, it's really quite simple. The SharedImageLists
component creates a single line of code in the InitializeComponent
routine, for each shared ImageList
you add with the SmartTag panel.
Listing 1 - The code the SharedImageLists
component generates inside the InitializeComponent
:
Me.LargeImageList = Me.SharedImageLists11.NewImageList( _
Me.components, CType(Me.SharedImageLists11.GetSharedImageLists, _
WindowsApplication2.SharedImageLists1).LargeImageList)
To understand what's going on here, we need to look at what the NewImageList
method does.
Listing 2 - Shows what the NewImageList
method above does:
Public Function NewImageList(ByVal component As IContainer, _
ByVal sharedImageList As ImageList) As ImageList
If SharedImageListCodeDomSerializer.InDesignerMode Then
Dim imageList As New ImageList
Me.ImageLists.Add(imageList, sharedImageList)
If component IsNot Nothing _
AndAlso component.Components IsNot Nothing Then
component.Add(imageList)
End If
Return imageList
Else
Return sharedImageList
End If
End Function
When in designer mode (running in the IDE), we create a new ImageList
component and site the component onto the supplied IContainer
. The ImageList
, along with the sharedImageList
, is now added to an internal Dictionary
collection and returned. The SharedImageListDesigner
uses this Dictionary
collection later when the ImageList
is sited.
We cannot simply pass the sharedImageList
argument through in design mode, because in design mode, components must be sited and they cannot be sited in more than one place at the same time (i.e., on both a UserControl
and a Form
). At runtime, the SharedImageList
component simply returns the passed sharedImageList
argument.
SharedImageListsDesigner
First, I'll show what happens to the ImageList
once it's been added to the Dictionary
in the NewImageList
method above. The SharedImageLists
component has a Designer Attribute so that whenever the component is created in design mode, an instance of the SharedImageListDesigener
class is also created.
The SharedImageListsDesigner
catches the shared ImageList
when it's added to the container in the OnComponentAdded
handler.
Listing 2 - OnComponentAdded
handler is where the the designer catches the shared ImageList
when it's sited.
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
The reason for using the OnComponentAdded
handler is because it's not possible to initialize the ImageList
in the NewImageList
method since inherited components do not have a valid site when the NewImageList
method is called. Inherited components are sited later by the IDesignerHost
. So in order to be able to work for both inherited and non inherited components, the OnComponentAdded
handler is used to catch when our shared ImageList
s are added. The most significant piece of code that gets called from here is inside InitalizeImageList
, and it's the CopyImageList
routine.
Listing 3 - Faking the shared ImageList
making a local copying in design mode, is done by the CopyImageList
method:
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
Since a component can only be sited in one place, in design mode, we need to fake that the ImageList
is the same shared ImageList
you'll see at runtime. So long as we show the end developer the same set of images at design time that appears at runtime, it does not matter if we use a separate ImageList
in the designer. It's at runtime that it's important that the same ImageList
gets used. So in design mode, each shared ImageList
is actually a separate copy of the real shared ImageList
.
In the CopyImageList
routine, we cannot simply use target.ImageStream = target.ImageStream
because of a bug (see KB 814349), so we have to copy the streams by using Serialize/Deserialize on the ImageStream
.
Most of the other code inside the SharedImageListDesigner
deals with making sure that our shared ImageList
's properties are read-only in the property browser so that the ImageList
's CodeDomSerializer
does not serialize a local copy of our ImageList
. This is done by implementing the ITypeDescriptorFilterService
interface and altering the ImageList
's property attributes so they have a ReadOnlyAttribute
and/or a DesignerSerializationVisibilityAttribute.Hidden
attribute added to them. The code below is the code that does the required filtering.
Listing 4 - Filtering ImageList
property attributes so that shared ImageList
s are read-only and never locally serialized:
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
One of the most challenging parts of this project was writing the one simple line of code below, into the InitailizeComponent
routine.
Listing 5 - The CodeDom statement that needs to be outputted by the CodeDomSerializer
:
Me.LargeImageList = Me.SharedImageLists11.NewImageList( _
Me.components, CType(Me.SharedImageLists11.GetSharedImageLists, _
WindowsApplication2.SharedImageLists1).LargeImageList)
The reason this caused the most problems is that ideally I wanted the SharedImageListsCodeDomSerializer
to do all the serializing so I did not have to mess with overriding the ImageList
's own CodeDomSerializer
. Unfortunately, CodeDomSerializer
s do not work this way. After many attempts to create the above ImageList
assignment with the code in the right order and without the default CodeDomSerialiser
creating redundant code, I had no choice but to find a way to override the default ImageList
CodeComSerializer
.
But how do you override the default CodeDomSerializer
for an existing component? I found the IDesignerSerializationProvider
interface which looked like it would do the job, so whenever the ImageList
component needed serializing, my CodeDomSerializer
would get called instead. So after some file tuning, I ended up with this code to replace the default ImageList
CodeDomSerializer
.
Listing 5 - The NewImageList
CodeDom method statements:
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
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
The next problem I found was that forms and controls started getting the newly named WSOD (White Screen of Darn). Even worse was, the IDE just crashed/disappeared when pasting a shared ImageList
.
After much investigation, it turned out that if the LargeImageList
variable is set to a public property, it all worked well. It appears there is either a bug or a limitation in the SerializerManager
that creates the CodeDom collection passed to our Deserialize
method. This results in the default CodeDomSerializer
trying to find a member on the SharedImageLists
instance that does not exist.
To fix this problem, the Deserialize
method is also overridden to add some extra code to determine if the correct CodeExpression
has been provided; if not, then we replace it with the correct expression, i.e., substituting a CodePropertyExpression
for a CodeFieldExpression
, and vice versa.
Listing 6 - The routine which deserializes a shared 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
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
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
methodI.Parameters(1) = _
New CodeFieldReferenceExpression(propRef.TargetObject, name)
End If
End If
ElseIf fieldRef IsNot Nothing Then
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
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)
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
The bulk of the code simply loops through the passed CodeDom statements to determine if the statements passed are for normal ImageList
creation or if it contains the NewImageList
method assignment. Once we have a NewimageList
statement, it's simple enough to call the NewImageList
method directly and then return the created ImageList
instance. If no NewImageList
statement is found, then the default ImageListCodeDomSerializer
works as normal, so we don't break any standard ImageList
code from deserializing.
The final piece of the puzzle
The above code was a success, and I now have working code that both serializes and deserializes the shared ImageList
, and cut and paste/undo/redo worked fine too. Except there was one more problem to solve. The deserialize code did not work the first time a Form
/UserControl
was loaded. Why? Well, the problem turned out to be, the code I used to load my custom ImageList CodeDomSerializer
was being loaded too late. I was using the TypeDescriptor.AddProvider
to add my IDesignerSerializationProvider
implementation inside the Initialize
override of the SharedImageListsDesigner
. The problem was that by the time the SharedImageListsDesigner
was loaded, it was too late and the shared ImageList
s had already been deserialized by the default ImageList
CodeDomSerializer
, and the WSOD reared its ugly head again. Anyway, to cut a long story short, the only way I could find to load the custom ImageListCodeDomSerializer
early enough was to create another CodeDomSerializer
for the SharedImageListComponent
. Only this time, the serialize/deserialize methods do nothing but call the default CodeDomSerialiser
. The important piece of code is in the class' constructor.
Listing 7 - The SharedImageLists
constructor that's used to replace the ImageList
CodeDom serializer.
Public Sub New()
TypeDescriptor.AddAttributes(GetType(ImageList), New _
Serialization.DesignerSerializerAttribute(_
GetType(SharedImageListCodeDomSerializer), _
GetType(Serialization.CodeDomSerializer)))
End Sub
All the code does is add an extra attribute to the TypeDescriptor
for the ImageList
so that my CodeDomSerializer
gets found first instead of the default ImageList CodeDomSerializer
. With all this done, the component is now ready to use, and works well in all my testing so far. I consider the AddAttributes
code about a bit of a hack, so if anyone can suggest a better way to accomplish this, then please let me know. Hopefully, you'll find the component useful.
History
- 1.0 - Released 01/March/2005.