TreeView BeforeSelect event

gwhizz

New member
Joined
Apr 29, 2025
Messages
3
Programming Experience
10+
My form has a SplitContainer with a TreeView in the top panel and a DataGridView in the bottom panel. The user selects a tree item and the item's data is displayed in the grid. The user can update the grid as needed and then click the Apply or Cancel button. All is well if the user clicks Apply or Cancel before selecting another tree item.

If the user is updating the grid and selects another tree item, I need to display a "Save Updates?" message with Yes and No buttons. If the user clicks Yes, I want to save the updates and then display the newly selected item's data.

The BeforeSelect event seemed like the best place to handle this situation, so that's what I did. The event fires as expected, but the message box seems to disrupt the event party. When the message box is dismissed, the BeforeSelect event is fired again and again. I also tried the AfterSelect event, but had the same results. I saw this same issue while googling, but no solution was offered.

I have displayed message boxes in event handlers before and don't remember having a problem. When I looked on the learn.microsoft.com site, their example for BeforeSelect has a message box. I'm guessing their example may not work.

Any ideas would be appreciated.
 
Show us the code. This is not something that we should need to ask for. The code with the issue is always relevant when asking questions about an issue with code. We can try writing some test code ourselves but it may not be the same as you have so may not behave the same. Provide us with the most minimal example you can that demonstrates the issue.
 
Show us the code. This is not something that we should need to ask for. The code with the issue is always relevant when asking questions about an issue with code. We can try writing some test code ourselves but it may not be the same as you have so may not behave the same. Provide us with the most minimal example you can that demonstrates the issue.

Don't have WinZip, so the form and code are attached as text files.

When the form is displayed, the treeview has 3 root nodes for tables in a database.

1) Select Rivers and the grid is populated as expected.

2) Select Interstates and the BeforeSelect event is fired.
- The code detects a different node was selected and displays a "Save Updates" message.
- When the message is dismissed, the BeforeSelect event fires for States even though Interstates was selected. It seems someone else is trying to do something "for" me. Or maybe I crashed the event party, and they select the first node.
- I wasn't expecting this, so the code gets in a loop with the message box.
 

Attachments

  • Form1.Designer.vb.txt
    9 KB · Views: 2
  • Form1.vb.txt
    22.1 KB · Views: 3
My form has a SplitContainer with a TreeView in the top panel and a DataGridView in the bottom panel. The user selects a tree item and the item's data is displayed in the grid. The user can update the grid as needed and then click the Apply or Cancel button. All is well if the user clicks Apply or Cancel before selecting another tree item.

If the user is updating the grid and selects another tree item, I need to display a "Save Updates?" message with Yes and No buttons. If the user clicks Yes, I want to save the updates and then display the newly selected item's data.

The BeforeSelect event seemed like the best place to handle this situation, so that's what I did. The event fires as expected, but the message box seems to disrupt the event party. When the message box is dismissed, the BeforeSelect event is fired again and again. I also tried the AfterSelect event, but had the same results. I saw this same issue while googling, but no solution was offered.

I have displayed message boxes in event handlers before and don't remember having a problem. When I looked on the learn.microsoft.com site, their example for BeforeSelect has a message box. I'm guessing their example may not work.

Any ideas would be appreciated.

To approach this effectively, you should use flags to manage the state and prevent the MessageBox from causing the undesirable looping behavior in the BeforeSelect event while still allowing you to prompt the user to save unsaved changes. Remember to replace the placeholder saving and data loading logic with your actual implementation. Hope this example applied to your code helps:
Applied Corrections:
Imports System.Runtime.InteropServices
Imports System.Drawing

