VB: ActiveX

FTP    ::    ActiveX |  1  |- 2 -|  3  |  4  |  5  |

   
Treeview - Operations

 

There are really five operations you can perform to change the hierarchy - modification, addition, duplication, deletion and relocation. In other words, you now have a functioning treeview control. What to do with it? If you modify it, in this case, it means only that you've renamed an item. You can add new nodes, of course, one by one. That's what was done to intially fill out the treeview. You can delete one node, or that and every sublist under it. You can move, or relocate, one node, and every sublist under it, either just within the same sorted list, or to another level entirely (to keep this simple, in the following examples, the lists are simply sorted alphabetically - not by any sort scalar). And you can copy a node, and even every sublist under it (which might be particularly handy for alternate treatments or languages, for ex; copy the whole 'book' at once). Of course, as with getting data into the treeview, none of this is built into the treeview control itself. All the treeview does is display whatever hierarchy is 'hard-coded' into it for that session. You have to create routines to do all of this, to both change the treeview, and change any underlying data in the table. One operation which doesn't change the data is, of course, searching. The treeview makes it easy since you can find a node based on the node's key. And here that key is based on the unique id of the underlying record in the table. Just run a query SELECT to find the unique id, and prefix the "a" to it. It does raise the question with regard to querying such an 'adjacency list', however.

 
Treeview - Modification

 

One can change the text shown in the treeview. In the properties box/window for the control, it should have its Label Edit property set to twvAuto, or 0. Then if you click and hold down the mouse button for a second or so, then release without moving, the entry should go into edit mode. You type in the new text, hit the Enter button. And not only will it be changed in the treeview, but you want the underlying table to reflect that same modification:

 
' in the module for the form with the tvw control
 
Private Sub tvw_KeyUp(KeyCode As Integer, ByVal Shift As Integer)
 
     Dim rst As DAO.Recordset, nodCurrent As node, objtree As TreeView
     Dim strSearch As String, strSQL As String, strParentKey As String
     Dim strKey As String, strText As String, strNew As String
 
     Set rst = fGetThisDB.OpenRecordset("tblSelfRefData", dbOpenDynaset)
     Set objtree = Me!tvw.Object
 
     Set nodCurrent = objtree.SelectedItem
     sRtnNullOrKey strKey, strText, nodCurrent
 
     Select Case KeyCode
 
         Case vbKeyReturn
 
             If strKey <> "" Then
                 rst.FindFirst "[anID]=" & strKey
 
                 ' Need strComp to catch changes in case, even if the letters remain the same
                 If StrComp(strText, rst![txCategory], 0) <> 0 Then
                     rst.Edit
                         rst![txCategory] = strText
                     rst.Update
 
                     If fTvwHasParent(nodCurrent) Then objtree.Nodes(nodCurrent.Parent.Index).Sorted = True
                 End If
             End If
 

 
         Case vbKeyInsert
 
             strNew = Trim$(InputBox(prompt:="Add new category"))
             sInsertNode strNew, strKey, rst, objtree, Me!lngProjID
 

 
         Case vbKeyDelete
 
             If strKey <> "" Then
 
                 objtree.Nodes.Remove nodCurrent.Index
                 sDeleteTreeRecords strKey, "tblSelfRefData"
 
             End If
 
     End Select
 
     rst.Close
     Set rst = Nothing
 
End Sub
 

 
 

Modification amounts to renaming a node. The renaming is caught when the Enter key is pressed. That also suggests that if the Enter key is not pressed, but simply that another node is clicked, that the change won't be reflected in the data, since it won't come through here. Now one could probably just use a SELECT (lookup) and UPDATE on the top, for the Enter key. But since a recordset is otherwise used, here, it's just as easy to use that. And note that when the node is renamed, that the superior node has to have Sorted again set, as noted previously.

 
Treeview - Addition

 

The same short routine also covers the call to the insertion/deletion of a single new node. You can see that inserting/adding a new node passes control to another function, the subroutine, sInsertNode:

 
Public Sub sInsertNode(strNew As String, strKey As String, ysnInsertMode As Boolean, _
                                   rst As DAO.Recordset, objtree As Object, idxProjID As Variant)
 
     Dim nodNew As node, strSearch As String, nodCurrent As node, idxSup As Long
     Set nodCurrent = objtree.SelectedItem
 
     If strNew <> "" Then
         idxSup = IIf(strKey = "", 0, strKey)
         If ysnInsertMode = False And idxSup > 0 Then
             rst.FindFirst "[anID]=" & strKey
             idxSup = rst![lngSupID]
         End If
 
         strSearch = "[lngProjID]=" & idxProjID & " And [txCategory]='" & strNew & "' And [lngSupID]=" & idxSup
         rst.FindFirst strSearch
 
         If rst.NoMatch Then
 
             With rst
                 .AddNew
                 ![txCategory] = strNew
                 ![lngProjID] = idxProjID
                 ![lngSupID] = idxSup
                 .Update
                 .Bookmark = .LastModified
             End With
 
             If idxSup = 0 Then
Set nodNew = objtree.Nodes.Add(, , "a" & rst![anID], strNew)
             ElseIf ysnInsertMode Then
Set nodNew = objtree.Nodes.Add(nodCurrent, tvwChild, "a" & rst![anID], strNew)
             Else
Set nodNew = objtree.Nodes.Add(nodCurrent.Parent, tvwChild, "a" & rst![anID], strNew)
             End If
 
             ' after adding category, keep in alphabetical order
             objtree.Nodes(nodNew.Index).Sorted = True
 
         Else
               MsgBox ("This has already been used.")
         End If
 
     End If
 
     Set nodNew = Nothing
     Set nodCurrent = Nothing
 
