YellowBox.Modeling.psm1

#region Types

enum ParseState
{
    Start
    Model
    Element
}

class ScriptTimer
{
    ScriptTimer()
    {
        $this.modelTimer = [YellowBox.Provider.ModelTimer]::new()
        $this.sourceIdentifier = "_YB_TimerElapsed{0:D8}" -f [ScriptTimer]::timerId++
        [ScriptTimer]::instances[$this.sourceIdentifier] = $this
        Register-ObjectEvent -InputObject $this.modelTimer -EventName "Elapsed" -SourceIdentifier $this.sourceIdentifier
    }

    [void] Dispose()
    {
        if (!$this.disposed) {
            $this.modelTimer.Dispose()
            Unregister-Event -SourceIdentifier $this.sourceIdentifier
            $this.disposed = $true
        }
    }

    [void] SetInterval($interval) { $this.modelTimer.Interval = $interval }
    [void] Start() { $this.modelTimer.Enabled = $true }
    [void] Stop() { $this.modelTimer.Enabled = $false }
    [scriptblock] $Elapsed

    hidden $modelTimer
    hidden [string] $sourceIdentifier
    hidden [bool] $disposed

    static [void] DisposeAllInstances()
    {
        foreach ($instance in [ScriptTimer]::instances.Values) { $instance.Dispose() }
    }

    static [void] OnElapsed($sourceIdentifier)
    {
        if ([ScriptTimer]::instances.ContainsKey($sourceIdentifier))
        {
            [ScriptTimer]::instances[$sourceIdentifier].Elapsed.Invoke()
        }
    }

    static hidden [uint32] $timerId
    static hidden $instances = @{}
}

#endregion

#region Global Variables

$ScriptBaseName = (ls $MyInvocation.MyCommand.Path).BaseName
$ScriptDirectoryName = (ls $MyInvocation.MyCommand.Path).DirectoryName

# Hash to map programmatic element identifiers to element references.
$IdElementMap = @{}

# List of operations to be executed after all the XML data has been read (e.g. patching forward
# references).
[ScriptBlock[]] $PatchOperations = @()

# Custom properties. Key is the markup property name, value a tuple containing type information
# and the custom property identifier resulting from custom property registration.
$CustomPropertyMap = @{}

#endregion

#region Helper methods

function Assert($Condition, $Message = "assert")
{
    if (!$Condition)
    {
        throw $Message
    }
}

<#
.SYNOPSIS
    Parses a rectangle from a text representation of rectangle coordinates.
 
.PARAMETER RectString
    Text representation of rectangle coordinates..
 
.OUTPUT
    Rectangle.
#>

function Parse-Rect([string] $RectString)
{
    $parts = $RectString.Split(',')

    [double] $x = [double]::Parse($parts[0])
    [double] $y = [double]::Parse($parts[1])
    [double] $width = [double]::Parse($parts[2])
    [double] $height = [double]::Parse($parts[3])

    New-Object System.Windows.Rect -ArgumentList $x, $y, $width, $height
}

<#
.SYNOPSIS
    Parses width or height information from a text representation.
 
.PARAMETER WidthOrHeightString
    Text representation of a width or height value followed by a unit.
    Examples:
    "40%" Forty percent of the width/height of the container.
    "20px" Twenty pixels.
    "*" Equal part of the width/height remaining after pixel or percentage claims have been
            assigned.
 
.OUTPUT
    Pair of quantity and unit values.
#>

function Parse-WidthOrHeight([string] $WidthOrHeightString)
{
    switch -Regex ($WidthOrHeightString)
    {
        "^(\d+(?:\.\d+)?)%$"  { ([double]::Parse($Matches[1])/100), ([YellowBox.Provider.LayoutUnit]::Percent) }
        "^(\d+(?:\.\d+)?)px$" { [double]::Parse($Matches[1]), ([YellowBox.Provider.LayoutUnit]::Pixel)}
        "^\*$"                { 0, ([YellowBox.Provider.LayoutUnit]::ShareRemainder) }
    }
}

<#
.SYNOPSIS
    Parses row/column count and heigth/width information from a textual representation.
 
.PARAMETER Dimension
    By reference. List to which the function appends height/width information for rows/columns.
 
.PARAMETER DimensionString
    Textual representation of row/column count and heigth/width.
    Examples:
    "3" Three rows/columns, each with "ShareRemainder" height/width.
    "*,*,*" Three rows/columns, each with "ShareRemainder" height/width.
    "*, 40%, *" Three rows/columns, the 2nd occupying 40% of the height/width, the others have
                    "ShareRemainder" height/width.
    "20px, *, *" Three rows/columns, the first is 20 pixels high/wide, the others have
                    "ShareRemainder" height/width.
 
