locked
Help with WPF form using Powershell to create mail users on Office 365 RRS feed

  • Question

  • Hello everyone,

    I'm looking for some help getting a piece of a utility I'm creating using powershell behind a WPF form. I am trying to get it to create mail users on Office 365 using text from fields that I have filled in in a WPF form. I can get it to connect to O365, but it seems like it's not connecting in the same session or something. If I start up a powershell window and put in the commands to connect and then use the new-msoluser command it creates the user, just not when it's running through my form. Can anyone help me figure out where I'm having a problem?

    Here is the code I'm using, I've redacted any confidential info:

    #===========================================================================
    # Elevate Powershell Instance - Needed for Office 365 Connection
    #===========================================================================
    param([switch]$Elevated)
    function Check-Elevation {
    $currentUser = New-Object Security.Principal.WindowsPrincipal $([Security.Principal.WindowsIdentity]::GetCurrent())
    $currentUser.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
    }
    if ((Check-Elevation) -eq $false)  {
    if ($elevated)
    {
    # could not elevate, quit
    }
     
    else {
     
    Start-Process powershell.exe -Verb RunAs -ArgumentList ('-noprofile -noexit -file "{0}" -elevated' -f ($myinvocation.MyCommand.Definition))
    }
    exit
    }
    
    #Configure variable for creating runspaces for multithreading
    $Global:syncHash = [hashtable]::Synchronized(@{})
    $newRunspace = [runspacefactory]::CreateRunspace()
    $newRunspace.ApartmentState = "STA"
    $newRunspace.ThreadOptions = "ReuseThread"
    $newRunspace.Open()
    $newRunspace.SessionStateProxy.SetVariable("syncHash",$syncHash)
    
    # Load WPF assembly if necessary
    [void][System.Reflection.Assembly]::LoadWithPartialName('presentationframework')
    
    $psCmd = [PowerShell]::Create().AddScript({
        #XAML Code goes here for the interface
        [xml]$XAML = @"
        <Window
                xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
                xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
                Title="Awesome IT Utility" Height="370" Width="620">
        <Grid x:Name="Background">
            <TextBlock x:Name="Result_Text" HorizontalAlignment="Left" Margin="23,283,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="565" FontFamily="Calibri" Height="36" Text="Result:"/>
            <TabControl x:Name="TabControlBox" TabStripPlacement="Left" HorizontalAlignment="Left" Height="224" Margin="1,52,-1,0" VerticalAlignment="Top" Width="602">
                <TabItem x:Name="User_Management_Tab" Header="User Management">
                    <TabControl x:Name="UMTabControlBox" HorizontalAlignment="Left" Height="218" Margin="1,0,-1,0" VerticalAlignment="Top" Width="482">
                        <TabItem x:Name="User_Creation_Tab" Header="User Creation">
                            <Grid x:Name="User_Management_Tab_Grid">
                                <Label x:Name="O365Auth_Label" Content="O365 Admin:" HorizontalAlignment="Left" Margin="11,161,0,0" VerticalAlignment="Top" FontFamily="Calibri"/>
                                <TextBox x:Name="O365Auth_Entry" HorizontalAlignment="Left" Height="18" Margin="91,165,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="175" FontFamily="Calibri"/>
                                <Button x:Name="ConnectEmail_Button" Content="O365 Connect" HorizontalAlignment="Left" Margin="272,162,0,0" VerticalAlignment="Top" Width="100" FontFamily="Calibri" FontSize="16"/>
                                <Button x:Name="CreateEmail_Button" Content="Check O365" HorizontalAlignment="Left" Margin="377,162,0,0" VerticalAlignment="Top" Width="90" FontFamily="Calibri" FontSize="16"/>
                            </Grid>
                        </TabItem>
                    </TabControl>
                </TabItem>
            </TabControl>
        </Grid>
    </Window>
    "@
    
        #Read the XAML
            $reader = (New-Object System.Xml.XmlNodeReader $xaml)
            $syncHash.Window = [Windows.Markup.XamlReader]::Load( $reader )
    
            [xml]$XAML = $XAML
                $xaml.SelectNodes("//*[@*[contains(translate(name(.),'n','N'),'Name')]]") | ForEach-Object{
                    #Find all of the form types and add them as members to the synchash
                    $syncHash.Add($_.Name,$syncHash.Window.FindName($_.Name))
                }
    
            $Script:JobCleanup = [hashtable]::Synchronized(@{})
            $Script:Jobs = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList))
    
        #Background runspace to clean up jobs
            $jobCleanup.Flag = $true
            $newRunspace = [runspacefactory]::CreateRunspace()
            $newRunspace.ApartmentState = "STA"
            $newRunspace.ThreadOptions = "ReuseThread"
            $newRunspace.Open()
            $newRunspace.SessionStateProxy.SetVariable("jobCleanup",$jobCleanup)
            $newRunspace.SessionStateProxy.SetVariable("jobs",$jobs)
            $jobCleanup.PowerShell = [PowerShell]::Create().AddScript({
                #Routine to handle completed runspaces
                Do {
                    foreach($runspace in $jobs) {
                        if ($runspace.Runspace.isCompleted) {
                            [void]$runspace.powershell.dispose()
                            $runspace.Runspace = $null
                            $runspace.powershell = $null                    
                        }
                    }
                    #Clean out unused runspace jobs
                    $temphash = $jobs.clone()
                    $temphash | Where-Object {
                        $_.runspace -eq $null
                    } | ForEach-Object {
                        $jobs.remove($_)
                    }
                    Start-Sleep -Seconds 1
                } while ($jobCleanup.Flag)
            })
            $jobCleanup.PowerShell.runspace = $newRunspace
            $jobCleanup.Thread = $jobCleanup.powershell.BeginInvoke()
     
    #===========================================================================
    # Define Functions for everything to work
    #===========================================================================
    
    #Shortcuts for properties
        $365Loaded = Get-Mailbox
    
    #Create Office 365 Account
        Function Connect-ExchangeOnline {
            Set-ExecutionPolicy Unrestricted
            $SecurePassword = Get-Content C:\securestring.txt | convertto-securestring
            $O365Username = $syncHash.O365Auth_Entry.Text
            $LiveCred = New-Object System.Management.Automation.PSCredential -ArgumentList $O365UserName, $SecurePassword
            $Global:Session365 = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://ps.outlook.com/powershell/ -Credential $LiveCred -Authentication Basic -AllowRedirection
           
           Import-PSSession $global:Session365
        }
    
        Function Get-EmailFields {
            $FullName = $syncHash.First_Name_Entry.Text + " " + $syncHash.Last_Name_Entry.Text
    
                $NewEmailHash =
                    @{ 
                        UserPrincipalName = $syncHash.Email_Address_Entry.Text;
                        DisplayName = $FullName;
                        Department = $syncHash.Department_Entry.Text;
                        FirstName = $syncHash.First_Name_Entry.Text;
                        ForceChangePassword = $false;
                        LastName = $syncHash.Last_Name_Entry.Text;
                        LicenseAssignment = "xxxxx:O365_BUSINESS_PREMIUM";
                        Office = $syncHash.Office_Entry.Text;
                        Password = $syncHash.Password_Entry.text;
                        PhoneNumber = $syncHash.Phone_Entry.Text;
                        Title = $syncHash.Title_Entry.Text;
                        UsageLocation = "US";
                    }
                    
                    $NewEmailHash
    
                    #New-Msoluser -UserPrincipalName $UserPrincipalName -DisplayName $DisplayName -Department $Department -FirstName $FirstName -ForceChangePassword $ForceChangePassword `
                    #-LastName $LastName -LicenseAssignment $LicenseAssignment -Office $Office -Password $Password -PhoneNumber $PhoneNumber -Title $Title -UsageLocation $UsageLocation
        }
    
    #===========================================================================
    # Buttons
    #===========================================================================
    
    #Email Account Creation buttons
        $syncHash.ConnectEmail_Button.Add_Click({
            try {
                if (!$365Loaded) {
                    Connect-ExchangeOnline
                    Start-Sleep -Milliseconds 10
                    Connect-MsolService
                    $syncHash.Result_Text.Text = "[SUCCESS] Connected to Office 365!" | Out-String
                }
            }
            catch {
                $syncHash.Result_Text.Text = "[ERROR] Can't connect : $_" | Out-String
        }
        })
    
        $syncHash.CreateEmail_Button.Add_Click({
            # Resolve Form Settings
            $NewEmail = Get-EmailFields
            # $NewEmUser = Get-EmUserFields
            try {
                if ($365Loaded) {
                    $syncHash.Result_Text.Text = "[SUCCESS] Office 365 commandlets loaded!" | Out-String
                }
                else {
                    $syncHash.Result_Text.Text = "[ERROR] Office 365 commandlets not loaded!" | Out-String
                }
            }
            catch {
                $syncHash.Result_Text.Text = "[ERROR] Can't create mailbox : $_" | Out-String
            }
            })
    
    #Closing cleanup
        $syncHash.Window.Add_Closed({
            Write-Verbose 'Halt runspace cleanup job processing'
            $jobCleanup.Flag = $False
            #Stop all runspaces
            $jobCleanup.PowerShell.Dispose()
            Remove-PSSession $global:Session365
            stop-process $PID
        })
    
    #===========================================================================
    # Shows the form
    #===========================================================================
    [void]$syncHash.Window.ShowDialog()
    $syncHash.Error = $Error
    })
    $psCmd.Runspace = $newRunspace
    $data = $psCmd.BeginInvoke()


    • Edited by Curtman7 Tuesday, January 16, 2018 10:56 PM Replaced code with less info
    Tuesday, January 16, 2018 7:17 PM