End Sub
 

 
 

Here a flag is sent to indicate whether the new node goes in the same level/list as the clicked node, or whether it goes in the sublist of that node. Again, the sort isn't a factor, since the sublists are alphabetically sorted, automatically, by the setting of the Sorted property. Personally, I think it's a needless complication. It's as easy to click on the superior node, and just always add to the sublist, period. Because one can move nodes around, if that isn't suitable, one can just relocate the node manually, in the treeview. Anyhow. And again, this reads/writes via a recordset, when a SELECT and UPDATE might be just as good.

The tree itself has another property - selecteditem - which is obviously useful. So by clicking or moving in the treeview, it's known which node is in question. This is assigned to a node type. And it's probably wise to check for duplicate text, as done here - though I suppose it's not absolutely necessary (it's the key that has to be unique). The assumption is that if there is no strKey, that it must be a top level node. So that gets a 0, in in that case. But if this toggle/switch is off, then any new node goes at the same level/list. That means the superior node is that which is superior to the selected node. Since this uses an auto-generated autonumber unique key for the records in the table, the final bookmark gets back to the record just created so that the newly generated id can be read. The only other thing is that the node must be correctly placed in the tree. If the toggle is on, it means place in the sublist, under the node; or same level if not. So here the node's parent property, seen above, is used if the toggle is off. And that's about that.

 
Treeview - Deletion

 

And again the same short routine, previously, also included a deletion method. Click on the node, and hit the Delete key. The logical thing is that the node, and all sublists under it, are deleted. That's just what the remove method of Nodes does. But that has to be then reflected in the underlying table:

 
' Remove all records from one-way, 'recursive' table, down to the last subcategory, using SQL queries
 
Public Sub sDeleteTreeRecords(lngID, strTableName)
     Dim strQuery As String, strWhere As String
 
     ' Create a hole, delete selected item
     fGetThisDB.Execute "DELETE * FROM " & strTableName & " WHERE anID=" & lngID
 
     strQuery = " FROM " & strTableName & _
                     " LEFT JOIN " & strTableName & " AS list2 ON " & strTableName & ".lngSupID = list2.anID" & _
                     " WHERE (" & strTableName & ".lngSupID>0) AND (list2.anID Is Null)"
 
     ' Repeatedly delete any records where the lngSupID points to nothing, until there are no more such records.
     ' (unless lngSupID is 0, which means it's a top level record)
     Do While fGetThisDB.OpenRecordset("SELECT Count(*) " & strQuery)(0) > 0
         fGetThisDB.Execute "DELETE " & strTableName & ".*" & strQuery
     Loop
 
End Sub
 

 
 

I can almost hear some saying, finally, he got sensible and started using SQL, which is how Access can work at its best. It just was particularly suitable, here.

To clear out all the sublists in the table, it's really pretty straightforward. No recursion is needed. One just makes a hole by deleting the selected node. Then its sublist, if one exists, is just hanging there. The superior node to which all the items point, is now gone. That unique number for the superior node is no longer found in the table. So you can just collect the 'strays'. And delete them. Then if those each had sublists, suddenly those sublists are left hanging. And all those items become 'strays'. They can be collected and simply deleted at once. And this just continues in a loop until no more 'strays' are found. Simple. It's also a bit self-correcting if 'strays' were created by program glitches or some other cause. This doesn't care how the 'strays' were created. It just burns every one, any time you delete so much as a single node from the treeview.

The "(0)" after SELECT Count(*) is just a shorthand to read the actual count of records matching the criteria. And the fGetThisDB is just the equivalent of CurrentDB. It came out of some brief online discussions about earlier Office versions and whether CurrentDB, alone, was always reliable. So just for curiosity, here's the fGetThisDB. Probably quite unnecessary. CurrentDB should be just fine, instead:

 
' Either return pointer to current db, or - if some problem - return NULL.

Public Function fGetThisDB()
    On Error GoTo Err_GetThisDB

    ' Code or db reset could clear the global, so make sure it's set.
    If strThisDBFilename = "" Then fSetCurDB
    
    ' Will likely never do anything but SET here, and exit.
    ' If it's not a dot-Name match with workspace 0, db 0, then it will raise an error.
    ' (dot-Name : should return the full path for database)
    ' (strThisDBFilename : full filename, left part of the path, something to uniquely identify)
    If InStr(1, DBEngine(0)(0).Name, strThisDBFilename) > 0 Then
        Set fGetThisDB = DBEngine(0)(0)
        Exit Function
    End If
    
Exit_GetThisDB:
    ' If the exceptional case that workspace 0, db 0 is not the database you expect,
    ' look through all databases in workspace 0 to find what you want - or return null.
    
    Dim numDbCount
    ' returns 1-based scalar - slide down to 0-based
    numDbCount = DBEngine(0).Databases.Count - 1
    
    Do While numDbCount > 0
        If InStr(1, DBEngine(0)(numDbCount).Name, strThisDBFilename) > 0 Then
            Set fGetThisDB = DBEngine(0)(numDbCount)
            Exit Function
        End If
        numDbCount = numDbCount - 1
    Loop
    
Exit_RtnNull:
    MsgBox "Can't properly locate the database : " & strThisDBFilename
    fGetThisDB = vbNullChar
    Exit Function

Err_GetThisDB:
    If Err.Number = 3265 Then Resume Exit_GetThisDB
    Resume Exit_RtnNull
    
End Function

 

 
 

So that only leaves Relocation and Duplication.

Continue