Caliburn.Micro Notify Change for Static Property

Normally in Caliburn.Micro and MVVM in general, you would have non-static properties that would be updated and notified like this:

private string _name;

public string Name
{
    get { return _name; }
    set
    {
        _name = value;
        NotifyOfPropertyChange(() => Name);
    }
}

However, if I had a static property, for example, if I wanted a property that could be accessed by another namespace, I would need to implement my own event handler as the default NotifyOfPropertyChange doesn’t operate on static properties.

This is the new EventHandler that I implemented:

public static event EventHandler<PropertyChangedEventArgs> StaticPropertyChanged;
private static void NotifyStaticPropertyChanged(string propertyName)
{
    StaticPropertyChanged?.Invoke(null, new PropertyChangedEventArgs(propertyName));
}

Finally, in order to use it. I would use this instead of the first example for non-static properties:

private string _name;

public string Name
{
    get { return _name; }
    set
    {
        _name = value;
        NotifyStaticPropertyChanged(() => Name);
    }
}

I hope this helps someone. Enjoy!

Setting Up Caliburn.Micro MVVM

This is a fairly lengthy post that shows how to set up an initial MVVM WPF form using Caliburn.Micro.

Let’s get started, we’ll open up Visual Studio and chose to create a new WPF:

Visual Studio -> New Project -> WPF App (.Net Framework)

I usually set the project name to something like WPFUI and my solution name to be something like MVVMProject or the actual product name. For example, Microsoft might use Microsoft Outlook. Probably not, but you get what I’m saying…

 

1)

First I will add Caliburn.Micro to my project. To do this go to Solution Explorer -> References -> Add NuGet Packages and search for Caliburn.Micro:

 

2)

Now we can delete the MainWindow.xaml

 

3)

Create a Views and ViewModels folder. This is where your viewmodels and views will call home.

 

4)

Create a new class in the ViewModels folder and call it ShellViewModel and make it public. You’ll also want to inherit from screen and add the using statement for Caliburn.Micro:

5)

Create a new window in the Views folder and call it ShellView

6)

Create a new class in the root directory called Bootstrapper and inherit from BootstrapperBase and add the using statement for Caliburn.Micro

7)

In App.xaml remove the StartupUri element and add the bootstrapper class as a resource by adding:

<ResourceDictionary>
    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary>
            <local:Bootstrapper x:Key="Bootstrapper" />
        </ResourceDictionary>
    </ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

Your app.xaml should look like this:

 

8)

Now go back into your Bootstrapper class and add the following:

public Bootstrapper()
{
    Initialize();
}
protected override void OnStartup(object sender, StartupEventArgs e)
{
    DisplayRootViewFor<ShellViewModel>();
}

You’ll also need to add a using statement for System.Windows and ProjectName.ViewModels.

The program should now launch and show an empty white screen with “ShellView” as the window’s title. This is the first entry into a hopefully long series into the world of Caliburn.Micro.

Enjoy! 😊

Don’t use $input in PowerShell Functions

Just a short post and a gentle reminder to check conventions before pulling my hair out over a simple issue.

This all started when I wanted to test something simple in PowerShell. I had started using C# more often and I think this is what caused my brain just to use $input as I would normally use something like this in C#.

In C# my test functions (or methods) might look something like this:

public string TestFunction(string input){
    Console.WriteLine(input);
}

In C#, the above works…

However, $input is a reserved variable. You can see a list of them all here. But much to my annoyance, the PowerShell ISE doesn’t flag this as an issue (technically it isn’t) and just runs like nothing is wrong (once again, technically nothing is wrong).

In PowerShell, this doesn’t work…

function Test-Function([string]$input){
    Write-Host $input
}

After messing for about thirty minutes, I finally found this knowledge out and renamed my parameter. I think you’ll like the name I continued to use… 😊

function Test-Function([string]$stupidFuckingInput){
    Write-Host $stupidFuckingInput
}

Enjoy!

Office Click-To-Run and XML Files

So, it used to be that we would install Office using a batch script that would invoke a setup.exe, assign a specific /configure flag and manually assign a specific XML file that contained the product that we wanted to install. This was bulky. It got too bulky when we needed to install 32-bit and 64-bit versions.

TIME FOR A CHANGE!

This is when I started thinking: “wow I really hate batch. I’m really glad I’m not the one that had to write this old script. Lets PowerShell this shit!”

First I needed a template XML file to modify, So this is what that looks like:

<Configuration>
  <Add OfficeClientEdition="64" Channel="Current">
    <Product ID="O365BusinessRetail">
      <Language ID="MatchOS" />
    </Product>
  </Add>
</Configuration>

This is the file that we will edit to say which product we want installing also if we want 64-bit or 32-bit.

