Press "Enter" to skip to content

2 – Multi Clock – Miglioriamo la User interface

In questo secondo articolo dedicato all’orologio multi fuso orario che abbiamo costruito nel precedente articolo, vogliamo migliorare la qualità del comportamento della nostra User Interface. Se avete provato la versione 1.0 del nostro orologio avrete notato 2 problemi che erano presenti nella gestione della configurazione degli orologi, se non lo aveste fatto, i problemi sono i seguenti:

  • Annullando le modifiche effettuate in configurazione, chiudendo senza salvare, sembrava che tali modifiche non fossero state applicate, ma alla chiusura e riapertura dell’applicazione invece le modifiche annullate venivano comunque applicate e gli orologi modificati.
  • Chiudendo la finestra senza salvare, il sistema non chiedeva alcuna conferma prima di annullare le modifiche.

Il motivo di questi due problemi, ovviamente correlati è il seguente:

  1. La modifica alla configurazione nella versione 1.0 viene effettuata direttamente sui dati attualmente in uso.
  2. A causa di questo, visto che alla chiusura della finestra principale i dati di configurazione vengono comunque automaticamente aggiornati per salvare le dimensioni della finestra, anche le modifiche annullate vengono salvate.
  3. Modificando direttamente i dati attuali di configurazione, non c’era modo di sapere se al momento della chiusura della finestra c’erano state variazioni quindi non era possibile notificare l’utente.

Vediamo quindi come ovviare ai problemi e rendere il nostro orologio un po’ più professionale.

Clonare una classe entity

Per ovviare al problema per cui le modifiche venivano fatte direttamente sui dati attuali, la soluzione è creare una copia dei dati per effettuare le modifiche ed applicarle solo quando l’utente le conferma. Questo ci da l’occasione di spiegare come si crea la funzione di clonazione di una classe.

In questo caso, la classe che ci serve clonare è la classe ClocksInfoCollection, che useremo nella finestra di modifica degli orologi e poi utilizzeremo per salvare le modifiche se richiesto dall’utente.

ClocksInfoCollection è una collection di oggetti ClockInfo, pertanto dobbiamo implementare una funzione di clonazione anche per l’elemento ed una della collection.

In questo caso, si tratterà di una funzione di Deep Clone e non di una funzione di Shallow Clone (o Deep Copy e Shallow Copy).

Che differenza c’è fra le due funzioni?

In una Deep Clone, vengono duplicati tutti gli oggetti all’interno dell’oggetto clonato, pertanto, nella nostra collection, creeremo un duplicato di ciascuno dei suoi elementi e se uno degli elementi contenesse delle altre classi che non fossero delle classi base, creeremmo una copia fisica anche di questi oggetti.

In una Shallow Clone invece, viene duplicato solo l’oggetto principale, nel nostro caso la collection di Clock Info, mentre gli oggetti che la popolano sarebbero semplicemente aggiunti alla nuova collection, questo, può rivelarsi utile in molti tipi di applicazione, ma non nel nostro caso, in quanto aggiungendo gli oggetti della collection principale a una nuova collection, il loro contenuto sarebbe comunque modificato dalla finestra di modifica della configurazione e quindi la soluzione al problema sarebbe una soluzione a metà. Vi scriverò il codice per la shallow clone così che possiate testarlo.

ClockInfo Clone

public ClockInfo Clone()
{
    ClockInfo item = new ClockInfo();
    Copy(item);
    return (item);
}
 

public void Copy(ClockInfo item)
{
    item.Name = this.Name;
    item.Order = this.Order;
    item.TimeZone = this.TimeZone;
}

Quando effettuo la clonazione di una classe, ovvero la creazione di un duplicato della classe nello stato in cui si trova al momento in cui il metodo viene eseguito, innanzi tutto preferisco suddividerla in due metodi, il metodo Copy che fisicamente copia i dati da una classe ad una copia, ed il metodo Clone vero e proprio, che prima di chiamare Copy, crea una nuova istanza della classe. Quando cloniamo una classe, copiamo i valori di tutte le sue property nella sua copia, ovviamente copiamo Tutte e Sole le property dotate di funzione Set, ma non solo, nel nostro caso, non copiamo la property DateAndTime, pure essendo questa dotata di Setter, ma è per un motivo molto semplice, il suo valore non ha necessità di essere copiato perché verrà ricalcolata immediatamente dai componenti della User Interface che mostrano gli orologi.

Perché fare due funzioni? E’ più pulito, e soprattutto, se per qualsiasi motivo doveste aver bisogno di generare la classe in cui copiare i dati in altro luogo, nel vostro codice, dovreste generare la Copy in seguito, se lo facciamo per abitudine, non avremo mai problemi.

ClocksInfoCollection Clone

Vediamo ora per prima la Deep Clone della nostra collection:

public ClocksInfoCollection Clone()
{
    ClocksInfoCollection item = new ClocksInfoCollection();
    Copy(item);
    return (item);
}
 
