Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Sharing and inheriting ImageLists across multiple forms and controls

0.00/5 (No votes)
7 Apr 2006 3  
A component that allows ImageLists to be inherited and shared globaly across multiple forms and controls, with full design time support.

Sample screenshot

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 ImageLists 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 UserControls and Forms? 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 ImageLists through shared methods or properties. This allows you to use the same ImageLists 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 ImageLists 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 ImageLists in design mode, as can be seen from the class diagram below.

Class Diagram

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:

Add New Item Template Dialog

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.

SharedImageLists design surface

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:

Smart Tag Panel

This SmartTag panel contains a dropdown list with all the ImageLists 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.

Shared ImageList added

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 Forms below, at runtime, all the controls are referencing the same ImageList component because all the ImageList handles are the same.

ImageList at runtime

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

      '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

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 ImageLists 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 ImageLists 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, CodeDomSerializers 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

    '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

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.

White Screen of Darn error message

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

      ' 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

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 ImageLists 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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here