Un piccolo Resource Manager in VB.NET

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 MemoryStream per mezzo delle quali leggiamo dall’assembly la risorsa e la “salviamo in memoria”, per così dire.
  • 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. Entrando invece decisamente nel codice della classe, possiamo a questo punto definirne i costruttori:

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

Analizziamo un momento i tre costruttori:

  1. Usando il primo costruttore, senza parametri, le risorse verranno cercate all’interno dello stesso assembly in cui si trova la classe ResourceManager.
  2. Per usare il secondo costruttore è invece necessario passare come parametro il percorso completo all’assembly che contiene le risorse.
  3. Il terzo costruttore, infine, è forse quello più utile, dal momento 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à:

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

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

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

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:

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

Tanto per fare gli originali, 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.

One thought on “Un piccolo Resource Manager in VB.NET

  1. Pingback: Un programma nella tray-bar – Il Gatto Informatico

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

*