Press "Enter" to skip to content

15 – Lavorare con i dati – User Interface Creiamo l’Interfaccia per la tabella Users

In questo articolo, costruiremo l’interfaccia utente per la visualizzazione e modifica dei dati della tabella TbUsers, in modo da fornire agli utenti che useranno il nostro programma qualcosa di semplice e funzionale. Come già indicato in precedenza, per le tabelle come questa che hanno un numero di campi piuttosto basso, usualmente io genero una finestra divisa in due porzioni, una parte contiene la lista completa del contenuto della tabella, una parte contiene invece il dettaglio del record selezionato all’interno della lista.

Per fare tutto questo agiremo sull’attuale Window modificandone la forma, per spostare la Result Textbox sotto ai command button trasformandola in una status bar, aggiungere una DataGrid per poter inserire la lista degli User, Aggiungere una serie di TextBlock e TextBox per ospitare i dati dei dettagli dello User Selezionato.

Oltre a questo, nel ViewModel della window aggiungeremo un metodo per caricare i dati dal database, aggiungeremo la lista dei tipi di login ammessi, così da poterli assegnare agli User, e aggiungeremo una variabile che ci permetterà di cambiare il colore del testo dei messaggi sulla status bar se si tratta di errori.

UsersWindowModel.cs

Iniziamo con le modifiche al ViewModel.

public List<TypeOfLogin> LoginTypes
{
	get
	{
		return mLoginTypes;
	}
	set
	{
		mLoginTypes = value;
		OnPropertyChanged(FLD_LoginTypes);
	}
}

La property LoginTypes ci fornirà la lista dei possibili valori della enumerazione Login Types in modo che possiamo selezionare per ogni utente quello che è più adatto.

public SolidColorBrush ResultTextBrush
{
	get
	{
		return mResultTextBrush;
	}
	set
	{
		mResultTextBrush = value;
		OnPropertyChanged(FLD_ResultTextBrush);
	}
}

La property ResultTextBrush invece ci permetterà di cambiare il colore del testo della textbox ResultText in caso di errore.

public User SelectedUser
{
	get
	{
		return mSelectedUser;
	}
	set
	{
		mSelectedUser = value;
		OnPropertyChanged(FLD_SelectedUser);
	}
}

Selected User è la property più importante di quelle aggiunte, perché la collegheremo al SelectedItem della Datagrid che ne determinerà lo User contenuto e poi collegheremo le sue property ai controlli del dettaglio della window per permettere di modificarne i valori.

public UsersWindowModel()
{
	Users = new ObservableCollection<User>();
	UseSqlServer = true;
	LoginTypes = new List<TypeOfLogin>();
	LoginTypes.Add(TypeOfLogin.None);
	LoginTypes.Add(TypeOfLogin.Manual);
	LoginTypes.Add(TypeOfLogin.Windows);
}

Modifichiamo il costruttore generando la collezione dei valori dell’enumerazione che saranno usati dalla combobox per selezionare il valore del tipo di login.

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

Generiamo un metodo che esegue semplicemente il caricamento dei dati all’interno della lista Users, copiando il pezzetto di codice relativo che avevamo inserito nella funzione di Test.

Con queste modifiche abbiamo quanto necessario a testare la prima bozza della User Interface, pertanto andiamo a modificare la parte Xaml della Window e il suo Code Behind per ottenere quanto abbiamo deciso di fare.

UsersWindow.Xaml

Vediamo quali modifiche abbiamo fatto alla parte Xaml della nostra window. Per questa prima User Interface utilizzeremo solo gli oggetti di base forniti da WPF, in seguito, potremo esplorare meglio i controlli prodotti da terze parti, soprattutto quelli gratuiti, così da mostrare anche controlli con funzionalità più sofisticate di quelle elementari fornite dai controlli standard.

<Grid>
        <Grid.RowDefinitions>
      <RowDefinition Height="*"/>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

La prima modifica che facciamo è alla Grid principale della Window, in cui aggiungiamo un ulteriore riga, dove andremo a spostare la TextBox collegata al ResultText che diventerà per noi una status bar ove dare messaggi all’utente su quello che accade nella nostra user interface.

<TextBox
    Grid.Row="2"
    Margin="4,2,4,2"
    HorizontalAlignment="Stretch"
    VerticalAlignment="Center"
    HorizontalContentAlignment="Left"
    VerticalContentAlignment="Top"
    VerticalScrollBarVisibility="Auto"
    TextWrapping="Wrap"
    MaxHeight="100"
    Foreground="{Binding ResultTextBrush, UpdateSourceTrigger=PropertyChanged}"
    Text="{Binding ResultText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
  

