Press "Enter" to skip to content

16 – Lavorare con i dati – User Interface Creiamo i Button per le azioni sui dati

In questa nuova puntata relativa alla User Interface del nostro mini progetto Multi Tier per la gestione della tabella Users, andremo a creare la bozza dei pulsanti di comando per le varie azioni da compiere sui dati, pertanto, oltre al caricamento implementato nella scorsa lezione, creeremo i pulsanti di aggiunta e cancellazione dei record della nostra tabella e ovviamente i pulsanti per salvare i dati oppure annullare le modifiche ai dati. Per implementare questi comandi andremo ad aggiungere al View Model i metodi che risponderanno alle azioni richieste dall’utente dell’applicazione e soprattutto implementeremo le property per implementare la gestione delle modifiche alla tabella.

Se ben ricordate qualche articolo fa, esattamente in questo articolo ho indicato quale tecnica intendevo utilizzare per tener traccia delle modifiche effettuate ai dati dalla nostra Window, per pilotare poi la classe dei servizi per aggiornare il database. Pertanto oggi interveniamo subito sul View Model.

Modifiche alla classe UsersWindowModel.cs

Le modifiche che faremo oggi saranno le seguenti:

  1. Creeremo una collection OriginalUsers, in cui metteremo i dati letti dalla tabella al caricamento.
  2. Creeremo un clone degli oggetti originali sulla ObservableCollection al caricamento.
  3. Creeremo una nuova collection che ospiterà la lista degli oggetti Aggiunti.
  4. Creeremo una nuova collection che ospiterà la lista degli oggetti Cancellati.
  5. Modificheremo quindi il metodo di caricamento dati per rispecchiare i requisiti indicati.
  6. Aggiungeremo un metodo per Aggiungere un nuovo record.
  7. Aggiungeremo un metodo per Cancellare il record attualmente selezionato.
  8. Aggiungeremo un metodo per Annullare le modifiche effettuate ricaricando i dati dal database.
  9. Aggiungeremo un metodo per Salvare le modifiche effettuate.

Una volta fatte queste modifiche, andremo a modificare la User Interface per testare questi nuovi metodi.

private int mNewUserID;

Prima modifica, definiamo una variabile a livello di classe che ci servirà per generare gli ID temporanei per gli User aggiunti alla lista, questi valori saranno poi aggiornati quando salveremo i dati sul Database, assumendo il valore definitivo.

private List<User> AddedUsers
{
	get
	{
		return mAddedUsers;
	}
	set
	{
		mAddedUsers = value;
		OnPropertyChanged(FLD_AddedUsers);
	}
}
 
private List<User> DeletedUsers
{
	get
	{
		return mDeletedUsers;
	}
	set
	{
		mDeletedUsers = value;
		OnPropertyChanged(FLD_DeletedUsers);
	}
}
 
private List<User> OriginalUsers
{
	get
	{
		return mOriginalUsers;
	}
	set
	{
		mOriginalUsers = value;
		OnPropertyChanged(FLD_OriginalUsers);
	}
}

Le tre collezioni che ci serviranno per la gestione dell’applicazione delle modifiche alla nostra lista degli User, credo possiate notare come tutte e tre le classi sono private, ovvero non visibili all’esterno del ViewModel, perché il loro uso è esclusivamente gestito dai metodi della classe e non ha necessità di essere esposto all’esterno.

internal void Load_Data()
{
	UsersDataManager udm = new UsersDataManager(AppContext.Instance.CnString, AppContext.Instance.DataSource);
	Result<List<User>> retSelect = udm.SelectAll();
	Users.Clear();
	OriginalUsers.Clear();
	AddedUsers.Clear();
	DeletedUsers.Clear();
	SelectedUser = null;
	if (retSelect.HasError)
	{
		ResultText = retSelect.Error;
		ResultTextBrush = new SolidColorBrush(Colors.Red);
		return;
	}
	foreach (User usr in retSelect.Data)
	{
		OriginalUsers.Add(usr.Clone());
		Users.Add(usr.Clone());
	}
	ResultText = "Data loaded correctly.";
	ResultTextBrush = new SolidColorBrush(Colors.Black);
}

