Qualche giorno fa, ero in ferie e ovviamente pioveva a dirotto, così ho deciso di fare una piccola applicazione che serve a me e ai miei colleghi in ufficio visto che lavoriamo quasi esclusivamente con aziende estere e più o meno tutte sono su fusi orari diversi dal nostro. Mentre sviluppavo la piccola applicazione che mi sono immaginata, mi sono resa conto che pure essendo semplice, potrebbe essere didatticamente interessante, pertanto, invece di farla divenire un progetto aziendale, l’ho fatta divenire un progetto DotNetwork, e la costruirò usando questo articolo, poi la modificherò aggiungendo nuove funzionalità e spiegando varie cose in alcuni articoli successivi.
L’applicazione verrà sviluppata in MVVM basico, cercando di far comprendere i concetti fondamentali del Binding e facendo vedere a chi si approccia alla programmazione C# XAML che non è difficile, che è potente e davvero ottima per creare applicazioni desktop.
La User Story
Partiamo indicando quali sono le richieste dei “clienti” in questo caso io e i miei colleghi. Spesso, ci troviamo ad organizzare video conferenze con i nostri clienti che si trovano su fuso orario diverso, pertanto se prima di chiamarli possiamo sapere che ore sono a casa loro evitiamo di svegliarli alle quattro del mattino, o di chiamarli mentre sono a pranzo o a cena. Per farlo, ci serve avere un orologio che possa mostrare l’ora di un diverso fuso orario. Ovviamente, l’appetito vien mangiando e la richiesta è divenuta, “Ma non potremmo avere un applicazione con una serie di orologi?”. Pertanto la storia è la seguente:
“Vorremmo un applicazione che ci mostri l’ora locale e l’ora di almeno due o tre diversi fusi orari, quelli che usiamo più spesso così è facile sapere come schedulare video conferenze, telefonate e anche appuntamenti.”
Technical requirements
Passiamo dalla storia raccontata dagli utenti alle note tecniche, cosa ci serve per realizzare quanto richiesto:
- Abbiamo bisogno di un oggetto che ci permetta di memorizzare le informazioni di configurazione di ogni orologio che vogliamo mostrare pertanto, la prima classe che ci serve è la classe informazioni sull’orologio che chiameremo ClockInfo.
- Dobbiamo creare più orologi, quindi ci servono più istanze dell’oggetto ClockInfo, quindi è opportuno avere una lista che li contenga, pertanto ci serve una classe collection che contenga le informazioni, la chiameremo ClocksInfoCollection.
- Oltre ai dati sugli orologi, avremo bisogno sicuramente di qualche altra informazione relativa all’applicazione, pertanto ci servirà una classe che possa contenere sia i dati relativi agli orologi che le ulteriori informazioni, questa nuova classe la chiameremo MultiClocksConfig.
- Per inserire le informazioni generiche di configurazione, ci servirà un oggetto adatto a memorizzarle su disco senza troppi problemi, questa classe conterrà due dati, un Nome ed un valore, considerato che saranno parametri di configurazione, la chiamiamo Setting.
- L’applicazione avrà una finestra principale che mostrerà gli orologi, questa sarà la MainWindow.
- I singoli orologi saranno tutti uguali e ne potremo avere una serie, pertanto è opportuno che ogni orologio sia un oggetto grafico, l’oggetto grafico più semplice da usare per generare un orologio, è uno User Control che chiameremo ClockControl.
- Per creare gli orologi, ci servirà una finestra di configurazione dove l’utente potrà inserire i parametri per ogni orologio che gli serve, questa sarà una seconda finestra che chiameremo SetClocksWindow.
- Mentre l’applicazione funziona, dovrà contenter le informazioni generali di contesto (come ad esempio i dati sugli orologi), che dovranno essere disponibili ad entrambe le finestre, questa classe, sarà un Singleton e si chiamerà AppContext.
- Per gestire le funzionalità dell’applicazione, ci serviranno dei Comandi, da utilizzare nelle finestre, pertanto creeremo una classe per ospitarli, che chiameremo AppCommands.
- Infine, ci servirà il necessario per effettuare il setup della nostra applicazione, visto che vogliamo distribuirla ai nostri utenti.
La Solution
Per creare la nostra applicazione, utilizziamo ovviamente un progetto applicazione WPF standard, come generato da visual studio:
Chiamiamo la soluzione MultiClock.
Ci verrà generato il progetto standard di WPF che contiene alcune classi, per le nostre esigenze lo modifichiamo nel modo seguente:
Abbiamo generato le cartelle che conterranno le varie classi, prima di iniziare però andiamo a modificare alcune cose a livello di progetto. Per fare questo, sul solution explorer facciamo doppio click su Properties per aprire la finestra di configurazione del progetto.
Lasciamo l’Assembly Name come proposto, questo sarà il nome del nostro file eseguibile: MultiClock.Exe. Cambiamo invece il Default Namespace inserendovi come prefisso DNW. Se avete usato un framework diverso dal 4.5.2, modificate il Target Framework per usare il 4.5.2, se volete seguire il progetto intero. L’icona standard dell’applicazione è stata da noi modificata con un icona della mia libreria personale, potete usare quella che volete, l’importante è che sia stata copiata sul root folder del progetto.
Nelle informazioni sull’assembly si aprono facendo click sul bottone Assembly Information, possiamo inserire i nostri dati come preferiamo.
Sulle informazioni di Build, dell’applicazione, ho mantenuto la compilazione Any CPU, ma ho tolto la check dall’opzione “Prefer 32-bit” che è stata introdotta in Visual Studio 2015, se lasciata selezionata produce un applicazione a 32bit, se deselezionata, l’applicazione funziona a 32bit sulle macchine x86 e a 64bit sulle macchine x64
Spostiamoci infine sulle opzioni di Firma dell’applicazione e firmiamo l’assembly con la chiave che uso per Dotnetwork se non ne avete generata mai una potete farlo al volo dalle opzioni della combobox. Tutte le applicazioni e le librerie che producete dovrebbero essere sempre firmate se volete saperne di più leggete Strong Named Assemblies Su MSDN.
Modificare App.xaml
Per iniziare a costruire la nostra applicazione, per prima cosa modifichiamo l’App.xaml, infatti, nell’App.xaml standard troveremo il seguente codice:
<Application x:Class="DNW.MultiClock.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" StartupUri="MainWindow.xaml"> <Application.Resources> </Application.Resources> </Application>
Ma nel nostro progetto abbiamo deciso di spostare le windows nella cartella Windows pertanto modifichiamo l’elemento StartupUri:
<Application x:Class="DNW.MultiClock.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" StartupUri="Windows\MainWindow.xaml"> <Application.Resources> </Application.Resources> </Application>
In questo modo, l’applicazione sa dove si troverà la MainWindow che andremo a generare.
Creiamo tutte le classi
Spostiamoci nella cartella Windows e generiamo, utilizzando “Add new Item” sul menu contestuale della cartella due classi window utilizzando il template WPF Window di Visual Studio.
Spostiamoci nella cartella Entities e generiamo le 3 classi per contenere le informazioni della nostra applicazione, utilizzando Add New Item sul menu contestuale della cartella per creare le tre classi utilizzando il template generico Class di Visual Studio.
Spostiamoci nella cartella Controls e generiamo lo User Control per i nostri orologi, utilizzando come sempre Add New Item sul menu contestuale della cartella e il template WPF User Control di Visual Studio.
Spostiamoci sulla cartella Context del progetto e generiamo la classe che conterrà i dati contestuali dell’applicazione, utilizzando Add New Item e il template Class di Visual Studio.
Spostiamoci sulla cartella Commands del progetto e generiamo un ulteriore classe generica dove creeremo i Command per la nostra applicazione con il template Class di Visual Studio.
Infine, spostiamoci sulla cartella Collections e generiamo una classe generica con il template Class di Visual Studio che diverrà poi la collection degli oggetti ClockInfo da utilizzare nell’applicazione.
Abbiamo creato tutte le classi che ci servono, ma ovviamente al momento sono vuote e inutili. Anche se, compilando e facendo partire l’applicazione dovremmo comunque ottenere una finestra vuota ma funzionante.
Le classi Entity e le classi di supporto all’applicazione
Iniziamo a produrre le classi che permetteranno di memorizzare i dati che useremo nel nostro programma, e le altre classi di supporto che non servono alla creazione dell’interfaccia utente.
Setting.cs
Questa è una classe che troverete in molti dei progetti passati del blog su cui state leggendo questo articolo, ma considerato che volevo creare un applicazione che contenesse tutto al suo interno, ho deciso di non utilizzare le librerie base di Dotnetwork, che sono scaricabili dal nostro sito, ma di creare tutto quello che serviva al suo interno.
using System; using System.ComponentModel; using System.Linq; using System.Text; namespace DNW.MultiClock.Entities { ///<summary> /// Class representing a named value used to memorize informations on the application state ///</summary> public class Setting : INotifyPropertyChanged { ... } }
La nostra classe Setting è molto semplice, ma essendo una classe che potremmo utilizzare a livello di User Interface, ho ritenuto opportuno implementare l’interfaccia INotifyPropertyChanged. Quest’interfaccia prevede l’implementazione dell’ evento PropertyChanged che deve essere sollevato dalle property della classe per segnalare la loro modifica a WPF che automaticamente aggiorna la User Interface al riguardo.
public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); }
L’implementazione dell’interfaccia INotifyPropertyChanged nella nostra classe
public const string FLD_Name = "Name"; private string mName; public string Name { get { return mName; } set { mName = value; OnPropertyChanged(FLD_Name); } }
La definizione relativa alla property Name (ricordo che un Setting è una coppia Name, Value).
public const string FLD_Value = "Value"; private string mValue; public string Value { get { return mValue; } set { mValue = value; OnPropertyChanged(FLD_Value); } }
La definizione relativa alla property Value.
public override string ToString() { return string.Format("{0} = {1}", Name, Value); }
Ho aggiunto l’implementazione del metodo ToString, il motivo per cui cerco di implementarlo sempre nelle classi dati è molto semplice, Visual Studio utilizza questo metodo per visualizzare automaticamente il contenuto di una classe quando siamo in Debug, pertanto per vedere cosa c’è dentro alle variabili mentre facciamo il debug di una applicazione è molto comodo implementarlo.
ClockInfo.cs
La classe che rappresenta la configurazione di un orologio.
using Newtonsoft.Json; using System; using System.ComponentModel; using System.Linq; using System.Text; using System.Xml.Serialization; namespace DNW.MultiClock.Entities { ///<summary> /// Clock Information, represents the configuration of a clock ///</summary> public class ClockInfo : INotifyPropertyChanged { ... } }
Come la precedente, anche questa classe implementa l’interfaccia che permette l’aggiornamento automatico dei dati a video alla modifica del contenuto, essendo un orologio, aggiorneremo il contenuto della classe e automaticamente MVVM e WPF aggiorneranno il video. Faccio notare due using che ho inserito, per poter utilizzare le due diverse serializzazioni, sia XML che JSON anche se in questo primo articolo implementeremo la versione JSON.
public ClockInfo() { TimeZoneName = TimeZoneInfo.Utc.StandardName; }
Il primo metodo che implementiamo è il costruttore, in cui imponiamo che la TimeZone dell’orologio, quando generato sia UTC ovvero Universal Standard Time, quindi l’ora di Greenwich senza alcun uso di Daylight Saving Time (ora legale).
public const string FLD_DateAndTime = "DateAndTime"; [JsonIgnore] [XmlIgnore] public DateTime DateAndTime { get { return mDateAndTime; } set { mDateAndTime = value; OnPropertyChanged(FLD_DateAndTime); OnPropertyChanged(FLD_Date); OnPropertyChanged(FLD_Time); } }
La property principale del nostro orologio contiene la data e ora come rappresentata sul fuso orario impostato nell’orologio stesso, questa proprietà è una proprietà che ha un uso solamente a runtime, quando stiamo visualizzando l’orologio, per questo è stata decorata con gli attributi JsonIgnore e XmlIgnore in modo che non venga salvata quando definiamo l’orologio. Faccio notare come alla modifica di questo valore, viene indicato che sono state modificate anche altre 2 property che guarda caso si chiamano Date e Time.
public const string FLD_Date = "Date"; [JsonIgnore] [XmlIgnore] public string Date { get { return DateAndTime.ToShortDateString(); } }
Questa proprietà a sola lettura restituisce la Data formattata dell’orologio, permettendo di visualizzare in due luoghi e in due modi diversi la data e l’ora del nostro orologio, anche essa è stata decorata con gli attributi perché sia ignorata in fase di serializzazione/deserializzazione anche se essendo read only dovrebbe essere automaticamente ignorata.
public const string FLD_Time = "Time"; [JsonIgnore] [XmlIgnore] public string Time { get { return DateAndTime.ToLongTimeString(); } }
Come la precedente, questa proprietà di sola lettura restituisce l’ora formattata del nostro orologio, anche in questo caso ci sono gli attributi per ignorarla in fase di serializzazione.
public const string FLD_Name = "Name"; private string mName; public string Name { get { return mName; } set { mName = value; OnPropertyChanged(FLD_Name); } }
Name ci permette di dare un nome o una descrizione di nostro gradimento all’orologio che sarà mostrata a video, questa è una delle property che saranno salvate in serializzazione.
public const string FLD_Order = "Order"; private int mOrder; public int Order { get { return mOrder; } set { mOrder = value; OnPropertyChanged(FLD_Order); } }
Order è un parametro gestito con dei tasti funzione specifici che permette di decidere in che ordine visualizzare gli orologi.
public const string FLD_TimeZoneName = "TimeZoneName"; private string mTimeZoneName; public string TimeZoneName { get { return mTimeZoneName; } set { mTimeZoneName = value; TimeZone = TimeZoneInfo.GetSystemTimeZones().FirstOrDefault(x => x.StandardName == value); if (TimeZone == null) { TimeZone = TimeZoneInfo.Utc; mTimeZoneName = TimeZone.StandardName; } OnPropertyChanged(FLD_TimeZoneName); } }
TimeZoneName è la property che ho scelto per salvare in modo semplice qual’è la time zone del nostro orologio, quando viene modificata, a sua volta modifica la property che rappresenta le vere informazioni sulla TimeZone, lo Standard Name della TimeZone, dovrebbe, e ci metto un condizionale purtroppo, essere un valore che non varia sul PC su cui è installato il programma, mentre la Descrizione completa della TimeZone, mi è sembrata essere eccessivamente verbosa per utilizzarla. Lo StandardName è compatto ed è comprensibile e come si può vedere, è facilmente utilizzabile per ricavare le informazioni sulla TimeZone dalla Collection delle Time Zones del sistema.
public const string FLD_TimeZone = "TimeZone"; private TimeZoneInfo mTimeZone; [JsonIgnore] [XmlIgnore] public TimeZoneInfo TimeZone { get { return mTimeZone; } set { mTimeZone = value; if (value != null) { mTimeZoneName = mTimeZone.StandardName; } OnPropertyChanged(FLD_TimeZone); OnPropertyChanged(FLD_TimeZoneName); } }
TimeZone, come potete vedere è collegata e dipende da TimeZoneName, ma è anche vero il contrario, infatti, vedremo come, quando l’applicazione carica i dati di configurazione degli orologi dal file JSON su cui sono salvati crea la TimeZone a partire dal suo nome. Viceversa, quando acquisiamo i dati della TimeZone a video, sulla Window di configurazione degli orologi, è la TimeZone a generare la TimeZoneName. L’uso della variabile a livello di classe, invece che del metodo Set della property TimeZoneName, impedisce che vi sia un Loop di aggiornamenti fra le 2 property. Anche in questo caso, questa property non viene salvata nei dati di configurazione.
public const string FLD_DST = "DST"; [JsonIgnore] [XmlIgnore] public string DST { get { if (TimeZone == null) return string.Empty; bool isDst = TimeZone.IsDaylightSavingTime(DateTimeOffset.Now); return isDst ? "DST On" : "DST Off"; } }
Questa property calcolata, serve per indicare se l’ora legale è attiva sull’orologio in questo istante.
public override string ToString() { StringBuilder sb = new StringBuilder(); sb.AppendFormat("{0} {1} ({2})", Name, TimeZoneName, DST); return (sb.ToString()); }
Anche in questo caso, il ToString è implementato per fini di debug, in modo da poter vedere cosa c’è dentro ad un oggetto ClockInfo a colpo d’occhio.
ClocksInfoCollection.cs
Dopo aver generato l’oggetto ClockInfo, andiamo a generare la classe che permetterà di conservarne un intera collezione.
using DNW.MultiClock.Entities; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; using System.Text; namespace DNW.MultiClock.Collections { ///<summary> /// A collection of Clocks configuration and working data ///</summary> public class ClocksInfoCollection : ObservableCollection<ClockInfo> { ... } }
Come potete osservare, ho creato una classe derivata da un Observable collection generica, questo perché la collezione verrà utilizzata dalla user interface, e l’Observable Collection fornisce automaticamente tutti gli eventi necessari a fare in modo che quando aggiungiamo o togliamo un elemento vengano sollevati gli eventi che permettono a WPF di aggiornare automaticamente i componenti visuali.
La classe implementa al suo interno 2 metodi, il primo, e più importante è quello che ci permette di gestire automaticamente l’ordinamento arbitrario degli orologi.
public void ReSort() { List<ClockInfo> sorted = new List<ClockInfo>(); foreach (ClockInfo cinfo in this.OrderBy(x => x.Order)) { sorted.Add(cinfo); } this.Clear(); foreach (ClockInfo cinfo in sorted) { this.Add(cinfo); } }
L’implementazione che ho fatto è davvero molto semplice, sono certa che vi sono dei metodi più ottimizzati e più sofisticati per creare una collezione ordinata (vi invito a cercare ad esempio la Keyed collection e verificare se può essere usata) ma nel nostro caso, visto il limitato numero di elementi e l’uso didattico ho preferito usare qualcosa di semplice. Quindi l’algoritmo crea una collezione ordinata degli elementi esistenti, pulisce la collezione attuale e vi rimette dentro gli elementi ordinati.
public override string ToString() { StringBuilder sb = new StringBuilder(); for (int i = 0; i < this.Count; i++) { sb.AppendLine(this[i].ToString()); } return (sb.ToString()); }
Ai fini del Debug, come per le altre classi dati, ho implementato un ToString che non fa altro che fare una lista dei contenuti della collezione.
MultiClocksConfig.cs
Questa è la classe che contiene tutti i dati di configurazione dell’applicazione MultiClock, quindi sia i dati di configurazione degli orologi che alcuni altri dati che potremo vedere poi all’interno delle nostre Window.
using DNW.MultiClock.Collections; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Linq; using System.Text; namespace DNW.MultiClock.Entities { ///<summary> /// Descrizione della classe: ///</summary> public class MultiClocksConfig : INotifyPropertyChanged { ... } }
Torniamo nella cartella Entities e creiamo la classe che conterrà tutti i dati della nostra applicazione, potremo definirla il Data Model dell’applicazione, ma non vorrei esagerare, pure utilizzando la tecnica MVVM vogliamo mostrare quanto sia facile usarla anche in modo semplificato, girando alla larga dall’ufficio complicazione affari semplici.
La nostra classe, anche in questo caso implementa INotifyPropertyChanged, visto che interagirà con la User Interface, e ora vediamo che cosa vi abbiamo inserito.
private const string FileName = "\\DNW\\MultiClocksConfig.json";
Questo è il nome del file in cui automaticamente saranno salvati i dati di configurazione dell’applicazione, vediamo come sarà utilizzato dai metodi di serializzazione e deserializzazione.
public MultiClocksConfig() { Clocks = new ClocksInfoCollection(); AutoSettings = new List<Setting>(); }
Nel costruttore della classe, vengono generate 2 collezioni di dati, la prima è la collezione degli orologi, la seconda è la collezione in cui sono automaticamente salvati i parametri di stato dell’applicazione.
public const string FLD_AutoSettings = "AutoSettings"; private List<Setting> mAutoSettings; public List<Setting> AutoSettings { get { return mAutoSettings; } set { mAutoSettings = value; OnPropertyChanged(FLD_AutoSettings); } }
La proprietà che rappresenta tutti i dati di stato automatici.
public ClocksInfoCollection Clocks { get { return mClocks; } set { mClocks = value; OnPropertyChanged(FLD_Clocks); } }
La proprietà che rappresenta la configurazione e lo stato corrente di tutti gli orologi gestiti dall’applicazione.
public static string GetConfigFileName() { return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), FileName); }
Questo è il metodo che compone il nome del file ove vengono salvati (e poi ricaricati) i dati di configurazione degli orologi e dell’applicazione. Al momento è un file arbitrario da noi imposto, che viene salvato in una sottocartella della cartella documenti dell’utente, pertanto l’applicazione è configurata in base alle preferenze dell’utente che la utilizza sul PC se due utenti la usassero, potrebbero avere degli orologi completamente diversi fra loro.
public static MultiClocksConfig LoadConfig(string path) { if (File.Exists(path)) { string data = File.ReadAllText(path); return JsonConvert.DeserializeObject<MultiClocksConfig>(data); } else { return new MultiClocksConfig(); } }
Questo metodo, carica i dati di configurazione salvati all’attivazione dell’applicazione, se l’applicazione non avesse alcun dato salvato (quindi la prima volta che viene lanciata) genera una classe vuota.
public void SaveConfig() { string saveFile = MultiClocksConfig.GetConfigFileName(); string dir = Path.GetDirectoryName(saveFile); if (!Directory.Exists(dir)) { Directory.CreateDirectory(dir); } File.WriteAllText(saveFile, Newtonsoft.Json.JsonConvert.SerializeObject(this)); }
Questo metodo, salva i dati di configurazione in modo che gli orologi configurati dall’utente possano essere ricaricati automaticamente al successivo avvio. Se siete dei principianti, potreste chiedervi perché il metodo Load ha la parola chiave static davanti e il metodo Save non ce l’ha? Il motivo è molto semplice, il metodo Load crea una nuova istanza della nostra classe MultiClocksConfig, quindi se non fosse statico, ci obbligherebbe ad istanziare inutilmente una classe per poterlo lanciare, quando poi il metodo ci restituisce un istanza nuova in modo che possiamo usarla nella nostra applicazione. Il metodo Save, invece, salva il contenuto dell’istanza corrente, pertanto non deve essere static.
public string GetAutoSetting(string name) { Setting stt = AutoSettings.FirstOrDefault(x => x.Name == name); if (stt != null) return stt.Value; return null; }
public void SetAutoSetting(string name, string value) { Setting stt = AutoSettings.FirstOrDefault(x => x.Name == name); if (stt == null) { stt = new Setting() { Name = name }; AutoSettings.Add(stt); } stt.Value = value; SaveConfig(); }
I due metodi qui sopra, permettono di salvare e di recuperare un oggetto di tipo Setting dalla nostra collezione AutoSetting, permettendoci di salvare dei dati di stato relativi all’applicazione, in questa versione, vedremo un primo dato importante, nelle lezioni successive probabilmente aggiungeremo nuovi dati a quelli già salvati.
AppCommands.cs
I Routed command, sono uno degli oggetti più comodi per gestire menu, menu contestuali, ribbon, toolbar e ogni altro tipo di oggetto attivo nell’applicazione. La cosa interessante che fanno è permettere di definire un solo metodo che gestisce un comando e applicare il comando a uno o più oggetti.
using System; using System.ComponentModel; using System.Linq; using System.Text; using System.Windows.Input; namespace DNW.MultiClock.Commands { ///<summary> /// Descrizione della classe: ///</summary> public class AppCommands { #region Public Fields /// <summary> /// Delete a clock /// </summary> public static readonly RoutedCommand DeleteClock = new RoutedCommand(); /// <summary> /// Create a new clock /// </summary> public static readonly RoutedCommand NewClock = new RoutedCommand(); /// <summary> /// Pull Down a clock /// </summary> public static readonly RoutedCommand OrderDown = new RoutedCommand(); /// <summary> /// Push up a clock /// </summary> public static readonly RoutedCommand OrderUp = new RoutedCommand(); /// <summary> /// Saves configured clocks /// </summary> public static readonly RoutedCommand Save = new RoutedCommand(); /// <summary> /// Set one or more clocks and save them /// </summary> public static readonly RoutedCommand SetClocks = new RoutedCommand(); #endregion Public Fields } }
La definizione dei routed command è molto semplice, pure essendo potente. Ovviamente vi possono essere dei command più articolati, ma come sempre, un passo alla volta.
AppContext.cs
using DNW.MultiClock.Entities; using System; using System.ComponentModel; using System.Linq; using System.Text; namespace DNW.MultiClock.Context { ///<summary> /// Descrizione della classe: ///</summary> public class AppContext { ... } }
Questa classe è un po’ particolare per questa applicazione, ma è una classe che utilizzo in tutte le mie applicazioni, il suo nome dovrebbe dirci qualche cosa, AppContext ovvero Contesto Applicativo, in questa applicazione in particolare, questa classe conterrà al suo interno l’istanza della classe che contiene i dati di configurazione e di stato degli orologi gestiti dall’applicazione, ma è solo per caso, perché questi dati servono ad entrambe le Window che compongono la nostra applicazione. Solitamente, la classe AppContext, nelle mie applicazioni contiene tutti e soli i parametri di configurazione che devono essere condivisi da tutti i componenti di una applicazione, se vi chiedete quali siano, l’esempio è semplice:
- La stringa di connessione a Sql Server (o altro database) se l’applicazione ne usa uno
- Le preferenze utente, quali ad esempio la lingua dell’interfaccia, oppure la dimesione delle font, i colori di determinate parti importanti dei contenuti.
- I dati di configurazione applicativi, ad esempio quelli della licenza, per gestire l’attivazione/disattivazione di funzionalità.
- Le cartelle di lavoro se l’applicazione fa uso di files sul file system.
Tutti questi solitamente sono dati che non sono specifici di una porzione dell’applicazione ma devono essere utilizzati ovunque, per questo, la classe AppContext è un Singleton, ovvero una classe speciale che viene istanziata una sola volta la prima volta che qualcuno ha bisogno del suo contenuto e poi rimane attiva e visibile per tutta la durata dell’applicazione o fino a che qualcuno non decide di resettarla.
Vediamo come si fa una classe Singleton.
private static volatile AppContext mInstance; public static AppContext Instance { get { if (mInstance == null) { lock (syncRoot) { if (mInstance == null) mInstance = new AppContext(); } } return mInstance; } }
private static object syncRoot = new Object();
La prima cosa da fare per definire un Singleton, è generare una property statica del tipo della classe stessa il cui nome, per convenzione, è sempre Instance. Come potete notare, Instance è una property ReadOnly, che viene generata automaticamente alla prima richiesta dal controllo sul fatto che sia null. L’uso del costrutto lock per la sua creazione è introdotto per renderla Thread Safe, ovvero fare in modo che se vi fossero due richieste contemporanee di generarla, sarebbero soddisfatte in sequenza evitando possibili inconsistenze. L’oggetto utilizzato per il lock è anche esso definito syncRoot per convenzione. A questo punto, ogni cosa all’interno della nostra classe AppContext può essere richiesta ovunque utilizzando la property Instance, e vedremo come sarà usata dalle nostre Windows.
public MultiClocksConfig ConfigData { get { if (mConfigData == null) { ConfigData = MultiClocksConfig.LoadConfig(MultiClocksConfig.GetConfigFileName()); } return mConfigData; } set { mConfigData = value; } }
All’interno della classe singleton, abbiamo una property (in questo caso NON statica, ed è importante che non lo sia, che viene inizializzata con una istanza della classe MultiClocksConfig, se ricordate quanto abbiamo visto nelle classi precedenti, se l’applicazione non fosse mai stata usata conterrebbe un istanza vuota, altrimenti i dati degli orologi salvati all’ultimo uso dell’applicazione.
public void Reset() { mConfigData = null; }
Il metodo Reset, ci permette, se necessario, di azzerare i dati per poterli rileggere da quelli salvati alla successiva richiesta. Probabilmente non avrà molto senso ancora per chi è un principiante, ma vedremo in questa e probabilmente nelle prossime lezioni quale sia il suo uso.
Le classi della User Interface, ovvero le classi Visuali
Descriviamo ora le tre classi visuali della nostra applicazione, ovvero lo User Control che rappresenterà i nostri orologi e le due finestre, la prima, la finestra principale dell’applicazione che mostra gli orologi, la seconda, la finestra che permette di configurare gli orologi che vogliamo utilizzare.
ClockControl.xaml e ClockControl.xaml.cs
Per i principianti, ogni classe visuale di WPF è solitamente composta da due file, il file .xaml che contiene tutto quello che riguarda la forma dell’oggetto e il file .xaml.cs che contiene invece il codice che serve a dare interattività alla parte visuale. Nelle applicazioni più complesse, solitamente c’è una terza classe, il ViewModel, che astrae la gestione dei dati di supporto al componente visuale. Nel nostro caso, essendo una applicazione molto semplice, il file .xaml.cs farà anche da ViewModel.
Quello che vogliamo ottenere in questa prima versione del nostro componente ClockControl è quanto visibile qui sopra, ovvero un oggetto che visualizza:
- Il nome dell’orologio
- Se il Daylight Saving Time è attivo oppure no (ora legale)
- La data corrente sul fuso orario richiesto
- L’ora corrente sul fuso orario richiesto
- Il nome standard del fuso orario
Per fare questo, nel nostro componente utilizzeremo alcuni controlli standard di WPF.
<UserControl x:Class="DNW.MultiClock.Controls.ClockControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:lctl="clr-namespace:DNW.MultiClock.Controls" xmlns:lcmd="clr-namespace:DNW.MultiClock.Commands" mc:Ignorable="d" d:DesignHeight="180" d:DesignWidth="300"> ... </UserControl>
Per prima cosa, la parte che riguarda il corrispondente XAML degli Using di C# e le dimensioni base del controllo.
- x:class – è l’attributo che contiene il nome della classe che stiamo rappresentando completo del suo intero namespace, è esattamente identica alla definizione di una partial class C# e sarà traslata proprio in tal modo dal compilatore XAML.
- xmlns: – i primi 4 sono standard nel template e sono le using dei componenti base di WPF
- xmlns:lctl – questo è il namespace che mappa i Controls, che guardacaso è la cartella dove si trova il nostro componente lctl mi piace perché è una abbreviazione concisa di local controls ma potete usare quello che preferite. Vedremo la sua utilità nelle prossime lezioni.
- xmlns:lcmd – Questo è il namespace che contiene i Commands, anche in questo caso è un nome convenzionale che uso io, potete usare quello che preferite per me lcmd suona bene come abbreviazione per local commands.
<Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> ... </Grid>
L’oggetto Grid di WPF è un ottimo oggetto per costruire interfacce tabellari, permette di definire righe e colonne ed inserire oggetti in ogni cella così generata, nel nostro caso, abbiamo creato 4 righe che ci permetteranno di inserire i vari dati. Se siete curiosi di sapere cosa significano le misure indicate per le altezze, ebbene Auto significa che la riga deve occupare tutto e solo lo spazio dell’oggetto o degli oggetti che contiene. mentre *(star) indica che la riga deve occupare tutto lo spazio che può. se volete capirne di più: potete guardare questo tutorial.
<TextBlock Grid.Row="0" Margin="0" Padding="4" HorizontalAlignment="Stretch" VerticalAlignment="Center" Background="Black" Foreground="#FFD2DF33" FontFamily="Courier New" FontSize="24" FontStyle="Normal" FontWeight="Bold" TextAlignment="Left" Text="{Binding Path=Clock.Name}"/>
All’interno delle nostre righe, iniziamo a mettere i controlli visuali, nella prima riga inseriemo un oggetto TextBlock il TextBlock è simile ad una Label di Windows Forms, contiene del testo formattato, in realtà è molto più potente perché a sua volta può contenere una collezione di oggetti Run ciascuno dei quali può a sua volta essere formattato. La sua forma è più simile alla rappresentazione di un paragrafo di testo, che a quella di una Label. Vediamo cosa significano i valori che abbiamo assegnato agli attributi dell’oggetto.
- Grid.Row – Questa è una Attached Property, ovvero una property che appartiene alla griglia che contiene il TextBlock e indica qual’è la riga della stessa in cui il controllo deve apparire, le righe vanno da 0 a N se indicata una riga inesistente il controllo andrà sulla prima o sull’ultima.
- Margin – Rappresenta lo spazio attorno al controllo stesso, in questo caso 0 ma può anche essere indicato con quattro numeri che sono in sequenza: Left, Top, Right, Bottom quindi Margin=”4,2,4,2″ vuol dire quattro unità di spazio a sinistra, due sopra, quattro a destra, due sotto. le unità di WPF sono piuttosto complesse e dipendono dalla risoluzione del video, pertanto vi consiglio un altra lettura: Com’è fatto un oggetto Thickness.
- Padding – Rappresenta lo spazio vuoto interno all’elemento prima del suo contenuto anche esso come il margine può essere espresso con un numero (nel nostro caso 4) oppure con 4 cifre, Left, Top, Right, Bottom.
- HorizontalAlignment – Rappresenta l’allineamento orizzontale del controllo nel suo contenitore, può essere Left, Right, Center oppure Stretch, l’ultimo valore, indica che il controllo deve allargarsi fino ad occupare tutto lo spazio orizzontale fornito dal suo contenitore.
- VerticalAlignment – Rappresenta l’allineamente verticale del controllo nel suo contenitore, può essere Top, Bottom, Center o Stretch, con lo stesso significato del precedente solo in verticale.
- Background – E’ il colore dello sfondo del controllo, nel nostro caso Black, può essere un colore nominato come in questo caso, oppure un codice esadecimale a otto cifre formato da #TTRRGGBB dove TT è l’opacità 00=trasparente FF=opaco, RR è il rosso, GG è il verde BB è il blu.
- Foreground – E’ il colore del testo, in questo controllo, nel nostro caso un bel giallo fastidio.
- FontFamily – Contiene il nome della font da usare, se è una font inesistente sul sistema, probabilmente il sistema approssimerà a qualcosa di simile o un valore a caso, per questo ho usato il Courier New che c’è ovunque.
- FontSize – La dimensione della font, sempre nelle unità specifiche di WPF.
- FontStyle – permette di indicare se usare il normale o l’italico.
- FontWeight – permette di assegnare il peso della font, nel nostro caso Bold per grassetto.
- TextAlignment – Left è l’allineamento del testo all’interno del perimetro del controllo, avendo usato lo Stretch per l’allineamento orizzontale è necessario usarlo per indicare di allinearlo a sinistra.
- Text – Questa è la property più importante, infatti è quella che contiene il testo visualizzato, come potete notare, non vi abbiamo messo un valore, ma qualcosa di più complicato, ovvero un Binding. A beneficio dei principianti, il Binding (tradotto come collegamento in italiano) ci permette di collegare qualsiasi Property di un controllo visuale al valore di una Property del DataContext in cui si trova il controllo stesso. In questo caso, il nostro Binding collega alla property Text il valore di Clock.Name ovvero la property Name di un oggetto di tipo ClockInfo.
<Grid Grid.Row="1"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <TextBlock Grid.Column="0" Margin="0" Padding="4" HorizontalAlignment="Stretch" VerticalAlignment="Center" Background="#FFD2DF33" Foreground="Black" FontFamily="Courier New" FontSize="16" FontStyle="Normal" FontWeight="Bold" TextAlignment="Right" Text="{Binding Clock.DST}"/> <TextBlock Grid.Column="1" Margin="0" Padding="4" HorizontalAlignment="Stretch" VerticalAlignment="Center" Background="Black" Foreground="#FFD2DF33" FontFamily="Courier New" FontSize="16" FontStyle="Normal" FontWeight="Bold" TextAlignment="Right" Text="{Binding Clock.Date}"/> </Grid>
Nella seconda riga della nostra Grid, dobbiamo mettere due oggetti, l’indicatore della Daylight Saving Time, e la data corrente. Per farlo, come ho detto prima, creiamo una nuova grid “concentrica” che ha 2 colonne, una grande come il suo contenuto dove mettiamo il segnale della DST, e l’altra che occupa il resto dello spazio per la data. Abbiamo utilizzato altre due TextBlock, e abbiamo utilizzato nuovamente il Binding per agganciare i controlli al dato presente nella Property Clock, rispettivamente le property DST e Date, inoltre la casella del DST ha i colori inversi rispetto al resto.
<TextBlock Grid.Row="2" Margin="0" Padding="4" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="Black" Foreground="#FFD2DF33" FontFamily="Courier New" FontSize="64" FontStyle="Normal" FontWeight="Bold" TextAlignment="Center" Text="{Binding Clock.Time}"/>
Nella terza riga della grid principale abbiamo inserito l’orologio, che abbiamo reso più grande e allineato al centro. Anche l’orologio è in Binding alla property Time della property Clock.
<TextBlock Grid.Row="3" Margin="0" Padding="4" HorizontalAlignment="Stretch" VerticalAlignment="Center" Background="Black" Foreground="#FFD2DF33" FontFamily="Courier New" FontSize="14" FontStyle="Normal" FontWeight="Bold" TextAlignment="Center" Text="{Binding Clock.TimeZoneName}"/>
Nell’ultima riga, abbiamo aggiunto il nome della Time Zone in modo che l’utente possa averlo sott’occhio sempre collegando in Binding la property Clock.TimeZoneName.
Vediamo ora invece cosa abbiamo scritto nel codice C# che sta dietro al nostro controllo:
using DNW.MultiClock.Entities; using System; using System.ComponentModel; using System.Linq; using System.Text; using System.Windows.Controls; using System.Windows.Threading; namespace DNW.MultiClock.Controls { /// <summary> /// Interaction logic for ClockControl.xaml /// </summary> public partial class ClockControl : UserControl, INotifyPropertyChanged { ... } }
La prima cosa da notare, nella classe è che, come vi ho anticipato parlando dello XAML, la classe viene indicata come partial class, e public, questo perché tutti gli oggetti che dovranno essere utilizzati da XAML dovranno essere pubblici. La nostra classe ClockControl, è una classe derivata da UserControl ed implementa l’interfaccia INotifyPropertyChanged, per fare in modo che le sue Proprietà, informino la User Interface quando il proprio valore cambia. Sono certa che potreste dirmi, perché invece di usare questa interfaccia non hai utilizzato le Dependency property di WPF, principalmente perché non sempre è possibile farlo e soprattutto perché questa applicazione è dedicata a chi sta iniziando, pertanto cerchiamo di utilizzare tutti i concetti noti a chi ha già fatto un po’ di OOP e in seguito potremo estendere le cose utilizzando gli strumenti specifici di WPF.
public ClockControl() { InitializeComponent(); DataContext = this; mClockTimer = new DispatcherTimer(); mClockTimer.Interval = TimeSpan.FromSeconds(1); mClockTimer.Tick += Updateclock; }
Nel costruttore del nostro controllo, troviamo l’InitializeComponent di sistema (attenzione ai principianti, non rimuovete quella riga di codice o tutto esploderà). Dopo di esso un’istruzione fondamentale, l’istruzione che indica al controllo dove si trovano i dati che deve mettere in Binding con i controlli, in questo caso in se stesso. Oltre a questo, viene generato e collegato ad un event handler un timer che lancerà un evento una volta al secondo e che sono certa possiate dedurre serva ad aggiornare l’orologio.
private DispatcherTimer mClockTimer;
La definizione del timer, in questo caso, siccome il timer è usato solo a livello di codice della classe, non ci serve una property, ci basta la variabile a livello di classe.
private void Updateclock(object sender, EventArgs e) { if (Clock != null) { Clock.DateAndTime = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, Clock.TimeZone); } }
L’event handler che risponde allo scattare del timer ad ogni secondo e va ad aggiornare l’orologio in base alle informazioni della TimeZone dell’oggetto Clock e alla data corrente in formato UTC (universal time, ovvero ora di greenwich senza DST per i Fan dei telefilm americani l’ora ZULU).
public const string FLD_Clock = "Clock"; private ClockInfo mClock; public ClockInfo Clock { get { return mClock; } set { mClockTimer.Stop(); mClock = value; mClockTimer.Start(); OnPropertyChanged(FLD_Clock); } }
La property Clock, che è del tipo della nostra classe ClockInfo e conterrà il necessario a fornire al controllo i dati da visualizzare. Come spero abbiate notato, se i dati dell’orologio vengono aggiornati, viene fatto fermare e poi ripartire il timer.
Il componente principale della nostra applicazione è fatto, adesso passiamo alla finestra di visualizzazione.
MainWindow.xaml – MainWindow.xaml.cs
La finestra principale del programma, conterrà al suo interno lo spazio per inserire i componenti orologio e il necessario a comandare i nostri orologi permettendoci di modificarne il numero e i fusi orari visualizzati.
Un esempio della nostra finestra principale, abbiamo deciso per una finestra senza titolo e senza pulsanti standard, il motivo? perché ci piaceva così, e perché ci permetteva di farvi vedere come si fa. Possiamo notare che la nostra finestra è così composta:
- Un area vuota, in cima, corredata di 2 bottoni, uno per chiudere l’applicazione, l’altro per andare a modificare gli orologi visualizzati, lo spazio vuoto ha anche un altro scopo, quello di dare una piccola area che permetta il Drag della finestra nello spazio dello schermo che la contiene.
- Lo spazio principale della finestra contiene l’orologio o meglio gli orologi,
- Nel caso lo spazio non fosse sufficiente a mostrare tutti gli orologi, appare automaticamente la scrollbar verticale.
- Nell’angolo inferiore destro c’è il grip (linguetta?) per poter allargare o stringere la finestra,
- Inoltre abbiamo dato alla finestra un bordo del colore degli orologi.
<Window x:Class="DNW.MultiClock.Windows.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:wr="clr-namespace:DNW.MultiClock.Windows" xmlns:lcmd="clr-namespace:DNW.MultiClock.Commands" mc:Ignorable="d" Title="Clock Window" Height="350" Width="525" WindowStyle="None" ResizeMode="CanResizeWithGrip" BorderBrush="#FFD2DF33" BorderThickness="4" Background="Black" AllowsTransparency="True" Loaded="Window_Loaded" MouseDown="Window_MouseDown"> ... </Window>
Potete vedere che la definizione della window è simile a quella dello User Control per quel che riguarda i Namespaces, il resto delle cose sono quelle che ci permettono di definirne l’aspetto:
- WindowStyle=None indica che la finestra non deve avere titolo e pulsanti standard.
- ResizeMode=CanResizeWithGrip, permette di ridimensionare la finestra e fa apparire il “grip” per afferrarla e allargarla o stringerla.
- BorderBrush=”#FFD2DF33” indica il colore giallo del bordo
- BorderThickness=4 Indica lo spessore del bordo della finestra.
- Background=Black Indica il colore di sfondo
- AllowTransparency=True permette di eliminare titolo, pulsanti e bordi standard.
- Loaded= E’ l’evento eseguito al caricamento della finestra che useremo per creare gli orologi.
- MouseDown= E’ l’evento che useremo per pilotare la possibilità di muovere la finestra a video afferrandola nella zona vuota accanto ai pulsanti, perché senza la zona del titolo non saremmo in grado di muoverla.
<Window.CommandBindings> <CommandBinding Command="{x:Static lcmd:AppCommands.SetClocks}" CanExecute="SetClocks_CanExecute" Executed="SetClocks_Executed" /> <CommandBinding Command="{x:Static ApplicationCommands.Close}" CanExecute="Close_CanExecute" Executed="Close_Executed"/> </Window.CommandBindings>
Il Tag CommandBindings, definisce una attached property dell’oggetto window che contiene una collezione di definizioni dei Command che la Window Gestisce e degli eventi a cui ciascuno di essi risponde. In questo caso abbiamo due bottoni, e due command, il command SetClocks, che modifica gli orologi, ed il command Close. potete notare come per pilotare Close non ho definito un command specifico ma ho usato un elemento della classe ApplicationCommands, che è una classe del framework che fornisce già una serie di command che possiamo utilizzare nelle nostre applicazioni. Ogni command risponde a 2 eventi, Can Execute, che ci permetterebbe di attivare o disattivare i pulsanti, e Executed, che è l’event handler da eseguire sul click.
<Window.ContextMenu> <ContextMenu> <MenuItem Header="Set Clocks" Command="{x:Static lcmd:AppCommands.SetClocks}"/> <MenuItem Header="Close" Command="{x:Static ApplicationCommands.Close}"/> </ContextMenu> </Window.ContextMenu>
Per dimostrare che I Command possono essere utilizzati da diversi elementi della User Interface, ho creato anche un menu contestuale per l’intera finestra, che fornisce due opzioni che corrispondono ai due command.
<Grid Background="Black"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> ... </Grid>
La Grid principale della window, come potete vedere contiene due sole righe, la prima che ospita i bottoni, e la seconda che ospiterà gli orologi.
<StackPanel Grid.Row="0" Margin="0" Orientation="Horizontal" FlowDirection="RightToLeft" > <Button ToolTip="Close Clock" Command="{x:Static ApplicationCommands.Close}" Background="Black" > <Image Stretch="Uniform" MaxHeight="16" MaxWidth="16" Source="pack://application:,,,/Images/btn_032_890.png"/> </Button> <Button ToolTip="Set Clocks" Command="{x:Static lcmd:AppCommands.SetClocks}" Background="Black" > <Image Stretch="Uniform" MaxHeight="16" MaxWidth="16" Source="pack://application:,,,/Images/btn_032_884.png"/> </Button> </StackPanel>
La prima riga della Grid, ospita al suo interno uno StackPanel, come il suo nome può farci indovinare, lo StackPanelpuò contenere una pila di oggetti che vengono impilati al suo interno secondo varie regole. Nel nostro caso, sono:
- Orientation = Horizontal, la nostra pila è orizzontale.
- FlowDirection = RightToLeft, la lista viene visualizzata da destra a sinistra
All’interno dello stack panel sono i due Button, che contengono un immagine ridotta a 16 x 16 pixel le 2 immagini sono state aggiunte al progetto nella cartella Images come “Resources” dell’applicazione e verranno incluse all’interno dell’eseguibile automaticamente dal compilatore. la stringa all’interno della property Source indica l’URI dell’immagine. Se volete capirne di più: Pack Uris in WPF su MSDN.
Come potete notare, abbiamo assegnato la property Command dei due button con lo stesso valore usato per il menu contestuale.
<ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto" Background="Black"> <StackPanel Name="ClocksContainer" Orientation="Vertical" > </StackPanel> </ScrollViewer>
L’ultima parte della nostra Window è composta da un controllo ScrollViewer, che ci fornisce la barra di scorrimento verticale e la virtualizzazione dei controlli. Che contiene al suo interno un secondo StackPanel stavolta con orientamento verticale. la Flow direction di default è TopDown quindi non serve indicarla. Ciò che dovete Notare invece è il fatto che questo Stack Panel, unico all’interno della finestra e anche del precedente controllo, è stato dotato di un valore sulla Property Name.
Al contrario di quanto era in Windows Forms e nei suoi predecessori, Visual C++ e Visual Basic, in WPF i controlli non devono avere un nome se non in un caso, quando tali controlli devono essere utilizzati dall’interno del codice C#. Nel nostro caso, penso possiate immaginare che lo stack panel ha un nome perché dovremo creare e inserire al suo interno i componenti ClockControl per i nostri orologi.
using DNW.MultiClock.Collections; using DNW.MultiClock.Context; using DNW.MultiClock.Controls; using DNW.MultiClock.Entities; using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Text; using System.Windows; using System.Windows.Input; using System.Windows.Media.Imaging; namespace DNW.MultiClock.Windows { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window, INotifyPropertyChanged { ... } }
Come il nostro User Control, la nostra Window deriva dalla classe Window standard di WPF e implementa INotifyPropertyChanged, per lo stesso motivo per cui lo implementa anche lo User Control.
public MainWindow() { InitializeComponent(); this.Icon = BitmapFrame.Create(new Uri("pack://application:,,,/btn884.ico", UriKind.RelativeOrAbsolute)); WindowStartupLocation = WindowStartupLocation.CenterScreen; DataContext = this; }
Il costruttore della Window, oltre allo standard Initialize component, fa tre cose importanti:
- Assegna un icona alla finestra, anche se non si vede a video serve per la toolbar del desktop.
- Assegna la Startup Location della window a CenterScreen, questo farà in modo che se avete più di un monitor, la finestra appaia sul monitor che state correntemente guardando e su cui avete posizionato il mouse.
- Assegna al DataContext la finestra stessa, anche in questo caso perché la finestra è il ViewModel di se stessa.
public ClocksInfoCollection Clocks { get { if (AppContext.Instance.ConfigData == null) return null; return AppContext.Instance.ConfigData.Clocks; } }
Questa property è definita semplicemente per essere uno shortcut, in modo da non dover digitare tutta la pappardella AppContext.Instance.ConfigData.Clocks ogni volta che dobbiamo accedere alla lista degli orologi.
protected override void OnClosed(EventArgs e) { if (this.WindowState == WindowState.Normal) { AppContext.Instance.ConfigData.SetAutoSetting(string.Format("{0}_Size", this.Name), string.Format("{0};{1}", this.Width, this.Height)); } base.OnClosed(e); }
protected override void OnInitialized(EventArgs e) { base.OnInitialized(e); string size = AppContext.Instance.ConfigData.GetAutoSetting(string.Format("{0}_Size", this.Name)); if (size != null) { string[] data = size.Split(';'); double width = this.Width; double height = this.Height; if (data.Length >= 1) { double.TryParse(data[0], out width); } if (data.Length >= 2) { double.TryParse(data[1], out height); } this.Width = width; this.Height = height; } }
Per dimostrarci a cosa servono gli AutoSettings e per rendere il nostro orologio user friendly, utilizziamo gli eventi Initialized e Closed della Window per salvare e poi ripristinare la dimensione della finestra, in questo modo, se un utente rimpicciolisse o ingrandisse la finestra a suo piacere, la ritroverebbe della stessa dimensione al riavvio.
private void Window_Loaded(object sender, RoutedEventArgs e) { if (AppContext.Instance.ConfigData.Clocks.Count == 0) { var timeZones = TimeZoneInfo.GetSystemTimeZones(); ClockInfo cli = new ClockInfo(); cli.Name = "Local Time"; cli.TimeZoneName = TimeZoneInfo.Local.StandardName; cli.Order = 0; Clocks.Add(cli); cli = new ClockInfo(); cli.Name = "UTC"; cli.TimeZoneName = TimeZoneInfo.Utc.StandardName; cli.Order = 1; Clocks.Add(cli); cli = new ClockInfo(); cli.Name = "New York"; TimeZoneInfo tzi = timeZones.FirstOrDefault(x => x.DisplayName.Contains("Eastern Time")); if (tzi != null) { cli.Order = 2; cli.TimeZoneName = tzi.StandardName; Clocks.Add(cli); } AppContext.Instance.ConfigData.SaveConfig(); } LoadClocks(); }
Al caricamento della finestra, se non ci sono orologi ne generiamo tre come esempio e poi eseguiamo il metodo che crea i componenti a video.
private void LoadClocks() { foreach (ClockInfo cinfo in Clocks.OrderBy(x => x.Order)) { ClockControl cctrl = new ClockControl(); cctrl.Clock = cinfo; ClocksContainer.Children.Add(cctrl); } }
Il metodo di creazione dei componenti ci mostra quanto sia semplice creare componenti da codice in WPF, in questo caso, per ogni orologio definito, viene creato un ClockControl, gli viene assegnato il corrispondente oggetto ClockInfo e viene aggiunto ai Children dello stack panel ClocksContainer che abbiamo creato nello xaml.
private void Window_MouseDown(object sender, MouseButtonEventArgs e) { if (e.ChangedButton == MouseButton.Left) { App.Current.MainWindow.DragMove(); } }
L’evento Mouse Down della finestra, viene gestito per permettere di spostare la finestra, con questo codice standard che reagisce al solo bottone sinistro, che è l’unico funzionante per default.
private void Close_CanExecute(object sender, CanExecuteRoutedEventArgs e) { e.CanExecute = true; } private void Close_Executed(object sender, ExecutedRoutedEventArgs e) { Application.Current.Shutdown(); }
Gli event handler collegati al command Close, in questo caso molto semplici, chiudono l’applicazione.
private void SetClocks_CanExecute(object sender, CanExecuteRoutedEventArgs e) { e.CanExecute = true; } private void SetClocks_Executed(object sender, ExecutedRoutedEventArgs e) { try { if (mSetClocksWindow != null) { mSetClocksWindow.WindowState = WindowState.Normal; mSetClocksWindow.Topmost = true; } else { mSetClocksWindow = new SetClocksWindow(); mSetClocksWindow.Icon = this.Icon; mSetClocksWindow.Owner = this; mSetClocksWindow.Closed += delegate { mSetClocksWindow = null; }; mSetClocksWindow.ClocksChanged += SetClocksWindow_ClocksChanged; mSetClocksWindow.Show(); } } catch (Exception ex) { MessageBox.Show(ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error); } }
Gli event handler del command SetClocks sono un po’ più complicati ma non troppo, anche questo bottone è sempre attivo, e utilizziamo una variabile a livello di classe per aprire la finestra di modifica degli orologi una sola volta, onde evitare che gli utenti ne aprano più sessioni e poi non capiscano perché non tutte le modifiche sono state accettate. La finestra ha anche un evento specifico, che solleva quando vengono modificati e salvati gli orologi.
private void SetClocksWindow_ClocksChanged(object sender, EventArgs e) { try { ClocksContainer.Children.Clear(); LoadClocks(); } catch (Exception ex) { MessageBox.Show(ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error); } }
Al salvataggio degli orologi, vengono ricaricati gli User Control con i dati modificati, quindi orologi eliminati, modificati oppure nuovi inserimenti.
SetClocksWindow.xaml – SetClocksWindow.xaml.cs
La terza ed ultima classe della User Interface, è quella che ci permetterà di poter generare gli orologi che ci servono e puntarli sul fuso orario più consono. Pertanto senza perdere tempo in merito, vediamo come è fatta.
<Window x:Class="DNW.MultiClock.Windows.SetClocksWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:wr="clr-namespace:DNW.MultiClock.Windows" xmlns:lcmd="clr-namespace:DNW.MultiClock.Commands" mc:Ignorable="d" Title="Set Clocks" Height="300" Width="300" Loaded="SetClocksWindow_Loaded"> ... </Window>
Le dichiarazioni delle using sono simili a quelle della main window, anzi, anche più semplici visto che questa sarà una normalissima finestra di windows, con titolo e pulsanti massimizza, minimizza eccetera. L’unica cosa da notare è l’event handler per l’evento Loaded della finestra, dove gestiremo il caricamento dei dati. Avviso in anticipo i lettori che questa è la versione 1.0 del progetto, pertanto faremo solo le cose essenziali, in seguito, vedremo come migliorare e dare maggiore professionalità al nostro orologio multi fuso.
<Window.CommandBindings> <CommandBinding Command="{x:Static lcmd:AppCommands.Save}" CanExecute="Save_CanExecute" Executed="Save_Executed" /> <CommandBinding Command="{x:Static lcmd:AppCommands.OrderUp}" CanExecute="OrderUp_CanExecute" Executed="OrderUp_Executed" /> <CommandBinding Command="{x:Static lcmd:AppCommands.OrderDown}" CanExecute="OrderDown_CanExecute" Executed="OrderDown_Executed" /> <CommandBinding Command="{x:Static lcmd:AppCommands.NewClock}" CanExecute="NewClock_CanExecute" Executed="NewClock_Executed" /> <CommandBinding Command="{x:Static lcmd:AppCommands.DeleteClock}" CanExecute="DeleteClock_CanExecute" Executed="DeleteClock_Executed" /> </Window.CommandBindings>
La prima cosa che facciamo, nella nostra Window, è dichiarare i command che verranno gestiti con i relativi event handler, quindi abbiamo un Salva, Sposta in su, Sposta in Giù, Nuovo, Cancella. La finestra, sarà per default in modifica, pertanto gli orologi esistenti potranno essere modificati in ogni momento.
<Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> </Grid>
La grid principale, conterrà 3 righe, come vedremo, la prima riga conterrà i pulsanti di comando, la seconda il dettaglio dell’orologio correntemente selezionato, la terza la lista di tutti gli orologi configurati.
<StackPanel Grid.Row="0" Orientation="Horizontal" FlowDirection="LeftToRight"> <Button ToolTip="Save on Disk" Command="{x:Static lcmd:AppCommands.Save}" > <Image Stretch="Uniform" MaxHeight="24" MaxWidth="24" Source="pack://application:,,,/Images/btn_032_367.png"/> </Button> <Button ToolTip="New Clock" Command="{x:Static lcmd:AppCommands.NewClock}" > <Image Stretch="Uniform" MaxHeight="24" MaxWidth="24" Source="pack://application:,,,/Images/btn_032_701.png"/> </Button> <Button ToolTip="New Clock" Command="{x:Static lcmd:AppCommands.DeleteClock}" > <Image Stretch="Uniform" MaxHeight="24" MaxWidth="24" Source="pack://application:,,,/Images/btn_032_474.png"/> </Button> <Button ToolTip="Move Up" Command="{x:Static lcmd:AppCommands.OrderUp}" > <Image Stretch="Uniform" MaxHeight="24" MaxWidth="24" Source="pack://application:,,,/Images/btn_032_910.png"/> </Button> <Button ToolTip="Move Down" Command="{x:Static lcmd:AppCommands.OrderDown}" > <Image Stretch="Uniform" MaxHeight="24" MaxWidth="24" Source="pack://application:,,,/Images/btn_032_911.png"/> </Button> </StackPanel>
Nella prima riga, abbiamo inserito lo StackPanel orizzontale che simula una toolbar, ho utilizzato questo approccio perché più semplice, in futuro magari potremo fare una nuova versione in cui useremo il ribbon. I bottoni, per mia scelta contengono solo un immagine, ma il ToolTip del button contiene una descrizione del comando che dovrebbe rendere user friendly detto button. potete vedere come i Command siano stati dichiarati in corrispondenza ai command impostati nella dichiarazione e che abbiamo agganciato le icone opportune, gentilmente fornite dalla mia collezione di icone aziendali. Se curioserete il progetto, vedrete che le icone sono in realtà grandi 32×32 ma ho dato loro una massima dimensione di 24×24 perché non fossero enormi. Possiamo anche volendo utilizzare Margin e Padding per rendere i button più grandi e più simili a quelli del ribbon, ma al momento manteniamo la semplicità.
<Grid Grid.Row="1"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <TextBlock Grid.Row="0" Grid.Column="0" Margin="4,2,4,2" HorizontalAlignment="Right" VerticalAlignment="Center" Text="Clock Name"/> <TextBox Grid.Row="0" Grid.Column="1" Margin="4,2,4,2" HorizontalAlignment="Stretch" VerticalAlignment="Center" TextAlignment="Left" TextWrapping="NoWrap" Text="{Binding Path=SelectedClock.Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/> <TextBlock Grid.Row="1" Grid.Column="0" Margin="4,2,4,2" HorizontalAlignment="Right" VerticalAlignment="Center" Text="Time Zone"/> <ComboBox Grid.Row="1" Grid.Column="1" Margin="4,2,4,2" HorizontalAlignment="Stretch" VerticalAlignment="Center" SelectedItem="{Binding SelectedClock.TimeZone, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" ItemsSource="{Binding TimeZones}"> <ComboBox.ItemTemplate> <DataTemplate> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/> </Grid.ColumnDefinitions> <TextBlock Grid.Column="0" Margin="4,2,4,2" HorizontalAlignment="Left" VerticalAlignment="Center" TextAlignment="Left" MinWidth="120" Text="{Binding StandardName}"/> <TextBlock Grid.Column="1" Margin="4,2,4,2" HorizontalAlignment="Left" VerticalAlignment="Center" TextAlignment="Left" MinWidth="120" Text="{Binding DisplayName}"/> <TextBlock Grid.Column="1" Margin="4,2,4,2" HorizontalAlignment="Right" VerticalAlignment="Center" TextAlignment="Left" MinWidth="30" Text="{Binding DST}"/> </Grid> </DataTemplate> </ComboBox.ItemTemplate> </ComboBox> </Grid>
La seconda riga della nostra Grid principale, contiene una Grid con 2 righe per 2 colonne, in questa grid, saranno ospitati i controlli che permetteranno la modifica dell’orologio selezionato, o la generazione di un nuovo orologio. Ho usato due controlli nuovi in questo caso, che non erano presenti nelle precedenti porzioni della UI, ovvero la TextBox, per poter inserire il nome, chi avesse usato windows forms può notare le somiglianze, il secondo controllo utilizzato è la Combobox che permette di selezionare il fuso orario in base a quelli presenti sul sistema.
Cosa dovete notare di questi due controlli:
- Per la Textbox, oltre alle property simili a quelle della textblock, la cosa più importante è il Binding come vedete ho collegato la property Text della TextBox, ad una property di un oggetto che si chiama SelectedClock, vedremo come la cosa funziona nel codice C# dietro alla finestra.
- Path = SelectedClock.Name, indica che la Textbox deve visualizzare e modificare il contenuto della property Name dell’oggetto SelectedClock, che probabilmente avete indovinato essere un ClockInfo.
- Mode = TwoWay, indica che il contenuto della TextBox è bidirezionale, quindi può essere cambiato da codice (quando mi sposto sulla lista per riflettere il contenuto dell’elemento selezionato, oppure ricevere la modifica dalla textbox stessa quando digito dei dati su di essa).
- UpdateSourceTrigger=PropertyChanged, indica che la modifica del contenuto deve essere comunicata alla User Interface al variare del valore della property che è in binding.
- Per la Combobox, ci sono alcune cose da osservare il Binding che non è singolo ma è a due diverse entità, ed il DataTemplate, che ci mostra come sia possibile costruire una combobox che visualizza più di un singolo campo della lista che le sta sotto in modo molto semplice.
- ItemsSource= Binding a TimeZones, indica alla combobox qual’è la lista da mostrare al suo interno.
- SelectedItem = Binding alla property TimeZone di SelectedClock, indica qual’è il valore che deve essere modificato dalla combobox.
- Mode= TwoWays, Anche in questo caso, come per la textbox abbiamo usato la modalità bidirezionale
- UpdateSourceTrigger=PropertyChanged, Anche in questo caso, abbiamo indicato che la modifica della property implica l’aggiornamento del dato.
<ListBox Grid.Row="2" ItemsSource="{Binding Clocks, UpdateSourceTrigger=PropertyChanged}" SelectedItem="{Binding SelectedClock, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" IsSynchronizedWithCurrentItem="True"> <ListBox.ItemTemplate> <DataTemplate> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> <ColumnDefinition Width="Auto"/> </Grid.ColumnDefinitions> <TextBlock Grid.Column="0" Margin="4,2,4,2" HorizontalAlignment="Right" VerticalAlignment="Center" TextAlignment="Right" MinWidth="30" Text="{Binding Order}"/> <TextBlock Grid.Column="1" Margin="4,2,4,2" HorizontalAlignment="Right" VerticalAlignment="Center" TextAlignment="Left" MinWidth="120" Text="{Binding Name}"/> <TextBlock Grid.Column="2" Margin="4,2,4,2" HorizontalAlignment="Right" VerticalAlignment="Center" TextAlignment="Left" Text="{Binding TimeZoneName}"/> <TextBlock Grid.Column="1" Margin="4,2,4,2" HorizontalAlignment="Right" VerticalAlignment="Center" TextAlignment="Left" MinWidth="30" Text="{Binding DST}"/> </Grid> </DataTemplate> </ListBox.ItemTemplate> </ListBox>
L’ultima riga della nostra grid principale, contiene una ListBox che, come la ComboBox in precedenza, può con molta semplicità essere configurata e formattata per mostrare non solo una colonna di dati ma tutti i dati importanti della configurazione del nostro orologio. Cosa annotiamo al riguardo:
- Anche la ListBox come la Combobox ha due diversi Binding
- ItemsSource, che è in Binding alla property Clocks che contiene la lista di tutti gli orologi configurati.
- SelectedItem, che è in Binding alla property SelectedClock, che conterrà quindi in ogni momento l’orologio correntemente selezionato.
- IsSynchronizedWithCurrentItem, è un indicatore per dire alla Listbox di forzare l’aggiornamento dell’item corrente, quindi spostare il focus quando modificato sia dall’utente che da codice.
- Gli elementi in binding sulle TextBlock usate nella ListBox e allo stesso modo nella ComboBox sono collegati utilizzando solo il Nome della property senza ulteriori dati e parametri perché in questo caso il Binding è relativo alla collection utilizzata come ItemsSource.
Passiamo ora all’implementazione della parte di codice che sta dietro a questa finestra e che gestisce le sue funzionalità.
using DNW.MultiClock.Collections; using DNW.MultiClock.Context; using DNW.MultiClock.Entities; using System; using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; using System.Text; using System.Windows; using System.Windows.Input; namespace DNW.MultiClock.Windows { /// <summary> /// Interaction logic for Window1.xaml /// </summary> public partial class SetClocksWindow : Window, INotifyPropertyChanged { ... } }
Come la precedente finestra, anche questa Window è derivata dalla classe base Window ed implementa l’interfaccia INotifyPropertyChanged.
public SetClocksWindow() { InitializeComponent(); WindowStartupLocation = WindowStartupLocation.CenterOwner; DataContext = this; TimeZones = new ObservableCollection<TimeZoneInfo>(); }
Nel costruttore oltre a decidere dove la finestra appare quando viene istanziata assegnamo al DataContext la finestra stessa, che farà quindi da ViewModel a se stessa, e generiamo la collection che ospiterà le TimeZones che sarà utilizzata dalla ComboBox per permettere di selezionare i possibili fusi orari dei nostri orologi.
public const string FLD_TimeZones = "TimeZones"; private ObservableCollection<TimeZoneInfo> mTimeZones; public ObservableCollection<TimeZoneInfo> TimeZones { get { return mTimeZones; } set { mTimeZones = value; OnPropertyChanged(FLD_TimeZones); } }
La property che definisce la collezione delle Time Zones, che rappresentano i possibili fusi orari.
public const string FLD_SelectedClock = "SelectedClock"; private ClockInfo mSelectedClock; public ClockInfo SelectedClock { get { return mSelectedClock; } set { mSelectedClock = value; OnPropertyChanged(FLD_SelectedClock); } }
La property SelectedClock che se ben ricordate abbiamo utilizzato per il Binding dei controlli nello XAML.
public ClocksInfoCollection Clocks { get { if (AppContext.Instance.ConfigData == null) return null; return AppContext.Instance.ConfigData.Clocks; } }
La property Clocks, che in questo caso è semplicemente uno shortcut per accedere alla Collection degli orologi che abbiamo predisposto all’interno della classe di supporto Singleton all’applicazione.
protected override void OnClosed(EventArgs e) { if (this.WindowState == WindowState.Normal) { AppContext.Instance.ConfigData.SetAutoSetting(string.Format("{0}_Size", this.Name), string.Format("{0};{1}", this.Width, this.Height)); } base.OnClosed(e); } protected override void OnInitialized(EventArgs e) { base.OnInitialized(e); string size = AppContext.Instance.ConfigData.GetAutoSetting(string.Format("{0}_Size", this.Name)); if (size != null) { string[] data = size.Split(';'); double width = this.Width; double height = this.Height; if (data.Length >= 1) { double.TryParse(data[0], out width); } if (data.Length >= 2) { double.TryParse(data[1], out height); } this.Width = width; this.Height = height; } }
Come per la finestra principale, anche in questo caso utilizziamo i due eventi Initialized e Closed della Window per caricare e per salvare la dimensione della finestra, in modo da farla apparire all’utente della dimensione che ha usato l’ultima volta che l’ha chiusa.
public event EventHandler ClocksChanged; protected virtual void OnClocksChanged(EventArgs e) { if (ClocksChanged != null) { ClocksChanged(this, e); } }
Definiamo un evento, a livello della nostra Window che scateneremo quando verranno salvate le modifiche agli orologi in modo che la MainWindow possa intercettarlo e rigenerare i componenti visuali.
private void SetClocksWindow_Loaded(object sender, RoutedEventArgs e) { var timeZones = TimeZoneInfo.GetSystemTimeZones(); foreach (TimeZoneInfo tzi in timeZones) { TimeZones.Add(tzi); } }
L’event handler dell’evento Loaded della Window, viene utilizzato per creare la lista dei fusi orari che verrà visualizzata dalla combobox.
private void DeleteClock_CanExecute(object sender, CanExecuteRoutedEventArgs e) { e.CanExecute = Clocks != null && SelectedClock != null; } private void DeleteClock_Executed(object sender, ExecutedRoutedEventArgs e) { try { if (MessageBox.Show("Are You sure you want to delete the currently selected clock?", "Confirm Deletion", MessageBoxButton.YesNo, MessageBoxImage.Warning) == MessageBoxResult.Yes) { Clocks.Remove(SelectedClock); } } catch (Exception ex) { MessageBox.Show(ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error); } }
Vediamo ora gli event handler che rispondono ai Command Button della Window, il primo è il Delete, innanzi tutto, possiamo vedere che in questo caso, l’event handler CanExecute attiva la funzione di cancellazione solo alla condizione che esistano degli orologi e che l’orologio selezionato non sia nullo.
La cancellazione, chiede conferma all’utente e procede ad eliminare l’orologio dalla collection.
private void NewClock_CanExecute(object sender, CanExecuteRoutedEventArgs e) { e.CanExecute = Clocks != null; } private void NewClock_Executed(object sender, ExecutedRoutedEventArgs e) { try { ClockInfo info = new ClockInfo(); int order = Clocks.Max(x => x.Order); order++; info.Order = order; Clocks.Add(info); SelectedClock = info; } catch (Exception ex) { MessageBox.Show(ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error); } }
Gli event handler del command New, anche in questo caso, il bottone New è attivo se la collezione degli orologi è stata inizializzata, se per qualche motivo non esistesse, il tasto non sarebbe attivo, questo è un ottimo modo per evitare errori.
La creazione di un nuovo orologio, genera un nuovo elemento di tipo ClockInfo, gli assegna l’ordine in modo che stia in fondo alla lista, lo aggiunge alla collection e poi lo pone nella property SelectedClock. Grazie alla “Magia” del Binding di WPF vedremo come la User Interface reagirà automaticamente selezionando l’oggetto sulla ListBox e portandolo all’interno del Dettaglio in modo che possa essere modificato in base a quel che serve all’utente.
private void OrderDown_CanExecute(object sender, CanExecuteRoutedEventArgs e) { e.CanExecute = Clocks != null && Clocks.Count > 1; } private void OrderDown_Executed(object sender, ExecutedRoutedEventArgs e) { try { int index = Clocks.IndexOf(SelectedClock); if (index < Clocks.Count - 1) { index++; ClockInfo next = Clocks[index]; int old = SelectedClock.Order; SelectedClock.Order = next.Order; next.Order = old; ClockInfo sel = SelectedClock; Clocks.ReSort(); SelectedClock = sel; } } catch (Exception ex) { MessageBox.Show(ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error); } } private void OrderUp_CanExecute(object sender, CanExecuteRoutedEventArgs e) { e.CanExecute = Clocks != null && Clocks.Count > 1; } private void OrderUp_Executed(object sender, ExecutedRoutedEventArgs e) { try { int index = Clocks.IndexOf(SelectedClock); if (index > 0) { index--; ClockInfo prev = Clocks[index]; int old = SelectedClock.Order; SelectedClock.Order = prev.Order; prev.Order = old; ClockInfo sel = SelectedClock; Clocks.ReSort(); SelectedClock = sel; } } catch (Exception ex) { MessageBox.Show(ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error); } }
Gli event handler per la gestione dei bottoni che permettono di spostare l’ordine degli orologi, entrambi sono attivi solo se c’è più di un orologio, entrambi attivano la propria funzionalità solo se è possibile, quindi OrderDown non viene eseguita se siamo sull’ultimo record, OrderUp non viene eseguita se siamo sul primo record. Possiamo vedere come entrambe utilizzino la funzione ReSort della collection dei Clock che abbiamo implementato per cambiare fisicamente l’ordine degli orologi nella lista.
private void Save_CanExecute(object sender, CanExecuteRoutedEventArgs e) { e.CanExecute = true; } private void Save_Executed(object sender, ExecutedRoutedEventArgs e) { try { AppContext.Instance.ConfigData.SaveConfig(); OnClocksChanged(new EventArgs()); } catch (Exception ex) { MessageBox.Show(ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error); } }
Gli event handler per la funzione di salvataggio degli orologi, in questo caso è sempre disponibile, ma in seguito la modificheremo inserendovi dei controlli. La sua funzionalità è quella di chiamare la funzione di scrittura dei dati JSON, da noi implementata nella Classe MultiClocksConfig e poi scatenare l’evento OnClocksChanged per fare in modo che la finestra principale ricarichi gli orologi.
E questo è tutto, come già indicato, questo è il primo articolo dedicato a questa applicazione, la sua versione 1.0 modello base, listo immediatamente alcune delle cose che mancano e che implementeremo in seguito:
- Se modifico gli orologi ma non schiaccio salva, la finestra principale non viene aggiornata, ma siccome i dati di configurazione sono salvati alla chiusura della Window quando l’applicazione riparte le modifiche vengono riportate lo stesso. Non è un comportamento corretto, vedremo come correggerlo.
- I colori di sfondo e dei caratteri sono fissi, all’utente potrebbe piacere cambiarli.
- Le dimensioni delle font e il tipo di font sono fissi, all’utente potrebbe piacere cambiarli.
- Dobbiamo creareil necessario per effettuare il setup dell’applicazione.
Queste sono alcune delle cose che saranno oggetto di futuri articoli.
Riepilogo
Che cosa abbiamo visto in questo lungo articolo dedicato principalmente a chi sta iniziando ad avvicinarsi a WPF XAML e C# e a chi vuol vedere come creare un applicazione funzionante.
- La creazione di classi per manipolare dati
- La predisposizione delle classi dati alla serializzazione JSON o XML
- La creazione di una collection con una funzione specifica per il sort fisico
- La creazione di Routed Command
- La creazione di un Singleton
- La creazione di uno User Control WPF
- L’uso del Dispatcher Timer all’interno di una applicazione WPF
- La creazione di una finestra senza titolo e pulsanti standard
- L’implementazione del ridimensionamento e della possibilità di trascinamento sulla finestra senza titolo.
- L’implementazione di un sistema per la generazione dinamica degli User Control
- L’implementazione di una Window per l’inserimento dati corredata di un controllo ComboBox, di una ListBox
- Il Binding dei dati ai controlli di una Window per la gestione dei dati in modalità MVVM.
- Il caricamento ed il salvataggio di dati su un file JSON.
Potete scaricare il progetto esempio dal link qui indicato:
Companion code, Multi Clock - Un applicazione in WPF
Per qualsiasi domanda, osservazione, commento, approfondimento, o per segnalare un errore, potete usare il link alla form di contatto in cima alla pagina.