Public Class frmMain
    '***********************************************************************************************
    '*** Start code to hide the checkbox on root nodes. Set DrawMode to OwnerDrawAll.
    '*** The treeview will not show in the designer. To edit the tree in the designer, temporarily
    '*** set DrawMode to Normal, apply edits, and set DrawMode back to OwnerDrawAll.
    '***********************************************************************************************
    Private Const TVIF_STATE As Integer = &H8
    Private Const TVIS_STATEIMAGEMASK As Integer = &HF000
    Private Const TV_FIRST As Integer = &H1100
    Private Const TVM_SETITEM As Integer = TV_FIRST + 63

    <StructLayout(LayoutKind.Sequential)>
    Public Structure TVITEM
        Public mask As Integer
        Public hItem As IntPtr
        Public state As Integer
        Public stateMask As Integer
        <MarshalAs(UnmanagedType.LPTStr)> Public lpszText As String
        Public cchTextMax As Integer
        Public iImage As Integer
        Public iSelectedImage As Integer
        Public cChildren As Integer
        Public lParam As IntPtr
    End Structure

    <DllImport("User32.dll", CharSet:=CharSet.Auto)>
    Private Shared Function SendMessage(ByVal hwnd As IntPtr, ByVal msg As Integer, ByVal wParam As IntPtr, ByRef lParam As TVITEM) As Integer
    End Function

    Private Sub HideRootCheckBox(ByVal node As TreeNode)
        Dim tvi As New TVITEM
        tvi.hItem = node.Handle
        tvi.mask = TVIF_STATE
        tvi.stateMask = TVIS_STATEIMAGEMASK
        tvi.state = 0
        SendMessage(treeMain.Handle, TVM_SETITEM, IntPtr.Zero, tvi)
    End Sub

    Private Sub TreeView1_DrawNode(ByVal sender As Object, ByVal e As DrawTreeNodeEventArgs) Handles treeMain.DrawNode
        ' Check if the handle is created before trying to hide the checkbox
        If e.Node.IsHandleCreated AndAlso e.Node.Parent Is Nothing Then
            HideRootCheckBox(e.Node)
        End If

        e.DrawDefault = True
    End Sub
    '***********************************************************************************************
    '*** End code to hide the checkbox on root nodes.
    '***********************************************************************************************

    Private Const COMMANDTITLE = "AAU with a tree"
    Private Const DIFFVAL = "(Different Values)"
    Private Const MSGLASTCHANCE = "You have unapplied updates. Do you want to save them?" ' Modified message

    Private m_bMyForm As Boolean ' Flag used in AfterCheck and AfterCollapse to indicate programmatic changes
    Private m_sActiveFeatureClass As String ' Tracks the currently active root node text (e.g., "States")
    Private m_treeContextNode As TreeNode ' Used for context menu

    ' *** New Flags for handling unsaved changes and BeforeSelect re-entrancy ***
    Private changesMade As Boolean = False
    Private isHandlingBeforeSelect As Boolean = False
    ' *************************************************************************

    Public Sub New()
        ' This call is required by the designer.
        InitializeComponent()

        ' Add any initialization after the InitializeComponent() call.
        m_bMyForm = False
        m_sActiveFeatureClass = ""
        m_treeContextNode = Nothing

        ' Hook up event handler for DataGridView changes
        ' In a real application, you would hook this up to an event
        ' that indicates a true data change, like CellValueChanged or CellValidated
        ' For this example, we'll add a simple way to simulate changes later if needed.
        ' AddHandler dgvMain.CellValueChanged, AddressOf dgvMain_CellValueChanged ' Example
    End Sub

    ' Example handler for DataGridView changes (uncomment and implement based on your grid's data source)
    ' Private Sub dgvMain_CellValueChanged(sender As Object, e As DataGridViewCellEventArgs)
    '     ' Check if the change is in a data row (not header or new row row)
    '     If e.RowIndex >= 0 AndAlso e.ColumnIndex >= 0 Then
    '         changesMade = True
    '     End If
    ' End Sub

    Private Sub frmMain_Activated(sender As Object, e As EventArgs) Handles Me.Activated
        ' Optionally clear selection when form is activated if no node should be selected initially
        ' treeMain.SelectedNode = Nothing
    End Sub

    Private Sub btnReset_Click(sender As Object, e As EventArgs) Handles btnReset.Click
        ' Consider prompting to save changes before resetting
        If changesMade Then
             Dim result As DialogResult = MessageBox.Show("You have unsaved changes. Do you want to save them before resetting?", "Save Changes?", MessageBoxButtons.YesNo, MessageBoxIcon.Warning)
             If result = DialogResult.Yes Then
                 SaveDataGridViewChanges() ' Call your save method
             End If
             ' If No is clicked, changesMade is already True, and ResetForm will set it to False
        End If

        Try
            ResetForm()
        Catch ex As Exception
            ' Handle exception
        Finally
            ' Cleanup or final actions
        End Try
    End Sub

    Private Sub treeMain_AfterCheck(sender As Object, e As TreeViewEventArgs) Handles treeMain.AfterCheck
        ' This event should only fire when a child node is checked/unchecked.
        ' m_bMyForm is used to prevent this handler from firing when checkboxes
        ' are programmatically changed in AfterCollapse.
        If m_bMyForm = False Then
            If e.Node.Parent IsNot Nothing Then ' Make sure it's a child node
                If e.Node.Checked Then
                    ' If a child node is checked, load its data
                    ' Consider if checking a child node should also trigger a save prompt
                    ' if a different child node from the same root had unsaved changes.
                    ' For now, we'll assume unsaved changes are tied to the root node selection.
                    PopGrid(e.Node.Text)
                    m_sActiveFeatureClass = e.Node.Parent.Text.Substring(0, InStr(e.Node.Parent.Text, " (") - 1) ' Update active feature class based on parent
                    changesMade = False ' Assume loading new data clears changes for the previous child data
                Else
                    ' If unchecked, clear the grid if this was the active data
                    ' Consider if unchecking should also prompt for save if there are changes
                    If m_sActiveFeatureClass <> "" AndAlso e.Node.Text = m_sActiveFeatureClass Then ' This logic might need refinement depending on how you identify the active child data
                         dgvMain.RowCount = 0
                         m_sActiveFeatureClass = ""
                         changesMade = False ' Clearing the grid implies discarding changes
                    End If
                End If
            End If
        End If
    End Sub

    Private Sub treeMain_AfterCollapse(sender As Object, e As TreeViewEventArgs) Handles treeMain.AfterCollapse
        ' After a collapse, we don't want checked child nodes visually.
        Dim iNumChecked As Integer = 0

        Try
            ' Set flag to indicate programmatic changes to checkboxes
            m_bMyForm = True

            For Each childNode As TreeNode In e.Node.Nodes
                If childNode.Checked Then
                    childNode.Checked = False
                    iNumChecked = iNumChecked + 1
                End If
            Next

            ' Reset flag
            m_bMyForm = False

            ' Consider if collapsing should also trigger a save prompt if children had unsaved changes.
            ' The current logic just shows a message box if any were checked.
            ' If you want to prompt to save here, you would check 'changesMade' and prompt.
            ' If iNumChecked > 0 AndAlso changesMade Then
            '    Dim result As DialogResult = MessageBox.Show(MSGLASTCHANCE, "Save Changes?", MessageBoxButtons.YesNo, MsgBoxStyle.Exclamation, COMMANDTITLE)
            '    If result = DialogResult.Yes Then
            '        SaveDataGridViewChanges() ' Call your save method
            '    End If
            '    changesMade = False ' Discard or saved
            ' End If

             ' Clear grid and active state if the collapsed node's feature class was active
             Dim collapsedFeatureClass As String = ""
             If e.Node.Text.Contains(" (") Then
                 collapsedFeatureClass = e.Node.Text.Substring(0, InStr(e.Node.Text, " (") - 1)
             End If

             If collapsedFeatureClass <> "" AndAlso collapsedFeatureClass = m_sActiveFeatureClass Then
                 dgvMain.RowCount = 0
                 m_sActiveFeatureClass = ""
                 changesMade = False ' Clearing the grid implies discarding changes
             End If


        Catch ex As Exception
            ' Handle exception
        Finally
            ' Ensure flag is reset even if an error occurs
            m_bMyForm = False
        End Try
    End Sub

    Private Sub treeMain_AfterSelect(sender As Object, e As TreeViewEventArgs) Handles treeMain.AfterSelect
        ' This event fires after a node has been successfully selected (After BeforeSelect).
        ' Load the grid data for the selected node here.
        ' The save prompt logic is handled in BeforeSelect.

        Try
            If e.Node.Parent Is Nothing Then ' Only load for root nodes as per your original logic
                Dim sFeatureClass As String = ""
                If e.Node.Text.Contains(" (") Then
                     sFeatureClass = e.Node.Text.Substring(0, InStr(e.Node.Text, " (") - 1)
                End If

                ' Update the active feature class after a successful selection
                m_sActiveFeatureClass = sFeatureClass

                ' Load the grid data for the newly selected root node
                PopGrid(e.Node.Text)
                ' PopGrid sets changesMade = False internally
            Else
                ' For child nodes, selection is cancelled in BeforeSelect,
                ' and data loading is handled in AfterCheck if they are checked.
                ' So, no action needed here for child nodes if your design keeps them unhighlighted.
            End If

        Catch ex As Exception
            ' Handle exception
        Finally
            ' Cleanup or final actions
        End Try
    End Sub

    Private Sub treeMain_BeforeSelect(sender As Object, e As TreeViewCancelEventArgs) Handles treeMain.BeforeSelect
        ' This event fires before a new node is selected.
        ' This is the place to check for unsaved changes and prompt the user.

        ' Prevent re-entrancy caused by showing the message box
        If isHandlingBeforeSelect Then
            Return
        End If

        ' Set the flag to indicate we are handling the BeforeSelect event
        isHandlingBeforeSelect = True

        Dim treeNode As TreeNode = e.Node ' The node the user is trying to select
        Dim currentNode As TreeNode = treeMain.SelectedNode ' The node that is currently selected

        Try
            ' Check if there are unsaved changes and the user is selecting a *different* root node
            If changesMade AndAlso currentNode IsNot Nothing AndAlso currentNode.Parent Is Nothing AndAlso treeNode.Parent Is Nothing AndAlso treeNode IsNot currentNode Then

                ' Display the save changes message box
                Dim result As DialogResult = MessageBox.Show(MSGLASTCHANCE, "Save Changes?", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Warning)

                Select Case result
                    Case DialogResult.Yes
                        ' User wants to save changes.
                        SaveDataGridViewChanges() ' Call your save method

                        ' Allow the new node selection to proceed.
                        e.Cancel = False
                        ' AfterSelect will handle loading data for the new node and setting changesMade = False.

                    Case DialogResult.No
                        ' User does not want to save changes.
                        ' Discard changes by resetting the flag.
                        changesMade = False

                        ' Allow the new node selection to proceed.
                        e.Cancel = False
                        ' AfterSelect will handle loading data for the new node and setting changesMade = False.

                    Case DialogResult.Cancel
                        ' User cancelled the selection change.
                        ' Prevent the new node from being selected.
                        e.Cancel = True

                        ' Re-select the previously selected node to visually revert the selection.
                        ' Temporarily remove the event handler to prevent this programmatic selection
                        ' from re-triggering BeforeSelect and causing a loop.
                        RemoveHandler treeMain.BeforeSelect, AddressOf treeMain_BeforeSelect

                        ' Restore the original node selection
                        If currentNode IsNot Nothing Then
                            treeMain.SelectedNode = currentNode
                        Else
                             ' If there was no previous selection (e.g., initial load),
                             ' you might want to select a default node or keep none selected.
                             ' For now, we'll just ensure nothing is selected if there was no previous node.
                             ' If treeMain.Nodes.Count > 0 Then
                             '     treeMain.SelectedNode = treeMain.Nodes(0)
                             ' End If
                        End If

                        ' Re-add the event handler
                        AddHandler treeMain.BeforeSelect, AddressOf treeMain_BeforeSelect
                End Select

            ElseIf treeNode.Parent IsNot Nothing Then ' Selected a child node
                ' Your existing logic for child nodes (check and cancel selection)
                e.Node.Checked = True
                e.Cancel = True
                ' AfterCheck will handle loading data for the checked child node and setting changesMade = False.

            ElseIf treeNode Is currentNode Then ' Selected the already selected root node
                ' Cancel the selection if the user clicks the same root node again
                e.Cancel = True

            Else
                ' No unsaved changes, or selecting the first root node.
                ' Allow the selection to proceed.
                e.Cancel = False
                ' AfterSelect will handle loading data for the new node and setting changesMade = False.
            End If

        Catch ex As Exception
            ' Handle exception

        Finally
            ' Ensure the flag is reset when the handler exits, regardless of how it exits.
            isHandlingBeforeSelect = False
        End Try

    End Sub

    Private Sub treeMain_NodeMouseClick(sender As Object, e As TreeNodeMouseClickEventArgs) Handles treeMain.NodeMouseClick
        ' Your existing context menu logic
        Try
            If e.Button = MouseButtons.Right Then
                m_treeContextNode = e.Node
                ' Show your context menu here, e.g.:
                ' Me.contextMenuStrip1.Show(treeMain, e.Location)
            End If
        Catch ex As Exception
            ' Handle exception
        Finally
            ' Cleanup
        End Try
    End Sub

    Private Sub mnuTree1Remove_Click(sender As Object, e As EventArgs) Handles mnuTree1Remove.Click
        ' Your existing node removal logic
        Try
            If m_treeContextNode IsNot Nothing Then
                ' Before removing, check if the node's data is currently loaded and has changes
                Dim featureClassToRemove As String = ""
                 If m_treeContextNode.Parent Is Nothing Then ' Root node
                     If m_treeContextNode.Text.Contains(" (") Then
                         featureClassToRemove = m_treeContextNode.Text.Substring(0, InStr(m_treeContextNode.Text, " (") - 1)
                     End If
                 Else ' Child node
                     featureClassToRemove = m_treeContextNode.Text
                 End If


                If changesMade AndAlso m_sActiveFeatureClass <> "" AndAlso (
                   (m_treeContextNode.Parent Is Nothing AndAlso featureClassToRemove = m_sActiveFeatureClass) OrElse
                   (m_treeContextNode.Parent IsNot Nothing AndAlso m_treeContextNode.Text = m_sActiveFeatureClass)) Then ' This comparison logic for child nodes might need adjustment based on how you set m_sActiveFeatureClass for children
                     Dim result As DialogResult = MessageBox.Show("You have unsaved changes for the item being removed. Do you want to save them?", "Save Changes?", MessageBoxButtons.YesNo, MsgBoxIcon.Warning)
                     If result = DialogResult.Yes Then
                         SaveDataGridViewChanges() ' Call your save method
                     End If
                     ' If No is clicked, changes are discarded with the removal
                 End If


                ' Remove the node
                Dim treeParent As TreeNode = m_treeContextNode.Parent
                m_treeContextNode.Remove()

                ' Update parent node text if it was a child
                If treeParent IsNot Nothing Then
                    If treeParent.Nodes.Count = 0 Then
                        treeParent.Remove()
                    Else
                        Dim sName As String = treeParent.Text
                        Dim iNdx As Integer = InStr(sName, " (")
                        sName = sName.Substring(0, iNdx)
                        sName = sName & " (" & treeParent.Nodes.Count & ")"
                        treeParent.Text = sName
                    End If
                End If

                m_treeContextNode = Nothing
                ' After removal, clear the grid and active state if the removed node was active
                If featureClassToRemove <> "" AndAlso featureClassToRemove = m_sActiveFeatureClass Then
                     dgvMain.RowCount = 0
                     m_sActiveFeatureClass = ""
                     changesMade = False ' Discarding data
                End If

            End If

        Catch ex As Exception
            ' Handle exception
        Finally
            ' Cleanup
        End Try
    End Sub

    Private Sub frmMain_Load(sender As Object, e As EventArgs) Handles Me.Load
        ResetForm()
    End Sub

    ' Removed the separate LastChance() method as its logic is now integrated into BeforeSelect

    Private Sub PopGrid(ByVal sSelectedNodeText As String)
        ' This is a hardcoded function to simulate populating the grid when a node is selected.
        ' In a real application, you would fetch data based on the node and bind it to the DGV.
        Try
            dgvMain.RowCount = 0
            dgvMain.ColumnCount = 2 ' Assuming a simple key/value structure for now

            ' Set column headers
            dgvMain.Columns(0).HeaderText = "Property"
            dgvMain.Columns(1).HeaderText = "Value"
            dgvMain.Columns(0).ReadOnly = True ' Make property column read-only
            dgvMain.Columns(1).ReadOnly = False ' Allow editing in value column

            ' Adjust column widths
            dgvMain.AutoResizeColumns(DataGridViewAutoSizeColumnsMode.AllCells)


            ' --- Simulate loading data based on node text ---
            If sSelectedNodeText.StartsWith("States (") Then
                 dgvMain.RowCount = 5
                 dgvMain(0, 0).Value = "ID" : dgvMain(1, 0).Value = DIFFVAL
                 dgvMain(0, 1).Value = "FIPS" : dgvMain(1, 1).Value = DIFFVAL
                 dgvMain(0, 2).Value = "STATE_NAME" : dgvMain(1, 2).Value = DIFFVAL
                 dgvMain(0, 3).Value = "STAABBRV" : dgvMain(1, 3).Value = DIFFVAL
                 dgvMain(0, 4).Value = "Test1" : dgvMain(1, 4).Value = "123"
            ElseIf sSelectedNodeText.StartsWith("Rivers (") Then
                 dgvMain.RowCount = 3
                 dgvMain(0, 0).Value = "RIVER_NAME" : dgvMain(1, 0).Value = DIFFVAL
                 dgvMain(0, 1).Value = "RIVER_TYPE" : dgvMain(1, 1).Value = "Perennial - Single Line"
                 dgvMain(0, 2).Value = "ID" : dgvMain(1, 2).Value = DIFFVAL
            ElseIf sSelectedNodeText.StartsWith("Interstates (") Then
                 dgvMain.RowCount = 3
                 dgvMain(0, 0).Value = "ID" : dgvMain(1, 0).Value = DIFFVAL
                 dgvMain(0, 1).Value = "ROUTE_NUMBER" : dgvMain(1, 1).Value = DIFFVAL
                 dgvMain(0, 2).Value = "ROUTE_NUMBER2" : dgvMain(1, 2).Value = "<Null>"
            ElseIf sSelectedNodeText = "Alabama" Then
                 dgvMain.RowCount = 5
                 dgvMain(0, 0).Value = "ID" : dgvMain(1, 0).Value = "1"
                 dgvMain(0, 1).Value = "FIPS" : dgvMain(1, 1).Value = "01"
                 dgvMain(0, 2).Value = "STATE_NAME" : dgvMain(1, 2).Value = "Alabama"
                 dgvMain(0, 3).Value = "STAABBRV" : dgvMain(1, 3).Value = "AL"
                 dgvMain(0, 4).Value = "Test1" : dgvMain(1, 4).Value = "123"
            ElseIf sSelectedNodeText = "Georgia" Then
                 dgvMain.RowCount = 5
                 dgvMain(0, 0).Value = "ID" : dgvMain(1, 0).Value = "10"
                 dgvMain(0, 1).Value = "FIPS" : dgvMain(1, 1).Value = "13"
                 dgvMain(0, 2).Value = "STATE_NAME" : dgvMain(1, 2).Value = "Georgia"
                 dgvMain(0, 3).Value = "STAABBRV" : dgvMain(1, 3).Value = "GA"
                 dgvMain(0, 4).Value = "Test1" : dgvMain(1, 4).Value = "123"
            ElseIf sSelectedNodeText = "Tennessee" AndAlso m_sActiveFeatureClass = "States" Then
                 dgvMain.RowCount = 5
                 dgvMain(0, 0).Value = "ID" : dgvMain(1, 0).Value = "41"
                 dgvMain(0, 1).Value = "FIPS" : dgvMain(1, 1).Value = "47"
                 dgvMain(0, 2).Value = "STATE_NAME" : dgvMain(1, 2).Value = "Tennessee"
                 dgvMain(0, 3).Value = "STAABBRV" : dgvMain(1, 3).Value = "TN"
                 dgvMain(0, 4).Value = "Test1" : dgvMain(1, 4).Value = "123"
            ElseIf sSelectedNodeText = "Tennessee" AndAlso m_sActiveFeatureClass = "Rivers" Then
                 dgvMain.RowCount = 3
                 dgvMain(0, 0).Value = "RIVER_NAME" : dgvMain(1, 0).Value = "Tennessee"
                 dgvMain(0, 1).Value = "RIVER_TYPE" : dgvMain(1, 1).Value = "Perennial - Single Line"
                 dgvMain(0, 2).Value = "ID" : dgvMain(1, 2).Value = "50"
            ElseIf sSelectedNodeText = "Tombigbee" Then
                 dgvMain.RowCount = 3
                 dgvMain(0, 0).Value = "RIVER_NAME" : dgvMain(1, 0).Value = "Tombigbee"
                 dgvMain(0, 1).Value = "RIVER_TYPE" : dgvMain(1, 1).Value = "Perennial - Single Line"
                 dgvMain(0, 2).Value = "ID" : dgvMain(1, 2).Value = "90"
            ElseIf sSelectedNodeText = "65" Then
                 dgvMain.RowCount = 3
                 dgvMain(0, 0).Value = "ID" : dgvMain(1, 0).Value = "533"
                 dgvMain(0, 1).Value = "ROUTE_NUMBER" : dgvMain(1, 1).Value = "65"
                 dgvMain(0, 2).Value = "ROUTE_NUMBER2" : dgvGrid(1, 2).Value = "<Null>"
            ElseIf sSelectedNodeText = "565" Then
                 dgvMain.RowCount = 3
                 dgvMain(0, 0).Value = "ID" : dgvMain(1, 0).Value = "532"
                 dgvMain(0, 1).Value = "ROUTE_NUMBER" : dgvMain(1, 1).Value = "565"
                 dgvMain(0, 2).Value = "ROUTE_NUMBER2" : dgvMain(1, 2).Value = "<Null>"
            ElseIf sSelectedNodeText = "20" Then
                 dgvMain.RowCount = 3
                 dgvMain(0, 0).Value = "ID" : dgvMain(1, 0).Value = "474"
                 dgvMain(0, 1).Value = "ROUTE_NUMBER" : dgvMain(1, 1).Value = "20"
                 dgvMain(0, 2).Value = "ROUTE_NUMBER2" : dgvMain(1, 2).Value = "<Null>"
            End If

            ' Apply specific styles after populating the data
            For i As Integer = 0 To dgvMain.RowCount - 1
                If dgvMain(0, i).Value IsNot Nothing AndAlso dgvMain(0, i).Value.ToString().EndsWith("ID") Then
                    dgvMain(0, i).Style.Font = New Font(dgvMain.DefaultCellStyle.Font, FontStyle.Bold Or FontStyle.Underline)
                    dgvMain(0, i).Style.ForeColor = Color.Red
                    ' Make the value cell for ID read-only and gray
                    If dgvMain.ColumnCount > 1 Then
                         dgvMain(1, i).Style.BackColor = Color.LightGray
                         dgvMain(1, i).ReadOnly = True
                    End If
                ElseIf dgvMain(0, i).Value IsNot Nothing AndAlso (dgvMain(0, i).Value.ToString().EndsWith("NAME") OrElse dgvMain(0, i).Value.ToString().EndsWith("NUMBER")) Then
                    dgvMain(0, i).Style.Font = New Font(dgvMain.DefaultCellStyle.Font, FontStyle.Bold)
                    dgvMain(0, i).Style.ForeColor = Color.Red
                End If

                ' Make cells with DIFFVAL read-only
                If dgvMain.ColumnCount > 1 AndAlso dgvMain(1, i).Value IsNot Nothing AndAlso dgvMain(1, i).Value.ToString() = DIFFVAL Then
                     dgvMain(1, i).Style.BackColor = Color.LightGray
                     dgvMain(1, i).ReadOnly = True
                End If
            Next


            dgvMain.ClearSelection()

            ' Reset the changesMade flag after loading new data
            changesMade = False

        Catch ex As Exception
            ' Handle exception
        Finally
            ' Cleanup
        End Try

    End Sub

    Private Sub ResetForm()
        'This function resets the form to its initial state.
        Try
            treeMain.Nodes.Clear()
            Dim treeNode As New TreeNode("States (3)")
            treeMain.Nodes.Add(treeNode)
            treeNode.Nodes.Add("Alabama")
            treeNode.Nodes.Add("Tennessee")
            treeNode.Nodes.Add("Georgia")

            treeNode = New TreeNode("Rivers (2)")
            treeMain.Nodes.Add(treeNode)
            treeNode.Nodes.Add("Tennessee")
            treeNode.Nodes.Add("Tombigbee")

            treeNode = New TreeNode("Interstates (3)")
            treeMain.Nodes.Add(treeNode)
            treeNode.Nodes.Add("65")
            treeNode.Nodes.Add("565")
            treeNode.Nodes.Add("20")

            ' treeMain.Sort() 'maybe later

            ' Clear the grid and reset state
            dgvMain.RowCount = 0
            m_sActiveFeatureClass = ""
            changesMade = False

            ' Ensure no node is selected initially
            treeMain.SelectedNode = Nothing

        Catch ex As Exception
            ' Handle exception
        Finally
            ' Cleanup
        End Try

    End Sub

    ' Add this placeholder method for your saving logic
    Private Sub SaveDataGridViewChanges()
        ' *** IMPORTANT: Implement your actual data saving logic here. ***
        ' This method should take the data from the DataGridView and persist it
        ' to your underlying data source (database, file, etc.).

        MessageBox.Show("Saving changes...", "Save", MessageBoxButtons.OK, MessageBoxIcon.Information) ' Placeholder message

        ' After successfully saving, reset the changesMade flag
        changesMade = False
    End Sub

    ' You'll need to add handlers to set changesMade = True when the user
    ' edits the DataGridView. A common place is the CellValueChanged event
    ' if you're manually managing data, or if using data binding, events
    ' on the BindingSource or DataTable.
    ' Example (uncomment and implement):
    ' Private Sub dgvMain_CellValueChanged(sender As Object, e As DataGridViewCellEventArgs) Handles dgvMain.CellValueChanged
    '     If e.RowIndex >= 0 AndAlso e.ColumnIndex >= 0 Then ' Ensure it's a data cell
    '         changesMade = True
    '     End If
    ' End Sub

