Quanti di noi, dopo aver completato un'applicazione, si sono trovati a dover realizzare una procedura di installazione e per mancanza di tempo/conoscenza si sono trasformati in piccioni viaggiatori e hanno installato "a manina" il programma sulle macchine dei clienti?
Nel mio caso, spesso. Ma ora che faccio un altro mestiere e sviluppo per passione, ho deciso di colmare questa lacuna. Pur non avendo grande esperienza in materia, ho pensato di scrivere questo articolo come viatico per chi come me sente il bisogno di approfondire la materia.
Fatta questa dovuta premessa, iniziamo con la definizione del processo di installazione di un applicativo su computer, che si trova su Wikipedia alla voce Installation (computer programs).
Nel caso di installazione di applicazioni .NET vi sono diverse modalità di installazione, che possiamo riassumere in quattro categorie:
- XCOPY: Copia di tutta la applicazione direttamente in una cartella destinazione;
- ClickOnce: Distribuzione remota di applicazioni autoaggiornanti;
- MSI: Creazione di un pacchetto .msi da utilizzare con Windows Installer;
- EXE: Creazione di un'applicazione di installazione.
La prima si basa sul fatto che le applicazioni .NET sono self-contained, visto che le applicazioni .net vengono compilate in assembly, e gli assembly sono auto-descrittivi poichè le informazioni che riguardano un assembly sono memorizzati nell'assembly stesso. Inoltre, con i nuovi strumenti di configurazione delle applicazioni offerti dal framework, è possibile evitare l'uso di chiavi di registro. In questo caso la macchina target deve avere il .Net framework già installato.
La seconda, ClickOnce, è la più recente ed interessante. Per una breve panoramica, consiglio di leggere Distribuire applicazioni utilizzando la tecnologia ClickOnce.
Anche ClickOnce ha delle limitazioni, perchè non è possibile:
- Gestire un installazione guidata da Wizard;
- Installare file condivisi tra vari utenti;
- Installare device driver;
- Installare assembly nella Global Assembly Cache;
- Installare un applicazione che sia condivisa da più utenti;
- Aggiungere voci nel menus Esecuzione Automatica e Start;
- Registrare componenti COM. (con Windows XP o superiore è possibile utilizzare ClickOnce anche in presenza di componenti COM grazie ad una tecnica denominata RegFreeCom);
- Registrare nuove estensioni.
La terza è IMHO la più opportuna, quando non si voglia o possa utilizzare ClickOnce, sia per mantenere una interfaccia standard con l'utente, sia per gli strumenti messi a disposizione dal framework .NET e da Visual Studio, col Wizard Setup di cui parleremo in seguito. Va da se che in questa categoria intendo includere anche i pacchetti MSI con suffisso .exe .
Per quanto riguarda la quarta categoria, poichè scrivere un programma di installazione ex novo è a dir poco una impresa titanica data la grande varietà di possibili configurazioni hardware e software dei PC, di solito si usano programmi di terze parti come Wise o InstallShield.
In questo articolo ci occuperemo della creazione di pacchetti MSI con l'ausilio di Visual Studio 2005, partendo dalla situazione più semplice sino ad arrivare alla crezione di pacchetti di installazione personalizzati.
Quoto, dalla documentazione Microsoft:
Visual Studio consente di creare due tipi di programmi di installazione: programmi Windows Installer (MSI) e moduli unione (MSM).
Windows Installer viene utilizzato per installare applicazioni e file su un altro computer, mentre i moduli unione per installare componenti condivisi da più applicazioni.
La principale differenza tra i due tipi riguarda i destinatari. Tutto ciò che verrà utilizzato direttamente dall'utente finale dell'applicazione (file eseguibili, documenti, pagine HTML) deve essere posto all'interno di un Windows Installer. Tutto ciò che sarà invece utilizzato soltanto da uno sviluppatore (DLL, controlli, risorse) deve essere posto all'interno di un modulo unione, che potrà quindi essere inserito dallo sviluppatore in un Windows Installer per essere distribuito all'utente finale.
La cosa più semplice da fare, per creare un pacchetto di installazione MSI con Visual Studio, è quello di aggiungere un progetto di Setup and Deployment alla nostra soluzione. Ve ne sono di diversi tipi:
- Setup: progetto vuoto per l'installazione di un'applicazione Windows form;
- Web Setup: progetto vuoto per l'installazione di un'applicazione Web;
- Merge Module: moduli unione, utilizzati per l'installazione di componenti;
- CAB: file scaricabile da web browser;
- Smart Device CAB: file scaricabile per smart device;
- Setup Wizard: come dice il nome stesso, lancia un wizard che predispone il progetto di setup secondo le nostre necessità.
Un progetto di setup così creato può eseguire le seguenti attività:
- Creare directories e copiare files nelle cartelle predifinite di destinazione (ad esempio: programmi, desktop utente, menù programmi utente, ecc.);
- Creare shurtcut dell'applicazione sul desktop e sul menu programmi;
- Creare delle chiavi di registro.
Se abbiamo bisogno di fare qualcosa di più, potrebbe essere necessario implementare una o più "custom action", cioè uno o più processi di installazione personalizzati. Per questo motivo, il framework .NET mette a disposizione una serie di classi, comprese nel namespace System.Configuration.Install, contenuto nell'assembly System.Configuration.Install.dll .
La classe più importante è la System.Configuration.Install.Installer.
Però la classe Installer, parafrasando il mondo del cinema, non è il regista quanto piuttosto l'attore che deve saper recitare due possibili copioni (installazione o disinstallazione), di cui il primo con due possibili finali: se tutto è andato bene si eseguono le attività di commit, altrimenti si esegue il rollback che si occupa di rimettere tutto come prima, come se il tentativo di installazione non fosse mai avvenuto.
Mentre la parte del regista la possono fare:
- InstallUtil.exe, che dalla riga di comando consente di eseguire una o più classi Installer;
- Windows Installer, se nel modulo MSI sono state specificate una o più custom action, a cui sono associate le rispettive classi Installer;
- Un programma che utilizza le classi AssemblyInstaller o ComponentInstaller per eseguire una classe Installer.
Per iniziare, vediamo come si comporta il comando InstallUtil.exe (date un occhiata alla documentazione per la sintassi e le varie opzioni disponibili):
- Per ogni assembly specificato nella riga di comando vengono identificate (tramite reflection) tutte le classi derivate dalla classe Installer che hanno come attributo [System.ComponentModel.RunInstallerAttribute(true)] e per ciascuna di esse:
- Viene istanziata la classe;
- Se si tratta di installazione(poichè nella riga di comando non è presente il parametro /u[ninstall]):
- Viene chiamato il metodo Install(IDictionary savedState)
- Se tutto è andato bene:
- viene chiamato il metodo Commit(IDictionary savedState)
- altrimenti, se qualcosa è andato storto:
- viene chiamato il metodo Rollback(IDictionary savedState)
- altrimenti, se si tratta di disinstallazione (poichè nella riga di comando è presente il parametro /u[ninstall]):
- viene chiamato il metodo Uninstall(IDictionary savedState)
Poichè l'installazione di una applicazione implica necessariamente la possibilità di disinstallarla, occorre registrare da qualche parte le informazioni relative allo stato dell'installazione in modo che in un secondo momento si possa sapere come disinstallare l'applicazione. A prima vista potrebbe non essere chiaro perchè chi sviluppa l'installer non possa sapere in anticipo cosa disinstallare, invece spesso e volentieri durante il processo di installazione alcune informazioni (ad esempio il path di una directory di dati o altro) vengono fornite dall'utente finale a run time e non possono quindi essere note a design time.
Ecco perchè chi ha progettato la classe Installer non ha ritenuto opportuno memorizzare lo stato di installazione in una proprietà della classe Installer (che "cessa di esistere" ogni volta che termina un'installazione) ma ha architettato le cose in modo da assegnare al regista (ad esempio Windows Installer) la responsabilità di registrare/recuperare tali informazioni, da passare come parametro ad ogni chiamata di metodo della classe Installer mediante una qualsiasi collezione che implementi l'interfaccia System.Collections.IDictionary (generalmente una Hashtable).
Per avere un esempio di come vengano gestite le informazioni di "stato d'installazione" quoto la già citata documentazione del comando InstallUtil.exe:
Se si esegue Installutil.exe su un assembly senza specificare alcuna opzione, i tre file seguenti verranno inseriti nella directory dell'assembly:
- InstallUtil.InstallLog : Contiene una descrizione generale dello stato dell'installazione.
- nomeassembly.InstallLog :Contiene informazioni specifiche della fase di commit del processo di installazione.
- nomeassembly.InstallState :Contiene i dati da utilizzare per disinstallare l'assembly.
Ora che abbiamo illustrato il contesto in cui viene utilizzata la classe Installer, vediamone in dettaglio i medodi principali:
- public Installer()
- public virtual void Install(System.Collections.IDictionary savedState)
- public virtual void Commit(System.Collections.IDictionary savedState)
- public virtual void Rollback(System.Collections.IDictionary savedState)
- public virtual void Uninstall(System.Collections.IDictionary savedState)
le proprietà:
- Context
- Contiene un oggetto di tipo System.Configuration.Installer.InstallContext che viene utilizzata per recuperare i parametri passati nella linea di comando quando si utilizza InstallUtil.exe. Maggiori informazioni possono essere lette su XYZ.
- HelpText
- Contiene il testo di aiuto (che dobbiamo scrivere noi sviluppatori della classe Installer) anch'esso relativo all'utilizzo di InstallUtil.exe, in particolare è sempre opportuno inserire le informazioni relative ai parametri che il nostro installer è in grado di utilizzare quando vengono inserite nella linea di comando quando si utilizza InstallUtil.exe.
- Installers
- Può contenere una collezione di oggetti Installer "figli".In fase di installazione, se la collezione non è vuota, le azioni eseguite sull'installer vengono eseguite iterativamente sugli installer figli e cosi via in modo recursivo.
- Parent
- Se l'oggetto Installer è "figlio" di un altro Installer (ovverossia appartiene alla collezione Installers dell'Installer "padre"), la proprietà Parent contiene il riferimento all'Installer "padre", altrimenti la proprietà Parent è pari a null.
e gli eventi:
- BeforeInstall
- Committing
- Committed
- AfterInstall
- BeforeRollback
- AfterRollback
- BeforeUninstall
- AfterUninstall
Infine, come appare evidente anche dal fatto che i metodi Install, Commit, Rollback e Uninstall sono virtuali, è importante chiarire che la classe Installer non viene usata direttamente, quanto piuttosto come classe base da cui derivare la nostra classe di installazione, in modo da poter fare l'override dei suddetti metodi.
Quindi, riassumendo, per eseguire installazioni personalizzate occorre:
- Creare una classe derivata dalla classe Installer (in Visual Studio potete utilizzare il template "Installer Class");
- Fare l'override dei metodi Install, Commit, Rollback e Uninstall (ricordandosi di chiamare il metodo della base come prima istruzione);
- Opzionalmente, scrivere dei metodi da attaccare agli eventi della classe base (ad esempio BeforeInstall, Committing, Committed, BeforeRollback, ecc.).
Inoltre:
- Se si usa InstallUtil.exe:
- Inserire l'installer (o gli installers) nel progetto della nostra applicazione, in modo da essere inclusi nell'assembly (.exe o .dll) che passeremo come parametro nella linea di comando;
- Aggiungere a tutte le classi CustomInstaller che vogliamo far eseguire l'attributo [System.ComponentModel.RunInstallerAttribute(true)].
- Se invece si usa un progetto di setup:
- Aggiungere un progetto alla nostra soluzione, e all'interno di questo progetto inserire tutti gli elementi occorrenti alla nostra installazione personalizzata;
- Aprire la finestra del "Custom Action Editor" del progetto di setup e aggiungere la custom action collegandola con ...
Ma in pratica, cosa ci mettiamo dentro i metodi Install, Commit, Rollback e Uninstall?
Facciamo alcuni esempi di installazioni personalizzate:
- Visualizzazione di un file readme alla fine dell'installazione (solo se tutto è andato bene)
- Visualizzazione di una pagina web del nostro sito con suggerimenti e workaround oppure con un modulo di richiesta assistenza se l'installazione non è andata a buon fine
- Installare e configurare un log eventi che l'applicazione legge o nel quale scrive durante l'esecuzione (a questo proposito è interessante notare che nell'assembly System.Configuration.Install.dll si trova anche una parte del namespace System.Diagnostic, relativamente alle sole classi EventLogInstaller e PerformanceMonitorInstaller).
Sempre all'interno dei suddetti metodi è possibile modificare anche lo stato dell'applicazione, semplicemente utilizzando i metodi dell'interfaccia IDictionary dell'oggetto savedState. Ogni volta che lasciate all'utente la possibilità di scegliere qualcosa o di definire un path di installazione diverso da quello standard (scelto da voi a design time) è importante aggiungere tali informazioni allo stato dell'installazione.
Farlo è semplicissimo, ad esempio: savedState.Add( "UserDataDirectoryName", theUserDataDirectoryName).
In questo modo potremo recuperare tale informazione in caso di rollback o di uninstall ed effettuare su tale directory le operazioni necessarie.
Ricordo ancora una volta che la gestione delle informazioni relative allo stato di installazione (ossia la memorizzazione dell'oggetto savedState) verranno salvate/recuperate dal programma di installazione, non dalla nostra classe di installazione personalizzata.
Spero di aver dato una sufficiente visione d'insieme per poter approfondire l'argomento e creare dei buoni programmi di installazione, che daranno un tocco di maggiore professionalità alle nostre applicazioni (e forse ci eviteranno un po' di grattacapi).