public void Copy(ClocksInfoCollection item)
{
    foreach (ClockInfo cInfo in this)
    {
        item.Add(cInfo.Clone());
    }
}

In una Deep Clone come potete vedere, il metodo Copy genera un duplicato di ognuno degli elementi della collection mentre il metodo Clone genera una collection nuova in cui porre i duplicati.

public ClocksInfoCollection ShallowClone()
{
    ClocksInfoCollection item = new ClocksInfoCollection();
    ShallowCopy(item);
    return (item);
}
 
public void ShallowCopy(ClocksInfoCollection item)
{
    foreach (ClockInfo cInfo in this)
    {
        item.Add(cInfo);
    }
}

Nella Shallow Clone invece, come potete notare, creiamo comunque una nuova collection, ma vi inseriamo sempre gli elementi della collection corrente. Perché usare questo tipo di copia? la funzione ReSort, ne è un buon esempio, quando voglio riordinare fisicamente una collection, non mi serve duplicarne il contenuto, devo solo riordinarlo in una nuova collection.

Modificare SetClocksWindow

Una volta predisposta la collection della definizione degli orologi per la clonazione, modifichiamo la nostra Window per la modifica degli orologi per utilizzare questa nuova modalità.

public const string FLD_Clocks = "Clocks";
 
private ClocksInfoCollection mClocks;
 
public ClocksInfoCollection Clocks
{
    get
    {
        return mClocks;
    }
    private set
    {
        mClocks = value;
        OnPropertyChanged(FLD_Clocks);
    }
}

Prima di tutto modifichiamo la nostra property Clocks in modo che non sia semplicemente un modo per visualizzare il contenuto della attuale collection di configurazione degli orologi come era prima.

private void SetClocksWindow_Loaded(object sender, RoutedEventArgs e)
{
    var timeZones = TimeZoneInfo.GetSystemTimeZones();
    foreach (TimeZoneInfo tzi in timeZones)
    {
        TimeZones.Add(tzi);
    }
    Clocks = AppContext.Instance.ConfigData.Clocks.Clone();
}

Modifichiamo ora l’event handler dell’evento Loaded della nostra Window, in modo che venga creata una copia degli orologi attuali all’interno della finestra.

private void Save_Executed(object sender, ExecutedRoutedEventArgs e)
{
    try
    {
        AppContext.Instance.ConfigData.Clocks.Clear();
        Clocks.ShallowCopy(AppContext.Instance.ConfigData.Clocks);
 
        AppContext.Instance.ConfigData.SaveConfig();
        OnClocksChanged(new EventArgs());
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
    }
}

Modifichiamo ora la funzione di salvataggio degli orologi in modo da salvare i nuovi orologi al posto dei vecchi. Abbiamo così l’occasione di utilizzare il metodo ShallowCopy che in questo caso è perfetto per trasferire gli oggetti dalla collezione di lavoro alla collezione finale. Rimangono fisse l’operazione di salvataggio e la notifica della modifica degli orologi così che la finestra principale possa rigenerare le visualizzazioni.

Potete testare l’applicazione e verificare che gli orologi vengono salvati ed aggiornati solo premendo il tasto salva, mentre le modifiche vengono eliminate se questo non viene fatto prima di chiudere la finestra.

Verificare se vi sono differenze alla chiusura della finestra

Se vogliamo avvisare l’utente che ha fatto delle modifiche non salvate, quando chiude la finestra, per fare in modo che confermi il fatto di non voler salvare le modifiche, abbiamo bisogno di verificare le differenze fra le due collezioni, quella attualmente usata e quella nuova. Per fare questo, dobbiamo implementare un metodo per comparare gli oggetti ClockInfo ed un metodo per comparare le collezioni. Vediamo come fare:

ClockInfo Compare

Due classi ClockInfo quando saranno uguali? Quando tutte le property relative alla configurazione sono uguali, guarda caso, sono le stesse property che utilizziamo per la funzione Copy:

public int CompareTo(object obj)
{
    int ret = -1;
 
    if (obj is ClockInfo)
    {
        ClockInfo val = (ClockInfo)obj;
        int comparator = 0;
 
        comparator = CompareStrings(this.Name, val.Name);
        if (comparator == 0)
        {
            comparator = CompareStrings(this.TimeZoneName, val.TimeZoneName);
            if (comparator == 0)
            {
                comparator = this.Order.CompareTo(val.Order);
            }
        }
        ret = comparator;
    }
 
    return (ret);
}
 
private int CompareStrings(string thisString, string valString)
{
    int comparator = 0;

    if (thisString == null && valString == null) comparator = 0;
    else if (thisString == null)
    {
        comparator = -1;
    }
    else if (valString == null) comparator = 1;
 
    if (comparator == 0)
    {
        comparator = thisString.CompareTo(valString);
    }
    return comparator;
}