Modifichiamo la TextBox collegata a ResultText spostandola dalla Row 0 alla Row 2 della grid, quindi sotto allo StackPanel con i Button di comando inoltre se avete osservato attentamente ho aggiunto un paio di attributi, il TextWrapping e la MacxHeight, ma quello più importante è il Foreground, che rappresenta il colore del testo, come potete notare, ho usato un Binding anche su questa property, in modo che tale attributo assumerà il valore della property appositamente creata nel ViewModel.

<Grid Grid.Row="0">
      <Grid.ColumnDefinitions>
          <ColumnDefinition Width="40*"/>
          <ColumnDefinition Width="60*"/>
      </Grid.ColumnDefinitions>

Al posto della TextBox, nella prima riga della Grid primaria creiamo una seconda Grid che sarà quella che ospiterà tutto il contenuto, questa grid avrà due colonne, la prima in cui porremo la lista degli User, la seconda in cui porremo i dettagli.

Come potete notare ho usato le unità relative per indicare che la colonna a sinistra è più piccola di quella a destra. Fate attenzione che il numero che utilizzate è arbitrario, ma quando seguito dall’asterisco assume connotazione proporzionale, mentre senza l’asterisco assume il valore dell’unità di rappresentazione grafica di WPF.

Nel codice qui sopra, se avessi scritto 400* e 600* avrei ottenuto lo stesso risultato. Il fatto di poter usare dei numeri arbitrari invece che delle percentuali, serve per poter effettuare un fine tuning quando vi sia necessità di creare molte colonne.

<DataGrid
    Grid.Column="0"
    ItemsSource="{Binding Users, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"   
    SelectedItem="{Binding SelectedUser, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
    AutoGenerateColumns="False"
    SelectionMode="Single"
    SelectionUnit="FullRow"
    CanUserResizeColumns="True"
    CanUserAddRows="False"
    CanUserDeleteRows="False"
    CanUserReorderColumns="True"
    CanUserResizeRows="True"
    CanUserSortColumns="True"
    RowHeaderWidth="20"
    >
          
    <DataGrid.Columns>
        <DataGridTextColumn Header="User Name" Binding="{Binding UserName}" Width="150" IsReadOnly="True"/>
        <DataGridTextColumn Header="WIndows User" Binding="{Binding WinUser}" Width="150" IsReadOnly="True"/>
        <DataGridTextColumn Header="Domain" Binding="{Binding WinDomain}" Width="300" IsReadOnly="True"/>
    </DataGrid.Columns>
</DataGrid> 

La DataGrid, è un controllo equivalente a quello delle Windows Forms, non era presente nella prima versione di WPF ma è stato aggiunto in seguito, con grande gioia di tutti noi. Come vedete dal numero degli attributi XAML, possiede molte property.

Vediamo quindi che cosa abbiamo settato e perché, prima di descrivere la parte delle Columns.

  • Grid.Column – Indica che starà sulla prima colonna della Grid che la contiene.
  • ItemsSource – E’ la property che collega la lista presente nel View Model alla DataGrid,
    • Binding può essere tradotto con Collegamento, dopo alla parola chiave, c’è la property Users a cui il collegamento è stato fatto oppure un Path che porta a tale property, lo vedremo nei binding dei controlli dettagli.
    • Mode – Indica come i dati possono “viaggiare” fra il controllo ed il View Model, in questo caso OneWay significa che il View Model fornisce i dati al controllo, ma il controllo stesso non modifica la lista.
    • UpdateSourceTrigger – si traduce con Interruttore Sorgente di Modifica, che in italiano è corretto ma non troppo chiaro, questa property Indica alla User Interface che cosa deve scatenare l’aggiornamento del controllo durante il funzionamento, PropertyChanged significa che l’evento PropertyChanged della lista User deve scatenare un aggiornamento della lista. Pertanto quando aggiungeremo o cancelleremo elementi alla lista, l’ObservableCollection scatenerà un evento PropertyChanged e la DataGrid vi reagirà aggiornandosi.
  • SelectedItem – E’ la property che collega l’elemento selezionato della DataGrid al View Model.
    • Binding SelectedUser – collega la property SelectedUser alla property della DataGrid.
    • ModeTwoWay, in questo caso, siccome vogliamo che le modifiche al Selected User possano essere dal Model all’Interfaccia e dall’Interfaccia al Model, usiamo questa clausola che permette la modifica bidirezionale dei dati.
    • UpdateSourceTriggerPropertyChanged, come per la property precedente, la modifica della property scatena la modifica dei dati.
  • AutoGenerateColumnsFalse, significa che le colonne della DataGrid non dovranno essere automaticamente generate dalla DataGrid quando i dati sono forniti dal ViewModel, ma invece c’è del codice che le definirà in modo specifico.
  • SelectionModeSingle, indica che permetteremo di selezionare un solo elemento per volta sulla lista, questo perché l’elemento selezionato pilota il dettaglio.
  • SelectionUnitFullRow, indica che cosa vogliamo che vi sia nella selezione, ovvero l’intero oggetto rappresentante la riga.
  • CanUserResizeColumnsTrue, si spiega da solo, permette infatti di allargare o stringere le colonne; con i successivi comandi ci permette di configurare come vogliamo che funzioni la nostra DataGrid.
  • CanUserAddRowsFalse, non permetteremo di aggiungere righe quindi record direttamente nella lista, lo faremo in modo diverso usando un apposito command button.
  • CanUserDeleteRowsFalse, come per il precedente, controlleremo in altro modo come cancellare righe dalla lista.
  • CanUserReorderColumnsTrue, attiva la possibilità di spostare le colonne trascinandole dalla barra del titolo.
  • CanUserResizeRowsTrue, attiva la possibilità di modificare la dimensione delle righe della Grid da parte dell’utente trascinando il row header.
  • CanUserSortColumnsTrue, attiva la possibilità di effettuare il sort su una colonna con un click sull’header, tenendo premuto Shift si può ordinare su più colonne.
  • RowHeaderWidth20, visualizza il row header prima della prima colonna, automaticamente è 0 quindi non visibile.

