Lessons learned while configuring the SharePoint Services Connector for FIM 2010 R2

I have now configured many SharePoint Management Agents, and initially I had severe problems finding out which attributes to populate with what. Here is the lessons I learned during this investigation.

Application ID

During configuration of the Management Agent, you are requested to input Application ID. I have never used it, and i guess it is used when you have multiple User Profile Service Applications.

Anchor

Do not bother with anchors. Instead just provision a connector space object and let it get the default anchor. You will never see the anchor anywhere except in FIM and internally in the SharePoint databases.

Manager attribute populating bug

There is a bug in SharePoint, where the manager attribute won’t be populated in the User Profile Service, even though you are flowing it with FIM. The reason is that the timer job “User Profile Service Application – User Profile ActiveDirectory Import Job” is not created if you configure “Enable External Identity Manager” directly. Instead, you have to first choose “Use SharePoint Active Directory Import” on the “Configure Synchronization Settings”, and let this job be created (takes 15 minutes), then switch to “Enable External Identity Manager”.

Parallellism

It is not supported to run multiple SharePoint MAs simultaneously. Not sure why, but a little bit of code snooping shows this is true.

Pictures

Pictures can be a bit difficult, especially when trying with limited permissions. First of all, if you use fiddler the attribute is actually called “PictureURL”. Also, technically it seems as though what actually happens when you use this connector and export a picture, you transfer the binary data (as base64 ofc) out in “PictureURL / Picture” and the API you talk to uploads these data as an original to your mysite, at the location “http://mysitehost.goodworkaround.com/User photos”. And then it stores the url of the picture in the User Profile Service.

First of all, the MySite host MUST BE IN THE SAME FARM. It is not possible to have pictures uploaded to a separate SharePoint farm. Second, there is a requirement for permissions on the mysitehost. You can grant these permissions with the following cmdlet:


$w = Get-SPWebApplication -Identity http://mysitehost.goodworkaround.com
$w.GrantAccessToProcessIdentity("gwrnd\managementAgentAccount")

If you do not give this permissions, FIM will not get any error message from SharePoint saying “sorry, we could not store this picture”. It will simply be “ok” even though the picture was not saved.

Also, as you can see in this TechNet article you need to run a cmdlet to actually generate the thumbnail photos.

ADFS authentication

To configure ADFS authentication the following attributes needs to be flowed from FIM to SharePoint:

SharePoint attribute Value
SPS-ClaimProviderID Name of the trusted identity provider in SP (case sensitive): “SAML Users”
SPS-ClaimProviderType Constant: “Trusted”
SPS-ClaimID Unique identifier – mail, userPrincipalname, employeeID etc. Must be what comes in the nameidentifier claim from ADFS
SID Do not flow anything
ProfileIdentifier someprefix:unique – where “unique” is the same as SPS-ClaimID (not required, but make it unique)
UserName Do not flow anything
AccountName Do not flow anything

Example user

SharePoint attribute Value
SPS-ClaimProviderID SAML Users
SPS-ClaimProviderType Trusted
SPS-ClaimID marius@goodworkaround.com
SID no flow
ProfileIdentifier gwrnd:marius@goodworkaround.com
UserName no flow
AccountName no flow – SharePoint will automatically populate this with something like “i:0\.t|SAML Users|marius@goodworkaround.com

Windows authentication

To configure Windows authentication the following attributes needs to be flowed from FIM to SharePoint:

SharePoint attribute Value
SPS-ClaimProviderID Constant: “Windows”
SPS-ClaimProviderType Constant: “Windows”
SID ObjectSID from Active Directory
ProfileIdentifier DOMAIN\sAMAccountName from Active Directory
UserName sAMAccountName from Active Directory
AccountName Do not flow anything

Example user

SharePoint attribute Value
SPS-ClaimProviderID Windows
SPS-ClaimProviderType Windows
SID – binary data –
ProfileIdentifier GWRND\marius
UserName marius
AccountName no flow

That’s, hope it saves you some time.

Using your PowerShell profile for something very useful

Have you ever found yourself writing the same PowerShell code over and over, thinking “there should be a built-in function for this”. Here is my trick for an even better PowerShell day! First I’ll show you how to create a PowerShell profile where you can define all of our favorite methods, and then I’ll show you how to use it on many computers, as you will probably want this on your servers as well as your desktop.

