Building a responsive TreeView that displays your AD Domain

Often times you might find yourself wanting to build a TreeView in your form that represents your AD domain structure. The following post will show you how to do that. (Note that I pieced together this solution with the assistance of many Google searches and other folks code samples, however this was done over time and I don’t have any sources available.)

First you’ll want to setup your form with with a TreeView object. You’ll then need to devise a way to initially populate the TreeView. I personally have a function for initially building the TreeView which you can then call with a button click, or the form loading. (In addition to having the TreeView built when the form loads, you’ll also need to create a script scoped variable called “DNsLoaded” that’s an array object. You’ll see why later on.) The function that builds the TreeView contains the following:

treeview-addomain

if ($treeNodes)
{
	$treeview.Nodes.remove($treeNodes)
}
$script:treeNodes = New-Object System.Windows.Forms.TreeNode
$Script:ADDomain = Get-ADDomain
$script:treeNodes.text = $ADDomain.DNSRoot
$script:treeNodes.Name = $ADDomain.DNSRoot
$script:treeNodes.Tag = $ADDomain.DistinguishedName
$script:treeNodes.StateImageIndex = 0
$script:treeNodes.SelectedImageIndex = 1
$treeView.Nodes.Add($script:treeNodes) | Out-Null
Get-NextLevel -selectedNode $treeNodes -dn $ADDomain.DistinguishedName
$treeNodes.Expand()

For a bit of explaining of what’s going on…

  • Lines 1-4 we looks to see if the $treeNodes variable already exists. If it does, it’s assumed the TreeView object is already populated and needs cleared.
  • Line 5 we create a base TreeNode object.
  • Line 6 we run the Get-ADDomain cmdlet.
  • Line 7-9 we populate the Text, Name, and Tag values of the base TreeNode object. The Text and Name values are populated with the DNS root (domain.local), while the Tag value is populated with the DN (DC=domain,DC=local).
  • Line 10 and 11 are only necessary if you have images that you want to use within the TreeView. The images a placed next to the node, with each node in this example being a container or OU.
  • Line 12 we add the TreeNode object to the TreeView.
  • Line 13 we run a function that we’ll cover next.
  • Line 14 causes the base node to expand in the TreeView.

The next function you’ll want to create is the one referenced above in line 13. This function is called anytime you want to load the next layer of objects in the TreeView.

treeview-ad-domain-get-nextlevel

function Get-NextLevel
{
	param (
		$selectedNode,
		$dn
	)
	$form.Cursor = 'WaitCursor'
	ForEach ($OU in (Get-ADObject -Filter 'ObjectClass -eq "organizationalUnit" -or ObjectClass -eq "container"' -SearchScope OneLevel -SearchBase $dn))
	{
		Add-Node -selectedNode $selectedNode -dn $OU.DistinguishedName -name $OU.Name -preload $true
	}
	$form.Cursor = 'Default'
}

To explain the Get-NextLevel function…

  • Lines 3-6 we collect two parameters, the node object that we’ll be adding new nodes to, and the distinguished name of the OU that’s currently selected.
  • Line 7 sets the the cursor to the busy cursor while the function processes.
  • Line 8 is where we run Get-ADObject, specifically searching for OU’s and containers, with the search scope set to one level (so that we don’t get objects more than one level deep) and the search base set to the distinguished name that was passed as a parameter to the function. For each object returned, we run line 10.
  • Line 10 we run the function Add-Node. This function will be covered next, however it simply adds a new node object to the node object received as a parameter. In addition, we pass the parameter ‘preload’ as true. This will be covered next.

The last function necessary to create is Add-Node.

treeview-ad-domain-add-node