Next, I needed to create a PowerShell script that would take a user’s input, edit the XML file accordingly and start the setup.exe with this flag. I also needed the bit-version that they wanted.

I started by defining the variables I would need for the script:

#Variables used for the installation
$bitVersion = ""
$officeProduct = ""
$pathToOffice = "\\path\to\office\folder"
$xmlFile = "OfficeXML.xml"
$pathToXMLFile = Join-Path -Path $pathToOffice -ChildPath $xmlFile

Then I created a function I would use to update the XML file. I needed two parameters, the product that they wanted installing and the bit version they wanted:

#Updates the XML file based on the input
function Update-XMLFile([string]$product, [string]$bit){

    try{
        #Loading the XML document
        [xml]$xmlDoc = Get-Content -Path $pathToXMLFile

        #Edit the document
        $xmlDoc.Configuration.Add.OfficeClientEdition = $bit
        $xmlDoc.Configuration.Add.Product.ID = $product

        #Save the document
        $xmlDoc.Save($pathToXMLFile)
    }catch{
        $errorMessage = $_.Exception.Message
        Write-Host $errorMessage -ForegroundColor Red
        Read-Host "The script encountered the above error - will now exit"
    }
}

I then created another function to start the installation. This also required two parameters, the bit version and the XML file name

#Function to start the installation
function Start-Installation([string]$bit, [string]$xmlName){
    try{
        .\setup.exe /configure $bit\$xmlName
    }catch{
        $errorMessage = $_.Exception.Message
        Write-Host $errorMessage
        Read-Host "The script encountered the above error - will now exit"
    }
}

My final function was a verification test. Since we want to only use 64-bit for future installations, I had to make sure that whoever was using the script knew this and would be competent enough to do a little bit of math:

#Function to check the user wants 32 bit
function Get-Verification(){
    $output = $false

    Write-Host "Are you sure you want to install 32-bit?" -ForegroundColor Red
    Write-Host "All new installs should use 64-bit instead"
    Write-Host "If you want to install 32-bit, complete the test below, otherwise enter the wrong answer"

    $firstNumber = Get-Random -Minimum 1 -Maximum 11
    $secondNumber = Get-Random -Minimum 1 -Maximum 11

    $sumToCheck = $firstNumber + $secondNumber

    $verificationInput = Read-Host "$($firstNumber) + $($secondNumber) = ?"

    if ($verificationInput -eq $sumToCheck){
        Write-Host "Fine! 32-bit will be installed..."
        $output = $true
    }else{
        Write-Host "Finally! 64-bit will be installed"
        $output = $false
    }
    return $output
}

Now that all my functions were defined, I could start with the actual meat of the script. This included cleaning the screen, asking the user some questions, launching the 32-bit verification is needed, updating the XML file using a switch statement and finally kicking off the installation. Heres what that looked like:

#Clear the screen
Clear-Host

#region Checking if the user wants 64 bit or 32 bit

do{

    Write-Host "Do you want" -NoNewline
    Write-Host " 64-bit " -NoNewline -ForegroundColor Yellow
    Write-Host "or" -NoNewline
    Write-Host " 32-bit " -NoNewline -ForegroundColor Green
    Write-Host "? (64 or 32): " -NoNewline
    $bitVersionInput = (Read-Host).ToUpper()
}while((64 ,32) -notcontains $bitVersionInput)

#endregion

#Check the user definitely wants 32 bit
if ($bitVersionInput -eq "32"){
    if (Get-Verification){
        $bitVersion = $bitVersionInput
    }else{
        $bitVersionInput = "64"
    }
}

#Update the bitVersion variable
$bitVersion = $bitVersionInput

#region Asking what product to install

#Ask the user what product they want to install
Write-Host @"

Please select one product from the below list

"@

Write-Host @"
1) Business Retail
2) ProPlus Retail

"@ -ForegroundColor Cyan

Write-Host @"
3) Visio Std Volume
4) Visio Pro Volume
5) Visio Pro Retail

"@ -ForegroundColor Green

Write-Host @"
6) Project Std Volume
7) Project Pro Volume
8) Project Pro Retail

"@ -ForegroundColor Gray

Write-Host @"
C) Cancel

"@ -ForegroundColor Red

do{
    $officeProductInput = (Read-Host "Enter a number").ToUpper()
}while((1,2,3,4,5,6,7,8, "C") -notcontains $officeProductInput)

#endregion

#Update the product variable
$officeProduct = $officeProductInput

#region Switch the input to see what it is and perform the required operation

