Press "Enter" to skip to content

Utility e User Experience in WPF

In questo progetto implementeremo una semplice utility per verificare la target platform delle Dll e degli Exe memorizzati su una cartella e ne approfitteremo per mostrare come utilizzare la classe AutoSettingsManager già  implementata nella console del MiniSqlAgent, per migliorare la User Experience. Inoltre, visto che è un utility che possiamo “utilizzare” creiamo anche il setup della stessa utilizzando InnoSetup e mostriamo come utilizzare Visual Studio per effettuare la build del setup grazie ai Post Build Events.

La scusa per creare la mini utility è stata fatto che in azienda abbiamo deciso di modificare la target platform di un progetto da x86 ad AnyCpu, perché è stato deciso di sfruttare al meglio le macchine a 64 bit per l’applicazione. Trattandosi di una applicazione strutturata, con molte Dll, effettuando le prove sulle macchine di sviluppo considerato che in Debug usiamo l’ x86 per poter avere l’edit e continue, rischiavamo di avere delle Dll compilate con target diversi e conseguenti errori inesistenti o funzionamenti strani. Questa utility permette di verificare che tutte le Librerie incluse in una cartella e tutte le sue sottocartelle siano compilate con la corretta target platform per  i test, utilizzeremo la reflection per verificare come è compilata una DLL oppure un Exe.

La soluzione

solution_01[6]

Si tratta di un progetto molto semplice, che comprende una sola window, abbiamo aggiunto poi quattro files .iss che sono quelli che utilizziamo per la compilazione del setup con InnoSetup.

La classe MainWindow

main_window_01[6]

La finestra dell’applicazione, vediamo che cosa contiene:

Il funzionamento è molto semplice, possiamo selezionare una cartella sul nostro disco, e tramite il tasto Check controllare per quale Target Platform sono compilate le DLL e gli Exe contenuti nella cartella. La checkbox permette di estendere il controllo alle sottocartelle.

Il risultato di quanto creato è questo:

result_01[6]

Visualizziamo l’architettura della dll che per i progetti AnyCpu è MSIL, per quelli a 32 bit è x86, per quelli a 64 bit è x64 e così via.

private void btnCheck_click(object sender, RoutedEventArgs e)
{
    if( !this.ctlDir.DirectoryName.XDwIsNullOrTrimEmpty() )
    {
        SearchOption searchOpt = CheckSubDirs ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
        string[] files = Directory.GetFiles(this.ctlDir.DirectoryName, "*.dll", searchOpt);
        List<string> allFiles = new List<string>();
        allFiles.AddRange(files);
        files = Directory.GetFiles(this.ctlDir.DirectoryName, "*.exe", searchOpt);
        allFiles.AddRange(files);
        StringBuilder sb = new StringBuilder();
        foreach (string str in allFiles)
        {
            try
            {
                AssemblyName asn = System.Reflection.AssemblyName.GetAssemblyName(str);
                sb.AppendLine(string.Format("ARCH: {0} For: {1} on {2}", asn.ProcessorArchitecture, Path.GetFileName(str), Path.GetDirectoryName(str)));

            }
            catch (Exception ex)
            {
                EventLogger.SendMsg(ex);
                sb.AppendLine(string.Format("ARCH: N/A For: {0} on {1}", Path.GetFileName(str), Path.GetDirectoryName(str)));
            }
            Result = sb.ToString();
        }

    }
    else
    {
        MessageBox.Show( "It is necessary to select a directory where to search the files to check.");
    }
}

Il codice che effettua il controllo degli assembly della cartella indicata è quello qui sopra riportato. Fatto salvo per l’acquisizione dei nomi di file e la formattazione dell’output su schermo, la sola riga che conta nel codice è la seguente:

AssemblyName asn = System.Reflection.AssemblyName.GetAssemblyName(str);

Che crea le informazioni necessarie. asn.ProcessorArchitecture è la property che contiene il valore che a noi interessa.

Non c’è altro.

Considerato che in questo blog ci piace fornire ai nostri lettori nuovi skill, utilizziamo questo progetto semplicissimo per fare un paio di cose utili.

Migliorare la User Experience

Siccome i programmatori sono pigri più dei sistemisti, dover selezionare ogni volta che l’applicazione parte la cartella su cui fare i controlli, è noioso, anche perchè se stiamo lavorando su un progetto probabilmente la cartella sarà  sempre la stessa. Pertanto, vogliamo memorizzare lo stato della User Interface quando la finestra si chiude.