Start by opening a PowerShell and type $profile. This default variable contains a path to your PowerShell profile, usually located in the Documents\WindowsPowerShell, which does not exist by default. Run the following two lines to create the folder, and create an empty file.

mkdir (Split-Path $profile) -ErrorAction SilentlyContinue;
if(!(Test-Path $profile))
{
	Set-Content -Path $profile -Value ""
}

After running these you have an empty profile. Use your favorite editor to edit the file.

# PS> ise $profile

The code inside this file will run each time your start a new PowerShell. Here you can define your own methods. What makes this very usefull is the possibility to create a method to download your PowerShell profile from the internet. Here is an example of such a function:

# Function to update the powershellprofile from the internet
function Update-PowerShellProfile() {
    [CmdletBinding()]
    Param()
    Write-Verbose "Updating PowerShell profile"

    if(!(Test-Path (Split-Path $profile))) {
        Write-Verbose ("Profile path did not exist, creating {0}" -f (Split-Path $profile))
        mkdir (Split-Path $profile)
    }

    Write-Debug "Creating System.Net.WebClient"
    $wc = New-Object System.Net.WebClient
    Write-Debug "Downloading file http://pastebin.com/raw.php?i=tdySrDgz"
    $wc.DownloadFile("http://pastebin.com/raw.php?i=tdySrDgz", $profile)

    Write-Output "Reload profile with the cmdlet:  . `$profile"
}

Basically it downloads a some stuff from pastebin and puts into the PowerShell profile. After this, you can either open a new PowerShell to run the profile again, or you can type “. $profile” to re-load the profile.

Here is an example of a full profile (a subset of methods I have in mine). I have my own PowerShell profile hosted in my Dropbox folder, but you choose wherever you want, just change the Update-PowerShellProfile method.


function Connect-ExchangeOnline{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$True,Position=0)]
        [System.Management.Automation.PSCredential]$Credentials
    )
    $Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://ps.outlook.com/powershell -Authentication Basic -AllowRedirection -Credential $Credentials
    Import-PSSession $session -DisableNameChecking
}

function Disconnect-ExchangeOnline {
    [CmdletBinding()]
    Param()
    Get-PSSession | ?{$_.ComputerName -like "*outlook.com"} | Remove-PSSession
}

# Function to update the powershellprofile from the internet
function Update-PowerShellProfile() {
    [CmdletBinding()]
    Param()
    Write-Verbose "Updating PowerShell profile"

    if(!(Test-Path (Split-Path $profile))) {
        Write-Verbose ("Profile path did not exist, creating {0}" -f (Split-Path $profile))
        mkdir (Split-Path $profile)
    }

    Write-Debug "Creating System.Net.WebClient"
    $wc = New-Object System.Net.WebClient
    Write-Debug "Downloading file http://pastebin.com/raw.php?i=tdySrDgz"
    $wc.DownloadFile("http://pastebin.com/raw.php?i=tdySrDgz", $profile)

    Write-Output "Reload profile with the cmdlet:  . `$profile"
}

# Set the PowerShell prompt to PS>
function prompt{
    Write-Host -ForegroundColor Red "PS" -NoNewline
    Write-Host -ForegroundColor White -NoNewline ">"
    return " "
}



function ConvertTo-Base64
{
    [CmdletBinding(DefaultParameterSetName='String')]
    [OutputType([String])]
    Param
    (
        # String to convert to base64
        [Parameter(Mandatory=$true,
                   ValueFromPipeline=$true,
                   ValueFromRemainingArguments=$false,
                   Position=0,
                   ParameterSetName='String')]
        [ValidateNotNull()]
        [ValidateNotNullOrEmpty()]
        [string]
        $String,

        # Param2 help description
        [Parameter(ParameterSetName='ByteArray')]
        [ValidateNotNull()]
        [ValidateNotNullOrEmpty()]
        [byte[]]
        $ByteArray
    )

    if($String) {
        return [System.Convert]::ToBase64String(([System.Text.Encoding]::UTF8.GetBytes($String)));
    } else {
        return [System.Convert]::ToBase64String($ByteArray);
    }
}



function ConvertFrom-Base64 {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$True,
                   Position=0,
                   ValueFromPipeline=$true)]
        [ValidateNotNull()]
        [ValidateNotNullOrEmpty()]
        [string]
        $Base64String
    )

    return [System.Text.Encoding]::UTF8.GetString(([System.Convert]::FromBase64String($Base64String)));
}