switch($officeProduct){
    
    #Business Retail
    1 { Update-XMLFile -product "O365BusinessRetail" -bit $bitVersion}
    #ProPlus
    2 { Update-XMLFile -product "O365ProPlusRetail" -bit $bitVersion}
    #Visio Std Volume
    3 { Update-XMLFile -product "VisioStd2019Volume" -bit $bitVersion}
    #Visio Pro Volume
    4 { Update-XMLFile -product "VisioPro2019Volume" -bit $bitVersion}
    #Visio Pro Retail
    5 { Update-XMLFile -product "VisioPro2019Retail" -bit $bitVersion}
    #Project Std Volume
    6 { Update-XMLFile -product "ProjectStd2019Volume" -bit $bitVersion}
    #Project Pro Volume
    7 { Update-XMLFile -product "ProjectPro2019Volume" -bit $bitVersion}
    #Project Pro Retail
    8 { Update-XMLFile -product "ProjectPro2019Retail" -bit $bitVersion}
    #Cancel
    "C" {Exit}
    default {Exit}
}

#endregion

#Start the installation
Write-Host "Installing..." -ForegroundColor Green
Start-Installation -bit $bitVersion -xmlName $xmlFile
Write-Host "This window can be closed"
Read-Host

Done!

If you’re wondering what the script looks like as a whole, wonder no longer:

#Variables used for the installation
$bitVersion = ""
$officeProduct = ""
$pathToOffice = "\\sandpdc\software\Office"
$xmlFile = "OfficeXML.xml"
$pathToXMLFile = Join-Path -Path $pathToOffice -ChildPath $xmlFile

#Updates the XML file based on the input
function Update-XMLFile([string]$product, [string]$bit){

    try{
        #Loading the XML document
        [xml]$xmlDoc = Get-Content -Path $pathToXMLFile

        #Edit the document
        $xmlDoc.Configuration.Add.OfficeClientEdition = $bit
        $xmlDoc.Configuration.Add.Product.ID = $product

        #Save the document
        $xmlDoc.Save($pathToXMLFile)
    }catch{
        $errorMessage = $_.Exception.Message
        Write-Host $errorMessage -ForegroundColor Red
        Read-Host "The script encountered the above error - will now exit"
    }
}

#Function to start the installation
function Start-Installation([string]$bit, [string]$xmlName){
    try{
        .\setup.exe /configure $bit\$xmlName
    }catch{
        $errorMessage = $_.Exception.Message
        Write-Host $errorMessage
        Read-Host "The script encountered the above error - will now exit"
    }
}

#Function to check the user wants 32 bit
function Get-Verification(){
    $output = $false

    Write-Host "Are you sure you want to install 32-bit?" -ForegroundColor Red
    Write-Host "All new installs should use 64-bit instead"
    Write-Host "If you want to install 32-bit, complete the test below, otherwise enter the wrong answer"

    $firstNumber = Get-Random -Minimum 1 -Maximum 11
    $secondNumber = Get-Random -Minimum 1 -Maximum 11

    $sumToCheck = $firstNumber + $secondNumber

    $verificationInput = Read-Host "$($firstNumber) + $($secondNumber) = ?"

    if ($verificationInput -eq $sumToCheck){
        Write-Host "Fine! 32-bit will be installed..."
        $output = $true
    }else{
        Write-Host "Finally! 64-bit will be installed"
        $output = $false
    }
    return $output
}

#Clear the screen
Clear-Host

#region Checking if the user wants 64 bit or 32 bit

do{

    Write-Host "Do you want" -NoNewline
    Write-Host " 64-bit " -NoNewline -ForegroundColor Yellow
    Write-Host "or" -NoNewline
    Write-Host " 32-bit " -NoNewline -ForegroundColor Green
    Write-Host "? (64 or 32): " -NoNewline
    $bitVersionInput = (Read-Host).ToUpper()
}while((64 ,32) -notcontains $bitVersionInput)

#endregion

#Check the user definitely wants 32 bit
if ($bitVersionInput -eq "32"){
    if (Get-Verification){
        $bitVersion = $bitVersionInput
    }else{
        $bitVersionInput = "64"
    }
}

#Update the bitVersion variable
$bitVersion = $bitVersionInput

#region Asking what product to install

#Ask the user what product they want to install
Write-Host @"

Please select one product from the below list

"@

Write-Host @"
1) Business Retail
2) ProPlus Retail

"@ -ForegroundColor Cyan

Write-Host @"
3) Visio Std Volume
4) Visio Pro Volume
5) Visio Pro Retail

"@ -ForegroundColor Green

Write-Host @"
6) Project Std Volume
7) Project Pro Volume
8) Project Pro Retail

"@ -ForegroundColor Gray

Write-Host @"
C) Cancel

"@ -ForegroundColor Red

do{
    $officeProductInput = (Read-Host "Enter a number").ToUpper()
}while((1,2,3,4,5,6,7,8, "C") -notcontains $officeProductInput)

#endregion

#Update the product variable
$officeProduct = $officeProductInput

#region Switch the input to see what it is and perform the required operation