Per comparare due classi, e verificare se sono uguali, vi sono due modi, il modo più immediato è quello di verificare se sono lo stesso oggetto, utilizzando l’operatore standard == che in questo caso chiamerebbe una GetHashCode per gli oggetti e valuterebbe se sono uguali. Nel nostro caso, avendo clonato le due collezioni, tutti gli oggetti sarebbero diversi, pertanto non è questo il tipo di comparazione da utilizzare. Quello che dobbiamo utilizzare è una comparazione dei contenuti, ovvero comparare property per property i dati per noi significativi di ogni coppia di classi. In questo caso ho utilizzato il Name, il TimeZoneName, perché non ci sono due time zones diverse con lo stesso nome, e infine il valore di Order. Se uno di questi dati è diverso, le due classi sono diverse.

Ho creato anche una piccola funzione per la comparazione delle stringhe che io trovo utile, in quanto per quelle che sono le mie regole di business, due stringhe nulle sono uguali, una stringa nulla è sempre minore di qualsiasi stringa non nulla, viceversa una stringa non nulla è sempre maggiore di qualsiasi stringa nulla. Ma questa è una convenzione che piace usare a me.

CompareTo per ClocksInfoCollection

Procediamo ora a creare il metodo compare per la nostra collezione:

public int CompareTo(object obj)
{
    int ret = -1;
 
    if (obj is ClocksInfoCollection)
    {
        ClocksInfoCollection val = (ClocksInfoCollection)obj;
 
        int comparator = this.Count.CompareTo(val.Count);
        if (comparator == 0)
        {
            for (int i = 0; i < this.Count; i++)
            {
 
                comparator = this[i].CompareTo(val[i]);
                if (comparator != 0) break;
            }
        }
        ret = comparator;
    }
 
    return (ret);
}

Due collezioni sono diverse se hanno un diverso numero di elementi. Inoltre, sono diverse se almeno una coppia degli elementi comparati seguendo l’indice standard, sono diversi.

Modifichiamo SetClocksWindow

All’interno della nostra Window per modificare gli orologi, creiamo prima di tutto un metodo per verificare se le collection sono diverse:

private bool ClocksHaveBeenModified()
{
    if (Clocks == null) return false;
    if (AppContext.Instance.ConfigData.Clocks == null) return false;
 
    return Clocks.CompareTo(AppContext.Instance.ConfigData.Clocks) != 0;
}

Questo metodo ritorna True se le due collezioni sono diverse, altrimenti ritorna False, ovviamente se una o entrambe le collection non sono state inizializzate, non è possibile manipolarle quindi ritorna False. Questo è un codice che serve a prevenire un errore che viene sollevato dall’event handler CanExecute del comando Save che ora vedremo.

private void Save_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
    e.CanExecute = ClocksHaveBeenModified();
}

Questa modifica, fa in modo che il tasto Save sia attivo solo se ci sono state modifiche. Siccome nell’inizializzazione della Window, viene eseguito anche questo metodo, ed è prima che la collezione degli orologi clonata sia creata, senza le due istruzioni di controllo messe nel metodo ClocksHaveBeenModified, sarebbe sollevato un errore.

protected override void OnClosing(CancelEventArgs e)
{
    if (ClocksHaveBeenModified())
    {
        if (MessageBox.Show("There are unsaved modifications, do you want to close anyway?", "Confirm closing", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.No)
        {
            e.Cancel = true;
        }
    }
 
    base.OnClosing(e);
}

La modifica ulteriore che facciamo, è quella di intercettare l’evento Closing della Window e chiedere conferma se vi sono modifiche. Attenzione, se utilizzerete questo codice nelle vostre applicazioni, perché in WPF non c’è il parametro che notifica se la chiusura della finestra è stata richiesta dall’utente oppure se è stata richiesta dal sistema (ad esempio per uno shut down o un logoff) pertanto questo potrebbe fare bloccare lo shutdown. Non ho idea dei motivi per cui non c’è in WPF mentre c’era nelle Windows Forms, ma consiglio di usare questi controlli pensandovi attentamente.

Riepilogo

Cosa abbiamo spiegato in questa lezione:

  • Come evitare che i dati correnti degli orologi in funzione siano direttamente modificati quando vogliamo cambiare la configurazione degli orologi. Per fare questo abbiamo spiegato alcune cose che spero possano essere interessanti.
  • Come scrivere un metodo per effettuare il Clone di una classe e ad essa agganciata, la funzione per effettuare la Copy di una classe.
  • Come scrivere un metodo per effettuare la Deep Copy di una collection
  • Come scrivere un metodo per effettuare la Shallow Copy di una collection, spiegando l’uso di entrambe.
  • Come scrivere un metodo per effettuare la CompareTo fra due classi.
  • Come scrivere un metodo per effettuare la CompareTo fra due collezioni.
  • Come attivare un tasto sull’interfaccia solo se ci sono state modifiche alla collezione degli orologi.
  • Come intercettare la chiusura della Window e chiedere all’utente se desidera perdere le modifiche effettuate quando vi fossero modifiche non salvate.

Potete scaricare il progetto esempio dal link qui indicato:

Per qualsiasi domanda, osservazione, commento, approfondimento, o per segnalare un errore, potete usare il link alla form di contatto in cima alla pagina.