Ed ora la definiziaone delle colonne, come potete notare, c’è un tag DataGrid.Columns, se non lo sapeste, così come Grid.Row e Grid.Column, Grid.RowDefinitions e Grid.ColumnDefinitions, indica che Columns è una Attached Property del controllo DataGrid, pertanto vi invito ad andare a dare un occhiata a cosa sono le Attached Properties su MSDN perché sono un argomento interessante se in futuro voleste modificare dei controlli o sviluppare dei controlli vostri per specifici usi.

Se osservate le colonne che ho definito, potrete notare che non ho definito una colonna per ognuna delle property dello User, ma che ne ho definite solo 3, è semplicemente per mostrare che non è necessario mostrare tutte le property di un oggetto in una grid, ma possiamo scegliere quelle significative per il nostro scopo, in questo caso ho scelto UserName, WinUser e WinDomain, le tre che considero utili per una ricerca. Credo possiate Notare due cose nella definizione delle colonne:

  1. Il Binding è molto semplice ed ha solo scritto il nome della property, questo perché il binding di una column è automaticamente relativo all’oggetto contenuto nella collection ItemsSource, pertanto la grid è in grado di capire senza alcun tipo di specifica che deve visualizzare la property UserName dell’oggetto della lista.
  2. Ho indicato IsReadOnly = True perché automaticamente la DataGrid lo pone a False e quindi con un click su un campo di una riga sarebbe possibile modificare i dati. In questo caso io non voglio che i dati siano modificati dalla lista ma solo dai controlli di dettaglio.
<StackPanel Grid.Column="1" Margin="2" Orientation="Vertical">

Nella seconda colonna mettiamo uno StackPanel che stavolta sarà verticale, e conterrà il dettaglio dell’Item selezionato.

<TextBlock Margin="2,4,2,4" Padding="4" HorizontalAlignment="Left" VerticalAlignment="Center"
     Text="ID"/>
<TextBlock Margin="2,4,2,4" Padding="4" HorizontalAlignment="Right" VerticalAlignment="Center"
     MinWidth="100"
     Text="{Binding Path=SelectedUser.ID, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Margin="2,4,2,4" Padding="4" HorizontalAlignment="Left" VerticalAlignment="Center"
     Text="User Name"/>
<TextBox Margin="2,4,2,4" 
         Padding="4" 
         HorizontalAlignment="Stretch" 
         VerticalAlignment="Center"
         TextAlignment="Left"
         Text="{Binding Path=SelectedUser.UserName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Margin="2,4,2,4" Padding="4" HorizontalAlignment="Left" VerticalAlignment="Center"
     Text="Windows User"/>
<TextBox Margin="2,4,2,4" 
         Padding="4" 
         HorizontalAlignment="Stretch" 
         VerticalAlignment="Center"
         TextAlignment="Left"
         Text="{Binding Path=SelectedUser.WinUser, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Margin="2,4,2,4" Padding="4" HorizontalAlignment="Left" VerticalAlignment="Center"
     Text="Domain"/>
<TextBox Margin="2,4,2,4" 
         Padding="4" 
         HorizontalAlignment="Stretch" 
         VerticalAlignment="Center"
         TextAlignment="Left"
         Text="{Binding Path=SelectedUser.WinDomain, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Margin="2,4,2,4" Padding="4" HorizontalAlignment="Left" VerticalAlignment="Center"
     Text="Password"/>
<TextBox Margin="2,4,2,4" 
         Padding="4" 
         HorizontalAlignment="Stretch" 
         VerticalAlignment="Center"
         TextAlignment="Left"
         Text="{Binding Path=SelectedUser.Password, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Margin="2,4,2,4" Padding="4" HorizontalAlignment="Left" VerticalAlignment="Center"
     Text="Computer"/>