switch($officeProduct){
    
    #Business Retail
    1 { Update-XMLFile -product "O365BusinessRetail" -bit $bitVersion}
    #ProPlus
    2 { Update-XMLFile -product "O365ProPlusRetail" -bit $bitVersion}
    #Visio Std Volume
    3 { Update-XMLFile -product "VisioStd2019Volume" -bit $bitVersion}
    #Visio Pro Volume
    4 { Update-XMLFile -product "VisioPro2019Volume" -bit $bitVersion}
    #Visio Pro Retail
    5 { Update-XMLFile -product "VisioPro2019Retail" -bit $bitVersion}
    #Project Std Volume
    6 { Update-XMLFile -product "ProjectStd2019Volume" -bit $bitVersion}
    #Project Pro Volume
    7 { Update-XMLFile -product "ProjectPro2019Volume" -bit $bitVersion}
    #Project Pro Retail
    8 { Update-XMLFile -product "ProjectPro2019Retail" -bit $bitVersion}
    #Cancel
    "C" {Exit}
    default {Exit}
}

#endregion

#Start the installation
Write-Host "Installing..." -ForegroundColor Green
Start-Installation -bit $bitVersion -xmlName $xmlFile
Write-Host "This window can be closed"
Read-Host

 

Quering and Adding Info To Access Database Using C#

In this post, I will show you how I created a program to extract and add data to an Access database. Before we get started, you can see my current specifications below:

Getting values from a table:

Using System.Data.OleDB;

//Create a new list to hold all the values
List<String> values = new List<String>();

//Build the connection string and SQL string
string connectionString = @$"Provider=Microsoft.ACE.OLEDB;Data Source = C:\Path\To\Access.accdb";
string sqlString = "SELECT * FROM Table_Name";

//Create a new connection to the Access file
using (OleDbConnection connection = new OleDbConnection(connectionString)){

    //Creating a new command
    OleDbCommand command = new OleDbCommand(sqlString, connection);
    
    //Try/catch to catch errors, DON'T DO THIS IN SERIOUS PROJECTS!
    try{
        
        //Opening the connection and reading the data
        connection.Open();
        using(OleDbDataReader reader = command.ExecuteReader()){
            while(reader.Read()){
                
                //Adding the value to the values list
                values.Add(reader["Field_Name"].ToString());
            }
        }
    }catch{ }
    
    //Closing the connection
    connection.Close();
}

//Sorting the list in ascending order
values.Sort();

 

Adding a new row to the table:

Using System.Data.OleDb;

//Building the connection string and SQL string
string connectionString = @$"Provider=Microsoft.ACE.OLEDB.12.0;Data Source = C:\Path\To\Access.accdb";
string sqlString = $"INSERT INTO Table_Name(Field_Name1, Field_Name2) VALUES ('{Field_Value1}','{Field_Value2}')";

//Creating a new connection to the Access file
using (OleDbConnection connection = new OleDbConnection(connectionString))
{
    //Build a new command
    using(OleDbCommand command = new OleDbCommand(sqlString, connection))
    {
        //Open the database connection and execute the write
        connection.Open();
        command.ExecuteReader();
    }
    //Close the database connection
    connection.Close();
}

Enjoy!

Performing Asynchronous Remote WMI Calls C#

This was a problem that I was stuck on for a long time. Originally, I made a program in PowerShell that myself and a couple of other people in the company used to query remote computers to retrieve certain information (SN etc). I wanted to make the move to using WPF with C# simply because of its scale, simplicity and flexibility. The things that I saw being done in WPF with C# was beyond anything I could ever dream with PowerShell.

After making the initial leap, I realised that I would how much code I would have to re-write/re-imagine into C# code. Go figure… One thing that I thought would be simple is performing asynchronous operations but this proved to be something I should use with caution. See, I could simply use Task.Run() and perform the actions, but this is like using a sledgehammer to drive in a nail. After a lot of research, I moved from using this approve to using an observer class to watch the operation and using the QueryInstancesAsync method instead of QueryInstances()

This is the code that I ended up with:

//Used to define what is returned in the async results
public static CimAsyncMultipleResults<CimInstance> GetValues(CimSession _session)
{
    return _session.QueryInstancesAsync(@"root\cimv2", "WQL", "SELECT Username FROM Win32_ComputerSystem");
}

//This watches the async progress
class CimInstanceWatcher : IObserver<CimInstance>
{
    public void OnCompleted()
    {
        Console.WriteLine("Done");
    }

    public void OnError(Exception e)
    {
        Console.WriteLine("Error: " + e.Message);
    }

    public void OnNext (CimInstance value)
    {
        Console.WriteLine("Value: " + value);
    }
}

private static void Main()
{
    //Leaving cimsession creation as sync because is happens "instantly"
    CimSession Session = CimSession.Create("PC-NAME");
    //Creating a new watcher object
    var instanceObject = new CimInstanceWatcher();
    //Subscribing the watcher object to the async call
    GetValues(Session).Subscribe(instanceObject);
    Console.ReadLine();
}

