Implementare il drag & drop all'interno di una TreeView
a cura di Renato Gentile (requisiti: nessuno)

Introduzione
Tra i vari controlli messi a disposizione degli sviluppatori (forniti con il Framework .Net), un ruolo di spicco viene ricoperto sicuramente dal controllo TreeView, che sembra fatto apposta per la modellazione di dati gerarchici, dove l'XML la fa sicuramente da padrone.

Ma, nonostante la potenza del controllo, uno dei maggiori problemi che spesso si incontrano quando lo si deve usare, è quello di poter modificare "al volo" la struttura che si sta modellando, senza perdere quanto fatto fino a quel punto.

E' da questa situazione che nasce l'idea di questo articolo. Ciò che verrà illustrato è sostanzialmente un'implementazione di un drag & drop "virtuale" tra i nodi di una TreeView. Ho usato il termine virtuale, perché, in verità, non verranno utilizzate le funzionalità di drag & drop messe a disposizione in modo nativo dal framework, anche se il risultato finale darà all'utente la stessa impressione, consentendogli di prendere un ramo e trascinarlo su un altro, oltre che aggiungere dei rami al volo, eliminarli, copiarli, tagliarli, ecc.

Tra l'altro, in questo modo si potranno (volendo) eliminare (o comunque lasciarli come ausilio) i pulsanti esterni, spesso scomodi e difficili da collocare, ma che comunque operano all'esterno dell'ambito della TreeView stessa.

Per la dimostrazione vera è propria, ho usato un banalissimo progetto Windows Form con un controllo TreeView e un ContextMenuStrip.