Modifichiamo il caricamento dei dati e invece di inserire nella collection Users i record letti dal database li Cloniamo, non una ma due volte, una sulla collection OriginalUsers che ci servirà per verificare i record modificati e una sulla collection Users che useremo per la visualizzazione e modifica sulla User Interface.

Inoltre abbiamo aggiunto l’azzeramento di tutte le collezioni di servizio e del SelectedUser questo, perché ricaricando i dati dal database annulliamo qualsiasi modifica esistente.

internal void Delete_User()
{
	if (SelectedUser == null)
		return;
	int usrIndex = Users.IndexOf(SelectedUser);
	if (SelectedUser.ID > 0)
	{
		DeletedUsers.Add(SelectedUser);
	}
	else
	{
		AddedUsers.Remove(SelectedUser);
	}
	Users.Remove(SelectedUser);
	if (Users.Count > usrIndex)
	{
		SelectedUser = Users[usrIndex];
	}
	else
	{
		if (Users.Count == 0)
		{
			SelectedUser = null;
		}
		else
		{
			SelectedUser = Users[Users.Count - 1];
		}
	}
}

Il metodo che cancella L’utente selezionato sulla lista, vediamo che cosa fa:

  1. Se l’utente selezionato è nullo (lista vuota o nessun record selezionato) esce senza fare nulla.
  2. Salva l’indice dello User corrente sulla collection Users.
  3. Se l’utente selezionato è un utente che è stato letto dal database, lo aggiunge alla collection dei Deleted Users, ovviamente se aggiungo e cancello un record senza salvare, questo non serve.
  4. Se l’utente non è un utente già salvato sul database, semplicemente lo elimina dalla collection AddedUsers.
  5. Elimina il SelectedUser dalla collection Users.
  6. Se la collection contiene un numero di utenti maggiore all’indice cancellato (ovvero se c’è un record successivo) Posiziona il Selected User sul record successivo a quello cancellato.
  7. Se ho cancellato l’ultimo record, cancella il selected user.
  8. Se invece ci sono ancora degli User nella collection, seleziona l’ultimo.

I passi da 6 a 8 sono una regola di business arbitraria che ho scelto io, ovvero se cancello un utente mi posiziono sul successivo se esiste o sull’ultimo della lista se esiste. Avrei potuto posizionarmi sul primo record o semplicemente non selezionare nulla, in questo caso è una regola che potete stabilire voi come è meglio per il tipo di interfaccia su cui state lavorando, il tipo di dati trattati eccetera.

internal void New_User()
{
	User item = new User();
	mNewUserID--;
	item.ID = mNewUserID;
	Users.Add(item);
	AddedUsers.Add(item);
	SelectedUser = item;
}

Il metodo che aggiunge un nuovo User e lo seleziona, oltre a questo gli da un ID negativo univoco che serve per riconoscere gli User Aggiunti ma non ancora salvati su database.

internal void Save_Data()
{
	List<User> updatedUsers = GetUpdatedUsers();
	UsersDataManager udm = new UsersDataManager(AppContext.Instance.CnString, AppContext.Instance.DataSource);
	Result<List<int>> retDeleted = null;
	retDeleted = udm.Delete(DeletedUsers);
	if (retDeleted.HasError)
	{
		ResultText = retDeleted.Error;
		ResultTextBrush = new SolidColorBrush(Colors.Red);
		return;
	}
	DeletedUsers.Clear();
 
	Result<List<User>> ret = null;
	ret = udm.Insert(AddedUsers);
	if (ret.HasError)
	{
		ResultText = ret.Error;
		ResultTextBrush = new SolidColorBrush(Colors.Red);
		return;
	}
	AddedUsers.Clear();
	udm.Update(updatedUsers);
	if (ret.HasError)
	{
		ResultText = ret.Error;
		ResultTextBrush = new SolidColorBrush(Colors.Red);
		return;
	}
	OriginalUsers.Clear();
	foreach (User usr in Users)
	{
		OriginalUsers.Add(usr.Clone());
	}
	ResultText = "Data saved correctly.";
	ResultTextBrush = new SolidColorBrush(Colors.Black);
}