function Split-String
{
    [CmdletBinding()]
    [OutputType([string[]])]
    Param
    (
        # The input string object
        [Parameter(Mandatory=$true,
                   ValueFromPipeline=$true,
                   Position=0)]
        [String] $InputObject,

        # Split delimiter
        [Parameter(Mandatory=$false,
                   ValueFromPipeline=$false,
                   Position=1)]
        [String] $Delimiter = "`n",

        # Do trimming or not
        [Parameter(Mandatory=$false,
                   ValueFromPipeline=$false,
                   Position=2)]
        [Boolean] $Trim = $true

    )

    if($Trim) {
        return $InputObject -split $Delimiter | foreach{$_.Trim()}
    } else {
        return $InputObject -split $Delimiter
    }
}




function ConvertFrom-ImmutableID
{
    [CmdletBinding()]
    [OutputType([GUID])]
    Param
    (
        # Param1 help description
        [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$false,
                   ValueFromPipeline=$true,
                   Position=0)]
        $ImmutableID
    )

    return [guid]([system.convert]::frombase64string($ImmutableID) )
}




function New-ObjectFromHashmap
{
    [CmdletBinding()]
    Param
    (
        # Param1 help description
        [Parameter(Mandatory=$true,
                   ValueFromPipeline=$true,
                   Position=0)]
        $Hashmap
    )

    Begin
    {
    }
    Process
    {
        New-Object -TypeName PSCustomObject -Property $Hashmap
    }
    End
    {
    }
}

Then for each new server you are working on, just find a way to bring the Update-PowerShellProfile method, run it and you have the same profile everywhere, such as my Connect-ExchangeOnline method or the Split-String method.

Have fun!

Creating a simple ADFS authenticated .NET site

Authenticating .NET sites with ADFS is pretty easy, especially when you create a new Visual Studio project and just point to the ADFS farm’s federation metadata. However, some times you might want an as simple ADFS authenticated site as possible, without MVC patterns or anything. In this article I will provide you with the simplest .NET site possible for authenticating with ADFS. Also, I will demonstrate how to host this site in Azure Websites.

Creating an Azure website

This site will also work on regular IIS or IISExpress servers, but here is how to configure an Azure website to host it.

Start by logging into the Azure mangement portal and creating a new website.

Choose a URL, choose a web hosting plan (or create one if you have not already done that). Click the OK-button.

After you have created the site, you should see it provisioning and then running. This can take a few momemts.

Click the site and go to the Dashboard of the site. On the Dashboard, click “Download the publish profile”.

Store the file and open it in your favorite text editor. Here you will find the Web Deploy and the FTP publishing method. Get the userName, userPWD and publishUrl for the FTP method. The publish url shoul be something like ftp://waws-prod-db3-013.ftp.azurewebsites.windows.net/site/wwwroot, the password about 50 characters and the username on the form website\$website, in my case extmin\$extmin.

Open an Explorer window, or any other FTP client, and go to the ftp url. Enter the username and password and you should see this.

You can safely delete the hostingstart.html file. You know have access to the storage area for your website, and can publish anything you’d like here. For the sake of this guide, you will want to get this file and extract the contents to this area. Also, your site will be available at https://sitename.azurewebsites.net.

Configuring ADFS

What we need to do is to add a new Relying Part Trust. Start by opening the ADFS management console, and clicking “Add Relying Party Trust” in the right column.

Choose to enter data manually.

Select any display name you’d like.

Choose AD FS Profile.

Enble support for WS Federation, and input the url of the website we created in Azure (or if you have the site on-premise, the url you have for it there).

Leave the default configuration for identifier. This is the “realm” from web.config if you choose to use something not default.

Click next on the rest of the pages, and finish. This should now open the following window with editing claim rules.

Click “Add…” and select “Send LDAP Attributes as claims”, and click next.

Here you can choose whatever attributes you want to send. Here is just an example. You can also choose to not send any attributes. The simple site will still authenticate you, but it will not know anything about the user (other than that the user was authenticated).

Click Finish and OK. You have now configured ADFS.

Explanation of each file in the website

Click here to download all files in the example. The following is an explanation of each file.

Default.aspx
Simple default page with codebehind. Shouldn’t really need any explanation.