Questo tipo di funzionalità  può tornare utile nella gestione quotidiana delle user interface dei vostri prodotti per poter ad esempio memorizzare il contenuto di una maschera di selezione dei filtri di stampa o di ricerca, memorizzare la posizione di una finestra a video o la sua dimensione. Per salvare i dati relativi al nome della cartella selezionata e allo stato della checkbox per il controllo delle sottocartelle, utilizzeremo la classe DnwAutoSettingsManagerBase creando un AutoSettingsManager per memorizzare lo stato della MainWindow.

La classe AutoSettingsManager

public class AutoSettingsManager : Dnw.Base.Entities.DnwAutoSettingsManagerBase
{
    private const string AUTOSTT_File = "Dnw\\CheckTargetPlatform\\CheckTargetPlatform_AUTO.xml";

    private string mAutoSettingsFileName;

    private static AutoSettingsManager mInstance;

    private static object syncRoot = new Object();

    public AutoSettingsManager()
    {
        mAutoSettingsFileName = Path.Combine(Environment.GetFolderPath(
            Environment.SpecialFolder.ApplicationData),
            AUTOSTT_File);
    }

    public override string AutoSettingsFileName
    {
        get
        {
            return (mAutoSettingsFileName);
        }
    }

    public static AutoSettingsManager Instance
    {
        get
        {
            if (mInstance == null)
            {
                lock (syncRoot)
                {
                    if (mInstance == null)
                        mInstance = new AutoSettingsManager();
                }
            }

            return mInstance;
        }
    }
}

Questa classe ci permette di salvare le informazioni di stato della UI su un file XML che verrà  memorizzato sulla cartella dei dati applicativi del sistema per l’utente che esegue il programma.

autosettings_file[6]

autosettings_content[7]

Come possiamo vedere, la cartella dei dati l’abbiamo inserita nella cartella “AppData\Roaming” dell’utente, che è indicata nelle linee guida Microsoft come cartella per le informazioni di stato applicative, perché appartiene all’utente che ha diritti di scrittura e lettura sulla cartella stessa, anche se poi, a livello di File Manager Windows, non é visibile senza attivare la visualizzazione delle cartelle di sistema.

Sull’immagine qui sopra possiamo vedere che abbiamo memorizzato la dimensione della finestra, la cartella selezionata e lo stato della checkbox. Vediamo come abbiamo fatto:

public partial class MainWindow : DnwBaseWindow, INotifyPropertyChanged
{
    private readonly static string SIZE_STT = string.Format("{0}_SIZE", typeof(MainWindow).Name);
    private readonly static string DIRS_STT = string.Format("{0}_DIRS", typeof(MainWindow).Name);
    private readonly static string CHKS_STT = string.Format("{0}_CHKS", typeof(MainWindow).Name);