Il metodo che salva su database effettua le seguenti operazioni:

  1. Acquisisce una lista degli User Modificati (potrebbe anche essere vuota)
  2. Istanzia lo UsersDataManager, inviandogli la stringa di connessione ed il tipo di database.
  3. Chiama per primo il metodo Delete, passandogli la lista degli User Cancellati.
  4. In caso di errore, visualizza l’errore sulla status bar ed esce cambiando il colore del testo in rosso.
  5. Altrimenti azzera la collection dei record cancellati.
  6. Se tutto è andato bene, Aggiunge gli User aggiunti alla collection.
  7. In caso di errore, visualizza l’errore sulla status bar ed esce cambiando il colore del testo in rosso.
  8. Se tutto è andato bene cancella la lista degli utenti aggiunti.
  9. Aggiorna i record modificati.
  10. In caso di errore, visualizza l’errore sulla status bar ed esce cambiando il colore del testo in rosso.
  11. Se tutto è andato bene cancella la collection dei record originali.
  12. Aggiunge un clone di tutti i record della collection Users che contengono i dati aggiornati (se osservate anche gli ID negativi sono stati sostituiti da quelli corretti) alla collection OriginalUsers per permettere di proseguire le modifiche.
  13. Visualizza un messaggio sulla status bar, con il testo di colore nero, che indica che il salvataggio è riuscito.

Se volessimo, invece di riaggiornare la collection OriginalUsers manualmente, potremmo rieseguire Load_Data, questo potrebbe essere utile se la Tabella Users può essere modificata da più postazioni contemporaneamente, in modo da acquisire le modifiche degli altri. In realtà se davvero avessimo una tabella con modifiche contemporanee da parte di più utenti, dovremmo gestire anche la concorrenza delle modifiche, pertanto creare un sistema più sofisticato che permetta di verificare anche le modifiche effettuate da altri sul database prima di salvare i dati, ma lasciamo questo tipo di funzionalità a futuri articoli su argomenti complessi e restiamo per ora alle basi con i piedi per terra.

internal void Undo_Edits()
{
	Load_Data();
}

Il metodo Undo, nel caso della nostra classe, può essere implementato semplicemente tornando a caricare i dati dal database cosa che automaticamente eliminerà qualsiasi modifica non salvata.

private List<User> GetUpdatedUsers()
{
	List<User> updatedUsers = new List<User>();
	//get just the users with a valid ID
	//added users have negative ID
	foreach (User usr in Users.Where(x => x.ID > 0))
	{
		User oUsr = OriginalUsers.FirstOrDefault(x => x.ID == usr.ID);
		if (oUsr != null)
		{
			int areEqual = usr.CompareTo(oUsr);
			if (areEqual != 0)
			{
				updatedUsers.Add(usr);
			}
		}
	}
	return (updatedUsers);
}

Il metodo che recupera tutti i record già esistenti quando abbiamo caricato i dati dal database che fossero stati modificati. Per farlo innanzi tutto seleziona solo i record già presenti sul database, poi per ognuno dei record potenzialmente modificati, trova il record originale, utilizzando l’ID (che non è modificabile) ed effettua una comparazione fra i 2 diversi record, se non sono uguali, aggiunge il record modificato alla lista che viene poi usata per salvare i dati sul database.

Le modifiche a UsersWindow.xaml

Vediamo ora come modifichiamo la User Interface per utilizzare i nuovi metodi generati nel View Model.

<Button
     Margin="4,2,4,2"
     Padding="10,4,10,4"
     Click="Delete_User">
     <TextBlock Text="Delete User"/>
</Button>
 
<Button
     Margin="4,2,4,2"
     Padding="10,4,10,4"
     Click="New_User">
     <TextBlock Text="New User"/>
</Button>
 