Preparazione del progetto
Ecco passo per passo come preparare il progetto per la dimostrazione. Eventualmente è possibile scaricare il progetto già pronto dall'area download.

  1. Creiamo un nuovo progetto in VB.NET e selezioniamo come modello un'applicazione Windows;
  2. Aggiungiamo alla Form1 un controllo TreeView e rinominiamolo tvXML (nel codice allegato all'articolo, per maggiore leggibilità, ho impostato il font della TreeView su Bold e il carattere su 10, oltre alla proprietà Dock su Fill).
    Ho chiamato la TreeView così per rimandarci idealmente ad una struttura XML;
  3. Aggiungiamo un controllo ContextMenuStrip lasciando il nome sul valore proposto di default;
  4. Nel ContextMenuStrip creiamo le seguenti voci:
    • Nuovo Nodo
    • Copia
    • Taglia
    • Incolla
    • Elimina
  5. Copiamo nel form il codice mostrato nel paragrafo seguente.

Ok, siamo pronti !

Il codice
Per semplicità, tutta la logica e i singoli passi intrapresi per sviluppare l'applicativo dimostrativo sono stati inseriti direttamente nel codice come commenti, così da rendere il tutto facilmente leggibile anche nell'ambiente di sviluppo.

E' però opportuno precisare che, al fine di ridurre la quantità di codice esposto e di limitarlo strettamente alla problematica in oggetto, sono state omessi i costrutti Try-Catch e simili.

Public Class Form1
  ' Il nodo originale che si vuole spostare...
  Private _sourceNode As TreeNode
  ' Il nodo su cui ci troviamo, ossia quello di destinazione
  ' quando si effettuerà lo spostamento...
  Private _currentHooverNode As TreeNode
  ' Traccia la pressione del pulsante sinistro del mouse... vedremo dopo dove
  Private _buttonsPressed As Boolean = False
  ' Il nodo di appoggio per il copia/taglia/incolla ...
  Private _copyNode As TreeNode

  Private Sub Form1_Load(ByVal sender As System.Object, _
                         ByVal e As System.EventArgs) Handles MyBase.Load
    Foo()
  End Sub

  Private Sub Foo()

    ' Carichiamo un po' di dati fittizi nella treeview ...
    Dim a As New TreeNode("AAA")
    Dim b As New TreeNode("BBB")
    Dim c As New TreeNode("CCC")
    Dim d As New TreeNode("DDD")
    Dim e As New TreeNode("EEE")

    b.Nodes.Add("111")
    b.Nodes.Add("222")

    c.Nodes.Add("333")
    c.Nodes.Add("444")

    d.Nodes.Add("555")
    d.Nodes.Add("666")

    e.Nodes.Add("777")
    e.Nodes.Add("888")

    a.Nodes.Add(b)
    a.Nodes.Add(c)
    a.Nodes.Add(d)
    a.Nodes.Add(e)

    tvXML.Nodes.Add(a)
    a.Expand()

  End Sub

  Private Sub tvXML_NodeMouseHover(ByVal sender As System.Object, _
                                   ByVal e As System.Windows.Forms.TreeNodeMouseHoverEventArgs) _
                                   Handles tvXML.NodeMouseHover
    ' Questo evento ci consente di sapere su quale nodo ci troviamo.
    ' Se il bottone del mouse è premuto, allora selezioniamo
    ' contestualmente il nodo per dare un effetto di selezione dinamica.
    ' Il nodo su cui ci troviamo, comunque sia, viene
    ' memorizzato nella variabile _currentHooverNode.
    _currentHooverNode = e.Node
    ' Ecco a cosa ci serve tracciare la pressione del pulsante del mouse
    If _buttonsPressed Then tvXML.SelectedNode = e.Node
  End Sub

  Private Sub tvXML_MouseUp(ByVal sender As System.Object, _
                            ByVal e As System.Windows.Forms.MouseEventArgs) Handles tvXML.MouseUp

    ' Quando viene rilasciato il pulsante snistro del mouse,
    If e.Button = Windows.Forms.MouseButtons.Left Then
      ' verifico se effettuare uno spostamento del nodo ...
      If _sourceNode IsNot Nothing And _currentHooverNode IsNot Nothing Then
        ' Devo spostare un nodo visto che esistono sia un nodo sorgente che uno di destinazione...

        Dim _newNode As TreeNode = _sourceNode.Clone ' Clono il nodo sorgente...

        _sourceNode.Remove() ' Elimino il nodo sorgente dalla treeview...


        If _currentHooverNode.Nodes.Count = 0 Then ' Il nodo destinatario è un ramo foglia?

          ' Se non è un ramo foglia, verifico se ha un padre (ossia se non è un ramo root)...
          If _currentHooverNode.Parent IsNot Nothing Then
            ' Se ha un padre, vi aggiungo il ramo originale ...
            _currentHooverNode.Parent.Nodes.Add(_newNode)
            EspandiRamo(_currentHooverNode.Parent) ' Espandiamo il ramo, se occorre...
          Else
            ' altrimenti aggiungo il nodo da spostare che diverrà il figlio del ramo destinatario
            _currentHooverNode.Nodes.Add(_newNode)
            EspandiRamo(_currentHooverNode) ' Espandiamo il ramo, se occorre...
          End If
        Else
          ' Se è un ramo foglia,
          ' aggiungo il ramo originale allo stesso ramo del nodo di destinazione
          _currentHooverNode.Nodes.Add(_newNode)
          EspandiRamo(_currentHooverNode) ' Espandiamo il ramo, se occorre...
        End If
        ' Ripristino il cursore ...
        tvXML.Cursor = Cursors.Arrow
        ' Aggiorno la treeview ...
        tvXML.Refresh()
      End If
    End If

    _buttonsPressed = False ' Resetto il flag che traccia la pressione del tasto sinistro...

    ' Anniento gli oggetti sorgente e destinazione
    ' per ripristinare la situazione allo stato originale.
    _currentHooverNode = Nothing
    _sourceNode = Nothing

  End Sub

  Private Sub tvXML_MouseDown(ByVal sender As System.Object, _
                              ByVal e As System.Windows.Forms.MouseEventArgs) _
                              Handles tvXML.MouseDown

    ' Se viene tenuto premuto il tasto sinistro del mouse su un nodo,
    ' probabilmente lo faccio per cominciare una operazione di spostamento ...
    If Not _currentHooverNode Is Nothing And e.Button = Windows.Forms.MouseButtons.Left Then
      ' ... se ho potuto tenere traccia anche dell'oggetto sorgente (su cui sono passato),
      ' allora posso cominciare la procedura di spostamento ...
      _buttonsPressed = True ' Traccio la pressione del bottone sinistro del mouse ...
      ' Imposto l'oggetto di origine su quello su cui mi trovo correntemente ...
      _sourceNode = _currentHooverNode
      ' Imposto il cursore della treeview sulla mano, per dar parvenza di un drag & drop ...
      tvXML.Cursor = Cursors.Hand
    End If

  End Sub

  Private Sub tvXML_MouseHover(ByVal sender As System.Object, _
                               ByVal e As System.EventArgs) Handles tvXML.MouseHover
    ' Questo evento avviene al di fuori dei nodi, per i quali si usa l'evento NodeMouseHover.
    ' Per questa ragione, se sto gestendo questo evento, vuol dire che mi trovo sulla treeview
    ' e non sui nodi, per cui non esiste un oggetto destinazione...
    ' Quindi, anche se viene premuto il tasto sinistro del mouse,
    ' non partirà alcuna gestione per lo spostamento dei nodi
    _currentHooverNode = Nothing
  End Sub

  Private Sub tvXML_NodeMouseClick(ByVal sender As System.Object, _
                                   ByVal e As System.Windows.Forms.TreeNodeMouseClickEventArgs) _
                                   Handles tvXML.NodeMouseClick
    If e.Button = Windows.Forms.MouseButtons.Right Then
      tvXML.SelectedNode = e.Node
    Else
      ' Questo evento viene generato a seguito di un click completo su un nodo,
      ' ossia mousedown e mouseup sullo stesso nodo.
      ' In tal caso, non essendoci stato di fatto nessuno spostamento,
      ' annullo tutte le operazioni preliminari già affettuate alla pressione del tasto sinistro
      ' del mouse ...
      _currentHooverNode = Nothing ' Annullo il nodo sorgente ...
      tvXML.Cursor = Cursors.Arrow ' Reimposto il cursore ...
      _buttonsPressed = False ' Traccio correttamente la pressione del tasto sinistro del mouse
    End If
  End Sub

  Private Sub NuovoNodoToolStripMenuItem_Click(ByVal sender As System.Object, _
                                               ByVal e As System.EventArgs) _
                                               Handles NuovoNodoToolStripMenuItem.Click
    ' Creiamo un nuovo nodo di appoggio ...
    Dim _newNode As New TreeNode
    ' Inseriamo un testo che chiarisca che stiamo modificando un'etichetta ...
    _newNode.Text = "Inserire il testo qui ..."
    ' Aggiungiamo il nuovo nodo al nodo selezionato,
    ' gli diamo il "fuoco" e lo passiamo in modifica ...
    tvXML.SelectedNode.Nodes.Add(_newNode)
    tvXML.SelectedNode = _newNode
    tvXML.LabelEdit = True
    ' Nel progetto, la proprietà LabelEdit della treeview è lasciata al valore di default False.
    ' Tuttavia, se lasciata su False, la chiamata al metodo BeginEdit solleverà un'eccezione.
    ' Quindi la impostiamo su True, facciamo le modifiche e la reimpostiamo su False.
    ' Volendo, la si può impostare di default su True omettendo questa questa doppia assegnazione
    tvXML.SelectedNode.BeginEdit()
    tvXML.LabelEdit = True
  End Sub

  Private Sub CopiaToolStripMenuItem_Click(ByVal sender As System.Object, _
                                           ByVal e As System.EventArgs) _
                                           Handles CopiaToolStripMenuItem.Click
    ' Copiamo il nodo correntemente selezionato ...
    If Not tvXML.SelectedNode Is Nothing Then
      _copyNode = tvXML.SelectedNode
    End If
  End Sub

  Private Sub IncollaToolStripMenuItem_Click(ByVal sender As System.Object, _
                                             ByVal e As System.EventArgs) _
                                             Handles IncollaToolStripMenuItem.Click
    ' Incolliamo, ossia aggiungiamo, il nuovo nodo al nodo selezionato ...
    ' Dato che non posso incollare direttamente il nodo memorizzato in _copyNode, perché inserirei
    ' due nodi uguali (e non è possibile farlo), allora clono il nodo appoggiandolo in una variabile
    ' temporanea e lo aggiungo al nuovo nodo ...
    If Not tvXML.SelectedNode Is Nothing Then
      Dim _localTN As TreeNode
      _localTN = _copyNode.Clone
      If Not _copyNode Is Nothing Then tvXML.SelectedNode.Nodes.Add(_localTN)
      ' Espandiamo il ramo, se occorre ...
      EspandiRamo(tvXML.SelectedNode)
    End If
  End Sub

  Private Sub TagliaToolStripMenuItem_Click(ByVal sender As System.Object, _
                                            ByVal e As System.EventArgs) _
                                            Handles TagliaToolStripMenuItem.Click
    ' Copiamo il nodo correntemente selezionato e tagliamo l'originale ...
    ' Qui non avremo problemi ad incollare perché il vecchio nodo non fa più parte della treeview
    If Not tvXML.SelectedNode Is Nothing Then
      _copyNode = tvXML.SelectedNode
      tvXML.Nodes.Remove(tvXML.SelectedNode)
    End If
  End Sub

  Private Sub EliminaToolStripMenuItem_Click(ByVal sender As System.Object, _
                                             ByVal e As System.EventArgs) _
                                             Handles EliminaToolStripMenuItem.Click
    ' L'eliminazione elimina definitivamente il nodo, senza possibilità di recupero.
    ' In tal caso, è buona educazione avvisare preventivamente l'utente !!
    If Not tvXML.SelectedNode Is Nothing Then
      If MessageBox.Show("Eliminare definitivamente il nodo" & tvXML.SelectedNode.Text.Trim() & _
                         " ed eventuali nodi figlio ?", "Elimina nodo", MessageBoxButtons.YesNo, _
                         MessageBoxIcon.Exclamation) = Windows.Forms.DialogResult.Yes Then
        tvXML.Nodes.Remove(tvXML.SelectedNode) ' Rimuoviamo il nodo ...
      End If
    End If
  End Sub

  Private Sub EspandiRamo(ByVal Ramo As TreeNode)

    ' Risolviamo l'espansione dei rami con una bella funzione ricorsiva,
    ' tipologia di funzione che nelle treeview serve davvero molto ...
    If Ramo Is Nothing Then Return
    Ramo.Expand()
    If Ramo.Nodes.Count > 0 Then

      For Each ramofiglio As TreeNode In Ramo.Nodes
        EspandiRamo(ramofiglio)
      Next
    End If

  End Sub

End Class

Conclusioni
Come abbiamo visto, l'implementazione di questo sistema di drag & drop è piuttosto spartana.
Prima di tutto, è assolutamente privo di qualunque parte grafica, ma questo è stato fatto volutamente, visto che ci avrebbe solo distratto dalla soluzione del problema principale.
Ma, grafica a parte, è palese che il controllo è migliorabile. Tra la migliorie possibili, c'è sicuramente quella di inserire i nodi in una posizione ben precisa anziché in fondo alla lista, oppure quella di fornire a tutto il sistema una sorta di "intelligenza" evitando, ad esempio, che vengano mischiati tra loro nodi di tipologia diversa. Questo potrebbe essere implementato usando la tecnica più semplice che mi viene in mente, cioè tramite la proprietà Tag del nodo padre (o nodo di destinazione). Se il Tag del nodo padre contiene un identificativo di una sorta di "specie" del nodo stesso, si può evitare che nodi di "specie" diverse vengano a contatto. Ma questa è solo una delle decine di possibili implementazioni e migliorie applicabili al progetto.

Inoltre, come si sarà sicuramente notato, non ho utilizzato il sistema di identificazione dei nodi proposta da MSDN, ossia a mezzo delle coordinate del nodo stesso, ma ho usato un sistema alternativo. Ovviamente ognuno è libero di implementare la modalità che preferisce, io ho semplicemente voluto mostrare un'alternativa.

Comunque sia, il mio obiettivo finale era quello di mostrare una possibile soluzione del problema, per colmare una lacuna del controllo TreeView, soluzione sulla cui base ognuno può poi lavorare a seconda dei propri gusti o delle proprie esigenze! Spero davvero di esserci riuscito ;-) !!
I sorgenti a corredo di questo articolo sono scaricabili dall'area download.
Per qualsiasi commento, suggerimento, domanda o critica, potete scrivere all'autore di questo articolo : Renato Gentile

Note sull'Autore
Renato Gentile vive a Triggiano (Bari) ed è nato nel 1974. Ha iniziato a lavorare come analista programmatore dal 1994 presso una concessionaria Olivetti, dove sviluppava applicativi in Cobol per privati ed enti pubblici. Da allora ha cambiato una serie di aziende, attraversando anche periodi da lavoratore autonomo, cominciando a sviluppare in VB dalla versione 4 e seguendolo in tutte le sue evoluzioni fino ad oggi. Sta per ottenere la certificazione MCTS su SQL Server 2005. Tra le esperienze lavorative più significative si annoverano collaborazioni in progetti per il Ministero della Difesa, la società dei trasporti urbani di Taranto (A.M.A.T.) e l'Acquedotto Pugliese. Attualmente lavora a Bari in una società di informatica in ambito sanitario. Hobby principale ... leggere, leggere e poi ancora leggere (libri di informatica ovviamente ;-)) oltre che arredare casa propria con la sua bellissima moglie !