Answers

  • Why are you trying to set values inside code called from the form using the synchash. This is unnecessary. Just use the form objects and variables directly. Overuse of a synchash can create issues with variable visibility because WPF is executing multiple treads concurrently and this can lead to issue. PS is not the best system to run WPF from which is why so many of us abandoned it a few years ago. Windows Forms are much more cooperative with PS.

    A synchash is automatic.  The contained variables are made available on both threads.  That is its purpose. One the variables are set then just use them and let the synchash do its job. Only when you need values from the other runspace will you have to access the synchash.


    \_(ツ)_/

    • Marked as answer by Curtman7 Friday, January 19, 2018 4:59 PM
    Wednesday, January 17, 2018 2:40 AM

All replies

  • I don't think you will find anyone to debug 300+ lines of code.  You need to strip this down to the simplest example of the issue.  Chances are, while doing that, you might trip over the bug.


    \_(ツ)_/

    Tuesday, January 16, 2018 10:10 PM
  • Sorry about that, I'm new to doing this so wasn't sure how much i needed to include. I've removed some more info, I think as much as I can without breaking it. I don't think I can make it any shorter because it needs to have the info for the form included which is a lot of the code there.
    Tuesday, January 16, 2018 10:58 PM
  • Guess you ae going to have to learn how to debug a script.

    There is no way we can run your code without wasting a lot of time. 

    Work out how you can ask a specific question. 


    \_(ツ)_/

    Wednesday, January 17, 2018 12:26 AM
  • Why are you trying to set values inside code called from the form using the synchash. This is unnecessary. Just use the form objects and variables directly. Overuse of a synchash can create issues with variable visibility because WPF is executing multiple treads concurrently and this can lead to issue. PS is not the best system to run WPF from which is why so many of us abandoned it a few years ago. Windows Forms are much more cooperative with PS.

    A synchash is automatic.  The contained variables are made available on both threads.  That is its purpose. One the variables are set then just use them and let the synchash do its job. Only when you need values from the other runspace will you have to access the synchash.


    \_(ツ)_/

    • Marked as answer by Curtman7 Friday, January 19, 2018 4:59 PM
    Wednesday, January 17, 2018 2:40 AM
  • Thank you JRV!! I switched over to using Windows Forms and was able to get everything working, and it also made the other functions I had in there a little easier as well.
    Friday, January 19, 2018 5:00 PM
  • Thank you JRV!! I switched over to using Windows Forms and was able to get everything working, and it also made the other functions I had in there a little easier as well.

    Yes. Forms are a better match to PS.  Forms run on the same thread as PS so there are no threading issues and the code and variables are all available directly.

    There is another issue with WPF and synchash.  Ever time we access a synchash we are asking for a lock and waiting on the lock.  This can cause a suspension of the thread which can cause the form to behave erratically.   In compiled code we have many ways to access variables and objects across threads which are not easily implementable in PS although many can be implemented.

    Stick with forms.  They can generally do everything except sexy interfaces.

    Here is a lot of info on forms in PS: https://info.sapien.com/index.php/guis


    \_(ツ)_/

    Friday, January 19, 2018 5:09 PM