<Button
     Margin="4,2,4,2"
     Padding="10,4,10,4"
     Click="Undo_Edits">
     <TextBlock Text="Undo Edits"/>
</Button>
            
<Button
     Margin="4,2,4,2"
     Padding="10,4,10,4"
     Click="Save_Data">
     <TextBlock Text="Save Data"/>
</Button>
 
<Button
     Margin="4,2,4,2"
     Padding="10,4,10,4"
     Click="Load_Data">
     <TextBlock Text="Load Data"/>
</Button>

Come potete notare, per il nostro primo test, abbiamo aggiunto 4 Button sopra al Button Load Data, con le 4 azioni che abbiamo aggiunto, quindi Salva, Annulla, Nuovo, Cancella. Abbiamo anche commentato i 2 button per fare i test, che al momento non ci servono.

Bonus per Moreno

Ho anche effettuato un altro paio di modifiche, per rispondere ad una domanda inviatami da uno di coloro che seguono questa serie, mi ha chiesto se fosse stato possibile evidenziare con un colore di sfondo più evidente di quello standard la TextBox attiva al momento, essendo questo fattibile in modo semplice, ho incluso la modifica. Fare lo stesso per la Combobox è molto più complicato, magari ne parleremo in seguito.

Ecco le modifiche effettuate.

<Window.Resources>
        <ResourceDictionary>
            <Style x:Key="FocusColor" TargetType="{x:Type TextBox}">
                <Style.Triggers>
                    <Trigger Property="IsFocused" Value="True">
                        <Setter Property="Background" Value="#FFbef070"/>
                    </Trigger>
                </Style.Triggers>
            </Style>
 
        </ResourceDictionary>
    </Window.Resources>

Ho creato una nuova sezione, subito sotto al tag Window in cima alla definizione XAML della nostra Window.

Le Resources di una Window, sono un “luogo” ove possiamo mettere vari tipi di oggetti, inseriti in un Resource Dictonary, in questo caso, ho definito uno stile il cui nome (Key) è FocusColor, che è assegnabile alle TextBox, tale stile definisce che, nel caso la property della TextBox IsFocused sia True, il colore del suo background diviene verde chiaro.

Questo serve a mostrarvi come, modificare i controlli a video in base allo Status degli stessi o di altri valori del View Model sia una operazione piuttosto semplice in una interfaccia WPF

Oltre a questo ho modificato tutte le Textbox nel seguente modo:

<TextBox 
         Name="TxtUserName"
         Margin="2,4,2,4" 
         Padding="4" 
         HorizontalAlignment="Stretch" 
         VerticalAlignment="Center"
         TextAlignment="Left"
         Style="{StaticResource FocusColor}"
         Text="{Binding Path=SelectedUser.UserName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">

Come potete notare, ho aggiunto l’attributo Style che ho inizializzato con un costrutto simile a quello del Binding, solo che invece di usare la parola chiave Binding (che mi collega al View Model) ho usato la parola chiave StaticResource, che invece mi permette di utilizzare gli oggetti definiti all’interno del Tag Resources della Window, in questo caso usando il valore della Key dello Stile definito. In questo modo, lo stile delle Textbox le farà divenire verdi quando hanno il focus in modifica.

Ho fatto ancora una modifica, a due soli controlli della finestra, uno è proprio la TextBox collegata a UserName, ovvero la prima delle Textbox di dettaglio.

Il secondo controllo è la DataGrid.

<DataGrid
    Name="UsersList"
...

Se non lo aveste ancora notato, ho dato a ciascuno di loro un Name, questo perché ho bisogno di poter interagire con i 2 controlli dal Code Behind della Window.

Le modifiche a UsersWindow.xaml.cs

Il code Behind della window è stato modificato perché abbiamo aggiunto gli Event Handler dei Button. Siccome non credo di avervi mai fatto vedere come si aggiunge automaticamente un Event Handler lo faccio ora:

Quando definite un Event Handler sullo XAML, per creare il metodo vuoto corrispondente nel code behind basta fare questo:

15_usersdb_12_01_create_eventHandler

