Un piccolo Resource Manager

pubblicato il 23/04/2007

In questo articolo scriveremo una classe che possa aiutarci ad estrarre le risorse (icone, immagini, testo) dagli assembly che compongono la nostra applicazione. Questa classe è autonoma e pertanto può essere inserita in un progetto già esistente oppure se ne può creare uno apposta (magari con una nnostra libreria di classi del genere). Per lo scopo didattico dell'articolo la classe sarà limitata all'estrazione di risorse di tipo icona (System.Drawing.Icon), ma può facilmente essere estesa per estrarre altri tipi di risorse (bitmap, stringhe, file mp3...).

Nel progetto che pereferiamo creiamo una nuova classe che chiameremo ResourceManager; come al solito prima della definizione della classe inseriamo le Imports necessarie:

Imports System.Drawing
Imports System.IO
Imports System.Reflection
Imports System.Text

Public Class ResourceManager

End Class

La namespace System.Drawing contiene la classe Icon, che è l'oggetto della nostra estrazione; la namespace System.IO contiene le classi Stream e MenoryStream per mezzo delle quali leggaimo dall'assembly la risorsa e la "memorizziamo in memoria" se mi passate il gioco di parole; la namespace System.Reflection contiene la classe Assembly di cui sfrutteremo alcuni metodi statici per accedere alle risorse dell'assembly. Ho accennato al fatto che la classe può essere utilizzata per estrarre qualsiasi risorsa che sia stata inclusa in un assembly; a questo scopo definiamo un enumerativo che rappresenti i possibili tipi di risorsa estraibili:

Public Enum ResourceType
  Icon = 0 ' est. = ICO
' Aggiungere qui i tipi interessati: ad ogni tipo corrisponde un estensione del file
End Enum

Normalmente preferisco definire gli enumerativi al di fuori della classe cui si riferiscono, ma si tratta di una mia scelta opinabile. Entranndo invece decisamente nel codice della classe, possiamo a questo punto definirne i costruttori:

#Region " Constructor"

  Public Sub New()
    m_Source = [Assembly].GetExecutingAssembly()

  End Sub

  Public Sub New(ByVal executablePath As String)
    m_Source = [Assembly].LoadFrom(executablePath)

  End Sub

  Public Sub New(ByVal sourceAssembly As [Assembly])
    m_Source = sourceAssembly

  End Sub

#End Region

Utilizzando il primo costruttore (senza parametri), le risorse verranno cercate all'interno dello stesso assembly in cui si trova la classe ResourceManager.; in alternativa è possibile usare il costruttore che richieda il percorso completo all'assembly che contiene le risorse; più utile è il costruttore che accetta come parametro l'assembly che contiene la risorse: in questo modo possiamo utilizzare la classe per estrarre risorse che si trovino in un qualsiasi assembly referenziato dall'applicazione anche se non ne conosciamo la locazione fisica nel file-system.

Abbiamo visto che i costruttori valorizzano una variabile m_Source: si tratta di una variabile privata di tipo System.Reflection.Assembly, resa accessibile all'esterno per mezzo di una proprietà:

#Region " Source"

  Private m_Source As System.Reflection.Assembly

  Public Property Source() As System.Reflection.Assembly
    Get
      Return m_Source

    End Get

    Set(ByVal Value As System.Reflection.Assembly)
      m_Source = Value

    End Set

  End Property

#End Region

Infine definisco una proprietà (in sola lettura) Errors che riporti gli eventuali errori durante l'estrazione delle risorse:

#Region " Errors"

  Private m_Errors As New Collections.Specialized.StringCollection

  Public ReadOnly Property Errors() As Collections.Specialized.StringCollection
    Get
      Return m_Errors

    End Get

  End Property

#End Region

Bene, adesso viene il codice di estrazione vero e proprio: ho cercato di suddividerlo il più possibile per rendere più comprensibile il compito di ciascun pezzo di codice:

#Region " Resource extraction"

  Public Function GetIcon(ByVal name As String) As Icon

    Dim SourceStream As Stream
    Dim ResourceStream As MemoryStream
    Dim bmp As Bitmap
    ' Variabile coerente con il tipo restituito dal metodo
    Dim result As Icon

    Errors.Clear()

    Try
      SourceStream = Source.GetManifestResourceStream(FormattaNomeRisorsa(name, type))

      If Not IsNothing(SourceStream) Then
        ' Il tipo passato alla FormattaNomeRisorsa deve essere coerente con il tipo restituito dal metodo
        ResourceStream = ConvertIntoMemStream(SourceStream)

      End If

      If Not IsNothing(ResourceStream) Then
        ' La gestione della risorsa deve essere coerente con il tipo restituito dal metodo
        bmp = New Bitmap(ResourceStream)
        result = Icon.FromHandle(bmp.GetHicon)

      End If

    Catch ex As Exception
      Errors.Add(String.Format("Errore: {0} in {1}", ex.Message, ex.TargetSite.Name))

    End Try

    Return result

  End Function

  Private Function ConvertIntoMemStream(ByVal InputStream As Stream) As MemoryStream

    Dim reader As BinaryReader
    Dim writer As BinaryWriter
    Dim result As MemoryStream
    Dim index As Long

    result = Nothing

    Try
      result = New MemoryStream
      result.SetLength(InputStream.Length)
      result.Position = 0

      reader = New BinaryReader(InputStream)
      writer = New BinaryWriter(result)

      For index = 1 To InputStream.Length
        writer.Write(reader.ReadByte())
        writer.Flush()

      Next

    Catch ex As Exception
      Errors.Add(String.Format("Errore: {0} in {1}", ex.Message, ex.TargetSite.Name))

    End Try

    Return result

  End Function

  Private Function FormattaNomeRisorsa(ByVal NomeRisorsa As String, ByVal type As ResourceType) As String

    Dim result As New StringBuilder

    result.AppendFormat("{0}.{1}", Source.GetName.Name, NomeRisorsa)

    Select Case type
      Case ResourceType.Icon
        result.Append(".ICO")

    End Select

    Return result.ToString()

  End Function

#End Region

Tanto per fare gli originaloni, cominciamo dalla fine: la routine FormattaNomeRisorsa restituisce il nome di una risorsa anteponendogli il nome dell'assembly e posponendogli l'estensione abbinata a quella risorsa (ICO per le icone, ad esempio). La routine GetIcon estrae dall'assembly indicato come origine (proprietà Source) lo stream associato alla risorsa il cui nome le è stato passato come parametro; questo stream viene datio in pasto alla routine ConvertIntoMemoryStream che converte lo stream generico in uno stream di memoria. Quest'ultimo infine verrà utilizzato per instanziare un oggetto di classe Bitmap; di questo oggetto viene invocato il metodo GetHIcon che restituisce il puntatore all'icona; questo puntatore viene passato come argomento al metodo (statico) FromHandle della classe Icon per ottenere l'icona vera e propria. Questo è quanto.

Volendo estendere la classe per aggiungere nuovi tipi di risorse, occorre intervenire in tre punti diversi della classe:

  1. Aggiungere un nuovo valore all'enumerativo ResourceType per ogni estensione gestita
  2. Aggiungere la gestione del nuovo valore nella routine FormattaNomeRisorsa
  3. Aggiungere un metodo pubblico che restituisca uno oggetto del tipo appena aggiunto nell'enumerativo; questo metodo sarà molto simile al metodo GetIcon appena descritto tranne che nei punti che ho messo in evidenza con commenti

Chiudo questo articolo ricordando velocemente i passi per includere una risorsa in un assembly (dando per scontato di averne aperto la solution in Visual Studio):

  1. Nel Solution Explorer, selezionare il progetto, fare apparire il menu contestuale (tasto destro del mouse) e scegliere la voce Add | Add existing item
  2. Nella dialog che apparirà selezionare il file da includere (es. TRAYMENU.ICO) e premere OK
  3. Nel Solution Explorer, selezionare il file appena giunto e premere il tasto F4 per visualizzarne le proprietà.
  4. Nel campo Build action scegliere la voce Embedded Resource al posto del valore Content che rappresenta il default

A questo punto possiamo ricompilare l'assembly per includere il file; si noti che se si fa uso di un code-obfuscator del codice IL è necessario informare l'eseguibile del fatto che questa risorsa non deve essere obfuscata.