Default.aspx.cs
A simple codebehind with a simple Page_Load that prints each claim provided. The important bit here is the following line. The ClaimsIdentity class provides a lot of methods necessary to actually get access to the different claims.


System.Security.Claims.ClaimsIdentity Identity = new System.Security.Claims.ClaimsIdentity(Thread.CurrentPrincipal.Identity);

bin/System.IdentityModel.Tokens.ValidatingIssuerNameRegistry
Important library used to validate the token signing. This is actually from NuGet – https://www.nuget.org/packages/System.IdentityModel.Tokens.ValidatingIssuerNameRegistry/.

Web.config
Each section is commented, and this is the only file that you need to edit.

Get ADFS token signing thumbprint.ps1
Run from any computer with PowerShell 4.0 (for example 2012 R2 server). Just right click and “Run with PowerShell”. Input the hostname of your ADFS farm, such as adfs.goodworkaround.com, and this script will get the federation metadata and extract the thumbprint. This is what you need in web.config, in the issuerNameRegistry.

Setting up the website

The only file you need to edit is the web.config file. Open web.config in your favorite editor and just replace the following:

Replace With
adfs.goodworkaround.com The FQDN of your ADFS
min.azurewebsites.net The FQDN of your website
7B0EBA22C68FD2375F95692EF9C1B90B563D8064 The thumbprint you get from Get ADFS token signing thumbprint.ps1

Drag all the files over in the deployment FTP, or whatever deployment method you choose, and you are good to go.

Testing

Open a browser and navigate to your site. You should see that you are immediately redirected to your ADFS.

Log in, and you should be redirected back to your site, which will show you your claims.

Let me know if you have any trouble!

Configuring the SharePoint Services Connector for FIM 2010 R2 for ADFS authentication

Here is a quick article on how to configure the SharePoint Services Connector for provisioning user profiles for ADFS authenticated users. I did not find any particularly good articles on the attributes required, so here is a quick reference on what I did no make things work with ADFS authentication.

This is not a guide on how to configure the MA. You should find good information on how to do that here.

There are 5 attributes that are important. Here is a table for you.

Attribute Initial only Description
SPS-ClaimID   This is the value of the identifier claim. This means that if you use userPrincipalname as identifier, this should be marius@goodworkaround.com, or if you use EmployeeID this should be 10032.
SPS-ClaimProviderID   This is the case sensitive name of the Trusted Identity Provider configured in SharePoint. If your Trusted Identity Provider is called “SAML Users”, this value should be “SAML Users”.
SPS-ClaimProviderType   When doing ADFS authentication, this should be the constant “Trusted”. (Btw, if you are doing Windows authentication, this should be “Windows”)
ProfileIdentifier   This value is a bit weird when it comes to ADFS authentication. It is required, and it must be unique, and it mst be on the form “someting:unique” (something colon unique). I usually fill this with “ID:value of SPS-ClaimID”; for example “ID:10032” or “ID:marius@goodworkaround.com“.
Anchor yes Another required value that must be unique. I use the same value as the SPS-ClaimID, so marius@goodworkaround.com or 10032. The reason this attribute must be configure as initial only, is that the Anchor will actually change and overwriting it may cause some strange behavior.

Functions for base 64 in PowerShell

There are no default functions available in PowerShell for encoding and decoding base 64 strings. However, the .NET libraries contain this. Here are two wrapper functions ConvertFrom-Base64 and ConvertTo-Base64 that you can use to make this easier.


function ConvertTo-Base64
{
    [CmdletBinding(DefaultParameterSetName='String')]
    [OutputType([String])]
    Param
    (
        # String to convert to base64
        [Parameter(Mandatory=$true,
                   ValueFromPipeline=$true,
                   ValueFromRemainingArguments=$false,
                   Position=0,
                   ParameterSetName='String')]
        [ValidateNotNull()]
        [ValidateNotNullOrEmpty()]
        [string]
        $String,

        # Param2 help description
        [Parameter(ParameterSetName='ByteArray')]
        [ValidateNotNull()]
        [ValidateNotNullOrEmpty()]
        [byte[]]
        $ByteArray
    )

    if($String) {
        return [System.Convert]::ToBase64String(([System.Text.Encoding]::UTF8.GetBytes($String)));
    } else {
        return [System.Convert]::ToBase64String($ByteArray);
    }
}



