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

#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
)
{
    # Hash to map element identifiers to element references.
    $IdElementMap = @{}

    # Hash to map custom annotation names to identifiers
    $CustomAnnotationTypes = @{}

    # 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 = @{}

    [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)
            {
                "Annotation"
                {
                    # Creates an Annotation pattern.
                    #
                    # Parents:
                    # Element: The element supporting the annotation pattern.
                    # Attributes:
                    # Target: Id of the element that is targeted by the annotation.
                    # TypeId: Annotation type identifier, represented as the name of a member of the AnnotationTypes enum.
                    # Author: Name of the annotation author.
                    # Children:
                    # (none)

                    $annotation = [YellowBox.Provider.AnnotationProvider]::new()
                    $annotation.TypeId = [YellowBox.AnnotationType]::Unknown

                    foreach ($attribute in $currXml.Attributes)
                    {
                        switch ($attribute.Name)
                        {
                            "Author" { $annotation.Author = $attribute.Value; break }
                            "Target"
                            {
                                [string] $targetId <# something we can capture by value #> = $attribute.Value
                                # delayed execution to enable forward references
                                $PatchOperations += { $annotation.Target = $IdElementMap[$targetId] }.GetNewClosure()
                                break
                            }
                            "TypeId"
                            {
                                $annotationType = $attribute.Value -as [YellowBox.AnnotationType]
                                if ($annotationType)
                                {
                                    $annotation.TypeId = $annotationType
                                }
                                elseif ($CustomAnnotationTypes.ContainsKey($attribute.Value))
                                {
                                    $annotation.TypeId = $CustomAnnotationTypes[$attribute.Value]
                                }
                                else
                                {
                                    throw "unknown annotation type '$($attribute.Value)'"
                                }
                                break
                            }
                            "TypeName" { $annotation.TypeName = $attribute.Value; break }
                            default { throw "unexpected Annotation attribute '$($attribute.Name)'"}
                        }
                    }

                    $currentElement.Patterns.Add(([YellowBox.PatternId]::Annotation), $annotation)

                    break
                }

                "AnnotationProperty"
                {
                    # Creates an Annotation property of an Element.
                    #
                    # An Element can have multiple AnnotationProperty children. Each one contributes to the arrays returned
                    # via the UIA_AnnotationTypesPropertyId and UIA_AnnotationObjectsPropertyId properties.
                    #
                    # Parents:
                    # Element: Element to which the annotation property is assigned. An Element can have 0 or more
                    # annotation properties.
                    # Attributes:
                    # Type: Annotation type, represented as the name of a member of the AnnotationTypes enum.
                    # Element: Optional. ID of the element representing the annotation.
                    # Children:
                    # (none)

                    AssertStates ([ParseState]::Element)

                    # Note that the 'System.Tuple.ItemX' accessors are read-only, meaning the respective values need to be passed
                    # to the tuple constructor. The code below is structured to account for this.

                    $annotation = $null

                    if ($currXml.Type)
                    {
                        $annotationType = $currXml.Type -as [YellowBox.AnnotationType]
                        if ($annotationType)
                        {
                            $annotation = [Tuple[int, WeakReference[YellowBox.Provider.ElementProvider]]]::new($annotationType, [WeakReference[YellowBox.Provider.ElementProvider]]::new($null))

                        }
                        elseif ($CustomAnnotationTypes.ContainsKey($currXml.Type))
                        {
                            $annotation = [Tuple[int, WeakReference[YellowBox.Provider.ElementProvider]]]::new($CustomAnnotationTypes[$currXml.Type], [WeakReference[YellowBox.Provider.ElementProvider]]::new($null))
                        }
                        else
                        {
                            throw "unknown annotation type '$($currXml.Type)'"
                        }
                    }

                    foreach ($attribute in $currXml.Attributes)
                    {
                        switch ($attribute.Name)
                        {
                            "Type" { <# handled above #> }
                            "Element"
                            {
                                # in case there is no 'Type' attribute that prompted the creation above
                                if ($null -eq $annotation)
                                {
                                    $annotation = [Tuple[int, WeakReference[YellowBox.Provider.ElementProvider]]]::new([YellowBox.AnnotationType]::Unknown, [WeakReference[YellowBox.Provider.ElementProvider]]::new($null))
                                }

                                [string] $annotatorId = $attribute.Value # value for lambda capture below

                                # delayed execution to enable forward references
                                $PatchOperations += { $annotation.Item2.SetTarget($IdElementMap[$annotatorId]) }.GetNewClosure()
                                break
                            }
                            default { throw "unexpected 'AnnotationProperty' attribute '$($attribute.Name)'" }
                        }
                    }

                    if ($null -eq $annotation)
                    {
                        throw "'AnnotationProperty' element needs at least one 'Type' or one 'Element' attribute"
                    }

                    $currentElement.Annotations.Add($annotation)
                }

                "CustomAnnotationType"
                {
                    # Registers a custom annotation type.
                    #
                    # The registration of a custom annotation type results in a name that can be used in places where annotation types can be specified.
                    #
                    # Parents:
                    # Model: Custom annotation types must be defined at the top level of the model.
                    # Attributes:
                    # Guid: GUID of the custom annotation type.
                    # Name: Symbolic name that can subsequently be used in places where annotation types can be specified.
                    # None of the existing annotation type names (see YellowBox.AnnotationType) can be used.
                    # Children:
                    # (none)

                    AssertStates ([ParseState]::Model)

                    $guid = $null
                    $name = $null

                    foreach ($attribute in $currXml.Attributes)
                    {
                        switch ($attribute.Name)
                        {
                            "Guid" { $guid = [System.Guid]::Parse($attribute.Value); break }
                            "Name"
                            {
                                $name = $attribute.Value
                                if ($name -as [YellowBox.AnnotationType]) { throw "custom annotation type name '$name' matches the name of a standard annotation type"}
                                if ($CustomAnnotationTypes.ContainsKey($name)) { throw "a custom annotation type with the name '$name' has already been defined" }
                                break
                            }
                            default {throw "unexpected 'CustomAnnotationType' attribute '$($attribute.Name)'"}
                        }
                    }

                    if ($guid -eq $null) { throw "'CustomAnnotationType' element is missing 'Guid' attribute" }
                    if ($name -eq $null) { throw "'CustomAnnotationType' element is missing 'Name' attribute" }

                    $id = (Register-UiaCustomAnnotationType -Guid $guid).LocalId
                    $CustomAnnotationTypes[$name] = $id
                    break
                }

                "CustomProperty"
                {
                    # Registers a custom property.
                    #
                    # Custom property registration results in a name that subsequently can be used as an attribute
                    # name on Element elements.
                    #
                    # Parents:
                    # Model
                    # Attributes:
                    # Guid: Custom property GUID.
                    # Name: Programmatic name of the custom property (passed on to UIA, irrelevant for the purposes of this script).
                    # Type: Custom property type, expressed as the name of an YellowBox.AutomationType enum member.
                    # MarkupName: Name of the attribute through which the custom property can be assigned to Element elements.
                    # Children:
                    # (none)

                    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 }
                            default {throw "unexpected 'CustomProperty' attribute '$($attribute.Name)'"}
                        }
                    }

                    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
                }

                "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
                }

                "Element"
                {
                    # UI Element.
                    #
                    # Attributes:
                    # AcceleratorKey:
                    # AccessKey:
                    # AutomationId:
                    # ...
                    # Children:
                    # AnnotationProperty
                    # ...

                    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
                }

                "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
                }

                "Model"
                {
                    AssertStates ([ParseState]::Start)
                    # outermost XML element, allows us to have non-UI-elements
                    $state = [ParseState]::Model
                    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