Premiamo il tasto destro tenendo il cursore posizionato sul nome dell’Event Handler in questo caso “New_User”, e selezioniamo Go To Definition. Se l’event handler non esiste viene creato, se esiste Visual Studio vi si posiziona sopra.

private void Load_Data(object sender, RoutedEventArgs e)
{
	try
	{
		Model.Load_Data();
	}
	catch (Exception ex)
	{
		MessageBox.Show(ex.Message, "ERROR", MessageBoxButton.OK, MessageBoxImage.Error);
	}
}

Il metodo Load_Data è rimasto uguale alla scorsa puntata, infatti le modifiche sono state solo nel View Model.

private void Delete_User(object sender, RoutedEventArgs e)
{
	try
	{
		Model.Delete_User();
		UsersList.Focus();
	}
	catch (Exception ex)
	{
		MessageBox.Show(ex.Message, "ERROR", MessageBoxButton.OK, MessageBoxImage.Error);
	}
}

Il metodo Delete_User, chiama il metodo sul View Model e poi riposiziona il focus sulla Lista Users.

private void New_User(object sender, RoutedEventArgs e)
{
	try
	{
		Model.New_User();
		TxtUserName.Focus();
		TxtUserName.SelectAll();
	}
	catch (Exception ex)
	{
		MessageBox.Show(ex.Message, "ERROR", MessageBoxButton.OK, MessageBoxImage.Error);
	}
}

Il metodo New_User fa qualcosa in più, infatti dopo aver generato e selezionato il nuovo User, posiziona il focus sullo UserName selezionandone il contenuto in modo che l’utente possa subito iniziare a scrivere.

private void Save_Data(object sender, RoutedEventArgs e)
{
	try
	{
		Model.Save_Data();
	}
	catch (Exception ex)
	{
		MessageBox.Show(ex.Message, "ERROR", MessageBoxButton.OK, MessageBoxImage.Error);
	}
}

Il metodo Save, semplicemente chiama il metodo del ViewModel.

private void Undo_Edits(object sender, RoutedEventArgs e)
{
	try
	{
		Model.Undo_Edits();
	}
	catch (Exception ex)
	{
		MessageBox.Show(ex.Message, "ERROR", MessageBoxButton.OK, MessageBoxImage.Error);
	}
}

Allo stesso modo fa il metodo Undo.

A questo punto, possiamo testare la nostra applicazione e verificare di essere in grado di inserire e cancellare nonché modificare dati sui nostri tre diversi database.

15_usersdb_12_02_edit_SQLServer

Qui sopra l’interfaccia collegata e le modifiche effettuate su SQL Server.

15_usersdb_12_03_edit_Access

Qui sopra l’interfaccia collegata e le modifiche effettuate su Access.

15_usersdb_12_03_edit_Sqlite

Qui sopra l’interfaccia collegata e le modifiche effettuate su SQLite.

15_usersdb_12_04_edit_SqlServer

E ovviamente, l’interfaccia che mostra come la textbox correntemente attiva è verde e non bianca con il bordino azzurro come nel caso dell’interfaccia standard che usa le impostazioni di Windows.

Per oggi ci fermiamo qui, nella prossima puntata vedremo come gestire la selezione del tipo di database e la Connection string al database come parametri di configurazione non hardcoded nell’applicazione. E poi andremo a mostrare un poche delle funzionalità che fanno di WPF il sistema ideale per creare applicazioni User Friendly.

Riepilogo

Cosa abbiamo visto in questo articolo:

  • Come implementare i metodi di creazione, cancellazione, salvataggio e annullamento modifiche sul View Model.
  • Come implementare i command button che le eseguono nello XAML.
  • Come implementare gli Event Handler nel Code Behind.
  • Come implementare uno stile per modificare il comportamento di un controllo Textbox
  • Un test di tutte le modifiche effettuate.

Potete scaricare il codice esempio con la parte discussa ed aggiunta in questo post al link seguente:

Se avete domande, curiosità, se trovate errori od ommissioni non abbiate timore ad usare la form di contatto in cima alla pagina.