Quel punto
a cura di Diego Cattaruzza (requisiti: nessuno)Premessa
Il punto di cui nel titolo è quello che nel codice separa i nomi degli oggetti dai nomi dei loro membri.
In altre parole, è un trucchetto per attirare lettori cosiddetti principianti di VB.Net, segnatamente quelli provenienti da VB6, sulla questione fondamentale del mondo della programmazione orientata agli oggetti: l'essenza dell'oggetto.In questo articolo mi propongo di illustrare che cosa è un oggetto (inteso come categoria, cioè non un oggetto qualsiasi, ad esempio una textbox, ma qualsiasi oggetto), come bisogna pensare ad esso e perché si seguono certe metodiche.
Un vettore di textbox con indice
Come pretesto (e materia) di discussione, ho scelto di creare un vettore di TextBox personalizzate dall'avere un indice, emulando il vettore di controlli che era possibile in VB6, anche se 'non è una buona idea' usare un vettore.
Dapprima, quindi, creo la classe che erediti dalla TextBox fornita dal Framework e che esponga una proprietà Index, nonché un comodo costruttore con parametro index.
Per fare ciò aggiungo un CustomControl col nome IndexedTextBox e, nel codice, aggiungo la riga che specifica da cosa eredita:Public Class IndexedTextBox Inherits System.Windows.Forms.TextBoxIntellisense mi avvisa che quella riga è sbagliata. Perché? Perché è già stato indicato che la Class IndexedTextBox eredita da un'altra classe. Dove? Nel file creato automaticamente da VB, che si chiama IndexedTextBox.Designer.vb, e che si può vedere se si clicca, in Solution Explorer. su Show All Files. In esso, infatti, trovo la riga System.Windows.Forms.Control. Ma a me non sta bene: io pretendo che erediti proprio dalla classe TextBox) e trovo anche tutto un sacco di codice che gestisce eventuali componenti del mio controllo. Dato che non ho intenzione di mettercene alcuno, tutto questo file è inutile, ergo lo chiudo e lo cancello. Ecco che Intellisense cambia idea: la riga di prima adesso è giusta. Cosa è successo? Ho eliminato una contraddizione.
Tutto questo per concludere che, a volte, conviene aggiungere una Class (vuota).Adesso che ho stabilito che il mio oggetto è una TextBox, aggiungo gli elementi che lo renderanno diverso da una TextBox normale, cioè la proprietà Index, per la quale ho bisogno del campo mIndex dove depositare il valore.
Private mIndex As Integer Public Property Index() As Integer Get Return mIndex End Get Set(ByVal value As Integer) mIndex = value End Set End PropertyA questo punto posso aggiungere un comodo costruttore che richieda l'indice:
Sub New(ByVal index As Integer) MyBase.New() Me.Index = index End SubEccoci arrivati ad un punto fondamentale: nella vera OOP, in .Net, ogni oggetto deve avere un metodo costruttore (in VB è il metodo New). Come tutti i metodi, può avere dei parametri. Quando si creavano classi in VB6, questo costruttore era nascosto. Adesso dobbiamo farci attenzione. Anche in Vb.Net, è nascosto, ma, se sappiamo che c'è, basta tirarlo fuori, vi farò vedere dopo). Così sappiamo anche sfruttarlo meglio.
Altra bella cosa: poiché la IndexedTextBox è una TextBox con un Index in più, possiamo (ci conviene) sfruttare tutto quanto ci offre la classe TextBox riferendoci alla 'classe da cui eredita', cioè alla 'classe base', attraverso la parola chiave MyBase. Quindi la uso per sfruttare il suo metodo New e per valorizzare la proprietà Index col parametro index (notate la differenza di iniziale maiuscola o minuscola, in uno dei pochi casi in cui questo è permesso da VB; è una delle cose da invidiare a C#, ma ci si arrangia benissimo ugualmente).Visual Studio mi ha anche automaticamente creato la sub OnPaint.
Così com'è, ci fosse o no, l'evento Paint avverrebbe ugualmente nelle modalità seguite dalla classe base TextBox. Perché c'è? Perché potremmo volere qualche ulteriore caratteristica grafica, a seconda di qualche regola di buisiness per questo o quel componente del controllo. Potrebbe essere utile per spiegare anche le parole Protected e Overrides ('è a visibilità protetta' e 'ridefinisce' il metodo OnPaint della classe da cui i controllo deriva), ma non voglio creare confusione. Quindi lo cancello.
La form di test
A questo punto posso dedicarmi alla form di test, che alla creazione del progetto Punto, di tipo Window Application, ho denominato PuntoFrm.
In essa, trascino dalla ToolBox un controllo NumericUpDown, di nome nudNumberOfIndexedTextbox, con Value 5 per avere un valore di default, Minimum 1 per impedire stupidaggini; tre pulsanti, btnGO, btnAnnienta e btnTogliUltimo, più una Label1 inzialmente invisibile.Volendo, potrei inserire il nuovo controllo IndexedTextBox, che vedo nella ToolBox, tra i Punto Components, ma mi serve farlo a tempo di esecuzione, non di progettazione, perché voglio emulare un vettore di controlli.
Quindi, nel codice, comincio col dichiarare un vettore di IndexedTextBox e implemento il codice del btnGO_Click, con dei commenti ad indicare man mano cosa fa:
Private Sub btnGO_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles btnGO.Click ' fermo la logica di aggiornamento grafico della form Me.SuspendLayout() ' leggo il valore contenuto nel controllo UpDown Dim q As Integer = Convert.ToInt32(TxtNUD.Value) ' ridimensiono a quel valore il vettore di IndexedTextBox ReDim indxTxtArr(q) ' implemento il ciclo di creazione dei controlli For i As Integer = 0 To q - 1 ' istanzio una IndexedTextBox passando l'indice Dim aTxt As IndexedTextBox = New IndexedTextBox(i) ' di essa imposto alcune proprietà With aTxt .Location = New System.Drawing.Point(120, 12 + i * 25) .Name = "IndexedTextBox" & i.ToString .Size = New System.Drawing.Size(120, 23) .TabIndex = 0 ' questa è ininfluente, per i nostri scopi .Text = "IndexedTextBox" & i.ToString End With ' assegno l'oggetto creato all'elemento iesimo del vettore indxTxtArr(i) = aTxt ' rendo quest'ultimo visibile indxTxtArr(i).Visible = True Next ' visualizzo i pulsanti btnAnnienta.Visible = True btnTogliUltimo.Visible = True ' ripristino la logica di aggiornamento della form Me.ResumeLayout() ' sposto il focus sulla prima IndexedTextBox indxTxtArr(0).Focus() End SubPoiché qualcuno potrebbe provare a creare decine di caselle, è opportuno impostare a True la proprietà AutoScroll della form.
Dopo questa azione, abbiamo un vettore di controlli? No. Abbiamo un vettore di indirizzi di memoria dove stanno i controlli.
I controlli veri e propri stanno nelle zone di memoria via via occupate dall'esecuzione della riga:Dim aTxt As IndexedTextBox = New IndexedTextBox(i)Solo qui sta la chiamata al metodo New che crea l'istanza oggetto della classe IndexedTextBox. Di questo oggetto, la variabile aTxt contiene solo l'indirizzo, il riferimento. Questo riferimento viene assegnato, poi, anche ad uno degli elementi del vettore indxTxtArr. A questo punto possiamo affermare (in senso lato, cioè sapendo bene di essere leggermente inesatti) che abbiamo un vettore di controlli. In effetti, potremmo riferirci a ciascuna IndexedTextBox attraverso il riferimento (gioco apposta con le parole) ad essa contenuto in uno degli elementi del vettore indxTxtArr. Ad esempio:
indxTxtArr(5).Text = "nuovo testo della casella numero 5"Ok. 'Noi' ce l'abbiamo, il vettore di controlli (diciamo). Ma la PuntoFrm? No. Infatti, se proviamo il programma, cliccare sul pulsante GO non produce apparentemente alcun risultato, anche se abbiamo impostato la proprietà Visible di ciascun controllo a True. Dobbiamo aggiungere ciascun controllo all'insieme dei controlli della form, perché essa ne sappia qualcosa e ne gestisca la visualizzazione. Conseguiamo questo risultato aggiungendo queste due righe subito prima del Next:
' e lo aggiungo all'insieme dei controlli della form Me.Controls.Add(indxTxtArr(i))Se adesso riproviamo, funziona come ci si aspetta. Ok. E adesso? Che ce ne facciamo? Voglio dire: se avessimo aggiunto delle textbox in fase di progettazione, ed avessimo cliccato due volte su una di esse, avremmo automaticamente ottenuto nel codice la creazione della Sub di evento nometextbox_TextChanged. Ma non possiamo avere la stessa cosa in fase di esecuzione. E non possiamo nemmeno inventarci tante diverse Sub nometextbox_TextChanged, perché non possiamo dire al compilatore che ciascuna di esse gestisce quell'evento di quella inesistente nometextbox, con la parola chiave Handles nometextbox.TextChanged.
Oppure sì? Sì. Si crea una Sub di nome TextBox_TextChanged, con la firma della Sub di evento, senza precisare che gestisce alcunché. In fase di esecuzione, quando creiamo le IndexedTextBox, aggiungiamo a questa sub la gestione dell'evento relativo a ciascuna IndexedTextBox, con la seguente riga, subito prima dell'End With:AddHandler .TextChanged, AddressOf TextBox_TextChangedChe significa: aggiungi la gestione dell'evento TextChanged di questa IndexedTextBox alla sub posta all'indirizzo della TextBox_TextChanged. Lo scrivo così, pedissequamente, perchè a volte le cose sembrano così facili che non ci si rende bene conto del significato: in questo caso si apprezza la presunta comodità dell'istruzione AddHandler senza notare che si usa un AddressOf.
In questa sub, da scrivere prima della riga mostrata sopra, implementiamo il codice per fare una banale comunicazione:Private Sub TextBox_TextChanged(ByVal sender As Object, ByVal e As System.EventArgs) Dim i As Integer = DirectCast(sender, IndexedTextBox).Index Label1.Text = String.Format("Stai scrivendo nella IndexTextBox{0}", i.ToString) Label1.Refresh() Label1.Visible = True End SubVoglio farvi notare che la sub riceve un parametro sender, cioè il riferimento all'oggetto cui è relativo l'evento da gestire, che è di tipo Object, generico. Questo significa che il runtime non può sapere che quell'Object ha la proprietà Index (ed infatti Intellisense, se scrivessimo 'sender-punto', non ce la presenterebbe, tra i membri di sender). Quindi glielo diciamo noi (che siamo intelligenti e lo sappiamo :o)), rendendo specifico l'oggetto generico con il metodo DirectCast, che tenta di fare una tipizzazione diretta. Di questo oggetto tipizzato, poi, possiamo accedere alla proprietà Index, e infatti a questo punto, se scriviamo il 'punto', Intellisense ce la presenta, perché conosce la classe che abbiamo creato prima.
Chi conosce VB6 ricorda CType. C'è anche in VB.Net. E' fuori luogo spiegarlo qui, ma mi limito a precisare che CType va usato per i tipi valore (integer, long, eccetera), DirectCast per i tipi riferimento (oggetti, stringhe, eccetera).
Se adesso provate a digitare qualcosa in una o nell'altra delle nuove caselle, vedrete la comunicazione nella Label1.In C#, si può fare in due modi: come si fa in VB (cioè scrivendo un metodo e aggiungendolo ai gestori di evento del controllo - sintassi diverse, ovviamente) o con un delegate anonimo:
item.TextChanged += delegate
{
this.Label1.Text = string.Format("Stai scrivendo nella IndexTextBox{0}", item.Index);
};Molto meno cervellotico dell'AddHandler, una delle poche cose di VB che non mi piacciono. Ma torniamo al nostro discorso.
E se facessimo senza il vettore? Proviamo:Private Sub btnAnnienta_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles btnAnnienta.Click Array.Clear(indxTxtArr, 0, indxTxtArr.Length) End SubAbbiamo usato il metodo statico Clear della classe Array. Che abbiamo ottenuto? Le caselle sono sempre lì... Provate a mettere un punto di interruzione sulla End Sub e riavviate. Quando il debug si ferma sul breakpoint, puntate il mouse sulla parola indxTxtArr ed espandete il tooltip che appare: troverete che il vettore è pieno di... niente, cioè tutti gli elementi sono stati annientati, posti a Nothing. Adesso, questa riga che prima avrebbe funzionato, provocherebbe un errore:
indxTxtArr(5).Text = "nuovo testo della casella numero 5" ' NullReferenceExceptionVa be', si può abbozzare... Diciamo che non è molto igienico affidarsi ad un vettore di riferimenti. Ok. Ma in fase di esecuzione, così come li abbiamo creati quando ne avevamo bisogno, dovremmo poterli distruggere, quando non ci servissero più. Siamo ancora allo stesso punto: dobbiamo dirlo alla form. Cioè dobbiamo togliere uno o più elementi dall'insieme dei controlli della form:
Private Sub btnTogliUltimo_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles btnTogliUltimo.Click Dim indice As Integer = -1, idx As Integer, theLast As String = String.Empty For Each ctl As Control In Me.Controls If ctl.GetType.Name = "IndexedTextBox" Then idx = DirectCast(ctl, IndexedTextBox).Index If idx > indice Then indice = idx theLast = ctl.Name End If End If Next Me.Controls.RemoveByKey(theLast) End SubHo voluto complicarmi un po' la vita, implementando il codice per eliminare sempre la IndexTextBox con l'indice più alto, non 'la prima che capita'. Quindi scandisco l'insieme Controls della form con un ciclo For Each: se il nome del tipo del controllo analizzato è quello che cerco, ne ottengo l'indice (sempre ricorrendo alla tipizzazione forzata) e lo confronto con una variabile apposita, per trovare il controllo con l'indice più alto e valorizzare col suo nome un'altra variabile, che alla fine passo al metodo RemoveByKey dell'insieme Controls.
Il metodo RemoveByKey annienta l'oggetto. Se mettete un breakpoint anche su questa riga End Sub e, quando vi ci ritrovate dopo aver riavviato, scrivete nella finestra Immediata una riga simile a questa:? me.Controls("IndexedTextBox4").Textotterrete questa risposta:
Referenced object has a value of 'Nothing'.La quale significa: il riferimento esiste ancora, ma l'oggetto non più. Dovreste ricrearne uno ed assegnargli quello stesso riferimento.
Se non avessimo annientato gli elementi del vettore indxTxtArr, e volessimo cancellarne uno specifico, dato il numero, avremmo usato questo codice:Dim controllo As String = indxTxtArr(numero).Name indxTxtArr(numero) = Nothing Me.Controls.RemoveByKey(controllo)oppure questo:
Me.Controls.Remove(indxTxtArr(numero))ed anche togliere l'ultimo diventa molto meno complicato (per il vettore, faccio vedere la versione C# che fa uso dei Generic):
Me.Controls.Remove(indxTxtArr(indxTxtArr.Length-1))this.Controls.Remove(indxTxtArr[ indxTxtArr.Length-1]);
Array.Resize<IndexedTextBox>(ref indxTxtArr, indxTxtArr.Length-1);Questo però implica che bisogna sempre verificare prima se un dato elemento del vettore contiene ancora un riferimento ad un controllo. Oppure si rende necessario gestire anche la rimozione dell'elemento dal vettore. Oppure ridimensionarlo, ad ogni rimozione o aggiunta di un elemento. Insomma, usare vettori di controlli 'non è una buona idea', in VB.Net. Semmai, è maggiormente consigliabile una Collection, con i suoi metodi Add e Remove.
Adesso fate quest'altro esperimento: lasciate 5 nel controllo UpDown, cliccate quattro volte, consecutivamente, sul pulsante GO: apparentemente, le caselle sono sempre cinque. In realtà sono venti. Sono quattro serie di cinque caselle sovrapposte alle precedenti.
Se adesso cliccate più volte sul pulsante TogliUltimo, troverete che dovrete farlo quattro volte per ogni casella.
Quel che più importa è che, mentre l'insieme Controls della Form contiene un certo numero di controlli con lo stesso nome e lo stesso indice - può accadere (presumo) perché i controlli vengono distinti tramite un ID, non tramite il Name - il vettore indxTxtArr contiene sempre lo stesso numero di riferimenti, cioè non contiene i riferimenti a tutte le caselle aggiunte.Ma questa è cattiva programmazione! Sì. L'ho fatto apposta per convincervi che un vettore di controlli non è il massimo della vita, perchè richiede un sacco di codice e di preoccupazioni per gestirlo. E soprattutto perché voglio far cogliere alcune sottigliezze cui si va incontro quando si trattano oggetti, e quale sia secondo me l'essenza della OOP: il riferimento.
Insomma, un oggetto è qualcosa a cui 'ci si riferisce' e ogni oggetto deve essere conosciuto attraverso il suo riferimento per poter essere acceduto dal codice (attraverso una variabile) o dal runtime (attraverso il costruttore).
Conclusione
Capite un po' meglio questa apparentemente ardua cosa che è l'oggetto? Spero proprio di sì. D'ora in poi, quando fate 'punto', dovreste rendervi meglio conto del 'succo' della programmazione orientata agli oggetti, apprezzando la potenza che vi viene data, di creare classi e controlli con apparentemente irrisoria facilità.
Certo che bisogna anche fare attenzione e curarsi di qualche dettaglio, ma non dovreste avere più 'paura' del mondo .Net.
Come al solito, questo articolo è corredato dal codice scaricabile dall'area Download, fornito sia nella versione VB che C#.