.OUTPUT
    None (function modifies 'Dimension' argument).
#>

function Parse-TableDimension([System.Collections.Generic.List`1[System.Tuple`2[double, YellowBox.Provider.LayoutUnit]]] $Dimension, [string] $DimensionString)
{
    switch -Regex ($DimensionString)
    {
        "^\d+$" 
        {
            for ($i = 0; $i -lt [int]::Parse($DimensionString); ++$i)
            {
                $Dimension.Add((New-Object -TypeName 'System.Tuple`2[double, YellowBox.Provider.LayoutUnit]' -ArgumentList 0, ([YellowBox.Provider.LayoutUnit]::ShareRemainder)))
            }
        }
        default
        {
            foreach ($part in ($DimensionString -split ","))
            {
                switch -Regex ($part.Trim())
                {
                    "^\*$"
                    {
                        $Dimension.Add((New-Object -TypeName 'System.Tuple`2[double, YellowBox.Provider.LayoutUnit]' -ArgumentList 0, ([YellowBox.Provider.LayoutUnit]::ShareRemainder)))
                    }

                    "^(\d+)%$"
                    {
                        [double] $value = [double]::Parse($Matches[1])
                        $Dimension.Add((New-Object -TypeName 'System.Tuple`2[double, YellowBox.Provider.LayoutUnit]' -ArgumentList $value, ([YellowBox.Provider.LayoutUnit]::Percent)))
                    }

                    "^(\d+)px$"
                    {
                        [double] $value = [double]::Parse($Matches[1])
                        $Dimension.Add((New-Object -TypeName 'System.Tuple`2[double, YellowBox.Provider.LayoutUnit]' -ArgumentList $value, ([YellowBox.Provider.LayoutUnit]::Pixel)))
                    }
                }
            }
        }
    }
}

<#
.SYNOPSIS
    Gets the closest ancestral Grid pattern.
 
.PARAMETER CurrentElement
    Element to start the search at.
 
.OUTPUT
    Grid pattern if the ancestor chain contains an element supporting the Grid pattern. Otherwise an
    exception is being thrown.
#>

function ClosestAncestralGridPattern($CurrentElement)
{
    for ($CurrentElement = $CurrentElement.Parent; $CurrentElement -ne $null; $CurrentElement = $CurrentElement.Parent)
    {
        $gridPattern = $null
        if ($CurrentElement.Patterns.TryGetValue(([YellowBox.PatternId]::Grid), ([ref]$gridPattern)))
        {
            return $gridPattern
        }
    }
    throw "no ancestral Grid pattern"
}

<#
.SYNOPSIS
    Parses numeric RuntimeId from a textual representation.
 
.PARAMETER RuntimeIdString
    Textual representation of RuntimeId.
 
.OUTPUT
    Numeric representation of RuntimeId.
 
.DESCRIPTION
    To avoid collisions with explicitly assigned RuntimeId value, we're using the upper half of the
    positive integer range.
#>

function Parse-RuntimeId([string] $RuntimeIdString)
{
    $id = [int]::Parse($RuntimeIdString)

    if ($id -gt [int]::MaxValue / 2)
    {
        throw "RuntimeId $RuntimeIdString encroaches on range reserved for implicitly assigned RuntimeIds"
    }

    $id
}

[int] $__CurrentImplicitRuntimeId_value = [int]::MaxValue

<#
.SYNOPSIS
    Gets a value for an implicitly assigned RuntimeId.
 
.OUTPUT
    Value for an implicitly assigned RuntimeId.
 
.DESCRIPTION
    To simplify AccML authoring, the Id attribute on Element nodes is optional. If absent, the
    RuntimeId value returned from this function is being used. To avoid collisions with explicitly
    assigned RuntimeId value, we're using the upper half of the positive integer range.
#>

function CurrentImplicitRuntimeId()
{
    if ($script:__CurrentImplicitRuntimeId_value -le [int]::MaxValue / 2)
    {
        throw "exhausted range reserved for implicitly assigned RuntimeIds"
    }
    ($script:__CurrentImplicitRuntimeId_value--)
}

function ConvertToAutomationType ([YellowBox.AutomationType] $Type, [string] $Value)
{
    switch($Type)
    {
        # in AutomationType enumeration order

        ([YellowBox.AutomationType]::Int) { return [int]::Parse($Value) }
        ([YellowBox.AutomationType]::Bool) { return [bool]::Parse($Value) }
        ([YellowBox.AutomationType]::String) { return $Value }
        ([YellowBox.AutomationType]::Double) { return [double]::Parse($Value) }

        default { throw "conversion to $Type not yet implemented" }
    }
}