    public MainWindow()
    {
        InitializeComponent();
        this.Icon = BitmapFrame.Create(new Uri("pack://application:,,,/dnwico.ico", UriKind.RelativeOrAbsolute));
        this.DataContext = this;
        this.AutoSettings = AutoSettingsManager.Instance;
        this.SizeSettingName = SIZE_STT;
        bool chk = false;
        bool.TryParse(AutoSettingsManager.Instance.GetSetting(CHKS_STT), out chk);
        this.CheckSubDirs = chk;
        this.ctlDir.DirectoryName = AutoSettingsManager.Instance.GetSetting(DIRS_STT);
    }

La classe Main Window é derivata da DnwBaseWindow, che abbiamo illustrato nel post Implementare una Window WPF con funzionalità  estese che contiene il necessario a gestire i setting automatici. Nel costruttore andiamo ad assegnare il manager alla property della classe base e recuperiamo i valori dei dati della UI, ricordiamo che la Window estesa ha già  il necessario a recuperare e salvare la dimensione della finestra.

protected override void OnClosed(EventArgs e)
{
    AutoSettingsManager.Instance.SetSetting(DIRS_STT, ctlDir.DirectoryName);
    AutoSettingsManager.Instance.SetSetting(CHKS_STT, CheckSubDirs.ToString());
    base.OnClosed(e);

}

Nel metodo OnClosed della Window, salviamo i valori del directory name e della property che rappresenta il valore della checkbox, sono due modi diversi per fare la stessa cosa, uno utilizzando il Name dell’oggetto in XAML e ricavandone una property, la seconda invece utilizzando una property in binding sulla checkbox, sono entrambi leciti e utilizzabili in base alle necessità .

Volendo spingerci oltre con la spiegazione di cose utili, vediamo anche come si fa a creare il Setup dell’applicazione utilizzando InnoSetup, che abbiamo già  utilizzato per creare il setup di un servizio Window.

; Script generated by the Inno Setup Script Wizard.
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!

#define MyAppName "CheckTargetPlatform"
#include "ISS_Version.iss";
#define MyAppPublisher "Dotnetwork.it"
#define MyAppURL "http://dotnetwork.it"
#define MyAppExeName "CheckTargetPlatform.exe"
#define MyDestFolder "Dnw"
#define MyLicenseFile "License.rtf"
#define MyIconFile "dnwico.ico"
#define MyLogoInstall "DnwLogoInstall.bmp"
#define MyLogoInstallSmall "DnwLogoInstallsmall.bmp"
#define MyAppGuid = "{{F3535496-C155-4524-88FF-1B841B6DBC31}"

[Setup]
; NOTE: The value of AppId uniquely identifies this application.
; Do not use the same AppId value in installers for other applications.
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
AppId={#MyAppGuid}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
VersionInfoVersion={#MyVersionInfoVersion}
;AppVerName={#MyAppName} {#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
DefaultDirName={pf}\{#MyDestFolder}\{#MyAppName}
DefaultGroupName={#MyDestFolder}
DisableProgramGroupPage=yes
LicenseFile={#MyLicenseFile}
OutputDir=..\InnoSetupCompiled
OutputBaseFilename={#MyAppName}Setup
SetupIconFile={#MyIconFile}
Compression=lzma
SolidCompression=yes
UninstallDisplayIcon={app}\{#MyAppExeName}
WizardImageBackColor=$542344
WizardImageFile={#MyLogoInstall}
WizardSmallImageFile={#MyLogoInstallSmall}
; "ArchitecturesInstallIn64BitMode=x64" requests that the install be
; done in "64-bit mode" on x64, meaning it should use the native
; 64-bit Program Files directory and the 64-bit view of the registry.
; On all other architectures it will install in "32-bit mode".
ArchitecturesInstallIn64BitMode=x64

[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
Name: "italian"; MessagesFile: "compiler:Languages\Italian.isl"

[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
;Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 0,6.1

[Files]
#include "ISS_Files.iss"

[Icons]
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"
Name: "{commondesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
;Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: quicklaunchicon

[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent


[code]
#include "ISS_DetectFramework4.iss"

Lo script di setup che abbiamo predisposto qui sopra è uno script neutro, può essere utilizzato per creare il setup di un eseguibile .Net effettuando le seguenti operazioni:

  1. Sostituire tutte le clausole #define all’inizio del file indicando i dati corretti, quindi il nome dell’applicazione, il nome dell’eseguibile, il vostro nome ed il vostro sito web, il nome del root destination folder dei vostri programmi, nel nostro caso Dnw significa che il path standard di destinazione sarà :
    c:\Program Files\Dnw\NomeApplicazione. Attenzione al file dell’icona, se come me utilizzate un .ico ricordatevi di far copiare l’icona come Content + Copy Always oppure l’icona di disinstallazione di windows non sarà  la vostra ma quella di sistema. I BMP noi li abbiamo personalizzati, ma se omessi Innosetup ha una serie di bmp standard. La cosa più importante da modificare è il GUID applicativo, perché quel GUID verrà  usato per riconoscere e aggiornare o disinstallare l’applicazione. Avere due applicazioni con lo stesso GUID potrebbe portare a strani risultati installandole sulla stessa macchina.
  2. Le tre righe evidenziate in giallo, corrispondono a 3 inclusioni di altrettanti files, ISS_Version contiene le variabili che definiscono il codice di versione del setup e dell’applicazione che saranno assegnati all’installazione, lo teniamo su un file separato perché la BUILD di Team foundation server ci permette di aggiornare la versione automaticamente (utilizzando una apposita serie di script da noi definiti), ISS_Files contiene la lista di tutti i files che compongono l’applicazione, anche questo file è stato creato come separato perchè abbiamo sviluppato un utility che è in grado di generare questa serie di dependencies automaticamente (promettiamo di pubblicarla prossimamente). ISS_DetectFramework4 è un file che contiene uno script in grado di riconoscere ed avvisare se il framework .NET necessario alla nostra applicazione (in questo caso il 4.0) non è installato sul sistema su cui si sta eseguendo il setup. E’ uno script standard, uguale per tutte le nostre applicazioni, quindi potremo averne un unica copia e usare sempre quella, in questo caso abbiamo preferito includerlo direttamente. Lo script non è opera nostra ma si trova pubblicato nei vari siti online dedicati a InnoSetup e noi siamo eternamente grati a chi lo ha scritto.

Vediamo cosa c’è nello script ISS_Files

Source: "bin\Debug\CheckTargetPlatform.exe"; DestDir: {app}; Flags: IgnoreVersion;
Source: "bin\Debug\Dnw.Base.v4.0.dll"; DestDir: {app}; Flags: IgnoreVersion;
Source: "bin\Debug\Dnw.Base.Wpf.v4.0.dll"; DestDir: {app}; Flags: IgnoreVersion;
Source: "bin\Debug\dnwIco.ico"; DestDir: {app}; Flags: IgnoreVersion;
Source: "bin\Debug\License.rtf"; DestDir: {app}; Flags: IgnoreVersion;

Per questa applicazione abbiamo l’exe, le 2 dll standard dotnetwork che abbiamo utilizzato quindi Base e Wpf, l’icona che utilizziamo per il setup e l’uninstaller e la licenza d’uso del programma.

Quando abbiamo parametrizzato questi files in base all’applicazione per cui creare il setup, c’è un ulteriore passo da fare, ovvero inserire il post build event relativo al progetto che generi il file di setup utilizzando il compilatore di InnoSetup.

call "%PROGRAMFILES%\Inno Setup 5\iscc.exe" "$(ProjectDir)ISS_Setup.iss"
cd $(ProjectDir)

La chiamata da fare è questa, ed è neutra, in modo che se manteniamo i nomi dei files ISS copiandoli da un progetto all’altro basta copiare ed incollare queste due righe nel post build event e tutto funziona.

Con questo comando, il setup viene compilato sia in Debug che in Release, se volessimo crearlo solo in Release, basta inserire una IF che controlli
$(ConfigurationName) la variabile di visual studio che contiene la modalità  in cui stiamo compilando.

the_setup[6]

Il file di setup sarà  creato su una sottocartella a livello della cartella della Solution che porta il nome standard di \InnoSetupCompiled, se non vi piace questa destinazione, basta modificare la seguente linea del file ISS_Setup.iss

[Setup]
...
...
OutputDir=..\InnoSetupCompiled

Nella cartella che preferite.

windows_setup[6]

Il pannello di controllo di windows, indicherà  l’applicazione installata con la riga qui sopra riportata.

Direi che abbiamo finito quindi riepiloghiamo che cosa abbiamo visto in questo post:

  • Abbiamo visto come si verifica l’architettura di compilazione di una Dll o di un Exe.
  • Abbiamo visto come salvare su un file dell’utente corrente il contenuto della UI per riproporlo all’avvio successivo dell’applicazione.
  • Abbiamo visto come configurare la creazione del setup di una applicazione .Net usando InnoSetup. e la sua integrazione con il post build event di visual studio per compilare automaticamente il setup.

Il codice del progetto esempio relativo alla nuova versione delle librerie di uso comune che comprende quanto spiegato in questo articolo è disponibile al link seguente:

Per qualsiasi domanda, approfondimento o curiosità , usate il link al modulo di contatto oppure il link al forum Microsoft dove troverete me ed altri esperti in grado di rispondervi.

Ricordiamo altresì che le librerie di uso comune vengono regolarmente aggiornate, pertanto se aveste scaricato una vecchia versione delle stesse, potrebbe non contenere tutte le classi usate in questo articolo, quindi aggiornate sempre anche le librerie di uso comune prima di compilare un progetto di test, scaricate sempre le librerie di uso comune dal link presente sull’articolo, saranno certamente quelle contenenti il codice sufficiente e necessario a compilare ed eseguire senza errori il progetto relativo all’articolo.