function ConvertFrom-Base64 {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$True,
                   Position=0,
                   ValueFromPipeline=$true)]
        [ValidateNotNull()]
        [ValidateNotNullOrEmpty()]
        [string]
        $Base64String
    )

    return [System.Text.Encoding]::UTF8.GetString(([System.Convert]::FromBase64String($Base64String)));
}

These two functions are a part of my PowerShell $profile, which I will create an article about later.

Converting LastLogonTimeStamp from Active Directory to Datetime

I often find myself needing to convert the LastLogonTimeStamp attribute from Active Directory to Datetime with PowerShell. What I have done, is that in my PowerShell profile ($profile) I have added a function ConvertFrom-LastLogonTimeStamp to get the value as a Datetime.

Easier to remember!


function ConvertFrom-LastLogonTimestamp
{
    [CmdletBinding()]
    [OutputType([datetime])]
    Param
    (
        # LastLogonTimestamp from AD
        [Parameter(Mandatory=$true,
                   ValueFromPipeline=$true,
                   Position=0)]
        [String] $LastLogonTimestamp
    )

    return [datetime]::FromFileTime($LastLogonTimestamp)
}

Graphing with PowerShell done easy

PowerShell is nice for getting textual output, csv output, xml output, etc, but there are no built in charting tools. However, luckily PowerShell is based on .NET, which means all the libraries for .NET is available to us. In this article I will show you how to use the Microsoft Chart Controls together with PowerShell to create charts.

The first thing you need to do is to install the Microsoft Chart Controls from here. After this, you can verify with the following lines of code whether you are able to load the assemblies.


