Le avventure in VB.Net di un principiante ex-VB6 - 14
a cura di Oscar Zanin e Diego Cattaruzza (requisiti: Visual Basic Express e SqlServer)Premessa
Nell'articolo precedente è stata illustrata la parte della FrmRicerca che interagisce con le altre form, in modo da terminare lo sviluppo della FrmStati.
In questa puntata si completa lo sviluppo della FrmRicerca.Ultimazione della FrmRicerca
Per finire il disegno della form, aggiungiamo una DataGridView:
- Name = "DgvDati"
- Location = 13; 145
- Size = 761; 311
- AllowUserToAddRows = False
- AllowUserToDeleteRows = False
- ReadOnly = True
Campi privati
Come abbiamo visto nell'articolo precedente, il costruttore riceve una struttura InfoRicerca e invoca due metodi per la configurazione dei controlli nella form, cioè alcune Label e alcune ComboBox, e delle colonne della DataGridView. Per le ComboBox, è opportuno inizializzare un vettore di riferimenti. Ecco quindi i campi privati:Private mInfoRicerca As InfoRicerca ' struttura di informazioni ricevuta dalla form Private mArrCombo() As ComboBox ' vettore di riferimenti alle combobox dei criteri Private mRigaSelezionata As Integer = -1 ' puntatore alla riga selezionataOltre al vettore di riferimenti alle Combobox, vediamo la struttura InfoRicerca e un puntatore alla riga selezionata nella griglia, dalla quale bisogna trarre i valori da restituire nella proprietà CampiChiave.
Metodi di configurazione: Label e ComboBox
Il primo metodo chiamato nel costruttore, PreparaCriteri, crea e dispone Label e ComboBox destinate all'impostazione dei criteri di ricerca:Private Sub PreparaCriteri() If mInfoRicerca.Criteri.Count > 0 Then Const xLabel As Integer = 13 ' location.x della prima label Const xCombo As Integer = 141 ' location.x della prima combo Const ctlWidth As Integer = 121 ' size.width di label e combo Const ctlHeight As Integer = 21 ' size.height di label e combo Const ctlStep As Integer = 256 ' scostamento da un controllo all'altro dello stesso tipo Const yRiga1 As Integer = 61 ' location.y dei controlli della prima riga Const yRiga2 As Integer = 103 ' location.y dei controlli della seconda rigaSe esistono criteri, vengono impostate le costanti di disegno dei controlli. Usare le costanti è utile per molte ragioni: è più facile ricordare il nome che il valore ed è più comodo modificare (eventualmente fosse necessario) il valore in un unico posto piuttosto che cercare tutte le ricorrenze di quel valore.
' ridimensiono opportunamente il vettore dei riferimenti alle combobox Array.Resize(mArrCombo, mInfoRicerca.Criteri.Count) ' per ciascun criterio For i As Integer = 0 To mInfoRicerca.Criteri.Count - 1 ' stabilisco la riga in cui saranno posizionati label e combobox Dim y As Integer = If(i < 3, yRiga1, yRiga2)Ridimensionato il vettore dei riferimenti alle ComboBox e impostato il ciclo di scansione del vettore dei criteri passato nella struttura InfoRicerca, si calcola la posizione verticale della coppia di controlli che si sta per creare (ricordiamo che i criteri possibili sono al massimo sei, disposti su due righe di tre).
' LABEL ' istanzio una label Dim aLbl As New Label ' ne imposto alcune proprietà With aLbl .Location = New System.Drawing.Point(xLabel + ctlStep * (i Mod 3), y) .Name = "lblCrit" & i.ToString .Size = New System.Drawing.Size(ctlWidth, ctlHeight) .Font = New Font(aLbl.Font.FontFamily, 10) .Text = mInfoRicerca.Criteri(i).Etichetta .TextAlign = ContentAlignment.MiddleRight .Visible = True End With ' e la aggiungo all'insieme dei controlli della form Me.Controls.Add(aLbl) ' COMBOBOX ' istanzio una Combobox Dim aCmb As New ComboBox ' ne imposto alcune proprietà With aCmb .Location = New System.Drawing.Point(xCombo + ctlStep * (i Mod 3), y) .Name = "cmbCrit" & i.ToString .AutoCompleteMode = AutoCompleteMode.SuggestAppend .AutoCompleteSource = AutoCompleteSource.ListItems .Size = New System.Drawing.Size(ctlWidth, ctlHeight) .Sorted = True .DropDownStyle = ComboBoxStyle.DropDown .TabIndex = i + 1 .Visible = True End With ' lo aggiungo all'insieme dei controlli della form Me.Controls.Add(aCmb) ' e ne assegno il riferimento all'elemento iesimo del vettore mArrCombo(i) = aCmbVengono create e aggiunte la Label e la ComboBox. Quindi si compila l'elenco della ComboBox, in base al tipo di origine dati passato in InfoRicerca.
Select Case mInfoRicerca.Criteri(i).OrigineDati Case TipoOrigineDati.Filtro If mInfoRicerca.Criteri(i).Valori.Count > 0 Then ' il membro Valori deve avere almeno un elemento mArrCombo(i).Items.Add(mInfoRicerca.Criteri(i).Valori(0).ToString) ' pre-imposto il testo mArrCombo(i).SelectedIndex = 0 ' disabilito la combobox mArrCombo(i).Enabled = False ' imposto il 'promemoria' per pre-caricare la griglia DgvDati.Tag = TipoOrigineDati.Filtro End IfSe il criterio per il quale si stanno creando i controlli deve costituire un filtro, il suo valore è indicato nel primo elemento del membro Valori, deve essere esposto dalla ComboBox, che deve essere disabilitata. Poiché in caso esista un filtro è oppportuno che la griglia appaia precompilata, all'apertura della form, si stabilisce questa necessità valorizzando il Tag della griglia.
Case TipoOrigineDati.ElencoValori If mInfoRicerca.Criteri(i).Valori.Count > 0 Then ' il membro Valori deve contenere gli elementi per la lista For v As Integer = 0 To mInfoRicerca.Criteri(i).Valori.Count - 1 mArrCombo(i).Items.Add(mInfoRicerca.Criteri(i).Valori(v).ToString) Next ' svuoto Text mArrCombo(i).SelectedIndex = -1 End IfSe gli elementi della ComboBox sono tratti da un elenco, questo deve essere presente nel membro Valori. La ComboBox non deve esporre alcun valore, all'apertura (Text vuoto).
Case TipoOrigineDati.DatiTabella PopolaCombo(i) ' svuoto Text mArrCombo(i).SelectedIndex = -1 End Select Next End If End SubSe gli elementi della ComboBox devono essere letti dal database, si richiama il metodo PopolaCombo, illustrato in seguito. Anche in questo caso la ComboBox non deve esporre alcun valore.
Private Sub PopolaCombo(ByVal numeroCombo As Integer) ' variabili di appoggio Dim campoVisualizzato As String = mInfoRicerca.Criteri(numeroCombo).NomeCampoVisualizzato Dim campoValore As String = mInfoRicerca.Criteri(numeroCombo).NomeCampoValore Dim tabella As String = mInfoRicerca.NomeTabella ' istanza e preparazione del Command Dim cmd As SqlCommand = New SqlCommand cmd.Connection = SqlHelper.ConnessioneDatabase If campoValore = campoVisualizzato Then cmd.CommandText = "SELECT DISTINCT " & campoValore & _ " FROM " & tabella & " ORDER BY " & campoVisualizzato Else cmd.CommandText = "SELECT DISTINCT " & campoValore & ", " & campoVisualizzato & _ " FROM " & tabella & " ORDER BY " & campoVisualizzato End If ' istanza e uso di DataAdapter, DataSet e DataView Dim adp As New SqlDataAdapter(cmd) Dim dst As New DataSet adp.Fill(dst, "Dati") Dim dv As New DataView(dst.Tables(0)) ' impostazione della combobox mArrCombo(numeroCombo).DataSource = dv mArrCombo(numeroCombo).DisplayMember = campoVisualizzato mArrCombo(numeroCombo).ValueMember = campoValore End SubPer valorizzare il testo del Command necessario per estrarre i dati per la ComboBox (del cui riferimento si passa l'indice), si controlla se sono necessari uno o due campi (differenza tra campoValore e campoVisualizzato). Quindi vengono istanziati gli oggetti necessari alla popolazione della DataView con la quale viene valorizzata la sorgente della ComboBox, di cui si impostano ValueMember e DisplayMember.
Metodi di configurazione: la DataGridView
Il secondo metodo chiamato nel costruttore, PreparaGriglia, aggiunge le colonne che formano la griglia e ne imposta i formati.Private Sub PreparaGriglia() ' controllo se la form va ridotta in altezza If mInfoRicerca.Criteri.Count < 4 Then ' sposto più in alto la griglia DgvDati.Top -= 42 ' e riduco l'altezza Me.Height -= 42 End IfDapprima si imposta la posizione verticale della griglia e, di conseguenza, l'altezza della form, a seconda del numero di righe impegnate da Label e ComboBox. Quindi si scandisce il membro Colonne della struttura InfoRicerca.
If mInfoRicerca.Colonne.Count > 0 Then ' pre-imposto un minimo di larghezza di intestazione DgvDati.RowHeadersWidth = 20 ' per ogni colonna For c As Integer = 0 To mInfoRicerca.Colonne.Count - 1 ' aggiungo una colonna del tipo giusto If mInfoRicerca.Colonne(c).CheckBox = False Then DgvDati.Columns.Add(New DataGridViewTextBoxColumn) Else DgvDati.Columns.Add(New DataGridViewCheckBoxColumn) End If ' ne imposto le proprietà DgvDati.Columns(c).Name = mInfoRicerca.Colonne(c).NomeCampo DgvDati.Columns(c).HeaderText = mInfoRicerca.Colonne(c).Intestazione DgvDati.Columns(c).AutoSizeMode = DataGridViewAutoSizeColumnMode.None If mInfoRicerca.Colonne(c).Visibile = True Then DgvDati.Columns(c).Width = mInfoRicerca.Colonne(c).Larghezza Else DgvDati.Columns(c).Width = 0 End IfDopo aver aggiunto il tipo richiesto di colonna a seconda del membro CheckBox della struttura Colonna, se ne impostano alcune proprietà, tra cui la larghezza, a seconda del membro Visibile.
Select Case mInfoRicerca.Colonne(c).TipoCampo Case SqlDbType.Char, SqlDbType.NChar, SqlDbType.NText, SqlDbType.NVarChar, _ SqlDbType.Text, SqlDbType.VarChar 'Testi DgvDati.Columns(c).DefaultCellStyle.Alignment = _ DataGridViewContentAlignment.MiddleLeft Case SqlDbType.BigInt, SqlDbType.Int, SqlDbType.SmallInt 'Numeri senza decimali DgvDati.Columns(c).DefaultCellStyle.Format = "#,###" DgvDati.Columns(c).DefaultCellStyle.Alignment = _ DataGridViewContentAlignment.MiddleRight Case SqlDbType.Decimal, SqlDbType.Float, SqlDbType.Money, SqlDbType.Real, _ SqlDbType.SmallMoney 'Numeri con decimali DgvDati.Columns(c).DefaultCellStyle.Format = "#,###.00" DgvDati.Columns(c).DefaultCellStyle.Alignment = _ DataGridViewContentAlignment.MiddleRight Case SqlDbType.Date, SqlDbType.DateTime, SqlDbType.DateTime2, SqlDbType.SmallDateTime 'Date DgvDati.Columns(c).DefaultCellStyle.Format = "dd/MM/yyyy" DgvDati.Columns(c).DefaultCellStyle.Alignment = _ DataGridViewContentAlignment.MiddleCenter Case SqlDbType.Time 'Ore DgvDati.Columns(c).DefaultCellStyle.Format = "HH:mm" DgvDati.Columns(c).DefaultCellStyle.Alignment = _ DataGridViewContentAlignment.MiddleCenter Case SqlDbType.Bit 'Booleani End SelectSuccessivamente si imposta formato e allineamento delle celle di questa colonna, a seconda del tipo del campo esposto.
DgvDati.Columns(c).ReadOnly = True Next ' imposto lo stile di default per il valore nullo nelle celle della griglia DgvDati.DefaultCellStyle.NullValue = "" ' nel caso esista almeno un criterio pre-impostato (filtro), popolo la griglia If DgvDati.Tag IsNot Nothing AndAlso _ DirectCast(DgvDati.Tag, TipoOrigineDati) = TipoOrigineDati.Filtro Then PopolaGriglia() End If ' If mInfoRicerca.Colonne.Count End SubInfine si imposta la condizione di sola lettura, il valore nullo e, se è il caso di pre-popolare la griglia, si invoca il metodo PopolaGriglia:
Private Sub PopolaGriglia() ' istanzio un oggetto Connection e tento di aprirlo Dim cnn As New SqlConnection cnn = SqlHelper.ConnessioneDatabase Try cnn.Open() Catch ex As Exception Messaggi.Errore("Non è stato possibile aprire il database. Impossibile proseguire", _ "Errore") BtnEsci.PerformClick() End Try ' istanzio un oggetto Command Dim cmd As New SqlCommand ' preparo la query di selezione dei dati che verrà costruita Dim stringaSQL As String = "SELECT " ' aggiungo l'elenco dei campi da esporre nella griglia For c As Integer = 0 To mInfoRicerca.Colonne.Count - 1 stringaSQL &= mInfoRicerca.Colonne(c).NomeCampo & ", " Next ' aggiungo la clausola From e l'inizio della clausola Where stringaSQL = stringaSQL.Substring(0, (stringaSQL.Length - 2)) & _ " FROM " & mInfoRicerca.NomeTabella & " WHERE "Nel metodo che popola la griglia si crea e si apre la connessione, si crea il Command e si comincia a costruire la query parametrica per la selezione dei dati. La prima parte è semplice: nomi dei campi richiesti e della tabella. Le cose si fanno interessanti al momento di costruire la clausola Where:
' flag per interporre " AND " tra un criterio e l'altro nella clausola Where Dim piuDiUnParametro As Boolean = False ' scandisco i criteri per trattare i parametri For c As Integer = 0 To mInfoRicerca.Criteri.Count - 1 ' prima aggiungo il criterio alla stringa sql If mArrCombo(c).Text.Trim <> String.Empty Then Dim operatoriConfronto As String() = {" = @", " > @", " >= @", " < @", " <= @"} If piuDiUnParametro Then stringaSQL &= " AND " Else piuDiUnParametro = True End If stringaSQL &= mInfoRicerca.Criteri(c).NomeCampoValore & _ operatoriConfronto(mInfoRicerca.Criteri(c).Confronto) & _ mInfoRicerca.Criteri(c).NomeCampoValore & " " End IfPrima di scandire il membro Criteri della struttura InfoRicerca, si prepara il flag piuDiUnParametro, utile per interporre l'operatore AND tra un criterio e l'altro.
Se la ComboBox corrispondente ha il Text valorizzato, si prepara il vettore degli operatori di confronto. La costruzione della stringa del criterio risulta quindi particolarmente semplificata.If mInfoRicerca.Criteri(c).ControlloValidita = True Then Dim tipoConvalida As TipoConv Select Case mInfoRicerca.Criteri(c).TipoCampoValore Case SqlDbType.Char, SqlDbType.NChar, SqlDbType.NText, SqlDbType.NVarChar, _ SqlDbType.Text, SqlDbType.VarChar 'Testi tipoConvalida = TipoConv.Testo Case SqlDbType.BigInt, SqlDbType.Int, SqlDbType.SmallInt 'Numeri senza decimali tipoConvalida = TipoConv.Migliaia_senza_decimali Case SqlDbType.Decimal, SqlDbType.Float, SqlDbType.Money, SqlDbType.Real, _ SqlDbType.SmallMoney 'Numeri con decimali tipoConvalida = TipoConv.Migliaia_con_decimali Case SqlDbType.Date, SqlDbType.DateTime, SqlDbType.DateTime2, _ SqlDbType.SmallDateTime 'Date tipoConvalida = TipoConv.Data End Select If Convalida(mInfoRicerca.Criteri(c).Valori(0), mInfoRicerca.Criteri(c).Etichetta, _ tipoConvalida, False) = False Then Messaggi.Errore("Criterio " & mInfoRicerca.Criteri(c).Etichetta & " non valido", _ "Errore nei criteri di ricerca") BtnEsci.PerformClick() End If End IfSe è richiesto il controllo della validità del valore esposto dalla ComboBox, si valorizza la variabile tipoConvalida a seconda del tipo del campo, e si fa buon uso del metodo Convalida della classe Convalidadati. In caso di mancata convalida, si esce dalla procedura.
' aggiungo il parametro al Command cmd.Parameters.AddWithValue("@" & mInfoRicerca.Criteri(c).NomeCampoValore, _ mArrCombo(c).Text.Trim) cmd.Parameters("@" & mInfoRicerca.Criteri(c).NomeCampoValore).SqlDbType = _ mInfoRicerca.Criteri(c).TipoCampoValore Next ' completo la stringa sql con l'ordinamento stringaSQL &= "ORDER BY " & mInfoRicerca.Ordinamento ' imposto le proprietà del Command cmd.CommandText = stringaSQL cmd.Connection = cnn ' ne ottengo un DataReader Dim dtr As SqlDataReader = cmd.ExecuteReader()Aggiunto il parametro al Command e terminato il ciclo, si completa la query di selezione con la clausola di ordinamento, la si assegna alla CommandText del Command, che viene connesso e usato per creare il DataReader necessario per la lettura dei dati da esporre nella griglia.
While dtr.Read() ' compilo un vettore di valori per le celle Dim arrayValori(mInfoRicerca.Colonne.Count) As Object For c As Integer = 0 To mInfoRicerca.Colonne.Count - 1 ' a seconda del tipo di campo Select Case mInfoRicerca.Colonne(c).TipoCampo Case SqlDbType.Char, SqlDbType.NChar, SqlDbType.NText, SqlDbType.NVarChar, _ SqlDbType.Text, SqlDbType.VarChar 'Testi arrayValori(c) = dtr.GetString(c) Case SqlDbType.BigInt, SqlDbType.Int, SqlDbType.SmallInt 'Numeri senza decimali arrayValori(c) = dtr.GetInt32(c) Case SqlDbType.Decimal, SqlDbType.Float, SqlDbType.Money, SqlDbType.Real, _ SqlDbType.SmallMoney 'Numeri con decimali arrayValori(c) = dtr.GetDecimal(c) Case SqlDbType.Date, SqlDbType.DateTime, SqlDbType.DateTime2, SqlDbType.SmallDateTime, _ SqlDbType.Time 'Date arrayValori(c) = dtr.GetDateTime(c) Case SqlDbType.Bit 'Booleani arrayValori(c) = dtr.GetBoolean(c) End Select Next ' preparo la riga nuova e la aggiungo Dim rigaNuova As New DataGridViewRow rigaNuova.CreateCells(DgvDati, arrayValori) DgvDati.Rows.Add(rigaNuova) End While ' chiudo gli oggetti dtr.Close() cnn.Close() End SubPer ogni riga, si prepara un vettore di valori per le celle, che vengono letti uno a uno con il metodo adatto, a seconda del tipo di campo. Con questo vettore si valorizza una nuova DataGridViewRow che viene aggiunta alla griglia. Terminata la compilazione di quest'ultima, si chiudono DataReader e Connection e si esce dalla procedura.
Altri metodi di servizio usati da pulsanti
L'utente imposta i criteri tramite le ComboBox e applica il filtro tramite il pulsante BtnFiltra, che vedremo in seguito.
Se non è soddisfatto del risultato può svuotare le ComboBox per reimpostare i criteri.''' <summary> ''' svuota le Text delle combobox ''' </summary> Private Sub SvuotaCombo() For Each cmb As ComboBox In Me.Controls.OfType(Of ComboBox)() ' solo di quelle non 'filtro', che sono disabilitate If cmb.Enabled = True Then cmb.SelectedIndex = -1 End If Next End Sub ''' <summary> ''' Svuota la griglia e resetta il puntatore alla riga selezionata ''' </summary> Private Sub SvuotaGriglia() If Not (DgvDati.Rows.Count < 1) Then DgvDati.Rows.Clear() DgvDati.Refresh() mRigaSelezionata = -1 End If End SubI commenti dovrebbero essere sufficienti a comprendere il codice.
Questi metodi sono usati dal metodo per il pulsante Svuota:Private Sub BtnSvuota_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles BtnSvuota.Click SvuotaCombo() SvuotaGriglia() End SubIl metodo SvuotaGriglia viene usato prima del metodo PopolaGriglia, nel metodo per il pulsante Filtra:
Private Sub BtnConferma_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles BtnConferma.Click If DgvDati.Rows.Count < 1 Then Exit Sub End If If mRigaSelezionata < 0 Then Messaggi.Avviso("Non è stata selezionata alcuna riga", "Errore") Else Dim riga As DataGridViewRow = DgvDati.Rows(mRigaSelezionata) For c As Integer = 0 To DgvDati.Columns.Count - 1 If mInfoRicerca.Colonne(c).Chiave = True Then mCampiChiave.Add(riga.Cells(c).Value.ToString) End If Next Me.DialogResult = Windows.Forms.DialogResult.OK End If End SubMetodi per la compilazione dei risultati
L'utente sceglie una riga della griglia:Private Sub DgvDati_RowEnter(ByVal sender As System.Object, _ ByVal e As System.Windows.Forms.DataGridViewCellEventArgs) _ Handles DgvDati.RowEnter mRigaSelezionata = e.RowIndex End SubViene così valorizzato il puntatore alla riga selezionata. A questo punto, l'utente conferma la scelta:
Private Sub BtnConferma_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles BtnConferma.Click If DgvDati.Rows.Count < 1 Then Exit Sub End If If mRigaSelezionata < 0 Then Messaggi.Avviso("Non è stata selezionata alcuna riga", "Errore") Else Dim riga As DataGridViewRow = DgvDati.Rows(mRigaSelezionata) For c As Integer = 0 To DgvDati.Columns.Count - 1 If mInfoRicerca.Colonne(c).Chiave = True Then mCampiChiave.Add(riga.Cells(c).Value.ToString) End If Next Me.DialogResult = Windows.Forms.DialogResult.OK End If End SubSe vengono superati i controlli di errore, viene compilato il vettore mCampiChiave, che verrà restituito alla form chiamante attraverso la proprietà CampiChiave, come abbiamo già visto nell'articolo precedente.
Si può infine concludere impostando a OK il risultato del dialogo, il che chiude la form.L'utente può anche rinunciare:
Private Sub BtnEsci_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles BtnEsci.Click Me.DialogResult = Windows.Forms.DialogResult.Cancel End SubViene semplicemente impostato a Cancel il risultato del dialogo.
Problema dei pulsanti di default
Una finestra di dialogo ha, di solito, un pulsante OK e un pulsante Cancel (annulla); infatti espone le proprietà AcceptButton e CancelButton, per ricevere i riferimenti ai Button Ok e Cancel, che vengono 'premuti' se l'utente preme i tasti Invio o Escape, rispettivamente.Però le due proprietà ricevono oggetti Button, non ToolStripButton, come i pulsanti Esci e Conferma. Si è resa quindi necessaria una emulazione del comportamento richiesto attraverso la gestione dei tasti.
Come forse vi ricordate dall'articolo precedente, è stata impostata a True la proprietà KeyPreview della FrmRicerca. E' stato fatto proprio a questo scopo:
Private Sub FrmRicerca_KeyDown(ByVal sender As Object, _ ByVal e As System.Windows.Forms.KeyEventArgs) Handles Me.KeyDown If e.KeyCode = Keys.Escape Then BtnEsci.PerformClick() ElseIf e.KeyCode = Keys.Enter Then BtnConferma.PerformClick() End If End SubCome vedete, se il tasto premuto è Invio o Escape, si 'preme' il relativo pulsante (metodo PerformClick), scatenando l'evento che porta alla chiusura della form con l'appropriato DialogResult.
Conclusione
In questa puntata si è completato lo sviluppo della FrmRicerca, una form di dialogo che verrà utilizzata da molte altre form di cui è composto questo progetto, delle quali tratteremo nelle prossime puntate.Il codice di PrimiPassi sviluppato fino a questo momento è come al solito disponibile in area download.
Anche per questa puntata, Diego mette a disposizione nel suo blog un post cui scrivere critiche, suggerimenti, richieste di chiarimento.