End Class
I've integrated the logic to handle unsaved changes and prevent the MessageBox from causing the BeforeSelect event loop into your existing code.

Next, add the changesMade and isHandlingBeforeSelect flags, modify the BeforeSelect and AfterSelect handlers, and add a placeholder for your save logic.

Changes and Explanations:
  1. changesMade Flag: A form-level boolean changesMade is introduced to track if any edits have been made in the dgvMain since the last load or save. You'll need to implement logic (likely in a DataGridView event like CellValueChanged or CellValidated) to set this flag to True when the user modifies data. I've added a commented-out example handler for CellValueChanged.
  2. isHandlingBeforeSelect Flag: This new form-level boolean flag is specifically used to prevent the BeforeSelect event handler from re-entering its logic while a MessageBox is displayed.
  3. treeMain_BeforeSelect Modifications:
    • The very first check now uses isHandlingBeforeSelect. If it's True, the handler immediately returns, preventing the loop.
    • The flag is set to True at the beginning of the handler's main logic.
    • The check for unsaved changes (changesMade) is performed when the user is selecting a different root node (changesMade AndAlso currentNode IsNot Nothing AndAlso currentNode.Parent Is Nothing AndAlso treeNode.Parent Is Nothing AndAlso treeNode IsNot currentNode).
    • If changes exist, the MessageBox.Show is called with Yes, No, and Cancel options.
    • The Select Case block handles the user's choice:
      • Yes: Calls SaveDataGridViewChanges (your implementation), sets changesMade = False, and allows the selection (e.Cancel = False).
      • No: Sets changesMade = False to discard changes, and allows the selection (e.Cancel = False).
      • Cancel: Sets e.Cancel = True to prevent the new selection. It then temporarily removes the BeforeSelect event handler, sets treeMain.SelectedNode back to the currentNode (the node that was selected before the attempted change), and re-adds the handler. This programmatic selection won't trigger the BeforeSelect event because the handler was temporarily removed.
    • The isHandlingBeforeSelect flag is reset in a Finally block to ensure it's always reset, even if an error occurs.
  4. treeMain_AfterSelect Modifications: This event is now solely responsible for loading the data into the DataGridView after a selection has been successfully made (i.e., BeforeSelect did not cancel it). The save prompt logic has been moved out.
  5. PopGrid Modification: changesMade is set to False at the end of PopGrid because loading new data means there are no unsaved changes for that data yet.
  6. SaveDataGridViewChanges Method: A placeholder method is added. You must replace the MessageBox inside this method with your actual code to save the data from the DataGridView. Make sure your save logic also sets changesMade = False upon successful completion.
  7. ResetForm and btnReset_Click: Added a check for changesMade in btnReset_Click to optionally prompt the user to save before resetting the form. ResetForm also sets changesMade = False.
  8. AfterCollapse: Kept the existing logic but added comments about potentially adding a save prompt here if collapsing should also consider unsaved changes from checked child nodes.
  9. mnuTree1Remove_Click: Added a check for changesMade before removing a node, in case the data associated with that node was currently loaded and modified.