This information was provided from Microsofts forums

Enjoy!

LAPS 3

Okay, this is probably my final update to the whole LAPS thing. I have created two iterations in the past but neither were really groundbreaking or my own design. Not that this update is groundbreaking either though. This is a further update to the below post:

LAPS WinForm 2

I wanted to completely redo my LAPS form (again) to make it my own design, responsive and ultimately better. This is what the final form looks like. It is completely responsive and resizeable:

I will include the source code here but the best place to download this would be from my TechNet gallery.

There are a couple of things you need to change in the form to make it work:

  • Adding your domain controller and domain root to the variables at the top of the script
  • Add your BASE64 data into the BASE64 variable to use your own logo

Heres the code:

#Enter your domain and domain controller below :)
$script:domainController = "DOMAIN CONTROLLER HERE" #E.G domaincontroller.domain.lan
$script:domainRoot = "DOMAIN ROOT HERE" #E.G domain.lan

#LOADING ASSEMBLIES
Add-Type -AssemblyName PresentationFramework, System.Drawing, System.Windows.Forms, WindowsFormsIntegration

#ICON FOR FORM
[string]$base64=@'
BASE64 DATA HERE
'@

#CREATING THE IMAGE FROM BASE64 DATA
$bitmap = New-Object System.Windows.Media.Imaging.BitMapImage
$bitmap.BeginInit()
$bitmap.StreamSource = [System.IO.MemoryStream][System.Convert]::FromBase64String($base64)
$bitmap.EndInit()
$bitmap.Freeze()

#LAPS WINDOW XML
[xml]$LAPSXaml = @"
<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    Title="LAPS UI" Height="400" Width="400" MinHeight="400" MinWidth="400" WindowStartupLocation="CenterScreen">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="2"/>
            <ColumnDefinition/>
            <ColumnDefinition Width="Auto" MinWidth="75"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto" MinHeight="7"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Label Content="ComputerName:" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Grid.Column="1" FontSize="14"/>
        <TextBox Name="Computer_Textbox" VerticalContentAlignment="Center" HorizontalAlignment="Stretch" Grid.Row="1" TextWrapping="Wrap" VerticalAlignment="Stretch" Margin="3" Grid.Column="1" FontSize="14"/>
        <Button Name="Search_Button" Content="Search" Grid.Column="2" HorizontalAlignment="Stretch" Grid.Row="1" VerticalAlignment="Stretch" Margin="0,3,5,3"/>
        <Label Content="Password" Grid.Column="1" HorizontalAlignment="Stretch" Grid.Row="2" VerticalAlignment="Stretch" FontSize="14"/>
        <TextBox Name="Password_Textbox" Grid.Column="1" HorizontalAlignment="Stretch" Grid.Row="3" TextWrapping="Wrap" Margin="3" VerticalAlignment="Stretch" IsReadOnly="True" FontSize="14"/>
        <Button Name="Copy_Button" Content="Copy" Grid.Column="2" HorizontalAlignment="Stretch" Grid.Row="3" Margin="0,3,5,3" VerticalAlignment="Stretch"/>
        <Label Content="Password Expires" Grid.Column="1" HorizontalAlignment="Stretch" Grid.Row="4" VerticalAlignment="Stretch" FontSize="14"/>
        <TextBox Name="Password_Ex_Textbox" Grid.Column="1" IsReadOnly="True" HorizontalAlignment="Stretch" Grid.Row="5" TextWrapping="Wrap" VerticalAlignment="Stretch" Margin="3" FontSize="14"/>
        <Label Content="New Expiration" Grid.Column="1" HorizontalAlignment="Stretch" Grid.Row="6" VerticalAlignment="Stretch" FontSize="14"/>
        <DatePicker Name="Date_Picker" Grid.Column="1" HorizontalAlignment="Stretch" Grid.Row="7" VerticalAlignment="Stretch" Margin="3" FontSize="14"/>
        <Button Name="Set_Button" Content="Set" Grid.Column="2" HorizontalAlignment="Stretch" Grid.Row="7" VerticalAlignment="Stretch" Margin="0,5,5,5"/>
        <GridSplitter IsEnabled="False" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Grid.Row="8" Grid.Column="1" Margin="5,2,5,2" Grid.ColumnSpan="2"/>
        <TextBox Name="Output_Textbox" VerticalScrollBarVisibility="Auto" IsReadOnly="True" HorizontalAlignment="Stretch" Grid.Row="9" TextWrapping="Wrap" Margin="1,5,1,1" VerticalAlignment="Stretch" Grid.ColumnSpan="3" FontSize="12"/>
    </Grid>
</Window>
"@