function CreateTimer()
{
    return [ScriptTimer]::new()
}

#endregion

function Show-UiaModel(
    [parameter(Mandatory = $true)][string] $ModelFile,
    [switch] $Render = $true
)
{

    [YellowBox.Provider.ElementProvider] $rootElement = $null
    [YellowBox.Provider.ElementProvider] $currentElement = $null
    [string] $closedHandler = $null
    [string] $closingHandler = $null
    [string] $keyPressHandler = $null
    [string] $shownHandler = $null

    [xml] $xml = Get-Content $ModelFile
    $currXml = $xml.FirstChild

    [ParseState] $state = [ParseState]::Start
    function AssertStates([ParseState[]] $ExpectedStates)
    {
        foreach ($expectedState in $ExpectedStates)
        {
            if ($state -eq $expectedState) { return }
        }

        throw "Expected states ($($ExpectedStates -join ', ')), actual state $state"
    }

    $cont = $true
    while ($cont)
    {
        # skip over comments, white space etc. Is there a navigation mode that can be used to do this?
        if ($currXml.NodeType -eq ([System.Xml.XmlNodeType]::Element))
        {
            switch ($currXml.LocalName)
            {
                "Model"
                {
                    AssertStates ([ParseState]::Start)
                    # outermost XML element, allows us to have non-UI-elements
                    $state = [ParseState]::Model
                    break
                }

                "CustomProperty"
                {
                    AssertStates ([ParseState]::Model)

                    $guid = $null
                    $programmaticName = $null
                    $type = $null
                    $markupName = $null

                    foreach ($attribute in $currXml.Attributes)
                    {
                        switch ($attribute.Name)
                        {
                            "Guid" { $guid = [System.Guid]::Parse($attribute.Value); break }
                            "Name" { $programmaticName = $attribute.Value; break }
                            "Type" { $type = [System.Enum]::Parse([YellowBox.AutomationType], $attribute.Value); break }
                            "MarkupName" { $markupName = $attribute.Value; break }
                        }
                    }

                    if ($guid -eq $null) { throw "'CustomProperty' element is missing 'Guid' attribute" }
                    if ($programmaticName -eq $null) { throw "'CustomProperty' element is missing 'Name' attribute" }
                    if ($type -eq $null) { throw "'CustomProperty' element is missing 'Type' attribute" }
                    if ($markupName -eq $null) { $markupName = $programmaticName }

                    $id = Register-UiaCustomProperty -Guid $guid -ProgrammaticName $programmaticName -Type $type
                    $CustomPropertyMap[$markupName] = @{ Id = $id; Type = $type }
                    break
                }

                "Element"
                {
                    AssertStates ([ParseState]::Model), ([ParseState]::Element)
                    $state = ([ParseState]::Element)

                    $newElement = $null

                    [int] $runtimeId = if ($currXml.HasAttribute("RuntimeId")) { Parse-RuntimeId $currXml.Id } else { CurrentImplicitRuntimeId }

                    if ($rootElement -eq $null)
                    {
                        $newElement = [YellowBox.Provider.RootProvider]::new($runtimeId)
                        $rootElement = $newElement
                    }
                    else
                    {
                        $newElement = [YellowBox.Provider.ElementProvider]::new($runtimeId)
                    }

                    if ($currentElement -ne $null)
                    {
                        $currentElement.Children.Add($newElement)
                    }

                    foreach ($attribute in $currXml.Attributes)
                    {
                        switch ($attribute.Name)
                        {
                            "AcceleratorKey" { $newElement.AcceleratorKey = $attribute.Value; break }
                            "AccessKey" { $newElement.AccessKey = $attribute.Value; break }
                            "AutomationId" { $newElement.AutomationId = $attribute.Value; break }
                            "BoundingRect" { $newElement.BoundingRectangle = Parse-Rect $attribute.Value; break }
                            "ClassName" { $newElement.ClassName = $attribute.Value; break }
                            "ControlType" { $newElement.ControlType = [System.Enum]::Parse([YellowBox.ControlType], $attribute.Value, $true); break }
                            "HasKeyboardFocus" { $newElement.HasKeyboardFocus = [bool]::Parse($attribute.Value); break }
                            "Id" {
                                $id = $attribute.Value
                                if ($id -notmatch "[a-zA-Z_]\w*") { throw "'$id' is not a valid identifier" }
                                if ((Get-Variable -Scope "Script" -Name $id -ErrorAction Ignore) -ne $null) { throw "an object with identifier '$id' already exists" }
                                Set-Variable -Scope "Script" -Name $id -Value $newElement
                                $IdElementMap[$id] = $newElement
                            }
                            "IsContentElement" { $newElement.IsContentElement = [bool]::Parse($attribute.Value); break }
                            "IsControlElement" { $newElement.IsControlElement = [bool]::Parse($attribute.Value); break }
                            "IsEnabled" { $newElement.IsEnabled = [bool]::Parse($attribute.Value); break }
                            "IsKeyboardFocusable" { $newElement.IsKeyboardFocusable = [bool]::Parse($attribute.Value); break }
                            "LandmarkType" { $newElement.LandmarkType = [System.Enum]::Parse([YellowBox.LandmarkType], $attribute.Value, $true); break }

                            "Layout" # alternatively, "Layout.Type"
                            {
                                switch ($attribute.Value)
                                {
                                    "HorizontalStack" { $newElement.Layout = New-Object YellowBox.Provider.HorizontalStackLayout }
                                    "VerticalStack"   { $newElement.Layout = New-Object YellowBox.Provider.VerticalStackLayout }
                                    "Table"
                                    {
                                        $table = New-Object YellowBox.Provider.TableLayout
                                        Parse-TableDimension $table.RowSpecs $currXml.'Layout.Rows'
                                        Parse-TableDimension $table.ColSpecs $currXml.'Layout.Columns'
                                        $newElement.Layout = $table
                                    }
                                }
                            }

                            "Layout.Column"
                            {
                                [UInt32] $col = [UInt32]::Parse($attribute.Value)

                                if (!($newElement.Parent.Layout -is [YellowBox.Provider.TableLayout]))
                                {
                                    throw "'Column' attribute requires 'Table' parent layout"
                                }

                                if ($col -ge $newElement.Parent.Layout.ColSpecs.Count)
                                {
                                    throw "'Column' attribute value must be less than the column count indicated by parent's 'Columns' attribute"
                                }

                                if ($newElement.LayoutItem -eq $null)
                                {
                                    $newElement.LayoutItem = New-Object YellowBox.Provider.TableLayoutItem
                                }

                                $newElement.LayoutItem.Column = $col
                            }

                            "Layout.ColumnSpan"
                            {
                                [UInt32] $colSpan = [UInt32]::Parse($attribute.Value)

                                if (!($newElement.Parent.Layout -is [YellowBox.Provider.TableLayout]))
                                {
                                    throw "'ColumnSpan' attribute requires 'Table' parent layout"
                                }

                                if ($colSpan -ge $newElement.Parent.Layout.ColSpecs.Count)
                                {
                                    throw "'ColumnSpan' attribute value must be less than the column count indicated by parent's 'Columns' attribute"
                                }

                                if ($newElement.LayoutItem -eq $null)
                                {
                                    $newElement.LayoutItem = New-Object YellowBox.Provider.TableLayoutItem
                                }

                                $newElement.LayoutItem.ColumnSpan = $colSpan
                            }

                            "Layout.Height"
                            {
                                if (!($newElement.Parent.Layout -is [YellowBox.Provider.VerticalStackLayout]))
                                {
                                    throw "'Height' attribute requires 'VerticalStack' parent layout"
                                }
                                $newElement.LayoutItem = New-Object YellowBox.Provider.VerticalStackLayoutItem -ArgumentList (Parse-WidthOrHeight $attribute.Value)
                            }

                            "Layout.Row"
                            {
                                [UInt32] $row = [UInt32]::Parse($attribute.Value)

                                if (!($newElement.Parent.Layout -is [YellowBox.Provider.TableLayout]))
                                {
                                    throw "'Row' attribute requires 'Table' parent layout"
                                }

                                if ($row -ge $newElement.Parent.Layout.RowSpecs.Count)
                                {
                                    throw "'Row' attribute value must be less than the row count indicated by parent's 'Rows' attribute"
                                }

                                if ($newElement.LayoutItem -eq $null)
                                {
                                    $newElement.LayoutItem = New-Object YellowBox.Provider.TableLayoutItem
                                }

                                $newElement.LayoutItem.Row = $row
                            }

                            "Layout.RowSpan"
                            {
                                [UInt32] $rowSpan = [UInt32]::Parse($attribute.Value)

                                if (!($newElement.Parent.Layout -is [YellowBox.Provider.TableLayout]))
                                {
                                    throw "'RowSpan' attribute requires 'Table' parent layout"
                                }

                                if ($rowSpan -ge $newElement.Parent.Layout.RowSpecs.Count)
                                {
                                    throw "'RowSpan' attribute value must be less than the row count indicated by parent's 'Rows' attribute"
                                }

                                if ($newElement.LayoutItem -eq $null)
                                {
                                    $newElement.LayoutItem = New-Object YellowBox.Provider.TableLayoutItem
                                }

                                $newElement.LayoutItem.RowSpan = $rowSpan
                            }

                            "Layout.Width"
                            {
                                if (!($newElement.Parent.Layout -is [YellowBox.Provider.HorizontalStackLayout]))
                                {
                                    throw "'Width' attribute requires 'HorizontalStack' parent layout"
                                }
                                $newElement.LayoutItem = New-Object YellowBox.Provider.HorizontalStackLayoutItem -ArgumentList (Parse-WidthOrHeight $attribute.Value)
                            }

                            "Level" { $newElement.Level = [int]::Parse($attribute.Value); break }

                            "LocalizedLandmarkType" { $newElement.LocalizedLandmarkType = $attribute.Value; break }

                            "Name" { $newElement.Name = $attribute.Value; break }

                            "NameRequested"
                            {
                                Register-ObjectEvent -InputObject $newElement -EventName 'NameRequested' -SourceIdentifier '_YB_MethodCalled' -MessageData "$($attribute.Value); `$Event.SourceEventArgs.Return()"
                                break
                            }

                            "OnClosed"
                            {
                                if ($newElement -ne $rootElement)
                                {
                                    throw "'OnClosed' attribute only supported on root element"
                                }
                                $closedHandler = $attribute.Value
                                break
                            }

                            "OnClosing"
                            {
                                if ($newElement -ne $rootElement)
                                {
                                    throw "'OnClosing' attribute only supported on root element"
                                }
                                $closingHandler = $attribute.Value
                                break
                            }

                            "OnKeyPress"
                            {
                                if ($newElement -ne $rootElement)
                                {
                                    throw "'OnKeyPress' attribute only supported on root element"
                                }
                                $keyPressHandler = $attribute.Value
                                break
                            }

                            "OnShown"
                            {
                                if ($newElement -ne $rootElement)
                                {
                                    throw "'OnShown' attribute only supported on root element"
                                }
                                $shownHandler = $attribute.Value
                                break
                            }

                            "PositionInSet" { $newElement.PositionInSet = [int]::Parse($attribute.Value); break }
                            "SizeOfSet" { $newElement.SizeOfSet = [int]::Parse($attribute.Value); break }
                            default
                            {
                                if ($CustomPropertyMap.ContainsKey($attribute.Name))
                                {
                                    $propertyInfo = $CustomPropertyMap[$attribute.Name]
                                    $newElement.SetProperty($propertyInfo.Id, (ConvertToAutomationType $propertyInfo.Type $attribute.Value))
                                }
                                else
                                {
                                    throw "Unknown 'Element' attribute '$($attribute.Name)'"
                                }
                            }
                        }
                    }

                    break
                }

                "CustomNavigation"
                {
                    $customNavigation = New-Object YellowBox.Provider.CustomNavigationProvider

                    foreach($attribute in $currXml.Attributes)
                    {
                        [string] $targetElementId = $attribute.Value

                        $PatchOperations += switch ($attribute.Name)
                        {
                            "Parent"
                            { { $customNavigation.Parent = $IdElementMap[$targetElementId] }.GetNewClosure(); break }

                            "PreviousSibling"
                            { { $customNavigation.PreviousSibling = $IdElementMap[$targetElementId] }.GetNewClosure(); break }

                            "NextSibling"
                            { { $customNavigation.NextSibling = $IdElementMap[$targetElementId] }.GetNewClosure(); break }

                            "FirstChild"
                            { { $customNavigation.FirstChild = $IdElementMap[$targetElementId] }.GetNewClosure(); break }

                            "LastChild"
                            { { $customNavigation.LastChild = $IdElementMap[$targetElementId] }.GetNewClosure(); break }

                            default
                            {
                                throw "invalid attribute '$($attribute.Name)'"
                            }
                        }
                    }

                    $currentElement.Patterns.Add(([YellowBox.PatternId]::CustomNavigation), $customNavigation)

                    break
                }

                "ExpandCollapse"
                {
                    $expandCollapse = New-Object YellowBox.Provider.ExpandCollapseProvider
                    if ($currXml.ExpandCollapseState -eq $null) 
                    {
                        throw "<ExpandCollapse/> element must have at least a 'ExpandCollapseState' attribute"
                    }
                    else 
                    {
                        $expandCollapse.ExpandCollapseState = $currXml.ExpandCollapseState
                    }
                    $currentElement.Patterns.Add(([YellowBox.PatternId]::ExpandCollapse), $expandCollapse)

                    break
                }

                "Grid"
                {
                    $grid = New-Object YellowBox.Provider.GridProvider
                    $grid.RowCount = $currXml.RowCount
                    $grid.ColumnCount = $currXml.ColumnCount
                    $currentElement.Patterns.Add(([YellowBox.PatternId]::Grid), $grid)

                    break
                }

                "GridItem"
                {
                    $gridItem = New-Object YellowBox.Provider.GridItemProvider
                    $gridItem.Row = $currXml.Row
                    $gridItem.Column = $currXml.Column

                    if($currXml.RowSpan -ne $null) { $gridItem.RowSpan = $currXml.RowSpan }
                    if($currXml.ColumnSpan -ne $null) { $gridItem.ColumnSpan = $currXml.ColumnSpan }

                    $currentElement.Patterns.Add(([YellowBox.PatternId]::GridItem), $gridItem)

                    break
                }

                "Invoke"
                {
                    $invoke = New-Object YellowBox.Provider.InvokeProvider
                    $currentElement.Patterns.Add(([YellowBox.PatternId]::Invoke), $invoke)

                    break
                }

                "Script"
                {
                    # dot-source instead to allow <Script/> blocks to define functions that can be called
                    # later?
                    Invoke-Expression $currXml.InnerText

                    break
                }

                "Selection"
                {
                    $selection = New-Object YellowBox.Provider.SelectionProvider
                    $selection.CanSelectMultiple = $currXml.CanSelectMultiple
                    $selection.IsSelectionRequired = $currXml.IsSelectionRequired
                    $currentElement.Patterns.Add(([YellowBox.PatternId]::Selection), $selection)

                    break
                }

                "SelectionItem"
                {
                    $selectionItem = New-Object YellowBox.Provider.SelectionItemProvider
                    $currentElement.Patterns.Add(([YellowBox.PatternId]::SelectionItem), $selectionItem)

                    if ([bool]::Parse($currXml.IsSelected))
                    {
                        $selectionItem.AddToSelection()
                    }

                    break
                }

                "Spreadsheet"
                {
                    $spreadsheet = New-Object YellowBox.Provider.SpreadsheetProvider
                    $currentElement.Patterns.Add(([YellowBox.PatternId]::Spreadsheet), $spreadsheet)

                    break
                }

                "SpreadsheetItem"
                {
                    $spreadsheetItem = New-Object YellowBox.Provider.SpreadsheetItemProvider
                    if ($currXml.HasAttribute("Formula"))
                    {
                        $spreadsheetItem.Formula = $currXml.Formula
                    }
                    $currentElement.Patterns.Add(([YellowBox.PatternId]::SpreadsheetItem), $spreadsheetItem)

                    break
                }

                "Table"
                {
                    $table = New-Object YellowBox.Provider.TableProvider

                    # TODO: parse and pass on RowOrColumnMajor value

                    if ($currXml.RowHeaders -eq $null -and $currXml.ColumnHeaders -eq $null)
                    {
                        throw "<Table/> element must have at least one of the 'RowHeaders' or 'ColumnHeaders' attributes"
                    }

                    if ($currXml.RowHeaders -ne $null)
                    {
                        if ($currXml.RowHeaders -match "^\s*\(\s*\d+\s*,\s*\d+\s*\)\s*(?:\s*\(\s*\d+\s*,\s*\d+\s*\)\s*)*\s*$")
                        {
                            # interpret RowHeaders attribute as a list of (rowIndex, columnIndex)
                            # coordinate pairs
                            $currXml.RowHeaders.Trim("()") -split "\)\s*\(" | %{
                                $arr = $_ -split ","
                                $row = [uint32]::Parse($arr[0])
                                $col = [uint32]::Parse($arr[1])
                                $PatchOperations += { $table.RowHeaders.Add($currentElement.Patterns[([YellowBox.PatternId]::Grid)].GetItem($row, $col)) }.GetNewClosure()
                            }
                        }
                        else
                        {
                            # interpret RowHeaders attribute as a list element IDs
                            $currXml.RowHeaders -split "," | %{ $_.Trim() } | %{
                                $id = $_
                                # delayed execution to enable forward references
                                $PatchOperations += { $table.RowHeaders.Add($IdElementMap[$id]) }.GetNewClosure()
                            }
                        }
                    }

                    if ($currXml.ColumnHeaders -ne $null)
                    {
                        if ($currXml.ColumnHeaders -match "^\s*\(\s*\d+\s*,\s*\d+\s*\)\s*(?:\s*\(\s*\d+\s*,\s*\d+\s*\)\s*)*\s*$")
                        {
                            # interpret ColumnHeaders attribute as a list of (rowIndex, columnIndex)
                            # coordinate pairs
                            $currXml.ColumnHeaders.Trim("()") -split "\)\s*\(" | %{
                                $arr = $_ -split ","
                                $row = [uint32]::Parse($arr[0])
                                $col = [uint32]::Parse($arr[1])
                                $PatchOperations += { $table.ColumnHeaders.Add($currentElement.Patterns[([YellowBox.PatternId]::Grid)].GetItem($row, $col)) }.GetNewClosure()
                            }
                        }
                        else
                        {
                            # interpret RowHeaders attribute as a list element IDs
                            $currXml.ColumnHeaders -split "," | %{ $_.Trim() } | %{
                                $id = $_
                                # delayed execution to enable forward references
                                $PatchOperations += { $table.ColumnHeaders.Add($IdElementMap[$id]) }.GetNewClosure()
                            }
                        }

                    }

                    $currentElement.Patterns.Add(([YellowBox.PatternId]::Table), $table)

                    break
                }

                "TableItem"
                {
                    $tableItem = New-Object YellowBox.Provider.TableItemProvider

                    if ($currXml.RowHeaders -eq $null -and $currXml.ColumnHeaders -eq $null)
                    {
                        throw "<TableItem/> element must have at least one of the 'RowHeaders' or 'ColumnHeaders' attributes"
                    }

                    if($currXml.RowHeaders -ne $null)
                    {
                        if ($currXml.RowHeaders -match "^\s*\(\s*\d+\s*,\s*\d+\s*\)\s*(?:\s*\(\s*\d+\s*,\s*\d+\s*\)\s*)*\s*$")
                        {
                            # interpret RowHeaders attribute as a list of (rowIndex, columnIndex)
                            # coordinate pairs
                            $currXml.RowHeaders.Trim("()") -split "\)\s*\(" | %{
                                $arr = $_ -split ","
                                $row = [uint32]::Parse($arr[0])
                                $col = [uint32]::Parse($arr[1])
                                $gridPattern = ClosestAncestralGridPattern $currentElement
                                $PatchOperations += { $tableItem.RowHeaderItems.Add($gridPattern.GetItem($row, $col)) }.GetNewClosure()
                            }
                        }
                        else
                        {
                            # interpret RowHeaders attribute as a list element IDs
                            $currXml.RowHeaders -split "," | %{ $_.Trim() } | %{
                                $id = $_
                                # delayed execution to enable forward references
                                $PatchOperations += { $tableItem.RowHeaderItems.Add($IdElementMap[$id]) }.GetNewClosure()
                            }
                        }
                    }

                    if($currXml.ColumnHeaders -ne $null)
                    {
                        if ($currXml.ColumnHeaders -match "^\s*\(\s*\d+\s*,\s*\d+\s*\)\s*(?:\s*\(\s*\d+\s*,\s*\d+\s*\)\s*)*\s*$")
                        {
                            # interpret ColumnHeaders attribute as a list of (rowIndex, columnIndex)
                            # coordinate pairs
                            $currXml.ColumnHeaders.Trim("()") -split "\)\s*\(" | %{
                                $arr = $_ -split ","
                                $row = [uint32]::Parse($arr[0])
                                $col = [uint32]::Parse($arr[1])
                                $gridPattern = ClosestAncestralGridPattern $currentElement
                                $PatchOperations += { $tableItem.ColumnHeaderItems.Add($gridPattern.GetItem($row, $col)) }.GetNewClosure()
                            }
                        }
                        else
                        {
                            # interpret ColumnHeaders attribute as a list element IDs
                            $currXml.ColumnHeaders -split "," | %{ $_.Trim() } | %{
                                $id = $_
                                # delayed execution to enable forward references
                                $PatchOperations += { $tableItem.ColumnHeaderItems.Add($IdElementMap[$id]) }.GetNewClosure()
                            }
                        }
                    }

                    $currentElement.Patterns.Add(([YellowBox.PatternId]::TableItem), $tableItem)

                    break
                }

                "Text"
                {
                    $text = New-Object YellowBox.Provider.TextProvider -ArgumentList $currXml.InnerText
                    $currentElement.Patterns.Add(([YellowBox.PatternId]::Text), $text)

                    break
                }

                "Toggle"
                {
                    $toggle = New-Object YellowBox.Provider.ToggleProvider

                    if ($currXml.ToggleState -ne $null) 
                    {
                        $toggle.ToggleState = $currXml.ToggleState
                    }

                    if ($currXml.OnToggle -ne $null)
                    {
                        Register-ObjectEvent -InputObject $toggle -EventName 'ToggleCalled' -SourceIdentifier 'MethodCalled' -MessageData $currXml.OnToggle
                    }

                    $currentElement.Patterns.Add(([YellowBox.PatternId]::Toggle), $toggle)

                    break
                }

                "Value"
                {
                    $value = New-Object YellowBox.Provider.ValueProvider
                    $value.IsReadOnly = $currXml.IsReadOnly
                    $value.Value = $currXml.InnerText
                    $currentElement.Patterns.Add(([YellowBox.PatternId]::Value), $value)

                    break
                }
            }
        }

        # tree iteration logic

        $nextXml = $currXml.PSBase.FirstChild
        if ($nextXml -ne $null)
        {
            if ($currXml.LocalName -eq "Element")
            {
                if ($currentElement -ne $null)
                {
                    $currentElement = $currentElement.Children[$currentElement.Children.Count - 1]
                }
                else
                {
                    $currentElement = $rootElement
                }
            }

            $currXml = $nextXml
            continue
        }

        $nextXml = $currXml.PSBase.NextSibling
        if ($nextXml -ne $null)
        {
            $currXml = $nextXml
            continue
        }

        $ascend = $true
        while ($ascend)
        {
            $nextXml = $currXml.ParentNode

            if ($nextXml.NodeType -eq ([System.Xml.XmlNodeType]::Document))
            {
                # we're back at the doc node
                $cont = $false # break out of outer loop
                break # break out of inner loop
            }

            if ($nextXml.LocalName -eq "Element") { $currentElement = $currentElement.Parent }

            $currXml = $nextXml

            $nextXml = $currXml.PSBase.NextSibling
            if ($nextXml -ne $null)
            {
                $currXml = $nextXml
                $ascend = $false
            }
        }
    }

    foreach ($patchOperation in $PatchOperations)
    {
        $patchOperation.Invoke()
    }

    $model = [YellowBox.Provider.Model]::new($rootElement, $Render)
    Register-ObjectEvent -InputObject $model.Form -EventName "Closed"   -SourceIdentifier "_YB_ModelFormClosed"
    Register-ObjectEvent -InputObject $model.Form -EventName "Closing"  -SourceIdentifier "_YB_ModelFormClosing"
    Register-ObjectEvent -InputObject $model.Form -EventName "KeyPress" -SourceIdentifier "_YB_ModelFormKeyPress"

    if ($shownHandler)
    {
        & $shownHandler $model
    }

    [bool] $cont = $true
    while ($cont)
    {
        $event = Wait-Event
        switch ($event.SourceIdentifier)
        {
            "_YB_ModelFormClosing"
            {
                # TODO: Accomodate CancelEventArgs
                # maybe via $event.SourceEventArgs.Return()
                if ($closingHandler) { & $closingHandler $event.SourceArgs }
                break
            }

            "_YB_ModelFormClosed"
            {
                if ($closedHandler) { & $closedHandler $event.SourceArgs }
                $cont = $false
                break
            }

            "_YB_ModelFormKeyPress"
            {
                if ($keyPressHandler) { & $keyPressHandler $event.SourceArgs }
                break
            }

            "MethodCalled" { Invoke-Expression $event.MessageData; break }

            default
            {
                if ($event.SourceIdentifier.StartsWith("_YB_TimerElapsed")) {
                    [ScriptTimer]::OnElapsed($event.SourceIdentifier)
                }
            }
        }

        Remove-Event -EventIdentifier $event.EventIdentifier
    }

    Unregister-Event -SourceIdentifier "_YB_ModelFormClosed"
    Unregister-Event -SourceIdentifier "_YB_ModelFormClosing"
    Unregister-Event -SourceIdentifier "_YB_ModelFormKeyPress"
    Unregister-Event -SourceIdentifier "_YB_MethodCalled" -ErrorAction Ignore
    [ScriptTimer]::DisposeAllInstances()

    $model.Dispose()
}

Export-ModuleMember -Function Show-UiaModel