Questo sito utilizza cookies solo per scopi di autenticazione sul sito e nient'altro. Nessuna informazione personale viene tracciata. Leggi l'informativa sui cookies.
Username: Password: oppure
Guida al Visual Basic .NET - PropertyGrid

Guida al Visual Basic .NET

Capitolo 70° - PropertyGrid

<< Precedente Prossimo >>

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:

    PropertyGrid1.jpg
  • 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:

PropertyGrid2.jpg

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:

PropertyGrid3.jpg
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
 


PropertyGrid4.jpg

<< Precedente Prossimo >>
A proposito dell'autore

C#, TypeScript, java, php, EcmaScript (JavaScript), Spring, Hibernate, React, SASS/LESS, jade, python, scikit, node.js, redux, postgres, keras, kubernetes, docker, hexo, etc...