Now, when the user clicks a different root node while changesMade is True, the BeforeSelect event will fire, display the message box without looping, and handle the user's choice before proceeding (or not proceeding) with the selection. The AfterSelect event will then load the data for the newly selected node if the selection was allowed.
 
Last edited:
To approach this effectively, you should use flags to manage the state and prevent the MessageBox from causing the undesirable looping behavior in the BeforeSelect event while still allowing you to prompt the user to save unsaved changes. Remember to replace the placeholder saving and data loading logic with your actual implementation. Hope this example applied to your code helps:
Applied Corrections:
Imports System.Runtime.InteropServices
Imports System.Drawing

Public Class frmMain
    '***********************************************************************************************
    '*** Start code to hide the checkbox on root nodes. Set DrawMode to OwnerDrawAll.
    '*** The treeview will not show in the designer. To edit the tree in the designer, temporarily
    '*** set DrawMode to Normal, apply edits, and set DrawMode back to OwnerDrawAll.
    '***********************************************************************************************
    Private Const TVIF_STATE As Integer = &H8
    Private Const TVIS_STATEIMAGEMASK As Integer = &HF000
    Private Const TV_FIRST As Integer = &H1100
    Private Const TVM_SETITEM As Integer = TV_FIRST + 63

    <StructLayout(LayoutKind.Sequential)>
    Public Structure TVITEM
        Public mask As Integer
        Public hItem As IntPtr
        Public state As Integer
        Public stateMask As Integer
        <MarshalAs(UnmanagedType.LPTStr)> Public lpszText As String
        Public cchTextMax As Integer
        Public iImage As Integer
        Public iSelectedImage As Integer
        Public cChildren As Integer
        Public lParam As IntPtr
    End Structure

    <DllImport("User32.dll", CharSet:=CharSet.Auto)>
    Private Shared Function SendMessage(ByVal hwnd As IntPtr, ByVal msg As Integer, ByVal wParam As IntPtr, ByRef lParam As TVITEM) As Integer
    End Function

    Private Sub HideRootCheckBox(ByVal node As TreeNode)
        Dim tvi As New TVITEM
        tvi.hItem = node.Handle
        tvi.mask = TVIF_STATE
        tvi.stateMask = TVIS_STATEIMAGEMASK
        tvi.state = 0
        SendMessage(treeMain.Handle, TVM_SETITEM, IntPtr.Zero, tvi)
    End Sub

    Private Sub TreeView1_DrawNode(ByVal sender As Object, ByVal e As DrawTreeNodeEventArgs) Handles treeMain.DrawNode
        ' Check if the handle is created before trying to hide the checkbox
        If e.Node.IsHandleCreated AndAlso e.Node.Parent Is Nothing Then
            HideRootCheckBox(e.Node)
        End If

        e.DrawDefault = True
    End Sub
    '***********************************************************************************************
    '*** End code to hide the checkbox on root nodes.
    '***********************************************************************************************

    Private Const COMMANDTITLE = "AAU with a tree"
    Private Const DIFFVAL = "(Different Values)"
    Private Const MSGLASTCHANCE = "You have unapplied updates. Do you want to save them?" ' Modified message

    Private m_bMyForm As Boolean ' Flag used in AfterCheck and AfterCollapse to indicate programmatic changes
    Private m_sActiveFeatureClass As String ' Tracks the currently active root node text (e.g., "States")
    Private m_treeContextNode As TreeNode ' Used for context menu

    ' *** New Flags for handling unsaved changes and BeforeSelect re-entrancy ***
    Private changesMade As Boolean = False
    Private isHandlingBeforeSelect As Boolean = False
    ' *************************************************************************

    Public Sub New()
        ' This call is required by the designer.
        InitializeComponent()

        ' Add any initialization after the InitializeComponent() call.
        m_bMyForm = False
        m_sActiveFeatureClass = ""
        m_treeContextNode = Nothing

        ' Hook up event handler for DataGridView changes
        ' In a real application, you would hook this up to an event
        ' that indicates a true data change, like CellValueChanged or CellValidated
        ' For this example, we'll add a simple way to simulate changes later if needed.
        ' AddHandler dgvMain.CellValueChanged, AddressOf dgvMain_CellValueChanged ' Example
    End Sub

    ' Example handler for DataGridView changes (uncomment and implement based on your grid's data source)
    ' Private Sub dgvMain_CellValueChanged(sender As Object, e As DataGridViewCellEventArgs)
    '     ' Check if the change is in a data row (not header or new row row)
    '     If e.RowIndex >= 0 AndAlso e.ColumnIndex >= 0 Then
    '         changesMade = True
    '     End If
    ' End Sub

    Private Sub frmMain_Activated(sender As Object, e As EventArgs) Handles Me.Activated
        ' Optionally clear selection when form is activated if no node should be selected initially
        ' treeMain.SelectedNode = Nothing
    End Sub

    Private Sub btnReset_Click(sender As Object, e As EventArgs) Handles btnReset.Click
        ' Consider prompting to save changes before resetting
        If changesMade Then
             Dim result As DialogResult = MessageBox.Show("You have unsaved changes. Do you want to save them before resetting?", "Save Changes?", MessageBoxButtons.YesNo, MessageBoxIcon.Warning)
             If result = DialogResult.Yes Then
                 SaveDataGridViewChanges() ' Call your save method
             End If
             ' If No is clicked, changesMade is already True, and ResetForm will set it to False
        End If

        Try
            ResetForm()
        Catch ex As Exception
            ' Handle exception
        Finally
            ' Cleanup or final actions
        End Try
    End Sub

    Private Sub treeMain_AfterCheck(sender As Object, e As TreeViewEventArgs) Handles treeMain.AfterCheck
        ' This event should only fire when a child node is checked/unchecked.
        ' m_bMyForm is used to prevent this handler from firing when checkboxes
        ' are programmatically changed in AfterCollapse.
        If m_bMyForm = False Then
            If e.Node.Parent IsNot Nothing Then ' Make sure it's a child node
                If e.Node.Checked Then
                    ' If a child node is checked, load its data
                    ' Consider if checking a child node should also trigger a save prompt
                    ' if a different child node from the same root had unsaved changes.
                    ' For now, we'll assume unsaved changes are tied to the root node selection.
                    PopGrid(e.Node.Text)
                    m_sActiveFeatureClass = e.Node.Parent.Text.Substring(0, InStr(e.Node.Parent.Text, " (") - 1) ' Update active feature class based on parent
                    changesMade = False ' Assume loading new data clears changes for the previous child data
                Else
                    ' If unchecked, clear the grid if this was the active data
                    ' Consider if unchecking should also prompt for save if there are changes
                    If m_sActiveFeatureClass <> "" AndAlso e.Node.Text = m_sActiveFeatureClass Then ' This logic might need refinement depending on how you identify the active child data
                         dgvMain.RowCount = 0
                         m_sActiveFeatureClass = ""
                         changesMade = False ' Clearing the grid implies discarding changes
                    End If
                End If
            End If
        End If
    End Sub

    Private Sub treeMain_AfterCollapse(sender As Object, e As TreeViewEventArgs) Handles treeMain.AfterCollapse
        ' After a collapse, we don't want checked child nodes visually.
        Dim iNumChecked As Integer = 0

        Try
            ' Set flag to indicate programmatic changes to checkboxes
            m_bMyForm = True

            For Each childNode As TreeNode In e.Node.Nodes
                If childNode.Checked Then
                    childNode.Checked = False
                    iNumChecked = iNumChecked + 1
                End If
            Next

            ' Reset flag
            m_bMyForm = False

            ' Consider if collapsing should also trigger a save prompt if children had unsaved changes.
            ' The current logic just shows a message box if any were checked.
            ' If you want to prompt to save here, you would check 'changesMade' and prompt.
            ' If iNumChecked > 0 AndAlso changesMade Then
            '    Dim result As DialogResult = MessageBox.Show(MSGLASTCHANCE, "Save Changes?", MessageBoxButtons.YesNo, MsgBoxStyle.Exclamation, COMMANDTITLE)
            '    If result = DialogResult.Yes Then
            '        SaveDataGridViewChanges() ' Call your save method
            '    End If
            '    changesMade = False ' Discard or saved
            ' End If

             ' Clear grid and active state if the collapsed node's feature class was active
             Dim collapsedFeatureClass As String = ""
             If e.Node.Text.Contains(" (") Then
                 collapsedFeatureClass = e.Node.Text.Substring(0, InStr(e.Node.Text, " (") - 1)
             End If

             If collapsedFeatureClass <> "" AndAlso collapsedFeatureClass = m_sActiveFeatureClass Then
                 dgvMain.RowCount = 0
                 m_sActiveFeatureClass = ""
                 changesMade = False ' Clearing the grid implies discarding changes
             End If


        Catch ex As Exception
            ' Handle exception
        Finally
            ' Ensure flag is reset even if an error occurs
            m_bMyForm = False
        End Try
    End Sub

    Private Sub treeMain_AfterSelect(sender As Object, e As TreeViewEventArgs) Handles treeMain.AfterSelect
        ' This event fires after a node has been successfully selected (After BeforeSelect).
        ' Load the grid data for the selected node here.
        ' The save prompt logic is handled in BeforeSelect.

        Try
            If e.Node.Parent Is Nothing Then ' Only load for root nodes as per your original logic
                Dim sFeatureClass As String = ""
                If e.Node.Text.Contains(" (") Then
                     sFeatureClass = e.Node.Text.Substring(0, InStr(e.Node.Text, " (") - 1)
                End If

                ' Update the active feature class after a successful selection
                m_sActiveFeatureClass = sFeatureClass

                ' Load the grid data for the newly selected root node
                PopGrid(e.Node.Text)
                ' PopGrid sets changesMade = False internally
            Else
                ' For child nodes, selection is cancelled in BeforeSelect,
                ' and data loading is handled in AfterCheck if they are checked.
                ' So, no action needed here for child nodes if your design keeps them unhighlighted.
            End If

        Catch ex As Exception
            ' Handle exception
        Finally
            ' Cleanup or final actions
        End Try
    End Sub

    Private Sub treeMain_BeforeSelect(sender As Object, e As TreeViewCancelEventArgs) Handles treeMain.BeforeSelect
        ' This event fires before a new node is selected.
        ' This is the place to check for unsaved changes and prompt the user.

        ' Prevent re-entrancy caused by showing the message box
        If isHandlingBeforeSelect Then
            Return
        End If

        ' Set the flag to indicate we are handling the BeforeSelect event
        isHandlingBeforeSelect = True

        Dim treeNode As TreeNode = e.Node ' The node the user is trying to select
        Dim currentNode As TreeNode = treeMain.SelectedNode ' The node that is currently selected

        Try
            ' Check if there are unsaved changes and the user is selecting a *different* root node
            If changesMade AndAlso currentNode IsNot Nothing AndAlso currentNode.Parent Is Nothing AndAlso treeNode.Parent Is Nothing AndAlso treeNode IsNot currentNode Then

                ' Display the save changes message box
                Dim result As DialogResult = MessageBox.Show(MSGLASTCHANCE, "Save Changes?", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Warning)

                Select Case result
                    Case DialogResult.Yes
                        ' User wants to save changes.
                        SaveDataGridViewChanges() ' Call your save method

                        ' Allow the new node selection to proceed.
                        e.Cancel = False
                        ' AfterSelect will handle loading data for the new node and setting changesMade = False.

                    Case DialogResult.No
                        ' User does not want to save changes.
                        ' Discard changes by resetting the flag.
                        changesMade = False

                        ' Allow the new node selection to proceed.
                        e.Cancel = False
                        ' AfterSelect will handle loading data for the new node and setting changesMade = False.

                    Case DialogResult.Cancel
                        ' User cancelled the selection change.
                        ' Prevent the new node from being selected.
                        e.Cancel = True

                        ' Re-select the previously selected node to visually revert the selection.
                        ' Temporarily remove the event handler to prevent this programmatic selection
                        ' from re-triggering BeforeSelect and causing a loop.
                        RemoveHandler treeMain.BeforeSelect, AddressOf treeMain_BeforeSelect

                        ' Restore the original node selection
                        If currentNode IsNot Nothing Then
                            treeMain.SelectedNode = currentNode
                        Else
                             ' If there was no previous selection (e.g., initial load),
                             ' you might want to select a default node or keep none selected.
                             ' For now, we'll just ensure nothing is selected if there was no previous node.
                             ' If treeMain.Nodes.Count > 0 Then
                             '     treeMain.SelectedNode = treeMain.Nodes(0)
                             ' End If
                        End If

                        ' Re-add the event handler
                        AddHandler treeMain.BeforeSelect, AddressOf treeMain_BeforeSelect
                End Select

            ElseIf treeNode.Parent IsNot Nothing Then ' Selected a child node
                ' Your existing logic for child nodes (check and cancel selection)
                e.Node.Checked = True
                e.Cancel = True
                ' AfterCheck will handle loading data for the checked child node and setting changesMade = False.

            ElseIf treeNode Is currentNode Then ' Selected the already selected root node
                ' Cancel the selection if the user clicks the same root node again
                e.Cancel = True

            Else
                ' No unsaved changes, or selecting the first root node.
                ' Allow the selection to proceed.
                e.Cancel = False
                ' AfterSelect will handle loading data for the new node and setting changesMade = False.
            End If

        Catch ex As Exception
            ' Handle exception

        Finally
            ' Ensure the flag is reset when the handler exits, regardless of how it exits.
            isHandlingBeforeSelect = False
        End Try

    End Sub

    Private Sub treeMain_NodeMouseClick(sender As Object, e As TreeNodeMouseClickEventArgs) Handles treeMain.NodeMouseClick
        ' Your existing context menu logic
        Try
            If e.Button = MouseButtons.Right Then
                m_treeContextNode = e.Node
                ' Show your context menu here, e.g.:
                ' Me.contextMenuStrip1.Show(treeMain, e.Location)
            End If
        Catch ex As Exception
            ' Handle exception
        Finally
            ' Cleanup
        End Try
    End Sub

    Private Sub mnuTree1Remove_Click(sender As Object, e As EventArgs) Handles mnuTree1Remove.Click
        ' Your existing node removal logic
        Try
            If m_treeContextNode IsNot Nothing Then
                ' Before removing, check if the node's data is currently loaded and has changes
                Dim featureClassToRemove As String = ""
                 If m_treeContextNode.Parent Is Nothing Then ' Root node
                     If m_treeContextNode.Text.Contains(" (") Then
                         featureClassToRemove = m_treeContextNode.Text.Substring(0, InStr(m_treeContextNode.Text, " (") - 1)
                     End If
                 Else ' Child node
                     featureClassToRemove = m_treeContextNode.Text
                 End If


                If changesMade AndAlso m_sActiveFeatureClass <> "" AndAlso (
                   (m_treeContextNode.Parent Is Nothing AndAlso featureClassToRemove = m_sActiveFeatureClass) OrElse
                   (m_treeContextNode.Parent IsNot Nothing AndAlso m_treeContextNode.Text = m_sActiveFeatureClass)) Then ' This comparison logic for child nodes might need adjustment based on how you set m_sActiveFeatureClass for children
                     Dim result As DialogResult = MessageBox.Show("You have unsaved changes for the item being removed. Do you want to save them?", "Save Changes?", MessageBoxButtons.YesNo, MsgBoxIcon.Warning)
                     If result = DialogResult.Yes Then
                         SaveDataGridViewChanges() ' Call your save method
                     End If
                     ' If No is clicked, changes are discarded with the removal
                 End If


                ' Remove the node
                Dim treeParent As TreeNode = m_treeContextNode.Parent
                m_treeContextNode.Remove()

                ' Update parent node text if it was a child
                If treeParent IsNot Nothing Then
                    If treeParent.Nodes.Count = 0 Then
                        treeParent.Remove()
                    Else
                        Dim sName As String = treeParent.Text
                        Dim iNdx As Integer = InStr(sName, " (")
                        sName = sName.Substring(0, iNdx)
                        sName = sName & " (" & treeParent.Nodes.Count & ")"
                        treeParent.Text = sName
                    End If
                End If

                m_treeContextNode = Nothing
                ' After removal, clear the grid and active state if the removed node was active
                If featureClassToRemove <> "" AndAlso featureClassToRemove = m_sActiveFeatureClass Then
                     dgvMain.RowCount = 0
                     m_sActiveFeatureClass = ""
                     changesMade = False ' Discarding data
                End If

            End If

        Catch ex As Exception
            ' Handle exception
        Finally
            ' Cleanup
        End Try
    End Sub

    Private Sub frmMain_Load(sender As Object, e As EventArgs) Handles Me.Load
        ResetForm()
    End Sub

    ' Removed the separate LastChance() method as its logic is now integrated into BeforeSelect

    Private Sub PopGrid(ByVal sSelectedNodeText As String)
        ' This is a hardcoded function to simulate populating the grid when a node is selected.
        ' In a real application, you would fetch data based on the node and bind it to the DGV.
        Try
            dgvMain.RowCount = 0
            dgvMain.ColumnCount = 2 ' Assuming a simple key/value structure for now

            ' Set column headers
            dgvMain.Columns(0).HeaderText = "Property"
            dgvMain.Columns(1).HeaderText = "Value"
            dgvMain.Columns(0).ReadOnly = True ' Make property column read-only
            dgvMain.Columns(1).ReadOnly = False ' Allow editing in value column

            ' Adjust column widths
            dgvMain.AutoResizeColumns(DataGridViewAutoSizeColumnsMode.AllCells)


            ' --- Simulate loading data based on node text ---
            If sSelectedNodeText.StartsWith("States (") Then
                 dgvMain.RowCount = 5
                 dgvMain(0, 0).Value = "ID" : dgvMain(1, 0).Value = DIFFVAL
                 dgvMain(0, 1).Value = "FIPS" : dgvMain(1, 1).Value = DIFFVAL
                 dgvMain(0, 2).Value = "STATE_NAME" : dgvMain(1, 2).Value = DIFFVAL
                 dgvMain(0, 3).Value = "STAABBRV" : dgvMain(1, 3).Value = DIFFVAL
                 dgvMain(0, 4).Value = "Test1" : dgvMain(1, 4).Value = "123"
            ElseIf sSelectedNodeText.StartsWith("Rivers (") Then
                 dgvMain.RowCount = 3
                 dgvMain(0, 0).Value = "RIVER_NAME" : dgvMain(1, 0).Value = DIFFVAL
                 dgvMain(0, 1).Value = "RIVER_TYPE" : dgvMain(1, 1).Value = "Perennial - Single Line"
                 dgvMain(0, 2).Value = "ID" : dgvMain(1, 2).Value = DIFFVAL
            ElseIf sSelectedNodeText.StartsWith("Interstates (") Then
                 dgvMain.RowCount = 3
                 dgvMain(0, 0).Value = "ID" : dgvMain(1, 0).Value = DIFFVAL
                 dgvMain(0, 1).Value = "ROUTE_NUMBER" : dgvMain(1, 1).Value = DIFFVAL
                 dgvMain(0, 2).Value = "ROUTE_NUMBER2" : dgvMain(1, 2).Value = "<Null>"
            ElseIf sSelectedNodeText = "Alabama" Then
                 dgvMain.RowCount = 5
                 dgvMain(0, 0).Value = "ID" : dgvMain(1, 0).Value = "1"
                 dgvMain(0, 1).Value = "FIPS" : dgvMain(1, 1).Value = "01"
                 dgvMain(0, 2).Value = "STATE_NAME" : dgvMain(1, 2).Value = "Alabama"
                 dgvMain(0, 3).Value = "STAABBRV" : dgvMain(1, 3).Value = "AL"
                 dgvMain(0, 4).Value = "Test1" : dgvMain(1, 4).Value = "123"
            ElseIf sSelectedNodeText = "Georgia" Then
                 dgvMain.RowCount = 5
                 dgvMain(0, 0).Value = "ID" : dgvMain(1, 0).Value = "10"
                 dgvMain(0, 1).Value = "FIPS" : dgvMain(1, 1).Value = "13"
                 dgvMain(0, 2).Value = "STATE_NAME" : dgvMain(1, 2).Value = "Georgia"
                 dgvMain(0, 3).Value = "STAABBRV" : dgvMain(1, 3).Value = "GA"
                 dgvMain(0, 4).Value = "Test1" : dgvMain(1, 4).Value = "123"
            ElseIf sSelectedNodeText = "Tennessee" AndAlso m_sActiveFeatureClass = "States" Then
                 dgvMain.RowCount = 5
                 dgvMain(0, 0).Value = "ID" : dgvMain(1, 0).Value = "41"
                 dgvMain(0, 1).Value = "FIPS" : dgvMain(1, 1).Value = "47"
                 dgvMain(0, 2).Value = "STATE_NAME" : dgvMain(1, 2).Value = "Tennessee"
                 dgvMain(0, 3).Value = "STAABBRV" : dgvMain(1, 3).Value = "TN"
                 dgvMain(0, 4).Value = "Test1" : dgvMain(1, 4).Value = "123"
            ElseIf sSelectedNodeText = "Tennessee" AndAlso m_sActiveFeatureClass = "Rivers" Then
                 dgvMain.RowCount = 3
                 dgvMain(0, 0).Value = "RIVER_NAME" : dgvMain(1, 0).Value = "Tennessee"
                 dgvMain(0, 1).Value = "RIVER_TYPE" : dgvMain(1, 1).Value = "Perennial - Single Line"
                 dgvMain(0, 2).Value = "ID" : dgvMain(1, 2).Value = "50"
            ElseIf sSelectedNodeText = "Tombigbee" Then
                 dgvMain.RowCount = 3
                 dgvMain(0, 0).Value = "RIVER_NAME" : dgvMain(1, 0).Value = "Tombigbee"
                 dgvMain(0, 1).Value = "RIVER_TYPE" : dgvMain(1, 1).Value = "Perennial - Single Line"
                 dgvMain(0, 2).Value = "ID" : dgvMain(1, 2).Value = "90"
            ElseIf sSelectedNodeText = "65" Then
                 dgvMain.RowCount = 3
                 dgvMain(0, 0).Value = "ID" : dgvMain(1, 0).Value = "533"
                 dgvMain(0, 1).Value = "ROUTE_NUMBER" : dgvMain(1, 1).Value = "65"
                 dgvMain(0, 2).Value = "ROUTE_NUMBER2" : dgvGrid(1, 2).Value = "<Null>"
            ElseIf sSelectedNodeText = "565" Then
                 dgvMain.RowCount = 3
                 dgvMain(0, 0).Value = "ID" : dgvMain(1, 0).Value = "532"
                 dgvMain(0, 1).Value = "ROUTE_NUMBER" : dgvMain(1, 1).Value = "565"
                 dgvMain(0, 2).Value = "ROUTE_NUMBER2" : dgvMain(1, 2).Value = "<Null>"
            ElseIf sSelectedNodeText = "20" Then
                 dgvMain.RowCount = 3
                 dgvMain(0, 0).Value = "ID" : dgvMain(1, 0).Value = "474"
                 dgvMain(0, 1).Value = "ROUTE_NUMBER" : dgvMain(1, 1).Value = "20"
                 dgvMain(0, 2).Value = "ROUTE_NUMBER2" : dgvMain(1, 2).Value = "<Null>"
            End If

            ' Apply specific styles after populating the data
            For i As Integer = 0 To dgvMain.RowCount - 1
                If dgvMain(0, i).Value IsNot Nothing AndAlso dgvMain(0, i).Value.ToString().EndsWith("ID") Then
                    dgvMain(0, i).Style.Font = New Font(dgvMain.DefaultCellStyle.Font, FontStyle.Bold Or FontStyle.Underline)
                    dgvMain(0, i).Style.ForeColor = Color.Red
                    ' Make the value cell for ID read-only and gray
                    If dgvMain.ColumnCount > 1 Then
                         dgvMain(1, i).Style.BackColor = Color.LightGray
                         dgvMain(1, i).ReadOnly = True
                    End If
                ElseIf dgvMain(0, i).Value IsNot Nothing AndAlso (dgvMain(0, i).Value.ToString().EndsWith("NAME") OrElse dgvMain(0, i).Value.ToString().EndsWith("NUMBER")) Then
                    dgvMain(0, i).Style.Font = New Font(dgvMain.DefaultCellStyle.Font, FontStyle.Bold)
                    dgvMain(0, i).Style.ForeColor = Color.Red
                End If

                ' Make cells with DIFFVAL read-only
                If dgvMain.ColumnCount > 1 AndAlso dgvMain(1, i).Value IsNot Nothing AndAlso dgvMain(1, i).Value.ToString() = DIFFVAL Then
                     dgvMain(1, i).Style.BackColor = Color.LightGray
                     dgvMain(1, i).ReadOnly = True
                End If
            Next


            dgvMain.ClearSelection()

            ' Reset the changesMade flag after loading new data
            changesMade = False

        Catch ex As Exception
            ' Handle exception
        Finally
            ' Cleanup
        End Try

    End Sub

    Private Sub ResetForm()
        'This function resets the form to its initial state.
        Try
            treeMain.Nodes.Clear()
            Dim treeNode As New TreeNode("States (3)")
            treeMain.Nodes.Add(treeNode)
            treeNode.Nodes.Add("Alabama")
            treeNode.Nodes.Add("Tennessee")
            treeNode.Nodes.Add("Georgia")

            treeNode = New TreeNode("Rivers (2)")
            treeMain.Nodes.Add(treeNode)
            treeNode.Nodes.Add("Tennessee")
            treeNode.Nodes.Add("Tombigbee")

            treeNode = New TreeNode("Interstates (3)")
            treeMain.Nodes.Add(treeNode)
            treeNode.Nodes.Add("65")
            treeNode.Nodes.Add("565")
            treeNode.Nodes.Add("20")

            ' treeMain.Sort() 'maybe later

            ' Clear the grid and reset state
            dgvMain.RowCount = 0
            m_sActiveFeatureClass = ""
            changesMade = False

            ' Ensure no node is selected initially
            treeMain.SelectedNode = Nothing

        Catch ex As Exception
            ' Handle exception
        Finally
            ' Cleanup
        End Try

    End Sub

    ' Add this placeholder method for your saving logic
    Private Sub SaveDataGridViewChanges()
        ' *** IMPORTANT: Implement your actual data saving logic here. ***
        ' This method should take the data from the DataGridView and persist it
        ' to your underlying data source (database, file, etc.).

        MessageBox.Show("Saving changes...", "Save", MessageBoxButtons.OK, MessageBoxIcon.Information) ' Placeholder message

        ' After successfully saving, reset the changesMade flag
        changesMade = False
    End Sub

    ' You'll need to add handlers to set changesMade = True when the user
    ' edits the DataGridView. A common place is the CellValueChanged event
    ' if you're manually managing data, or if using data binding, events
    ' on the BindingSource or DataTable.
    ' Example (uncomment and implement):
    ' Private Sub dgvMain_CellValueChanged(sender As Object, e As DataGridViewCellEventArgs) Handles dgvMain.CellValueChanged
    '     If e.RowIndex >= 0 AndAlso e.ColumnIndex >= 0 Then ' Ensure it's a data cell
    '         changesMade = True
    '     End If
    ' End Sub