#LOADING XAML
$LAPSReader=(New-Object System.Xml.XmlNodeReader $LAPSXaml)
$LAPSWindow=[Windows.Markup.XamlReader]::Load($LAPSReader)
$LAPSWindow.Icon = $bitmap

#ASSIGNING CONTROLS
$Computer_Textbox = $LAPSWindow.FindName("Computer_Textbox")
$Search_Button = $LAPSWindow.FindName("Search_Button")
$Password_Textbox = $LAPSWindow.FindName("Password_Textbox")
$Copy_Button = $LAPSWindow.FindName("Copy_Button")
$Password_Ex_Textbox = $LAPSWindow.FindName("Password_Ex_Textbox")
$Date_Picker = $LAPSWindow.FindName("Date_Picker")
$Set_Button = $LAPSWindow.FindName("Set_Button")
$Output_Textbox = $LAPSWindow.FindName("Output_Textbox")

#FUNCTION TO SET OUTPUT TEXTBOX
function set-output-textbox{
    param(
        [string]$value,
        [bool]$date
    )
    if ($date){
        $Output_Textbox.Text = ("[$(Get-Date)] - $value `r`n")
    }else{
        $Output_Textbox.Text = $value
    }
}

#FUNCTION TO UPDATE OUTPUT TEXTBOX
function update-output-textbox{
    param(
        [string]$value,
        [bool]$date
    )
    if ($date){
        $Output_Textbox.AppendText("[$(Get-Date)] - $value `r`n")
    }else{
        $Output_Textbox.AppendText("     $value `r`n")
    }
    $Output_Textbox.ScrollToEnd()
}

#FUNCTION TO UPDATE FORM
function update-form{
    [System.Windows.Forms.Application]::DoEvents()
}

#FUNCTION TO UPDATE PASSWORD TEXTBOX
function update-password-textbox($value){
    $Password_Textbox.Text = $value
}

#FUNCTION TO UPDATE PASSWORD EX TEXTBOX
function update-passwordex-texbox($value){
    $Password_Ex_Textbox.Text = $value
}

#FUNCTION TO SET CONTROLS
function set-controls{
    param(
        [bool]$switcher,
        [bool]$setswitcher
    )
    $Search_Button.IsEnabled = $switcher
    $Set_Button.IsEnabled = $setswitcher
    $Date_Picker.IsEnabled = $setswitcher
}

#DECIDE IF COPY BUTTON SHOULD BE ENABLED
$Copy_Button.IsEnabled = $false
$Password_Textbox.Add_TextChanged({
    if ($Password_Textbox.Text.Length -gt 0){
        $Copy_Button.IsEnabled = $true
    }else{
        $Copy_Button.IsEnabled = $false
    }
})

#MAKING COMPUTER NAME UPPERCASE ON FOCUS LOST
$Computer_Textbox.Add_LostFocus({
    $Computer_Textbox.Text = $Computer_Textbox.Text.ToUpper()
})

#COPY BUTTON LOGIC
$Copy_Button.Add_Click({
    Set-Clipboard -Value $Password_Textbox.Text
})

#COMPUTER TEXTBOX KEYDOWN LOGIC
$Computer_Textbox.Add_KeyDown({
    if ($args.Key -eq 'Enter'){
        $Search_Button.RaiseEvent((New-Object -TypeName System.Windows.RoutedEventArgs $([System.Windows.Controls.Button]::ClickEvent)))
    }
})

#DISABLING CONTROLS ON FORM LOAD
set-controls -switcher $true -setswitcher $false

#WELCOME MESSAGE ON FORM LOAD
$Output_Textbox.HorizontalContentAlignment="Center"
$Output_Textbox.VerticalContentAlignment="Center"
set-output-textbox -date $false -value "Welcome to version 3 of this form! It is now responsive and a lot cleaner in the background. Nothing you ever had to worry about though :)"

