Wednesday 25 March 2015

[C#] Progress Bar with Text



using System.Windows.Forms;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.ComponentModel;

public class NewProgressBar : ProgressBar
{
  public NewProgressBar()
  {
    this.SetStyle(ControlStyles.UserPaint, true);
    base.ForeColor = Color.LimeGreen;
    base.BackColor = Color.LightGreen;
  }

  private Font _Font = SystemFonts.DefaultFont;
  private string _Text = "";
  private bool _ShowPercent = true;

  [DefaultValue(typeof(System.Drawing.Color), "LimeGreen")]
  public override Color ForeColor
  {
    get { return base.ForeColor; }
    set { base.ForeColor = value; }
  }

  [DefaultValue(typeof(System.Drawing.Color), "LightGreen")]
  public override Color BackColor
  {
    get { return base.BackColor; }
    set { base.BackColor = value; }
  }

  [Category("Appearance")]
  [Description("The font of the progress text.")]
  [Browsable(true), EditorBrowsable(EditorBrowsableState.Always)]
  [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
  public override Font Font {
    get { return _Font; }
    set
    {
      if (value == null) 
        ResetFont(); 
      else
        _Font = value;
      this.Invalidate();
    }
  }

  public override void ResetFont()
  {
    this.Font = SystemFonts.DefaultFont; 
  }

    private bool ShouldSerializeFont() 
  { 
    return !this.Font.Equals(SystemFonts.DefaultFont); 
  }

  [Category("Appearance")]
  [Description("The text of the progress text.")]
  [Browsable(true), EditorBrowsable(EditorBrowsableState.Always)]
  [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
  public override string Text
  {
    get { return _Text; }
    set
    {
      if (value == null)
        ResetText();
      else
        _Text = value;
      this.Invalidate();
    }
  }

  public override void ResetText()
  {
    this.Text = "";
  }

  private bool ShouldSerializeText()
  {
    return !this.Text.Equals("");
  }
  
  [Category("Appearance")]
  [Description("Show the percentage on the control.")]
  [Browsable(true), EditorBrowsable(EditorBrowsableState.Always)]
  [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
  public bool ShowPercent
  {
    get { return _ShowPercent; }
    set 
    {
      _ShowPercent = value;
      this.Invalidate();
    }
  }

  public void ResetShowPercent()
  {
    this.ShowPercent = true;
  }

  private bool ShouldSerializeShowPercent()
  {
    return !this.ShowPercent.Equals(true);
  }

  protected override void OnPaintBackground(PaintEventArgs pevent)
  {
    // None... Helps control the flicker.
  }

  protected override void OnPaint(PaintEventArgs e)
  {
    const int inset = 2; // A single inset value to control teh sizing of the inner rect.

    using (Image offscreenImage = new Bitmap(this.Width, this.Height))
    {
      using (Graphics offscreen = Graphics.FromImage(offscreenImage))
      {
        Rectangle rect = new Rectangle(0, 0, this.Width, this.Height);

        if (ProgressBarRenderer.IsSupported)
          ProgressBarRenderer.DrawHorizontalBar(offscreen, rect);

        rect.Inflate(new Size(-inset, -inset)); // Deflate inner rect.
        rect.Width = (int)(rect.Width * ((double)this.Value / this.Maximum));
        if (rect.Width == 0) rect.Width = 1; // Can't draw rec with width of 0.

        LinearGradientBrush brush = new LinearGradientBrush(rect, this.BackColor, this.ForeColor, LinearGradientMode.Vertical);
        offscreen.FillRectangle(brush, inset, inset, rect.Width, rect.Height);

        e.Graphics.DrawImage(offscreenImage, 0, 0);
        offscreenImage.Dispose();

        int percent = (int)(((double)(this.Value - this.Minimum) / (double)(this.Maximum - this.Minimum)) * 100);

        using (Graphics gr = this.CreateGraphics())
        {
          string text = this.Text.ToString();
          string percentText = "";

          if (_ShowPercent)
          {
            if (gr.MeasureString(text + " (100%)", _Font).Width > this.Width)
            {
              while (gr.MeasureString(text + "... (100%)", _Font).Width > this.Width)
                text = text.Substring(0, text.Length - 1);

              text += "...";
            }

            percentText = text + " (" + percent.ToString() + "%)";
          }
          else
          {
            if (gr.MeasureString(text, _Font).Width > this.Width)
            {
              while (gr.MeasureString(text + "...", _Font).Width > this.Width)
                text = text.Substring(0, text.Length - 1);

              text += "...";
            }

            percentText = text;
          }

          gr.DrawString(percentText,
            _Font,//SystemFonts.DefaultFont,
            Brushes.Black,
            new PointF(this.Width / 2 - (gr.MeasureString(percentText, _Font).Width / 2.0F),
            this.Height / 2 - (gr.MeasureString(percentText, _Font).Height / 2.0F)));
        }
      }
    }
  }
}

Friday 20 March 2015

[PS] Assign Office 365 Licenses Based on Group Membership

In order to easily license my staff and student users for Office 365, I have developed a PowerShell script to license users based on their group membership, and thought that I would share it with you all.

What the script does:
  1. Connects to Office 365
  2. Loops through the script's specified groups (Student/Staff)
  3. For each specified group, it:
    1. Populates a list of members of that group that are currently unlicensed.
    2. Checks to see if there are enough licences available, and warns you if there is not (continues regardless)
    3. Assigns each found user the specified licenses
    4. Sets the UsageLocation for each user to "AUSTRALIA"
    5. Waits for the users' mailboxes to become ready (can take a few minutes from when the license is applied), and then sets their TimeZone to "AUS Eastern Standard Time" and language to "English (Australia)"
    6. If their username (UPN) is "<AdUsername>@<SchoolName>.onmicrosoft.com" it'll change it to "<AdUsername>@<SchoolDomain>"
Requirements:
  • Requires PowerShell 3.0+
  • Requires the "Azure Active Directory Module" for PowerShell installed (link is in the script's comment header)
  • You'll also need a Global Admin account for your school's Office 365 environment to connect
Notes:
  • Before running the script, please ensure that you have your staff/student groups synced with Office 365 
  • Also make sure that you have the specified licenses in your Office 365 account (the script by default uses the "Office 365 Education for Students" and "Office 365 Education for Faculty" licenses you get when a staff/student signs up for ProPlus)
  • For more detailed output, run the script with the -Verbose parameter, and it'll display what it's doing instead of just listing the users that it's processing
  • The script below is set to enable the full package for Staff, but to enable all except Lync for students. Modify the DisabledPlans part on lines 123 and 145 if you want yours different.
Changes you need to do to the script:
  • On line 43, you will need to specify your school's Office 365 admin email address to connect to your Office 365 environment
  • On line 92 , you will need to enter your tenant name from Office 365 (used for license skus)
  • On line 93, you will need to enter your school's domain name (to "fix" your users' usernames)
  • On lines 96 and 97, you will need to enter your staff and student groups (that your staff and students are direct members of- nested groups probably won't work)
Example output (normal):
PS C:\Windows\system32> D:\Scripts\PowerShell\AssignOffice365Licenses-New.ps1
Please enter in your Office365 admin credentials ...
Connecting to Office365 ...
WARNING: The names of some imported commands from the module 'tmp_wh33y2z1.1io' include unapproved verbs that might make them less discoverable. To find the commands with unapproved verbs, run the Import-Module command again with the Verbose parameter. For a list of approved verbs, type Get-Verb.

ModuleType Version    Name                                ExportedCommands                                                                                                                    
---------- -------    ----                                ----------------                                                                                                                    
Script     1.0        tmp_wh33y2z1.1io                    {Add-AvailabilityAddressSpace, Add-DistributionGroupMember, Add-MailboxFolderPermission, Add-MailboxPermission...}                  
Connected.

Processing Faculty users in group GG_Staff...
    No unlicensed Faculty users found.

Processing Student users in group GG_Students...
Assigning licenses to Student users...
    Processing test@contoso.com...
Done.

Setting regional information for Student users...
    Processing test@contoso.com...
Done.

Example output (verbose):
PS C:\Windows\system32> D:\Scripts\PowerShell\AssignOffice365Licenses-New.ps1 -Verbose
Please enter in your Office365 admin credentials ...
Connecting to Office365 ...
WARNING: The names of some imported commands from the module 'tmp_wh33y2z1.1io' include unapproved verbs that might make them less discoverable. To find the commands with unapproved verbs, run the Import-Module command again with the Verbose parameter. For a list of approved verbs, type Get-Verb.

ModuleType Version    Name                                ExportedCommands                                                                                                                    
---------- -------    ----                                ----------------                                                                                                                    
Script     1.0        tmp_wh33y2z1.1io                    {Add-AvailabilityAddressSpace, Add-DistributionGroupMember, Add-MailboxFolderPermission, Add-MailboxPermission...}                  
Connected.

Processing Faculty users in group GG_Staff...
    No unlicensed Faculty users found.

Processing Student users in group GG_Students...
VERBOSE: Found 1 Student users.

VERBOSE: Checking license availability...
VERBOSE:     Done.
Assigning licenses to Student users...
    Processing test@contoso.com...
VERBOSE: Setting UsageLocation to AU...
VERBOSE:     Success!
VERBOSE: Applying Student Office 365 Education license...
VERBOSE:     Success!

Done.

Setting regional information for Student users...
    Processing test@contoso.com...
VERBOSE: Waiting for user's mailbox to be created (may take a few minutes)...
VERBOSE:     Mailbox ready
VERBOSE: Updating TimeZone information...
VERBOSE:     Success!
Done. 

Code:
<#
.SYNOPSIS
This script assigns Office 365 licenses to unlicensed users in certain groups, and sets their regional settings.

.DESCRIPTION
This script gets a list of all unlicensed users in each group specified in $Groups.
For each unlicensed user in that group, it assigns them licenses specified by that group's "LicenseSKU" section.
After the licenses are assigned, it then sets their regional information such as "UsageLocation" and "TimeZone".
If your local domain name is not the same as your external domain name, and the user has been given a
<companyname>.onmicrosoft.com UPN, this script will also change that user's UPN to match that of your external domain.

.NOTES
Author      : Robert Brandon - Kurnai College
Created     : 13/03/2015
Last Edited : 20/03/2015
Requires    : Azure Active Directory Module (MSOnline)
              PowerShell v3.0+

.LINK
"Wait for mailbox" code based on: http://www.edugeek.net/forums/cloud-services/127364-office-365-licensing-powershell-query.html#post1160518
"License by group" code based on: http://365lab.net/2014/04/15/office-365-assign-licenses-based-on-groups-using-powershell/
Azure Active Directory Module   : https://support.office.com/en-ca/article/Windows-PowerShell-cmdlets-for-Office-365-06a743bb-ceb6-49a9-a61d-db4ffdf54fa6
#>

#Requires -Version 3.0
#Requires -Modules MSOnline

# Import verbosity settings
[CmdletBinding()]
param()

# Set Verbosity to disabled during the connection attempt (otherwise several lines of imported commands will be listed)
$oldverbose = $VerbosePreference
$VerbosePreference = "SilentlyContinue"

# Import required module
Import-Module MSOnline

# Try connecting to Office365.
Try {
  # This part prompts you for your Office365 admin credentials.
  Write-Output "Please enter in your Office365 admin credentials ..."
  $LiveCred = Get-Credential -Credential "admin@yourschool.vic.edu.au"  # Set your Office 365 admin username here.
  
  
  Write-Output "Connecting to Office365 ..."
  Try {
    # Try to connect using auto-detect proxy settings
    $proxysettings = New-PSSessionOption -ProxyAccessType AutoDetect
    $Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://ps.outlook.com/powershell/ -Credential $LiveCred -Authentication Basic -AllowRedirection -SessionOption $proxysettings  -ErrorAction 'Stop' -WarningAction "SilentlyContinue"
  } catch {
    # Error connecting with proxy, try direct.
    $Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://ps.outlook.com/powershell/ -Credential $LiveCred -Authentication Basic -AllowRedirection  -ErrorAction 'Stop' -WarningAction "SilentlyContinue"
  }
  Import-PSSession $Session -AllowClobber -WarningAction "SilentlyContinue"
  connect-msolservice -credential $LiveCred -WarningAction "SilentlyContinue"
  Write-Output "Connected.`r`n"
} catch {
  # There was an error connecting. Notify the user.
  Write-Warning "Unable to Connect to Office 365. Did you enter the details correctly?`r`n$_"
  Write-Output ""
  
  # Uncomment this to prompt the user before exiting
  #if (-Not $psISE) {
    ## Wait for user to press key before exiting (prevents window from auto-closing).
    #Write-Output "Finished. Press any key to continue ..."
    #$x = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
  #}
  
  exit
}

# Return verbosity to its previous state
$VerbosePreference = $oldverbose

# Set verbose text colour to cyan on black if not running in PowerShell ISE (to make it different to the warning colour)
if (-Not $psISE) {
  $HostData = (Get-Host).PrivateData
  $OldVerboseBackColor = $HostData.VerboseBackgroundColor
  $OldVerboseForeColor = $HostData.VerboseForegroundColor
  $HostData.VerboseBackgroundColor = "black"
  $HostData.VerboseForegroundColor = "cyan"
}

# Set Script Variables.
# Regional Settings:
$UsageLocation        = 'AU'
$TimeZone             = 'AUS Eastern Standard Time'
$Language             = 3081 # 3081 = English (Australia)

# Company Info:
$CompanyName          = 'yourschool'            # Set this to your Tenant Name
$UPN                  = 'yourschool.vic.edu.au' # Set this to your external domain name

# Group Info:
$StudentGroup         = 'GG_Students' # Set this to your student group
$FacultyGroup         = 'GG_Staff'    # Set this to your staff group

# Available Licenses:
#   NOTE: Full SkuId will be "<CompanyName>:<License>_<UserType>".
#   (E.g. "yourschool:STANDARDWOFFPACK_IW_STUDENT" for a Student license)
$Office365Pack        = "STANDARDWOFFPACK_IW"
$E1                   = "STANDARDWOFFPACK"

# License Options:
$SharepointStandard   = 'SHAREPOINTSTANDARD_EDU'
$Office365OnlineApps  = 'SHAREPOINTWAC_EDU'
$Lync                 = 'MCOSTANDARD'
$Yammer               = 'YAMMER_EDU'
$Exchange             = 'EXCHANGE_S_STANDARD'
$ProPlus              = 'OFFICESUBSCRIPTION'

# Group->License Assignment
$Groups = @{
  # Student Group
  'Student' = @{ 
    # Licenses to apply to this group. Add/Remove as needed.
    #   NOTE: List them in the order you want them assigned.
    #         - Avoid any licence conflicts by adding license options to DisabledPlans to disable them.
    'LicenseSKU' = [ordered]@{
      'Office 365 Education' = @{
        LicenseSkuId  = "${CompanyName}:${Office365Pack}_STUDENT"
        DisabledPlans = @($Lync)
      }

      # Sample example of a second license that would apply after the above
      # NOTE: This sample excludes the products also available in the 'Office 365 Education' license to avoid license conflicts.
      #'E1 Lync' = @{
      #  LicenseSkuId  = "${CompanyName}:${E1}_STUDENT"
      #  DisabledPlans = @($SharepointStandard, $Office365OnlineApps, $Exchange)
      #}
    }
                
    GroupName = $StudentGroup
  }                           
  
  # Faculty Group
  'Faculty' = @{ 
    # Licenses to apply to this group. Add/Remove as needed.
    #   NOTE: List them in the order you want them assigned.
    #         Avoid any licence conflicts by adding the license options to DisabledPlans
    'LicenseSKU' = [ordered]@{
      'Office 365 Education' = @{
        LicenseSkuId  = "${CompanyName}:${Office365Pack}_FACULTY"
        DisabledPlans = @()
      }
    }
                
    GroupName = $FacultyGroup
  }
}

# Loop through each group to assign licenses to their members:
foreach ($group in $Groups.Keys) {
  $GroupName  = $Groups[$group].GroupName
  $GroupID    = (Get-MsolGroup -All | Where-Object {$_.DisplayName -eq $GroupName}).ObjectId
 
  Write-Output "Processing $group users in group $GroupName..."
  Try {
    $GroupMembers = (Get-MsolGroupMember -GroupObjectId $GroupID -All -ErrorAction 'Stop' | Where-Object {$_.IsLicensed -eq $false}).EmailAddress
  } catch {
    Write-Warning "Unable to get members of the ""$GroupName"" group. Please check that it is spelled correctly, and synced to Office 365."
    Write-Output ""
    continue
  }
 
  # Continue loop if no unlicensed users were found.
  if ($GroupMembers.Count -eq 0) {
    Write-Output "    No unlicensed $group users found."
    Write-Output ""
    continue
  }
  
  # At least 1 unlicensed user was found in the current group.
  # Loop through the licenses for this group, and check if there are enough available.
  Write-Verbose "Found $($GroupMembers.Count) $group users.`r`n "
  Write-Verbose "Checking license availability..."
  foreach ($license in $Groups[$group]['LicenseSKU'].Keys) {
    $LicenseSKU     = Get-MsolAccountSku | Where-Object {$_.AccountSKUID -eq $Groups[$group]['LicenseSKU'][$license].LicenseSkuId}
    $LicenseOptions = New-MsolLicenseOptions -AccountSkuId $Groups[$group]['LicenseSKU'][$license].LicenseSkuId -DisabledPlans $Groups[$group]['LicenseSKU'][$license].DisabledPlans -ErrorAction Stop -WarningAction Stop
    
    $Groups[$group]['LicenseSKU'][$license].Add('LicenseOptions', $LicenseOptions)
    
    if ($LicenseSKU.ActiveUnits - $LicenseSKU.ConsumedUnits -lt $GroupMembers.Count) { 
      Write-Warning "Not enough $group $license licenses for all users (need $($GroupMembers.Count), but only have $($LicenseSKU.ActiveUnits - $LicenseSKU.ConsumedUnits) available), please remove assigned user licenses or obtain more."
    }
  }
  Write-Verbose "    Done."
  
  # For each user in the current group, assign them licenses.
  Write-Output "Assigning licenses to $group users..."
  foreach ($User in $GroupMembers) {
    Write-Output "    Processing ${User}..."
    
    # Set the user's UsageLocation (required to assign licenses). This helps with where to physically store the user's data.
    Write-Verbose "Setting UsageLocation to $UsageLocation..."
    Try {
      Set-MsolUser -UserPrincipalName $User -UsageLocation $UsageLocation -ErrorAction Stop -WarningAction Stop
      Write-Verbose "    Success!"
    } catch {
      Write-Warning "    Error setting UsageLocation for ${User}: $_"
    }
    
    foreach ($license in $Groups[$group]['LicenseSKU'].Keys) {
      Write-Verbose "Applying $group $license license..."
      Try {
        Set-MsolUserLicense -UserPrincipalName $User -AddLicenses $Groups[$group]['LicenseSKU'][$license].LicenseSkuId -LicenseOptions $Groups[$group]['LicenseSKU'][$license].LicenseOptions -ErrorAction Stop -WarningAction Stop
        Write-Verbose "    Success!"
      } catch {
        Write-Warning "    Error assigning $license license to ${User}: $_"
      }
    }
    
    Write-Output ""
  }
  Write-Output "Done."
  Write-Output ""
  
  # Set regional info
  # NOTE: Mailbox code based on: http://www.edugeek.net/forums/cloud-services/127364-office-365-licensing-powershell-query.html#post1160518
  Write-Output "Setting regional information for $group users..."
  foreach ($User in $GroupMembers) {
    Write-Output "    Processing ${User}..."
    $sleeptime    = 5 #number of seconds to sleep per loop
    $maxattempts  = 60 #number of times to loop before giving up - 5 minutes
    $attempts     = 0
    $testresult   = ""
    
    # Wait for the user's mailbox to be created before continuing (otherwise the TimeZone change will fail).
    Write-Verbose "Waiting for user's mailbox to be created (may take a few minutes)..."
    
    # Test if the mailbox is connected yet
    $testresult = (Test-MapiConnectivity $User -ErrorAction "SilentlyContinue" -WarningAction "SilentlyContinue").Result
    
    # Keep repeating the test until the mailbox is ready, or the number of max attempts has been reached.
    While (($testresult -ne "Success") -and ($attempts -lt $maxattempts) ) { 
      Sleep -s $sleeptime;
      $testresult = (Test-MapiConnectivity $User -ErrorAction "SilentlyContinue" -WarningAction "SilentlyContinue").Result
      $attempts++ 
    }
    
    if ( $testresult -eq "Success" ) {
      Write-Verbose "    Mailbox ready"
      
      Write-Verbose "Updating TimeZone information..."
      Try {
        Set-MailboxRegionalConfiguration -Identity $User -Language $Language -TimeZone $TimeZone
        Write-Verbose "    Success!"
      } catch {
        Write-Warning "    Error setting time-zone for ${User}: $_"
      }
    } else {
      Write-Warning "    $User mailbox not ready. Unable to set TimeZone"
    }
    
    # Update user's UPN, if needed.
    Try {
      $UserUPN = $User.Split("@")[1]
      
      if ($UserUPN.ToLower() -ne $UPN.ToLower()) {
        Write-Verbose "Fixing user's UPN..."
        $NewUPN       = $User.Split("@")[0].ToLower() + "@" + $UPN.ToLower()
        $UserObjectId = (Get-MsolAccountSku | Where-Object {$_.UserPrincipalName -eq $User}).ObjectId
        
        Set-MsolUserPrincipalName -ObjectId $UserObjectId -NewUserPrincipalName $NewUPN
        Write-Verbose "    Success! Updated UPN from $User to $NewUPN"
      }
    } catch {
      Write-Warning "    Error updating ${User}'s UPN: $_"
    }
  }
  
  Write-Output "Done."
  Write-Output ""
}

if (-Not $psISE) {
  # Return verbose text colour to normal.
  $HostData.VerboseBackgroundColor = $OldVerboseBackColor
  $HostData.VerboseForegroundColor = $OldVerboseForeColor

  # Uncomment this to prompt the user before exiting
  ## Wait for user to press key before exiting (prevents window from auto-closing).
  #Write-Output "Finished. Press any key to continue ..."
  #$x = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
}