function Add-Node
{
	param (
		$selectedNode,
		$dn,
		$name,
		$preload
	)
	If (!($DNsLoaded -contains $dn))
	{
		$newNode = new-object System.Windows.Forms.TreeNode
		$newNode.Name = $dn
		$newNode.Text = $name
		$newNode.StateImageIndex = 0
		$newNode.SelectedImageIndex = 1
		If ($preload -eq $true)
		{
			ForEach ($OU in (Get-ADObject -Filter 'ObjectClass -eq "organizationalUnit" -or ObjectClass -eq "container"' -SearchScope OneLevel -SearchBase $dn))
			{
				Add-Node -selectedNode $newNode -dn $OU.DistinguishedName -name $OU.Name -preload $false
			}
		}
		Else
		{
			$Script:DNsLoaded += $dn
		}
		$selectedNode.Nodes.Add($newNode) | Out-Null
		return $newNode
	}
}

Now to explain the Add-Node function…

  • Lines 3-8 we collect 4 parameters. SelectedNode (the node in the treeview that we’re adding a node to), DN (the Distinguished Name of the OU we’re creating a node for), Name (the Name of the OU we’re creating a node for), and PreLoad (More details shortly…).
  • Line 9 we check to see if the DNsLoaded array contains the DN passed to the function. More details on why this is done later, however if the DN is not present in the array, lines 11 through 28 are processed.
  • Lines 11-15 we create a new node object and set the name (to the DN), text (to the name), and image states.
  • Lines 16-26 is where the PreLoad parameter comes into play. The PreLoad parameter is used to populate the new node being create with the next level of OU’s. This is done so that in the TreeView you’ll see a plus indicator next to the new node that was created in line 11 if there are sub OU’s to the OU passed on line 5. One line 20 you’ll see that the Add-Node function is called again, except this time with PreLoad set to false. This is so that we don’t try to load all the OU’s in our domain all at once. This is what makes this a responsive solution to populating the TreeView with a domain structure.
  • Line 27 we add the new node to the selected node that was passed to the Add-Node function.
  • Line 28 we return the NewNode object, however this isn’t necessary.

The last thing that needs done before this will work properly is to create a BeforeExpand event for the TreeView. In the BeforeExpand event, put the following:

treeview-ad-domain-before-expand

$treeview.SelectedNode = $_.Node
foreach ($node in $treeview.SelectedNode.Nodes)
{
	Get-NextLevel -selectedNode $node -dn $node.Name
}
  • Line 1 we’re ensuring that the node being expanded gets set as the SelectedNode. Then, because we’ve already pre-populated the sub-nodes (The OU’s within the OU we’re selecting.), we do a ForEach on each sub-node, and run the Get-NextLevel function.

Throw this all together and you’ll have yourself a very responsive TreeView of your AD Domain.

 

Providing a filter for your data results in a PowerShell form

I’ve often found myself wanting to provide filtering functionality in my forms for datagridviews that contain a lot of results. The following is what I’ve done to provide filtering functionality.

  1. Have a datagridview that contains one or more columns that you want to be able to filter text in.
  2. Create a timer in your form with an interval of around 500ms. ¬†Create a ‘Tick’ event for the timer with the following:

    filter-timer_tick

    $timer.Stop()
    $datagridview.CurrentCell = $null
    If ($textbox.Text.Length -eq 0)
    {
    	foreach ($row in ($datagridview.Rows))
    	{
    		$datagridview.Rows[$row.Index].Visible = $true
    	}
    }
    Else
    {
    	foreach ($row in ($datagridview.Rows))
    	{
    		If ($row.Cells[$columnIndex/Name].Value -like "$($textbox.Text)*")
    		{
    			$datagridview.Rows[$row.Index].Visible = $true
    		}
    		Else
    		{
    			$datagridview.Rows[$row.Index].Visible = $false
    		}
    	}
    }
  3. Create a textbox within the form, then add a ‘TextChanged’ event with the following:

    filter-textbox_textchanged

    $timer.Stop()
    $timer.Start()

With the above in place if someone starts typing into the textbox and pauses for 500ms, the timer will execute and filter the datagridview for text that’s in the textbox. In the above example I use ‘-like’ when evaluating the cell content so that the use of a wildcard character is controlled by the textbox.

In the event that your datagridview (or data source in general) contains a ton of records, you could improve the experience by moving the actual filtering process to a Job, and then changing the cursor of the form to ‘WaitCursor’ until the job finishes.