[void][Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
[void][Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms.DataVisualization")

This should hopefully just return without output. If so, you are good to go.

To make things a bit easier than working directly with objects, I have made a simple module to create new charts, add datasets to them and display them. The following can either be just pasted into a PowerShell, or better, added to a separate “.psm1” file and loaded with Import-Module.

# Load assembly for Microsoft Chart Controls for Microsoft .NET Framework 3.5
Write-Verbose "Loading assemblies"
[void][Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
[void][Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms.DataVisualization")


function New-Chart
{
    [CmdletBinding()]
    [OutputType([System.Windows.Forms.DataVisualization.Charting.ChartArea])]
    Param
    (
        # Dataset
        [Parameter(Mandatory=$false,
                   ValueFromPipelineByPropertyName=$false,
                   Position=0)]
        $Dataset,

        # Width
        [Parameter(Mandatory=$false,
                   ValueFromPipelineByPropertyName=$false)]
        [int]$Width = 500,

        # Height
        [Parameter(Mandatory=$false,
                   ValueFromPipelineByPropertyName=$false)]
        [int]$Height = 500,

        # X Interval
        [Parameter(Mandatory=$false,
                   ValueFromPipelineByPropertyName=$false)]
        [int]$XInterval,

        # Y Interval
        [Parameter(Mandatory=$false,
                   ValueFromPipelineByPropertyName=$false)]
        [int]$YInterval,

        # X Title
        [Parameter(Mandatory=$false,
                   ValueFromPipelineByPropertyName=$false)]
        [string]$XTitle,

        # Y Title
        [Parameter(Mandatory=$false,
                   ValueFromPipelineByPropertyName=$false)]
        [string]$YTitle,

        # Title
        [Parameter(Mandatory=$false,
                   ValueFromPipelineByPropertyName=$false)]
        [string]$Title


    )

    # Create chart
    Write-Verbose "Creating chart $Width x $Height"
    $Chart = New-object System.Windows.Forms.DataVisualization.Charting.Chart
    $Chart.Width = $Width
    $Chart.Height = $Height
    $Chart.Left = 0
    $Chart.Top = 0

    # Add chart area to chart
    $ChartArea = New-Object System.Windows.Forms.DataVisualization.Charting.ChartArea
    $Chart.ChartAreas.Add($ChartArea)

    # Set titles and lables
    if($Title) {
        Write-Verbose "Setting title: $Title"
        [void]$Chart.Titles.Add($Title)
    } else {
        Write-Verbose "No title provided"
    }

    if($YTitle) {
        Write-Verbose "Setting Ytitle: $YTitle"
        $ChartArea.AxisY.Title = $YTitle
    } else {
        Write-Verbose "No Ytitle provided"
    }

    if($XTitle) {
        Write-Verbose "Setting Xtitle: $XTitle"
        $ChartArea.AxisX.Title = $XTitle
    } else {
        Write-Verbose "No Xtitle provided"
    }

    if($YInterval) {
        Write-Verbose "Setting Y Interval to $YInterval"
        $ChartArea.AxisY.Interval = $YInterval
    }

    if($XInterval) {
        Write-Verbose "Setting X Interval to $XInterval"
        $ChartArea.AxisX.Interval = $XInterval
    }

    if($Dataset) {
        Write-Verbose "Dataset provided. Adding this as ""default dataset"" with chart type line."
        [void]$Chart.Series.Add("default dataset")
        $Chart.Series["default dataset"].Points.DataBindXY($Dataset.Keys, $Dataset.Values)
        $Chart.Series["default dataset"].ChartType = [System.Windows.Forms.DataVisualization.Charting.SeriesChartType]::Line
    }

    return $Chart
}




function Add-ChartDataset
{
    [CmdletBinding()]
    [OutputType([System.Windows.Forms.DataVisualization.Charting.ChartArea])]
    Param
    (
        # Chart
        [Parameter(Mandatory=$true,
                   ValueFromPipeline=$true,
                   Position=0)]
        $Chart,

        # Dataset
        [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$false,
                   Position=0)]
        $Dataset,

        # DatasetName
        [Parameter(Mandatory=$false,
                   ValueFromPipelineByPropertyName=$false,
                   Position=1)]
        [string]$DatasetName = "Added dataset",

        # SeriesChartType = http://msdn.microsoft.com/en-us/library/system.windows.forms.datavisualization.charting.seriescharttype(v=vs.110).aspx
        [Parameter(Mandatory=$false,
                   ValueFromPipelineByPropertyName=$false,
                   Position=2)]
        [string]$SeriesChartType = "Line"
    )

    Write-Verbose "Adding series $Datasetname"
    [void]$Chart.Series.Add($DatasetName)

    Write-Verbose "Adding data binding"
    $Chart.Series[$DatasetName].Points.DataBindXY($Dataset.Keys, $Dataset.Values)

    Write-Verbose "Setting chart type to $SeriesChartType"
    $Chart.Series[$DatasetName].ChartType = [System.Windows.Forms.DataVisualization.Charting.SeriesChartType]::$SeriesChartType

    return $Chart
}





function Show-Chart
{
    [CmdletBinding()]
    [OutputType([void])]
    Param
    (
        # Chart
        [Parameter(Mandatory=$true,
                   ValueFromPipeline=$true,
                   Position=0)]
        $Chart
    )

    # display the chart on a form
    $Chart.Anchor = [System.Windows.Forms.AnchorStyles]::Bottom -bor [System.Windows.Forms.AnchorStyles]::Right -bor [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Left
    $Form = New-Object Windows.Forms.Form
    $Form.Text = "PowerShell Chart"
    $Form.Width = $chart.Width
    $Form.Height = $chart.Height + 50
    $Form.controls.add($Chart)
    $Form.Add_Shown({$Form.Activate()})
    $Form.ShowDialog() | Out-Null
}

The functions are documented in the module, and available with Get-Help.

Here is a demo script using the functions:

Import-Module .\GoodWorkaroundCharts-v0.1.psm1 -Force

# Create simple dataset
$simpleDataset = @{
    "Microsoft" = 800
    "Apple" = 250
    "Google" = 400
    "RIM" = 0
}

# Create chart and show it
New-Chart -Dataset $simpleDataset | Show-Chart



# Create ordered hashmap
$osloTemperature = [ordered]@{}

# Request weather data for Oslo, and put into dataset
[xml]$weather = (Invoke-WebRequest -Uri http://www.yr.no/place/Norway/Oslo/Oslo/Oslo/varsel.xml).Content
$weather.weatherdata.forecast.tabular.time | foreach {
    $osloTemperature[$_.from] = $_.temperature.value
}

# Create chart, add dataset and show
New-Chart -Title "Temperature in Oslo" -XInterval 4 -YInterval 2 -Width 1200 |
    Add-ChartDataset -Dataset $osloTemperature -DatasetName "Temperature" -SeriesChartType Spline -OutVariable tempChart |
    Show-Chart

# Save the chart as a PNG to the desktop
$tempChart.SaveImage($Env:USERPROFILE + "\Desktop\Chart.png", "PNG")

Hope that helps someone!