End Class
I've integrated the logic to handle unsaved changes and prevent the MessageBox from causing the BeforeSelect event loop into your existing code.

Next, add the changesMade and isHandlingBeforeSelect flags, modify the BeforeSelect and AfterSelect handlers, and add a placeholder for your save logic.

Changes and Explanations:
  1. changesMade Flag: A form-level boolean changesMade is introduced to track if any edits have been made in the dgvMain since the last load or save. You'll need to implement logic (likely in a DataGridView event like CellValueChanged or CellValidated) to set this flag to True when the user modifies data. I've added a commented-out example handler for CellValueChanged.
  2. isHandlingBeforeSelect Flag: This new form-level boolean flag is specifically used to prevent the BeforeSelect event handler from re-entering its logic while a MessageBox is displayed.
  3. treeMain_BeforeSelect Modifications:
    • The very first check now uses isHandlingBeforeSelect. If it's True, the handler immediately returns, preventing the loop.
    • The flag is set to True at the beginning of the handler's main logic.
    • The check for unsaved changes (changesMade) is performed when the user is selecting a different root node (changesMade AndAlso currentNode IsNot Nothing AndAlso currentNode.Parent Is Nothing AndAlso treeNode.Parent Is Nothing AndAlso treeNode IsNot currentNode).
    • If changes exist, the MessageBox.Show is called with Yes, No, and Cancel options.
    • The Select Case block handles the user's choice:
      • Yes: Calls SaveDataGridViewChanges (your implementation), sets changesMade = False, and allows the selection (e.Cancel = False).
      • No: Sets changesMade = False to discard changes, and allows the selection (e.Cancel = False).
      • Cancel: Sets e.Cancel = True to prevent the new selection. It then temporarily removes the BeforeSelect event handler, sets treeMain.SelectedNode back to the currentNode (the node that was selected before the attempted change), and re-adds the handler. This programmatic selection won't trigger the BeforeSelect event because the handler was temporarily removed.
    • The isHandlingBeforeSelect flag is reset in a Finally block to ensure it's always reset, even if an error occurs.
  4. treeMain_AfterSelect Modifications: This event is now solely responsible for loading the data into the DataGridView after a selection has been successfully made (i.e., BeforeSelect did not cancel it). The save prompt logic has been moved out.
  5. PopGrid Modification: changesMade is set to False at the end of PopGrid because loading new data means there are no unsaved changes for that data yet.
  6. SaveDataGridViewChanges Method: A placeholder method is added. You must replace the MessageBox inside this method with your actual code to save the data from the DataGridView. Make sure your save logic also sets changesMade = False upon successful completion.
  7. ResetForm and btnReset_Click: Added a check for changesMade in btnReset_Click to optionally prompt the user to save before resetting the form. ResetForm also sets changesMade = False.
  8. AfterCollapse: Kept the existing logic but added comments about potentially adding a save prompt here if collapsing should also consider unsaved changes from checked child nodes.
  9. mnuTree1Remove_Click: Added a check for changesMade before removing a node, in case the data associated with that node was currently loaded and modified.
Now, when the user clicks a different root node while changesMade is True, the BeforeSelect event will fire, display the message box without looping, and handle the user's choice before proceeding (or not proceeding) with the selection. The AfterSelect event will then load the data for the newly selected node if the selection was allowed.

Thanks, I'll absorb this and give it a go. In the larger app, I do know when there are unapplied updates.
 
Back
Top