#SEARCH BUTTON LOGIC
$Search_Button.Add_Click({

    #DISABLING CONTROLS ON BUTTON PRESS
    $Output_Textbox.HorizontalContentAlignment="Left"
    $Output_Textbox.VerticalContentAlignment="Top"
    set-controls -switcher $false -setswitcher $false
    update-password-textbox -value $null
    update-passwordex-texbox -value $null
    $Date_Picker.Text = $null

    if ($Computer_Textbox.Text.Length -le 0){
        #OUTPUT IF EMPTY SEARCH AND ENABLING CONTROLS
        set-output-textbox -date $true -value "Input cannot be empty"
        set-controls -switcher $true -setswitcher $false    
    }else{
        set-output-textbox -date $true -value "Please Wait"
        
        #PUTTING INPUT INTO VARIABLE
        $script:computerName = $Computer_Textbox.Text

        #CREATING A SYNCHRONISED HASHTABLE
        $script:syncHash = [hashtable]::Synchronized(@{})

        #CREATING SEARCH RUNSPACE
        $searchRunspace = [runspacefactory]::CreateRunspace()
        $searchRunspace.ApartmentState = "STA"
        $searchRunspace.ThreadOptions = "ReuseThread"
        $searchRunspace.Open()
        $searchRunspace.SessionStateProxy.SetVariable("syncHash",$syncHash)
        $searchRunspace.SessionStateProxy.SetVariable("computerName",$computerName)
        $searchRunspace.SessionStateProxy.SetVariable("domainController",$domainController)

        #POWERSHELL TO BE RAN IN RUNSPACE
        $searchPowerShell = [powershell]::Create().AddScript({
            $syncHash.searchADComputer = Get-ADComputer -Identity $computerName
            $syncHash.searchInvoke = Invoke-Command -ComputerName $domainController -ScriptBlock { Get-AdmPwdPassword -ComputerName $args[0] } -ArgumentList $computerName | Select-Object Password, ExpirationTimeStamp
        })

        #ASSIGNING RUNSPACE TO POWERSHELL
        $searchPowerShell.Runspace = $searchRunspace
        #STARTING THE RUNSPACE AND POWERSHELL
        $searchObject = $searchPowerShell.BeginInvoke()

        #REFRESHING UNTIL POWERSHELL IS COMPLETE
        do{
            Start-Sleep -Milliseconds 100
            update-form
        }while (!$searchObject.IsCompleted)

        #ENDING POWERSHELL INVOKE AND DISPOSING OF RUNSPACE
        $searchPowerShell.EndInvoke($searchObject)
        $searchPowerShell.Dispose()
    
        if ($syncHash.searchADComputer){
            #COMPUTER IS FOUND ON DOMAIN
            if ($syncHash.searchInvoke){
                #INVOKE SUCCESSFUL
                $admpwdPassword = $syncHash.searchInvoke.password
                $admpwdPasswordExpiration = $syncHash.searchInvoke.ExpirationTimeStamp
                $admpwdPasswordExpirationFormatted = $admpwdPasswordExpiration.ToString("dd/MM/yyyy hh:mm:ss")

                #UPDATING FIELDS
                update-output-textbox -date $true -value "Information retrieved"
                update-password-textbox -value $admpwdPassword
                update-passwordex-texbox -value $admpwdPasswordExpirationFormatted
                set-controls -switcher $true -setswitcher $true
            }else{
                #INVOKE FAILED
                update-output-textbox -date $true -value "Failded to retrieve password information"
                update-password-textbox -value $null
                update-passwordex-texbox -value $null
                set-controls -switcher $true -setswitcher $false
            }
        }else{
            #COMPUTER NOT FOUND ON DOMAIN
            update-output-textbox -date $true -value "Host not found on domain"
            update-password-textbox -value $null
            update-passwordex-texbox -value $null
            set-controls -switcher $true -setswitcher $false
        }
    }
})

#SET EXPIRATION BUTTON LOGIC
$Set_Button.Add_Click({
    
    #DISABLING CONTROLS ON BUTTON PRESS
    set-controls -switcher $false -setswitcher $false

    if ($Date_Picker.Text.Length -le 0){
        #OUTPUT IF EMPTY DATE AND ENABLING CONTROLS
        update-output-textbox -date $true -value "No date selected"
        set-controls -switcher $true -setswitcher $true
    }else{
        #GETTING NEW DATES FOR EXPIRATION
        $newExpirationString = $Date_Picker.SelectedDate.ToString("MM/dd/yyyy")
        $script:newExpirationDate = [datetime]::ParseExact($newExpirationString, 'MM/dd/yyyy', $null)
        
        #OUTPUTTING FRIENDLY EXPIRATION TO OUTPUT TEXTBOX
        update-output-textbox -date $true -value "Setting expiration to $newExpirationString..."

        #CREATING SEARCH RUNSPACE
        $setRunspace = [runspacefactory]::CreateRunspace()
        $setRunspace.ApartmentState = "STA"
        $setRunspace.ThreadOptions = "ReuseThread"
        $setRunspace.Open()
        $setRunspace.SessionStateProxy.SetVariable("syncHash",$syncHash)
        $setRunspace.SessionStateProxy.SetVariable("computerName",$computerName)
        $setRunspace.SessionStateProxy.SetVariable("domainController",$domainController)
        $setRunspace.SessionStateProxy.SetVariable("newExpirationDate",$newExpirationDate)

        #POWERSHELL TO BE RAN IN RUNSPACE
        $setPowerShell = [powershell]::Create().AddScript({
            try{
                $syncHash.setInvoke = Invoke-Command -ComputerName $domainController -ScriptBlock {Reset-AdmPwdPassword -ComputerName $args[0] -WhenEffective $args[1] } -ArgumentList $computerName, $newExpirationDate -ErrorAction Stop
                try{
                    Invoke-GPUpdate -Computer $computerName -ErrorAction Stop
                    $syncHash.setGPUpdate = $true
                }catch{
                    #GP UPDATE FAILED
                    $syncHash.setGPUpdate = $null
                }
            }catch{
                #CHANGING EXPIRATION FAILED
                $syncHash.setInvoke = $null
            }
        })

        #ASSIGNING RUNSPACE TO POWERSHELL
        $setPowerShell.Runspace = $setRunspace
        #STARTING THE RUNSPACE AND POWERSHELL
        $setObject = $setPowerShell.BeginInvoke()

        #REFRESHING UNTIL POWERSHELL IS COMPLETE
        do{
            Start-Sleep -Milliseconds 100
            update-form
        }while (!$setObject.IsCompleted)

        #ENDING POWERSHELL INVOKE AND DISPOSING OF RUNSPACE
        $setPowerShell.EndInvoke($setObject)
        $setPowerShell.Dispose()

        #CHECKING PASSWORD EXPIRATION SUCCESS
        if ($syncHash.setInvoke){
            update-output-textbox -date $true -value "Successfully reset password expiration date"
            #CHECKING GP UPDATE SUCCESS
            if ($syncHash.setGPUpdate){
                update-output-textbox -date $true -value "Succesfully ran GP update"
            }else{
                update-output-textbox -date $true -value "Failed to run GP update, this is probably due to permissions"
            }
        }else{
            update-output-textbox -date $true -value "Failed to reset password expiration date"
        }

        #RESETTING CONTROLS
        set-controls -switcher $true -setswitcher $true
    }
})

