|
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
 |
|