Thursday, June 2, 2011

PowerShell GUIs: Active Directory TreeView

So this is a modified bit of code I wrote to build a tree-view of the local Active Directory OU structure. I'm using this snippet in another piece of code but it *should* work just fine in any domain. Please let me know of any modifications or fixes necessary.



function Add-Node { 
        param ( 
            $selectedNode, 
            $name
        ) 
        $newNode = new-object System.Windows.Forms.TreeNode  
        $newNode.Name = $name 
        $newNode.Text = $name 
        $selectedNode.Nodes.Add($newNode) | Out-Null 
        return $newNode 
} 

function Get-NextLevel {
    param (
        $selectedNode,
        $dn
   )
   
    $OUs = Get-ADObject -Filter 'ObjectClass -eq "organizationalUnit" -or ObjectClass -eq "container"' -SearchScope OneLevel -SearchBase $dn

    If ($OUs -eq $null) {
        $node = Add-Node $selectedNode $dn
    } Else {
        $node = Add-Node $selectedNode $dn
        
        $OUs | % {
            Get-NextLevel $node $_.distinguishedName
        }
    }
}
 
function Build-TreeView { 
    if ($treeNodes)  
    {  
          $treeview1.Nodes.remove($treeNodes) 
        $form1.Refresh() 
    } 
    
    $treeNodes = New-Object System.Windows.Forms.TreeNode 
    $treeNodes.text = "Active Directory Hierarchy" 
    $treeNodes.Name = "Active Directory Hierarchy" 
    $treeNodes.Tag = "root" 
    $treeView1.Nodes.Add($treeNodes) | Out-Null 
     
    $treeView1.add_AfterSelect({ 
        $textbox1.Text = $this.SelectedNode.Name
    }) 
     
    #Generate Module nodes 
    $OUs = Get-NextLevel $treeNodes $strDomainDN
    
    $treeNodes.Expand() 
} 
 