#CHECK FOR AD MODULE AND TEST IF ON LOCAL DOMAIN/NETWORK
if ( Test-Connection $domainRoot -Count 1 -Quiet){
    #DOMAIN IS ACCESSIBLE
    if (Get-Module -List ActiveDirectory ){
        #AD MODULE INSTALLED
        #FORM WILL BE DISPLAYED WITHOUT ANY MODIFICATIONS
    }else{
        #AD MODULE NOT INSTALLED
        set-output-textbox -date $false -value "Install the AD module and restart"
        set-controls -switcher $false -setswitcher $false
        $Computer_Textbox.IsEnabled = $false
    }
}else{
    #DOMAIN ISN'T ACCESSIBLE
    set-output-textbox -date $false -value "$domainRoot is not accessible"
    set-controls -switcher $false -setswitcher $false
    $Computer_Textbox.IsEnabled = $false
}   

#REMOVING PROCESS ON FORM CLOSE
$LAPSWindow.Add_Closing({
    try{
        $syncHash.Clear() | Out-Null
    }catch{}
    
    Stop-Process -Name "LAPS" -ErrorAction SilentlyContinue
})

#DISPLAY FORM WHILST TESTING
$app = [Windows.Application]::new()
$app.run($LAPSWindow)

Enjoy!

SharePoint 2013 Prerequisite Installer Failing – Unable to Install IIS

Literally been banging my head against the table, wall and floor over this one. I kept getting the error you can see below when trying to run the SharePoint 2013 prerequisite installer:

After a LOT of research and trying a bunch of different “solutions”, all completely failing to fix the issue, I look into the error log file. I know, right. What a nerd!? There is referenced trying to use a “ServerManagerCMD.exe” in the system32 directory. I looked and could only find a “ServerManager.exe”.

“Well shit”, I thought to myself. Microsoft is bloody amazing and SharePoint is the cherry on top of the cake.

So to fix this, I copied the “ServerManager.exe” file and renamed it to be “ServerManagerCMD.exe”:

And surely enough, after closing the installer and retrying, everything went swimmingly!

Enjoy!

Change ownCloud User Home in MYSQL

So I recently created a new ownCloud 10 server to get away from ownCloud 9. This meant creating a new CentOS 7 VM bladybladyblah…

One thing that caught me out, among many to do with ownCloud, was that the original user created during the setup process couldn’t save or view files after I had reconfigured the home directory to be more secure.

After looking in the MYSQL database, I saw that the original user’s home directory had not been updated to match the new path. To check this I used the following commands and looked for the home column:

USE owncloud;
SELECT * FROM oc_accounts;

After those commands, I updated the users home setting by using the following command:

UPDATE oc_accounts SET home="/new/dir/username" WHERE user_id="user";

Nice simple fix for an issue that was driving me up the wall.

Hope you enjoy!

LAPS WinForm 2

New and improved LAPS WinForm because the original one, found here, was kind of crap. It didn’t handle exceptions very well and I don’t think the group policy update worked at all after some further debugging.

I am please to present the new GUI for LAPS:

The best place to download this from would be my TechNet gallery

Enjoy!