Questo sito utilizza cookies, anche di terze parti, per mostrare pubblicità e servizi in linea con il tuo account. Leggi l'informativa sui cookies.
Username: Password: oppure
Guida al Visual Basic .NET - DataGridView  Parte I

Guida al Visual Basic .NET

Capitolo 77° - DataGridView Parte I

<< Precedente Prossimo >>
Introduzione

DataGridView è uno dei controlli più potenti e grandi del Framework .NET. Consente la visualizzazione di dati in una tabella ed è per questo motivo fortemente correlato all'uso dei database, anche se naturalmente supporta, tramite la proprietà DataSource, il binding di qualsiasi oggetto:

DataGridView.jpg
Ecco un elenco delle proprietà interessanti:

  • AreAllCellsSelected(includeInvisible As Boolean) : restituisce True se tutte le celle sono selezionate. Se includeInvisible è True, include nel controllo anche quelle colonne che normalmente sono nascoste (è possibile nascondere colonne indesiderate, come ad esempio l'ID);
  • AllowUserTo AddRows, DeleteRows, OrderColumns, ResizeColumns, ResizeRows: una serie di proprietà booleane che determinano se l'utente sia o meno in grado di aggiungere o rimuovere righe, ordinare le colonne o ridimensionare sia le righe che le colonne;
  • AlternatingRowsDefaultCellStyle : specifica il CellStyle (un insieme di proprietà che definiscono l'aspetto estetico di una cella) per le righe di posto dispari. Se questo valore è diverso da DefaultCellStyle, avremo le righe di stile alternato (ad esempio una di un colore e una di un altro), da cui il nome di questo membro;
  • AutoResize... : tutti i metodi che iniziano con "AutoResize" servono per ridimensionare righe o colonne;
  • AutoSizeColumnsMode : proprietà enumerata che determina in che modo le colonne vengano ridimensionate quando del testo va oltre i confini visibili. I valori che può assumere sono:
    • None : le colonne rimangono sempre della stessa larghezza, a meno che l'utente non le ridimensioni manualmente;
    • AllCells : la colonna viene allargata affinché il testo di tutte le celle sottostanti e della stessa intestazione sia visibile;
    • AllCellsExceptHeader : la colonna viene allargata in modo che solo il testo di tutte le celle sottostanti sia visibile;
    • ColumnHeader : la colonna viene allargata in modo che il testo dell'header sia interamente visibile;
    • DisplayedCells : come AllCells, ma solo per le celle visibili nei margini del controllo;
    • DisplayedCellsExceptHeader : come AllCellsExceptHeader, ma solo per le celle visibili nei margini del controllo;
    • Fill : le colonne vengono ridimensionate affinché la loro larghezza totale sia quanto più possibile vicina all'area effettivamente visibile a schermo, nei limiti imposti dalla proprietà MinimumWidth di ciascuna colonna.
  • AutoSizeRowsMode : come sopra, ma per le righe;
  • CancelEdit() : termina l'editing di una cella e annulla tutte le modifiche ad essa apportate;
  • CellBorderStyle : proprietà enumerata che definisce come sia visualizzato il bordo delle celle. Inutile descriverne tutti i valori: basta provarli tutti per vedere come appaiono;
  • ClearSelection: deseleziona tutte le celle selezionate
  • ColumnCount / RowCount : determina il numero iniziale di colonne o righe visualizzate sul controllo
  • ColumnHeaders / RowHeaders : imposta lo stile di visualizzazione, i bordi, le dimensioni e la visibilità delle intestazioni
  • Columns : insieme di tutte le colonne del controllo. Tramite questa proprietà è possibile determinare quali siano i tipi di valori che si possono immettere in una cella. Nella finestra di dialogo durante la scrittura del programma, infatti, quando si aggiunge una colonna non a runtime, viene anche chiesto quale debba essere il suo tipo, proponendo una gamma abbastanza ampia di possibilità (textbox, combobox, checkbox, image, button, linklabel)
  • CurrentCell : imposta o restituisce la cella selezionata (un oggetto di tipo DataGridViewCell)
  • CurrentCellAddress : restituisce due coordinate sotto forma di Point che indicano la colonna e la riga della cella selezionata
  • CurrentRow : indica la riga contenente la cella selezionata (o la riga selezionata se tutte le sue celle sono selezionate);
  • DefaultCellStyle : specifica lo stile predefinito per una cella; ogni cella, poi, può modificare il proprio aspetto mediante la proprietà Style;
  • DisplayedPartialColumns/Rows(includePartial As Boolean) : restituisce il numero di colonne / righe visibili nel controllo. Se includePartial è True, include nel conteggio anche quelle che si vedono solo parzialmente;
  • EditMode : proprietà enumerata che indica in che modo sia possibile iniziare a modificare il contenuto di una cella. I valori che può assumere sono:
    • EditOnEnter : inizia la modifica quando la cella viene selezionata, quando riceve il focus oppure quando viene premuto invio su di essa;
    • EditOnF2 : inizia la modifica quando l'utente preme F2 sulla cella selezionata;
    • EditOnKeystroke : inizia la modifica quando viene premuto un qualsiasi tasto (alfanumerico) mentre la cella ha il focus;
    • EditOnKeystrokeOrF2 : è palese...
    • EditProgrammatically : inizia la modifica solo quando viene esplicitamente richiamato da codice il metodo BeginEdit;
  • FirstDisplayedCell : ottiene o imposta un riferimento alla prima cella visualizzata;
  • GetCellCount(Filter) : restituisce il numero di celle che soddisfano il filtro impostato. Filter non à altro che un valore enumerato codificato a bit che contempla questi valori: Displayed (celle visualizzate), Frozen (celle che è impossibile scrollare), None (celle che sono in stato di default), ReadOnly (celle a sola lettura), Resizable and ResizableSet (se specificati entrambi, indicano le celle ridimensionabili), Selected (celle selezionate), Visible (celle visibili, nel senso che è possibile vederle, non che sono effettivamente visualizzate);
  • HitTest(x, y) : restituisce un valore strutturato contenente riga e colonna della cella che si trova alle coordinate relative x e y (in pixel);
  • IsCurrentCellDirty : indica se la cella corrente contiene modifiche non salvate;
  • IsCurrentCellInEditMode : indica se la cella corrente è in modalità edit (modifica);
  • IsCurrentRowDirty : indica se la riga corrente contiene modifiche non salvate;
  • Item(x,y): restituisce la cella alle coordinate x e y (colonna e riga). La sua proprietà più importante è Value, che restituisce o imposta il valore contenuto nella cella, che può essere un testo, un valore booleano, una combobox eccetera
  • MultiSelect: determina qualora sia possibile selezionare più celle, colonne o righe insieme;
  • Row... : tutte le proprietà che iniziano con "Row" sono analoghe a quelle spiegate per Column;
  • ReadOnly : determina se l'utente possa o meno modificare i contenuto delle celle
  • SelectedCells/Columns/Rows : restituisce un insieme delle celle/colonne/righe correntemente selezionate;
  • SelectionMode : proprietà enumerata che indica come debba avvenire la selezione. Può assumere 5 valori: CellSelect (solo la cella), FullRowSelect (tutta la riga), FullColumnSelect (tutta la colonna), RowHeaderSelect (solo l'intestazione della riga) o ColumnHeaderSelect (solo l'intestazione della colonna);
  • ShowCellToolTip : indica se visualizzare il tooltip della cella;
  • ShowEditingIcon : indica se visualizzare l'icona di modifica;
  • Selected Cells, Columns, Row: tre collezioni che restituiscono un insieme di tutte le celle, colonne o righe selezionate;
  • Sort(a, b): utilissima procedura che ordina la colonna a della datagridview secondo un ordine ascendente o discendente definito da un enumeratore di tipo ComponentModel.ListSortDirection;
  • StandardTab : indica se il pulsante Tab viene usato per ciclare i controlli (true) o le celle all'interno del datagridview (false);

Come avete visto c'è una marea di membri, e un numero consistente di questi sono dedicati ad impostare l'"estetica" del controllo. Ma tutto questo non è nulla se confrontato alla quantità di eventi che DataGridView espone (e al numero di disturbi mentali che è solita causare nei programmatori sani).

Un classico esempio di gestionale

La DataGridView è un controllo usatissimo soprattutto in quei noiosissimi programmi che qualcuno chiama gestionali, e che un gran numero di poveri programmatori è costretto a scrivere per guadagnarsi il pane quotidiano. Per questo motivo, il prossimo esempio sarà particolarmente realistico. Andremo a scrivere un'applicazione per gestire clienti e ordini.
Prima di iniziare, creiamo le tabelle che ci serviranno per il programma (sì, useremo un database):

CREATE TABLE `customers` (               
    `ID` int(11) NOT NULL AUTO_INCREMENT,  
    `FirstName` char(150) DEFAULT NULL,    
    `LastName` char(150) DEFAULT NULL,     
    `Address` char(255) DEFAULT NULL,      
    `PhoneNumber` char(30) DEFAULT NULL,   
    `RegistrationDate` date NOT NULL,      
    `AccountType` int(5) DEFAULT '0',      
    PRIMARY KEY (`ID`)                     
    );
    
CREATE TABLE `orders` (                      
    `ID` int(11) NOT NULL AUTO_INCREMENT,      
    `CustomerID` int(11) NOT NULL,             
    `ItemName` char(255) NOT NULL,             
    `ItemPrice` float unsigned NOT NULL,       
    `ItemCount` int(10) unsigned DEFAULT '1',  
    `CreationDate` date NOT NULL,              
    `Settled` tinyint(1) NOT NULL,             
    PRIMARY KEY (`ID`)                         
    );

E, per completezza, bisogna aggiungere anche le rispettive rappresentazioni tabulari in un nuovo dataset (si tratta solo di ricopiare).
Lo scopo del programma consiste nel gestire una serie di clienti e di ordini associati, indicando lo stato di ciascuno e le sue condizioni di pagamento. Avremo, quindi, una datagridview per contenere i clienti, una per contere gli ordini e una listview per visualizzare un riepilogo delle informzioni. Inoltre, iniziamo subito con una casistica un po' complicata: prima di tutto vogliamo che il tipo dell'account di ciascun cliente sia visualizzato sottoforma di icona; poi vogliamo anche che la seconda datagridview visualizzi solo gli ordini associati al cliente correntemente selezionato. La prima richiesta verrà gestita completamente da codice, ma è opportuno che si aggiunga un'ImageList contenente almeno tre piccole immagini (16x16 vanno bene), mentre per la seconda avremo bisogno un po' di aiuto da parte di BindingSource. Nei capitoli precedenti, infatti, si è visto che questo componente espone una proprietà Filter mediante la quale possiamo restringere l'insieme di dati visualizzati sotto certi parametri (aggiungete quindi anche un BindingSource di nome bsOrders, ed impostate il suo DataSource sul dataset principale, e il suo DataMember su Orders). Ecco il codice:

Imports MySql.Data.MySqlClient

'Prima di iniziare:
' - MainDatabase è un'istanza di AppDataSet, ossia il
' dataset tipizzato creato mediante l'editor che contiene le
' due tabelle.
' - bsOrders è il BindingSource che useremo come sorgente
' dati per dgvOrders. Ricordatevi che:
'   bsOrders.DataSource = MainDatabase
'   bsOrders.DataMember = "Orders"   o   MainDatabase.Orders
' - AllowUserToAddRows = False per entrambi i datagridview

Public Class Form1

    Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        Dim Connection As New MySqlConnection("Server=localhost; Database=appdata; Uid=root; Pwd=root;")
        Dim Adapter As New MySqlDataAdapter()

        Try
            Connection.Open()
            Adapter.SelectCommand = New MySqlCommand("SELECT * FROM Customers;", Connection)
            Adapter.Fill(MainDatabase.Customers)
            Adapter.SelectCommand = New MySqlCommand("SELECT * FROM Orders;", Connection)
            Adapter.Fill(MainDatabase.Orders)
        Catch Ex As Exception
            MessageBox.Show("Impossibile connettersi al database!", Me.Text, MessageBoxButtons.OK, MessageBoxIcon.Error)
        Finally
            Connection.Close()
        End Try

        'Imposta la sorgente dati di dgvCustomers. Quando questa
        'proprietà viene impostata, crea automaticamente tutte
        'le colonne necessarie, ne imposta il tipo e
        'l'intestazione. Per questo motivo, tutte le colonne che
        'andremo ad usare iniziano ad esistere solo dopo questa
        'riga di codice. Tutte quelle che erano state definite
        'prima vengono eliminate.
        dgvCustomers.DataSource = MainDatabase.Customers
        'Nasconde la prima e la settima colonna, ossia l'ID e
        'l'AccountType. La prima non deve essere visibile poiché
        'contiene dati che non riguardano l'utente, mentre
        'l'ultima la nascondiamo perchè avevamo detto di
        'voler visualizzare il tipo di account con un'icona
        dgvCustomers.Columns(0).Visible = False
        dgvCustomers.Columns(6).Visible = False

        'Crea una nuova colonna di datagridview adatta a contenere
        'immagini. Esistono molti tipi di colonna (button, combobox,
        'linklabel, image, eccetera...), di cui la più comune
        'è una che contiene solo testo. Il tipo di una 
        'colonna indica il tipo di dati che tutte le celle ad essa
        'sottostanti devono contenere. In questo caso vogliamo
        'che l'ultima colonna contenga una piccola immagine
        'indicante il livello dell'account (ci saranno tre livelli)
        Dim Img As New DataGridViewImageColumn
        'Imposta l'immagine di default
        Img.Image = imgAccountTypes.Images(0)
        'E l'intestazione della colonna
        Img.HeaderText = "AccountLevel"
        'Quindi la aggiunge a quelle esistenti
        dgvCustomers.Columns.Add(Img)

        'Poi cicla attraverso tutte le righe, controllando il
        'contenuto della settima cella di ogni riga (ossia il
        'valore corrdispondente ad AccountType, un numero intero),
        'e imposta il contenuto dell'ottava prelevando la
        'rispettiva immagine dall'imagelist.
        'Ricordate che la colonna di indice 6, pur essendo
        'nascosta, esiste comunque
        For Each Row As DataGridViewRow In dgvCustomers.Rows
            Row.Cells(7).Value = imgAccountTypes.Images(CInt(Row.Cells(6).Value))
        Next

        'Imposta come sorgente di dati di dgvOrders il binding
        'source bsOrders, specificato in precedenza
        dgvOrders.DataSource = bsOrders
        'E nasconde le prime due colonne, ossia CustomerID e ID
        dgvOrders.Columns(0).Visible = False
        dgvOrders.Columns(1).Visible = False
        'Impone di visualizzare solo le tuple di ID pari a -1,
        'ossia nessuna
        bsOrders.Filter = "ID=-1"
    End Sub

    Private Sub dgvCustomers_KeyDown(ByVal sender As System.Object, ByVal e As System.Windows.Forms.KeyEventArgs) Handles dgvCustomers.KeyDown
        'Intercettiamo la pressione di un pulsante quando l'utente
        'si trova nell'ultima colonna (quella dell'icona)
        If dgvCustomers.CurrentCell.ColumnIndex = 7 Then
            'Ottiene il valore di AccountType
            Dim CurValue As Int32 = CInt(dgvCustomers.CurrentRow.Cells(6).Value)

            'Se è stato premuto +, aumenta il valore di 1
            'Se è stato premuto -, lo decrementa di 1
            If e.KeyCode = Keys.Oemplus Then
                CurValue += 1
            ElseIf e.KeyCode = Keys.OemMinus Then
                CurValue -= 1
            End If

            'Fa in modo di non andare oltre i limit imposti
            If CurValue < 0 Then
                CurValue = 0
            ElseIf CurValue > 2 Then
                CurValue = 2
            End If

            'Quindi imposta il nuovo valore di AccountType
            dgvCustomers.CurrentRow.Cells(6).Value = CurValue
            'E la corrispondente nuova immagine
            dgvCustomers.CurrentCell.Value = imgAccountTypes.Images(CurValue)
        End If
    End Sub

    'L'evento DataError viene generato ogniqualvolta l'utente
    'non rispetti i vincoli imposti dal database. Ad esempio,
    'viene generato se un campo marcato come NOT NULL viene
    'lasciato vuoto, o se una data non è valida, o
    'se un numero è troppo grande o troppo piccolo,
    'oppure ancora se non viene soddisfatto il vincolo di
    'unicità, eccetera...
    'In questi casi, se il programmatore non gestisce l'evento,
    'appare una finestra di default che riporta tutto il testo
    'dell'eccezione e vi assicuro che non è una bella cosa
    Private Sub dgvCustomers_DataError(ByVal sender As System.Object, ByVal e As System.Windows.Forms.DataGridViewDataErrorEventArgs) Handles dgvCustomers.DataError
        Dim Result As DialogResult

        'Riporta l'errore all'utente, e lascia scegliere se 
        'modificare i dati incompatibili oppure annullare
        'le modifiche e cancellare la riga
        Result = MessageBox.Show("Si ? verificato un errore di compatibilit? dei dati immessi. Messaggio:" & _
            Environment.NewLine & e.Exception.Message & Environment.NewLine & _
            "E' possibile che dei dati mancanti compromettano il database. Premere S? per modificare opportunamente " & _
            "tali valori, o No per cancellare la riga.", Me.Text, MessageBoxButtons.YesNo, MessageBoxIcon.Exclamation)

        If Result = Windows.Forms.DialogResult.Yes Then
            'Annulla questo evento: non viene generata la
            'finestra di errore
            e.Cancel = True
            'Pone il cursore sulla casella corrente e obbliga
            'ad iniziare l'edit mode. Il valore booleano tra
            'parentesi indica di selezionare l'intero contenuto
            'della cella corrente
            dgvCustomers.BeginEdit(True)
        Else
            'Annulla l'eccezione e l'evento, quindi cancella
            'la riga corrente
            e.ThrowException = False
            e.Cancel = True
            'Le righe "nuove", ossia quelle in cui non è
            'stato salvato ancora nessun dato, non possono essere
            'eliminate (così dice il datagridview...)
            If Not dgvCustomers.CurrentRow.IsNewRow Then
                dgvCustomers.Rows.Remove(dgvCustomers.CurrentRow)
            End If
        End If
    End Sub

    'Questa procedura aggiorna la ListView con alcuni dettagli
    'sullo stati dei pagamenti
    Private Sub RefreshPreview(ByVal CustomerID As Int32)
        lstPreview.Items.Clear()

        'Cerca il cliente
        Dim Customer As AppDataSet.CustomersRow = _
            MainDatabase.Customers.FindByID(CustomerID)

        'Se non esiste, esce
        If Customer Is Nothing Then
            Exit Sub
        End If
        
        Dim TotalPaid, TotalUnpaid As Single
        Dim Delay As TimeSpan

        'Notate che qui agiamo direttamente sul dataset,
        'perchè contiene campi tipizzati, e ci consente di
        'utilizzare meno operatori di cast
        For Each Order As AppDataSet.OrdersRow In MainDatabase.Orders
            'Conta solo gli ordini associati a un cliente
            If Order.CustomerID <> CustomerID Then
                Continue For
            End If

            'Se l'ordine è stato pagato, aggiunge il
            'totale alla variabile TotalPaid, altrimenti a
            'TotalUnpaid.
            'Se l'ordine non è stato pagato, inoltre,
            'calcola il ritardo maggiore nel pagamento
            If Order.Settled Then
                TotalPaid += Order.ItemPrice * Order.ItemCount
            Else
                TotalUnpaid += Order.ItemPrice * Order.ItemCount
                If Date.Now - Order.CreationDate > Delay Then
                    Delay = Date.Now - Order.CreationDate
                End If
            End If
        Next

        Dim Item1 As New ListViewItem
        Item1.Text = "Ammontare pagato"
        Item1.SubItems.Add(String.Format("{0:N2}?", TotalPaid))

        Dim Item2 As New ListViewItem
        Item2.Text = "Oneri futuri"
        Item2.SubItems.Add(String.Format("{0:N2}?", TotalUnpaid))

        Dim Item3 As New ListViewItem
        Item3.Text = "Ritardo pagamento"
        Item3.SubItems.Add(CInt(Delay.TotalDays) & " giorni")

        Dim DaysLimit As Int32
        'Un diverso tipo di account permette un maggior ritardo
        'nei pagamenti...
        Select Case Customer.AccountType
            Case 0
                DaysLimit = 60
            Case 1
                DaysLimit = 90
            Case 2
                DaysLimit = 120
            Case Else
                DaysLimit = 60
        End Select

        'Se il cliente ha superato il limite con almeno uno
        'dei suoi ordini, la riga viene colorata in rosso
        If Delay.TotalDays > DaysLimit Then
            Item3.ForeColor = Color.Red
        End If

        lstPreview.Items.Add(Item1)
        lstPreview.Items.Add(Item2)
        lstPreview.Items.Add(Item3)
    End Sub

    'Evento generato quando l'utente si posizione su una riga
    Private Sub dgvCustomers_RowEnter(ByVal sender As System.Object, ByVal e As System.Windows.Forms.DataGridViewCellEventArgs) Handles dgvCustomers.RowEnter
        'Se si tratta di una riga valida...
        If e.RowIndex < dgvCustomers.Rows.Count And e.RowIndex >= 0 Then
            Dim CurID As Int32 = CInt(dgvCustomers.Rows(e.RowIndex).Cells(0).Value)
            'Aggiorna il filtro di bsOrders, per visualizzare solo gli
            'ordini di quel dato cliente
            bsOrders.Filter = "CustomerID=" & CurID
            'E aggiorna la listview
            RefreshPreview(CurID)
        End If
    End Sub

    'Quando una cella di dgvOrders viene modificata, aggiorna
    'la listview...
    Private Sub dgvOrders_CellEndEdit(ByVal sender As System.Object, ByVal e As System.Windows.Forms.DataGridViewCellEventArgs) Handles dgvOrders.CellEndEdit
        Try
            RefreshPreview(CInt(dgvCustomers.CurrentRow.Cells(0).Value))
        Catch Ex As Exception

        End Try
    End Sub
End Class

Ed ecco come potrebbe presentarsi:

DataGridView2.jpg
Come avrete certamente notato, fatta eccezione per l'unica procedura RefreshPreview, abbiamo agito solo sul datagridview e non sul dataset. Questo accade perchè l'applicazione creata è "stratificata": può essere considerata come un particolare caso di applicazione 3-tier. L'architettura three-tier indica una particolare strutturazione del software in cui ci sono tre layer (strati) principali: data layer, buisness layer e gui layer. Il primo si dedica alla gestione dei dati persistenti (nel nostro caso, il database), il secondo si occupa di fornire delle logiche funzionali, ossia metodi che gestiscono le informazioni in maniera da rispecchiare il funzionamento dell'applicazione e che permettono di interfacciarsi più facilmente con il data layer (il nostro buisness layer è il dataset, rappresentazione oggettiva e funzionale dei dati persistenti) mentre il terzo ha il compito di mediare l'interazione con l'utente attraverso l'interfaccia grafica. Sentirete parlare molto spesso di questo tipo di architettura nel campo dei gestionali.

<< Precedente Prossimo >>
A proposito dell'autore

Programmatore e analista .NET 2005/2008/2010 (in particolare C# e VB.NET), anche nell'implementazione Mono per Linux. Conoscenze approfondite di Pascal, PHP, XML, HTML 4.01/5, CSS 2.1/3, Javascript (e jQuery). Conoscenze buone di C, LUA, GML, Ruby, XNA, AJAX e Assembly 68000. Competenze basilari di C++, SQL, Hlsl, Java.