Questo controllo è davvero molto complesso: rappresenta una griglia delle proprietà, esattamente la stessa che lo sviluppatore usa per modificare le caratteristiche dei vari controlli nel form designer. La sua enorme potenza sta nel fatto che, attraverso la reflection, riesce a gestire qualsiasi oggetto con facilità. Le si può associare un controllo del form, su cui l'utente può agire a proprio piacimento, ma anche una classe, ad esempio le opzioni del programma, con cui sarà quindi possibile interagire molto semplicemente da un'unica interfaccia. Le proprietà e i metodi importanti sono:
- CollapseAllGridItems : riduce al minimo tutte le categorie
- ExpandAllGridItems : espande al massimo tutto le categorie
- PropertySort : proprietà enumerata che indica come debbano essere ordinati gli elementi, se alfabeticamente, per categorie, per categorie e alfabeticamente oppure senza alcun ordinamento
- PropertyTabs : collezione di tutte le possibili schede della PropertyGrid. Una scheda, ad esempio, è costituita dal pulsante "Ordina
alfabeticamente", oppure, nell'ambiente di sviluppo, dal pulsante "Mostra eventi" (quello con l'icona del fulmine). Aggiungerne una significa
aggiungere un pulsante che possa modificare il modo in cui il controllo legge i dati dell'oggetto. Ecco un esempio preso da un articolo
sull'argomento reperibile su The Code Project:
- SelectedGridItem : restituisce l'elemento selezionato, un oggetto GridItem che gode di queste proprietà:
- Expandable : indica se l'elemento è espandibile. Sono espandibili tutte quelle proprietà il cui tipo sia un tipo reference: in parole povere, essa deve esporre al proprio interno altre proprietà (non sono soggetti a questo comportamento le strutture, in quanto tipi value, a meno che esse non espongano a loro volta delle proprieta'). Per i tipi definiti dal programmatore, la PropertyGrid non è in grado di fornire una rappresentazione che possa essere espansa a run-time: a questo si può supplire in modo semplice facendo uso di certi attributi come si vedrà fra poco
- Expanded : indica se l'elemento è correntemente espanso (sono visibili tutti i suoi membri)
- GridItems : se Expandable = True, questa proprietà restituisce una collezione di oggetti GridItem che rappresentano tutte le proprietà interne a quella corrente
- GridItemType : proprietà enumerata in sola lettura che specifica il tipo di elemento. Può assumere quattro valori: ArrayValue (un oggetto array o a una collezione in genere), Category (una categoria), Property (una qualsiasi proprieta') e Root (una proprietà di primo livello, ossia che non possiede alcun livello gerarchico al di sopra di se stessa)
- Label : il testo dell'elemento
- Parent : se la proprietà è un membro d'istanza di un'altra proprietà, restituisce quest'ultima (ossia quella che sta al livello gerarchico superiore)
- PropertyDescriptor : restituisce un oggetto che indica come si comporta la proprietà nella griglia, quale sia il suo testo, la descrizione, se sia modificabile o meno (a run-time o solo durante la scrittura del programma), se sia visualizzata nella griglia, quale sia il delegate da invocare nel momento in cui questa viene modificata e infine, il più importante, l'oggetto usato per convertire tutta la proprietà in un valore sintetico di tipo stringa. Tutti questi attributi sono specificati durante la scrittura di una proprietà che supporti la visualizzazione in una PropertyGrid, come si vedrà in seguito
- Value : restituisce il valore della proprieta'
- SelectedObject : la proprietà più importante. Imposta l'oggetto che PropertyGrid gestisce: ogni modifica dell'utente sul controllo si ripercuoterà in maniera identica sull'oggetto, esattamente come avviene nell'ambiente di sviluppo; vengono anche intercettati tutti gli errori di casting e gestiti automaticamente
- SelectedObjects : è anche possibile far sì che vengano gestiti più oggetti contemporaneamente. Se questi sono dello stesso tipo, ogni modifica si ripercuoterà su ognuno nella stessa maniera. Se sono di tipo diverso, verranno visualizzate solo le proprietà in comune
- SelectedTab : restituisce la scheda selezionata
In questo capitolo mi concentrerò sul caso in cui si debba interfacciare PropertyGrid con un oggetto nuovo creato da codice.
Binding di classi create dal programmatore
Per far sì che PropertyGrid visualizzi correttamente una classe creata dal programmatore, basta assegnare un oggetto di quel tipo alla proprietà SelectedObject, poichè tutto il processo viene svolto tramite reflection. Tuttavia ci sono alcune situazioni in cui questo processo ha bisogno di un aiuto esterno per funzionare: quando le proprietà sono di tipo reference (stringhe escluse), non vengono visulizzati tutti i loro membri, poichè il controllo non è in grado di convertire un valore adatto in stringa. Ad esempio, se si deve leggere un oggetto di tipo Person, il nome e la data di nascita verranno analizzati correttamente, ma il campo Fretello As Person come verrà interpretato? Non è possibile far stare una classe su una sola riga, poichè non si conosce il modo di convertirla in un valore rappresentabile (in questo caso, in una stringa). Lo strumento che Vb.Net fornisce per arginare questo problema è un attributo, di nome TypeConverter, definito nel namespace System.ComponentModel (dove, tra l'altro, sono situati tutti gli altri attributi usati in questo capitolo). Questo accetta come costruttore un parametro di tipo Type, che espone il tipo di una classe con la funzione di convertitore. Ad esempio:
'Questa classe ha la funzione di convertire Person in stringa Public Class PersonConverter '(Per convenzione, i convertitori di questo tipo, devono 'terminare con la parola "Converter" '... End Class Public Class Person Private _Name As String Private _Birthday As Date Private _Brother As Person '... 'Per la proprietà Brother (fratello), si applica l'attributo 'TypeConverter, specificando quale sia la classe convertitore. 'Si utilizza solo il tipo perchè la classe, come vedremo 'in seguito, espone solo metodi d'istanza, ma che possono 'essere utilizzati da soli semplicemente fornendo i parametri 'adeguati. Perciò sarà il programma stesso a creare, 'a runtime, un oggetto di questo tipo e ad usarne la funzioni <TypeConverter(GetType(PersonConverter))> _ Public Property Brother() As Person '... End Class
Ecco un esempio di come si presenterà il controllo dopo aver fornito queste direttive:
La classe che implementa il convertitore deve ereditare da ExpandableObjectConverter (una classe definita anch'essa in System.ComponentModel)
e deve sovrascrivere tramite polimorfismo alcune funzioni: CanConvertFrom (determina se si può convertire da tipo dato), CanConvertTo
(determina se si può convertire nel tipo dato), ConvertFrom (converte, in questo caso, da String a Person, e in generale al tipo di cui
si sta scrivendo il convertitore), ConvertTo (converte, in questo caso, da Person a String, e in generale dal tipo in questione a stringa).
Questa era la parte più difficile, di cui si avrà un buon esempio nel codice a seguire: quello che bisogna anlizzare ora consente di
definire alcune piccole caratteristiche per personalizzare l'aspetto di una proprietà. Ecco una lista degli attributi usati e delle loro
descrizioni:
- DisplayName : modifica il nome della proprietà in modo che venga visualizzata a run-time un'altra stringa. Accetta un solo parametro del costruttore, il nuovo nome (nell'esempio, si rimpiazza la denominazione inglese con la rispettiva traduzione italiana)
- Description : definisce una piccola descrizione per la proprieta'
- Browsable : determina se il valore della proprietà sia modificabile dal controllo: l'unico parametro del costruttore è un valore Boolean
- [ReadOnly] : indica se la proprietà è in sola lettura oppure no. Come Browsable accetta un unico parametro booleano
- DesignOnly : specifica se la proprietà si possa modificare solo durante la scrittura del codice e non durante l'esecuzione
- Category : il nome della categoria sotto la quale deve venire riportata la proprietà: l'unico parametro è di tipo String
- DefaultValue : il valore di default della proprietà. Accetta diversi overload, a seconda del tipo
- DefaultProperty : applicato alla classe che rappresenta il tipo dell'oggetto visualizzato, indica il nome della proprietà che è selezionata di default nella PropertyGrid
Prima di procedere con il codice, ecco uno screenshot di come dovrebbe apparire la veste grafica in fase di progettazione:
C'è anche un'ImageList con un'immagine per gli elementi della listview lstBooks e un ContextMenuStrip che contiene le voci "Aggiungi" e
"Rimuovi", sempre assegnato alla listview. Inoltre, sia la lista che la PropertyGrid sono inserite all'interno di uno SplitContiner.
Ecco il codice della libreria (nel Solution Explorer, cliccare con il pulsante destro sul progetto, quindi scegliere Add New Item e poi
Class Library):
'Questo namespace contiene gli attributi necessari a 'impostare le proprietà in modo che si interfaccino 'correttamente con PropertyGrid Imports System.ComponentModel 'Quando si usa uno statementes Imports, la prima voce 'si riferisce al nome del file *.dll in s?. Dato che si 'vuole BooksManager sia consierato come una namespace, non 'bisogna aggiungere un altro namespace BooksManager in questo file 'L'autore del libro, con eventuale biografia Public Class Author 'Il nome completo Private _Name As String 'Data di nascita e morte Private _Birth, _Death As Date 'Indica se l'autore è ancora vivo Private _IsStillAlive As Boolean 'Una piccola biografia Private _Biography As String <DisplayName("Nome"), _ Description("Il nome dell'autore."), _ Browsable(True), _ Category("Generalita'")> _ Public Property Name() As String Get Return _Name End Get Set(ByVal Value As String) _Name = Value End Set End Property <DisplayName("Piccola biografia"), _ Description("Un riassunto delle parti più significative della " & _ "vita dell'autore."), _ Browsable(True), _ Category("Dettagli")> _ Public Property Biography() As String Get Return _Biography End Get Set(ByVal Value As String) _Biography = Value End Set End Property <DisplayName("Data di nascita"), _ Description("La data di nascita dell'autore."), _ Browsable(True), _ Category("Generalita'")> _ Public Property Birth() As Date Get Return _Birth End Get Set(ByVal Value As Date) 'Nessun controllo: la data di nascita può essere 'spostata a causa di uno sbaglio, che altrimenti 'potrebbe produrre un'eccezione _Birth = Value End Set End Property <DisplayName("Data di morte"), _ Description("Data di morte dell'autore."), _ Browsable(True), _ Category("Generalita'")> _ Public Property Death() As Date Get Return _Death End Get Set(ByVal Value As Date) 'Bisogna assicurarsi che la data di morte sia 'posteriore a quella di nascita If Value.CompareTo(Me.Birth) < 1 Then 'Genera un'eccezione Throw New ArgumentException("La data di morte deve " & _ "essere posteriore a quella di nascita!") Else 'Prosegue l'assegnazione _Death = Value 'Impostando la data di morte si suppone che l'autore 'non sia più in vita... Me.IsStillAlive = False End If End Set End Property <DisplayName("Vive"), _ Description("Determina se l'autore è ancora in vita."), _ Browsable(True), _ Category("Generalita'")> _ Public Property IsStillAlive() As Boolean Get Return _IsStillAlive End Get Set(ByVal Value As Boolean) _IsStillAlive = Value End Set End Property 'Un nome e una data di nascita sono obbligatori Sub New(ByVal Name As String, ByVal Birth As Date) Me.Name = Name Me.Birth = Birth Me.IsStillAlive = True End Sub 'Tuttavia, il controllo PropertyGrid richiede un costruttore 'senza parametri Sub New() Me.Birth = Date.Now Me.IsStillAlive = True End Sub End Class Public Class IsbnConverter 'Facendo derivare questa classe da ExpandableObjectConverter 'si comunica al compilatore che questa classe è usata per 'convertire in stringa un valore rappresentabile in una 'PropertyGrid. Così facendo, sarà possibile modificare 'il codice agendo sulla stringa complessiva e non 'obbligatoriamente sulle varie parti Inherits ExpandableObjectConverter 'Determina se sia possibile convertire nel tipo dato Public Overrides Function CanConvertTo(ByVal Context As ITypeDescriptorContext, _ ByVal DestinationType As Type) As Boolean 'Si può convertire in Isbn, dato che questa classe è 'scritta apposta per questo If (DestinationType Is GetType(Isbn)) Then Return True End If Return MyBase.CanConvertFrom(Context, DestinationType) End Function 'Determina se sia possibile convertire dal tipo dato Public Overrides Function CanConvertFrom(ByVal Context As ITypeDescriptorContext, _ ByVal SourceType As Type) As Boolean 'Si può convertire da String, dato che questa classe è 'scritta apposta per questo If (SourceType Is GetType(String)) Then Return True End If Return MyBase.CanConvertFrom(Context, SourceType) End Function 'Converte da stringa a Isbn Public Overrides Function ConvertFrom(ByVal Context As ITypeDescriptorContext, _ ByVal Culture As Globalization.CultureInfo, _ ByVal Value As Object) As Object If TypeOf Value Is String Then Dim Str As String = DirectCast(Value, String) 'Cerca di creare un nuovo oggetto isbn Try Dim Obj As Isbn = Isbn.CreateNew(Str) Return Obj Catch ex As Exception MessageBox.Show(ex.Message, "Books Manager", _ MessageBoxButtons.OK, MessageBoxIcon.Exclamation) Return New Isbn End Try End If Return MyBase.ConvertFrom(Context, Culture, Value) End Function 'Converte da Isbn a stringa Public Overrides Function ConvertTo(ByVal Context As ITypeDescriptorContext, _ ByVal Culture As Globalization.CultureInfo, _ ByVal Value As Object, ByVal DestinationType As Type) As Object If DestinationType Is GetType(String) And _ TypeOf Value Is Isbn Then Dim Temp As Isbn = DirectCast(Value, Isbn) Return Temp.ToString End If Return MyBase.ConvertTo(Context, Culture, Value, DestinationType) End Function End Class 'Il codice ISBN, dal primo gennaio 2007, deve obbligatoriamente 'essere a tredici cifre. Per questo motivo metterò solo 'questo tipo nel sorgente 'P.S.: per convenzione, gli acronimi con più di due lettere devono 'essere scritti in Pascal Case Public Class Isbn 'Un codice è formato da: 'Un prefisso (3 cifre) - solitamente 978 indica un libro in generale Private _Prefix As Int16 = 978 'Un identificativo linguistico (da 1 a 5 cifre): indica 'il paese di provenienza dell'autore - in Italia è 88 Private _LanguageID As Int16 = 88 'Un prefisso editoriale (da 2 a 6 cifre): indica l'editore Private _PublisherID As Int64 = 89637 'Un identificatore del titolo Private _TitleID As Int32 = 15 'Un codice di controllo che può variare da 0 a 10. 10 viene 'indicato con X, perciò lo imposto come Char Private _ControlChar As Char = "9" <DisplayName("Prefisso"), _ Description("Prefisso del codice, costituito da tre cifre."), _ Browsable(True), _ Category("Isbn")> _ Public Property Prefix() As Int16 Get Return _Prefix End Get Set(ByVal Value As Int16) If Value = 978 Or Value = 979 Then _Prefix = Value Else Throw New ArgumentException("Prefisso non valido!") End If End Set End Property <DisplayName("ID Lingua"), _ Description("Identifica l'area da cui previene l'autore."), _ Browsable(True), _ Category("Isbn")> _ Public Property LanguageID() As Int16 Get Return _LanguageID End Get Set(ByVal Value As Int16) _LanguageID = Value End Set End Property <DisplayName("ID Editore"), _ Description("Identifica il marchio dell'editore."), _ Browsable(True), _ Category("Isbn")> _ Public Property PublisherID() As Int32 Get Return _PublisherID End Get Set(ByVal Value As Int32) _PublisherID = Value End Set End Property <DisplayName("ID Titolo"), _ Description("Identifica il titolo del libro."), _ Browsable(True), _ Category("Isbn")> _ Public Property TitleID() As Int32 Get Return _TitleID End Get Set(ByVal Value As Int32) _TitleID = Value End Set End Property <DisplayName("Carattere di controllo"), _ Description("Verifica la correttezza degli altri valori."), _ Browsable(True), _ Category("Isbn")> _ Public Property ControlChar() As Char Get Return _ControlChar End Get Set(ByVal Value As Char) _ControlChar = Value End Set End Property Public Sub New() End Sub 'Restituisce in forma di stringa il codice Public Overrides Function ToString() As String Return String.Format("{0}-{1}-{2}-{3}-{4}", _ Me.Prefix, Me.LanguageID, Me.PublisherID, _ Me.TitleID, Me.ControlChar) End Function 'Metodo statico factory per costruire un nuovo codice ISBN. Se 'si mettesse questo codice nel costruttore, l'oggetto verrebbe 'comunque creato anche se il codice inserito fosse errato. 'In questo modo, la creazione viene fermata e restituito 'Nothing in caso di errori Shared Function CreateNew(ByVal StringCode As String) As Isbn 'Con le espressioni regolari, ottiene le varie parti Dim Split As New System.Text.RegularExpressions.Regex( _ "(?<Prefix>d{3})-(?<Language>d{1,5})" & _ "-(?<Publisher>d{2,6})-(?<Title>d+)-(?<Control>w)") Dim M As System.Text.RegularExpressions.Match = _ Split.Match(StringCode) 'Se la lunghezza del codice, senza trattini, è di '13 caratteri e il controllo tramite espressioni regolari 'ha avuto successo, procede If StringCode.Length = 17 And M.Success Then Dim Result As New Isbn With Result .Prefix = M.Groups("Prefix").Value .LanguageID = M.Groups("Language").Value .PublisherID = M.Groups("Publisher").Value .TitleID = M.Groups("Title").Value .ControlChar = M.Groups("Control").Value End With Return Result Else Throw New ArgumentException("Il codice inserito è errato!") End If End Function End Class 'Una classe che rappresenta un libro Public Class Book Private _Title As String 'Si suppone che un libro abbia meno di 32767 pagine XD Private _Pages As Int16 = 100 'Si possono anche avere più autori: in questo caso si ha 'una lista a tipizzazione forte. Private _Authors As New List(Of Author) 'L'eventuale serie a cui il libro appartiene Private _Series As String 'Casa editrice Private _Publisher As String 'Data di pubblicazione Private _PublicationDate As Date 'Argomento Private _Subject As String 'Costo in euro Private _Cost As Single = 1.0 'Ristampa Private _Reprint As Byte = 1 'Codice ISBN13 Private _Isbn As New Isbn <DisplayName("Titolo"), _ Description("Il titolo del libro."), _ Browsable(True), _ Category("Editoria")> _ Public Property Title() As String Get Return _Title End Get Set(ByVal Value As String) _Title = Value End Set End Property <DisplayName("Collana"), _ Description("La collana o la serie a cui il libro appartiene."), _ Browsable(True), _ Category("Editoria")> _ Public Property Series() As String Get Return _Series End Get Set(ByVal Value As String) _Series = Value End Set End Property <DisplayName("Editore"), _ Description("La casa editrice."), _ Browsable(True), _ Category("Editoria")> _ Public Property Publisher() As String Get Return _Publisher End Get Set(ByVal Value As String) _Publisher = Value End Set End Property <DisplayName("Pagine"), _ Description("Il numero di pagine da cui il libro è composto."), _ DefaultValue("100"), _ Browsable(True), _ Category("Dettagli")> _ Public Property Pages() As Int16 Get Return _Pages End Get Set(ByVal Value As Int16) If Value > 0 Then _Pages = Value Else Throw New ArgumentException("Numero di pagine insufficiente!") End If End Set End Property <DisplayName("Autore/i"), _ Description("L'autore o gli autori."), _ Browsable(True), _ Category("Editoria")> _ Public ReadOnly Property Authors() As List(Of Author) Get Return _Authors End Get End Property <DisplayName("Pubblicazione"), _ Description("La data di pubblicazione della prima edizione."), _ Browsable(True), _ Category("Dettagli")> _ Public Property PublicationDate() As Date Get Return _PublicationDate End Get Set(ByVal Value As Date) _PublicationDate = Value End Set End Property <DisplayName("Codice ISBN"), _ Description("Il codice ISBN conformato alla normativa di 13 cifre."), _ Browsable(True), _ Category("Editoria"), _ TypeConverter(GetType(IsbnConverter))> _ Public Property Isbn() As Isbn Get Return _Isbn End Get Set(ByVal Value As Isbn) _Isbn = Value End Set End Property <DisplayName("Ristampa"), _ Description("Il numero della ristampa."), _ DefaultValue(1), _ Browsable(True), _ Category("Dettagli")> _ Public Property Reprint() As Byte Get Return _Reprint End Get Set(ByVal Value As Byte) If Value > 0 Then _Reprint = Value Else Throw New ArgumentException("Ristampa: valore errato!") End If End Set End Property <DisplayName("Costo"), _ Description("Il costo del libro, in euro."), _ Browsable(True), _ Category("Editoria")> _ Public Property Cost() As Single Get Return _Cost End Get Set(ByVal Value As Single) If Value > 0 Then _Cost = Value Else Throw New ArgumentException("Inserire prezzo positivo!") End If End Set End Property End Class
E il codice del form:
Class Form1 Private Sub strAddBook_Click(ByVal sender As Object, _ ByVal e As EventArgs) Handles strAddBook.Click Dim Title As String = _ InputBox("Inserire il titolo del libro:", "Books Manager") 'Controlla che la stringa non sia vuota o nulla If Not String.IsNullOrEmpty(Title) Then Dim Item As New ListViewItem(Title) Dim Book As New Book() Book.Title = Title Item.ImageIndex = 0 Item.Tag = Book lstBooks.Items.Add(Item) End If End Sub Private Sub lstBooks_SelectedIndexChanged(ByVal sender As Object, _ ByVal e As EventArgs) Handles lstBooks.SelectedIndexChanged 'Esce dalla procedura se non ci sono elementi selezionati If lstBooks.SelectedIndices.Count = 0 Then Exit Sub End If 'Altrimenti procede pgBook.SelectedObject = lstBooks.SelectedItems(0).Tag End Sub