#Generated Form Function 
function GenerateForm { 
 
    #region Import the Assemblies 
    Import-Module ActiveDirectory
    [reflection.assembly]::loadwithpartialname("System.Windows.Forms") | Out-Null 
    [reflection.assembly]::loadwithpartialname("System.Drawing") | Out-Null 
    $objIPProperties = [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties()
    $strDNSDomain = $objIPProperties.DomainName.toLower()
    $strDomainDN = $strDNSDomain.toString().split('.'); foreach ($strVal in $strDomainDN) {$strTemp += "dc=$strVal,"}; $strDomainDN = $strTemp.TrimEnd(",").toLower()
    #endregion 
     
    #region Generated Form Objects 
    $form1 = New-Object System.Windows.Forms.Form 
    $treeView1 = New-Object System.Windows.Forms.TreeView 
    $label1 = New-Object System.Windows.Forms.Label
    $textbox1 = New-Object System.Windows.Forms.TextBox
    $InitialFormWindowState = New-Object System.Windows.Forms.FormWindowState 
    #endregion Generated Form Objects 
     
    #---------------------------------------------- 
    #Generated Event Script Blocks 
    #---------------------------------------------- 
    $button1_OnClick=  
    { 
    $form1.Close() 
     
    } 
     
    $OnLoadForm_StateCorrection= 
    {Build-TreeView 
    } 
     
    #---------------------------------------------- 
    #region Generated Form Code 
    $form1.Text = "Form" 
    $form1.Name = "form1" 
    $form1.DataBindings.DefaultDataSourceUpdateMode = 0 
    $form1.ClientSize = New-Object System.Drawing.Size(400,500) 
     
    $treeView1.Size = New-Object System.Drawing.Size(350,375)
    $treeView1.Name = "treeView1" 
    $treeView1.Location = New-Object System.Drawing.Size(15,15)
    $treeView1.DataBindings.DefaultDataSourceUpdateMode = 0 
    $treeView1.TabIndex = 0 
    $form1.Controls.Add($treeView1)
    
    $label1.Name = "label1" 
    $label1.Location = New-Object System.Drawing.Size(15,400)
    $label1.Size = New-Object System.Drawing.Size(350,20)
    $label1.Text = "Selected Value:"
    $form1.Controls.Add($label1) 
    
    $textbox1.Name = "textbox1" 
    $textbox1.Location = New-Object System.Drawing.Size(15,425)
    $textbox1.Size = New-Object System.Drawing.Size(350,20)
    $textbox1.Text = ""
    $form1.Controls.Add($textbox1) 
     
     
    #endregion Generated Form Code 
     
    #Save the initial state of the form 
    $InitialFormWindowState = $form1.WindowState 
    #Init the OnLoad event to correct the initial state of the form 
    $form1.add_Load($OnLoadForm_StateCorrection) 
    #Show the Form 
    $form1.ShowDialog()| Out-Null 
     
} #End Function 
 
#Call the Function 
GenerateForm


15 comments:

  1. is executing for me for a long time... memory looks stable maybe gradually upticking at 144 MB CPU seems dead though...have a large directory to scan though... wish you'd have had a picture of what the diagram may look like... GBT

    ReplyDelete
  2. Sorry for the late response. I had to build a VM environment to stand up a dummy domain to run this on for the screen shot. The screen shot is attached. I'm not sure why this is taking long in your environment unless you have a massive AD structure and/or are running it on older hardware. As in, I'm running it on a 2 year-old 32-bit machine against a production AD environment with ~500 OUs and it only takes about 5-10 seconds to build the directory structure.

    ReplyDelete
  3. thank you very much - this is awesome script, very usefull for me


    little question-
    did you build it with primalforms? if this true - maybe you can share all project, not only exported ps1 script?

    ReplyDelete
  4. I ask this because dont know win.forms very well, but I need to add button (selec, ok, or whatever its name would be) wich would make selected ou as variable

    than I would use this variable in other task

    I think that with primalforms I can do it

    p.s. sorry for my English )

    ReplyDelete
  5. Thanks, this is great!
    So if I wanted to utilize this for creating a new user account as part of my GUI script that asks for other things necessary for creating an account, how could I make the OU a variable?

    Would I just have to do something like:
    $userOU=$textbox1.Text
    ?

    ReplyDelete
  6. Is there a way to replicate this without AD webservices in the domain?

    ReplyDelete
  7. @Kestrel, yes exactly. You'd probably want to do a little more checking than that to make sure you have a value and a valid value at that but that is the general idea.

    @Anonymous, yes, you would be able to recreate this process using straight LDAP calls through the [ADSI] provider but it is just cleaner using the native PowerShell AD cmdlets from RSAT. All you would need to do is replace the Get-ADObject portion of acquiring the OUs/Containers with an LDAP call that did the same. The rest of the code should function more-or-less the same.

    ReplyDelete
  8. This would load faster if you modified it so that you loaded each level of OUs only when clicking on the expand button.

    ReplyDelete
  9. Excellent post! It inspired me to recreate this in Powershell Studio and give it a more native look & feel using icons. I also added a New OU button to create OU's on the fly. Pretty cool stuff. Check it out on my blog if you like. itmicah.wordpress.com

    ReplyDelete
  10. @Anonymous
    I also have same problem while executing above script.....it takes forever to open in a form..
    Did you get any solution to it....I tried itimach's post (script) also but same response...not sure what's going on....

    ReplyDelete
  11. When I get some time, I'll write a pure LDAP call version to see if that'll speed it up. I wrote this a long time ago so I'm sure there are some performance issues I could deal with since the last time I wrote this code. ;)

    ReplyDelete
  12. I modified this to use ldap instead of ad modules. I also modified it to not rebuild the form as most AD forests structures dont change that often. How do I upload to you my attempt to make it work better? Also the selected ou becomes a global variable that you can utilize for other tasks.

    ReplyDelete
  13. hi, i modified your script to not be dependent on the ActiveDirectory module.

    function Get-NextLevel {
    param (
    $selectedNode,
    $dn
    )

    $results = @()
    $a = [adsisearcher]([adsi]"LDAP://$dn")
    $a.Filter = "(&(objectClass=organizationalUnit))"
    $a.SearchScope ="OneLevel"
    $List = ($a.FindAll()).getEnumerator().properties

    foreach ($OB in $List){
    $aA = $OB.distinguishedname
    $aB = $OB.name
    $aC = $OB.objectclass
    $aE = $OB.objectguid

    $details = @{
    DistinguishedName = "$aA"
    Name = "$bB"
    ObjectClass = "$cC"

    }
    $results += New-Object PSObject -Property $details
    }

    If ($results -eq $null) {
    $node = Add-Node $selectedNode $dn
    } Else {
    $node = Add-Node $selectedNode $dn

    $results | % {
    Get-NextLevel $node $_.distinguishedName
    }
    }
    }

    ReplyDelete