<TextBox Margin="2,4,2,4" 
         Padding="4" 
         HorizontalAlignment="Stretch" 
         VerticalAlignment="Center"
         TextAlignment="Left"
         Text="{Binding Path=SelectedUser.Computer, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Margin="2,4,2,4" Padding="4" HorizontalAlignment="Left" VerticalAlignment="Center"
     Text="Login Type"/>
<ComboBox Margin="2,4,2,4" 
          Padding="4" 
          HorizontalAlignment="Stretch" 
          VerticalAlignment="Center"
          ItemsSource="{Binding LoginTypes}"
          SelectedItem="{Binding Path=SelectedUser.LoginType, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>

Nello Stack Panel ho inserito TextBlock e TextBox che permettono di modificare le property del SelectedUser, la sola differenza dei binding di questa serie di controlli potete notare essere l’uso dell’attributo Path al posto del semplice nome della property, perché in questo caso mettiamo in Binding la property di un oggetto, pertanto per fare in modo che il controllo sappia esattamente cosa utilizzare indichiamo specificamente Oggetto.Property.

Se guardate bene nei controlli, ci sono 2 piccole cose non regolari, la prima è che per il campo ID ho usato una TextBlock, perché il numero non può essere modificato da noi ma assegnato in automatico. La seconda è la combobox per la selezione del LoginType. La combobox, essendo un controllo List come la DataGrid, ha due Binding, quello alla lista dei valori su ItemsSource, il secondo sul SelectedItem per la variabile da modificare.

Considerando che stiamo muovendoci a piccoli passi, non elimino ancora i pulsanti di test ma aggiungerò invece un Button per caricare i dati nella lista.

<Button
    Margin="4,2,4,2"
    Padding="10,4,10,4"
    Click="Load_Data">
    <TextBlock Text="Load Data"/>
</Button>

Il click di questo button sarà collegato ad un event handler che è la sola modifica che faremo al code behind della classe.

UsersWindow.xaml.cs

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 nostro event handler chiamerà semplicemente il metodo che abbiamo implementato nel View Model.

A questo punto non ci resta che fare un test:

14_usersdb_10_02_loaded[4]

Se facciamo partire la nostra applicazione e premiamo Load Data, questo è quello che accadrà, verrà caricata la lista nella grid.

14_usersdb_10_02_selected[4]

Selezionando una riga sulla DataGrid, come potete notare, i controlli del dettaglio vengono riempiti. Vi invito a provare a modificare il testo nelle textbox, vedrete che modificando UserName, WinUser o WinDomain la modifica sarà riflessa dalla cella della DataGrid.

Faccio notare che nelle 2 immagini precedenti sulla “Status Bar” ovvero la nostra textbox di stato, c’è il risultato del caricamento dati scritto in nero.

Siccome sviluppando una applicazione bisogna testare anche i casi di errore, proviamo a mandare in errore il caricamento dati:

14_usersdb_10_00_stopsqlserver[4]

Per farlo ho aperto il Sql Server Configuration Manager e selezionando i SQL Server Services ho fermato il servizio principale del server SQL.

14_usersdb_10_01_error[4]

Rifacendo partire l’applicazione da Visual Studio, e cliccando sul button Load Data, dopo circa un minuto ottengo un errore, infatti il programma non riesce a connettersi al server. Come vedete, la dimensione della textbox si è adattata alla massima dimensione possibile ed è apparsa la scrollbar, inoltre, il testo non è più nero ma rosso.

Provate a far ripartire il servizio di SQL Server dal Sql Server Configuration Manager, poi cliccate di nuovo su Load Data e i dati saranno correttamente caricati.

Ovviamente questa interfaccia è ancora molto grezza, ma credo sia buona cosa fare un passo alla volta e vedere punto per punto svilupparsi la nostra applicazione. Nella prossima puntata, creeremo i pulsanti di comando per le azioni di modifica del database e tutte le parti del View Model connesse.

Riepilogo

In questo articolo abbiamo parlato dei seguenti argomenti:

  • Creare Property e codice nel View Model che siano utilizzati dalla User Interface
  • Come funziona il binding di una DataGrid standard
  • Come effettuare il binding del SelectedItem della DataGrid ad una property del ViewModel
  • Come configurare la DataGrid e tutti gli attributi che ne determinano il funzionamento
  • Come configurare le colonne della datagrid
  • Come caricare i dati nel ViewModel e vederli apparire nei controlli della Window
  • Come collegare property diverse dal contenuto, nell’esempio il colore del testo della textbox che funge da status bar.

Potete scaricare il codice del progetto al seguente link:

Per qualsiasi domanda, curiosità, o se trovate qualche errore usate il link al modulo di contatto che trovate a inizio pagina.