Commands/Types.PS1XML/Write-TypeView.ps1

function Write-TypeView
{
    <#
    .Synopsis
        Writes extended type view information
    .Description
        PowerShell has a robust, extensible types system. With Write-TypeView, you can easily add extended type information to any type.
        This can include:
            The default set of properties to display (-DefaultDisplay)
            Sets of properties to display (-PropertySet)
            Serialization Depth (-SerializationDepth)
            Virtual methods or properties to add onto the type (-ScriptMethod, -ScriptProperty and -NoteProperty)
            Method or property aliasing (-AliasProperty)
    .Link
        Out-TypeView
    .Link
        Add-TypeView
    #>

    [OutputType([string])]
    param(
    # The name of the type.
    # Multiple type names will all have the same methods, properties, events, etc.
    [Parameter(Mandatory,ValueFromPipelineByPropertyName,Position=0)]
    [String[]]
    $TypeName,

    # A collection of virtual method names and the script blocks that will be used to run the virtual method.
    [ValidateScript({
        if ($_.Keys | Where-Object {$_-isnot [string]}) {
            throw "Must provide the names of script methods"
        }
        if ($_.Values | Where-Object {$_ -isnot [ScriptBlock]}) {
            throw "Must provide script blocks to handle each method"
        }
        return $true
    })]
    [Collections.IDictionary]$ScriptMethod = @{},

    # A Collection of virtual property names and the script blocks that will be used to get the property values.
    [ValidateScript({
        $in = $_
        foreach ($kv in $in.GetEnumerator()) {
            if ($kv.Key -isnot [string]) {
                throw "Must provide the names of script properties"
            }
            if ($kv.Value.Count -gt 2) {
                throw "No more than two scripts can be provided"
            }
            foreach ($_ in $kv.Value) {
                if ($_ -isnot [ScriptBlock]) {
                    throw "Must provide script blocks to handle each property"
                }
            }
        }
        return $true
    })]
    [Collections.IDictionary]$ScriptProperty,

    # A collection of fixed property values.
    [ValidateScript({
        if ($_.Keys | Where-Object { $_-isnot [string] } ) {
            throw "Must provide the names of note properties"
        }
        return $true
    })]
    [Collections.IDictionary]$NoteProperty,

    # A collection of property aliases
    [ValidateScript({
        foreach ($kv in $_.GetEnumerator()) {
            if ($kv.Key -isnot [string] -or $kv.Value -isnot [string]) {
                throw "All keys and values in the property rename map must be strings"
            }
        }
        return $true
    })]
    [Collections.IDictionary]$AliasProperty,

    # A collection of scripts that may create events.
    # These will become ScriptMethods.
    # * Send_NameOfEvent will call the generator and optionally send an event. Arguments will may be passed along to the event.
    # * Register_NameOfEvent({}) will call Register-EngineEvent to register an event handler
    # * Unregister_NameOfEvent([[PSEventSubscriber]) will call Unregister-EngineEvent to remove the handler.
    [ValidateScript({
        if ($_.Keys | Where-Object {$_-isnot [string]}) {
            throw "Must provide the names of events methods"
        }
        if ($_.Values | Where-Object {$_ -isnot [ScriptBlock] -and $_ -notlike '*New-Event*'}) {
            throw "Must provide script blocks for values, and each must contain New-Event"
        }
        return $true
    })]
    [Collections.IDictionary]$EventGenerator,

    # A list of event names.
    # These will become ScriptMethods.
    # * Send_NameOfEvent will call the generator and optionally send an event. Arguments will be sent as event and message data.
    # * Register_NameOfEvent({}) will call Register-EngineEvent to register an event handler
    # * Unregister_NameOfEvent([[PSEventSubscriber]) will call Unregister-EngineEvent to remove the handler.
    [string[]]$EventName,

    # The default display.
    # If only one propertry is used, this will set the default display property.
    # If more than one property is used, this will set the default display member set.
    [string[]]$DefaultDisplay,

    # The ID property
    [string]$IdProperty,

    <#
    
    The serialization depth.

    Serialization depth can be used to minimize the overhead of objects.
    
    If the type is deserialized, this is the depth of subpropeties that will be stored.
    
    For instance, a serialization depth of 3 would store:
      * an object,
      * it's subproperties
      * those objects' subproperties
      
    The default serialization depth is 2.
    #>
    
    [int]$SerializationDepth = 2,

    # The reserializer type used for recreating a deserialized type.
    # If none is provided, consider using -Deserialized
    [Type]$Reserializer,

    # Property sets define default views for an object. A property set can be used with Select-Object
    # to display just that set of properties.
    [ValidateScript({
        if ($_.Keys | Where-Object {$_ -isnot [string] } ) {
            throw "Must provide the names of property sets"
        }
        if ($_.Values |
            Where-Object {$_ -isnot [string] -and  $_ -isnot [Object[]] -and $_ -isnot [string[]] }){
            throw "Must provide a name or list of names for each property set"
        }
        return $true
    })]
    [Collections.IDictionary]$PropertySet,

    # If provided, will hide any properties in the list from automatically being displayed.
    [string[]]$HideProperty,

    # If set, will generate an identical typeview for the deserialized form of each typename.
    [switch]$Deserialized
    )

    begin {        
        $RegisterMethod = {
            param(
            [Parameter(Mandatory)]
            [string]
            $SourceIdentifier
            )
            [ScriptBlock]::Create(@"
param([ScriptBlock]`$EventHandler, `$SourceIdentifier = '$SourceIdentifier')
Register-EngineEvent -SourceIdentifier `$SourceIdentifier -Action `$EventHandler
"@
)
        }

        $UnregisterMethod =
            [ScriptBlock]::Create(@"
param(`$EventHandler)
if (`$Eventhandler -is [Management.Automation.PSEventSubscriber]) {
    `$Eventhandler | Unregister-Event
} elseif (`$eventHandler -is [string]) {
    Get-EventSubscriber -SourceIdentifier `$EventHandler -ErrorAction SilentlyContinue | Unregister-Event
} elseif (`$eventHandler -is [int]) {
    Get-EventSubscriber -SubscriptionID `$EventHandler -ErrorAction SilentlyContinue | Unregister-Event
} elseif (`$eventHandler -is [ScriptBlock]) {
    Get-EventSubscriber |
        Where-Object { (`$_.Action.Command -replace '\s') -eq (`$eventHandler -replace '\s')} | Unregister-Event
} else {
    throw "Handler must be a [PSEventSubscriber], [ScriptBlock], a [string] SourceIdentifier, or an [int]SubscriptionID"
}
"@
)

        $DebugBuild = 
            $DebugPreference -ne 'SilentlyContinue' -and $DebugPreference -ne 'Ignore'
    }

    process {
        if ($Deserialized -and $TypeName -notlike 'Deserialized.*') {
            $typeName =
                foreach ($tn in $TypeName) {
                    $tn, "Deserialized.$tn"
                }
        }


        # Before we get started, we want to turn the abstract idea of Events into ScriptMethods
        if ($EventGenerator) { # Event Generators come first
            foreach ($evtGen in $EventGenerator.GetEnumerator()) {
                $evt = $evtGen.Key.Substring(0,1).ToUpper() + $evtGen.Key.Substring(1)
                $sendMethodName = "Send_$evt"
                $registerMethodName = "Register_$evt"
                $UnregisterMethodName = "Unregister_$evt"
                if ($ScriptMethod[$sendMethodName] -or     # If we already have Send_,
                    $ScriptMethod[$registerMethodName] -or # Register_,
                    $ScriptMethod[$unregisterMethodName]   # Unregister_
                ) {
                    # the user wants it that way.
                    continue
                }
                $ScriptMethod[$sendMethodName]     = $evtGen.Value
                $ScriptMethod[$registerMethodName] =
                    & $RegisterMethod "$($TypeName -replace '^Deserialized\.').$($evtGen.Key)"
                $ScriptMethod[$UnregisterMethodName] =
                    & $UnregisterMethod "$($TypeName -replace '^Deserialized\.').$($evtGen.Key)"
            }
        }
        elseif ($EventName) {
            foreach ($evtName in $EventName) {
                $evt = $evtName.Substring(0,1).ToUpper() + $evtName.Substring(1)
                $sendMethodName = "Send_$evt"
                $registerMethodName = "Register_$evt"
                $UnregisterMethodName = "Unregister_$evt"
                if ($ScriptMethod[$sendMethodName] -or     # If we already have Send_,
                    $ScriptMethod[$registerMethodName] -or # Register_,
                    $ScriptMethod[$unregisterMethodName]   # Unregister_
                ) {
                    # the user wants it that way.
                    continue
                }
                $evtSourceId = "$($TypeName -replace '^Deserialized\.').$evt"
                $ScriptMethod[$sendMethodName]     = [ScriptBlock]::Create("
                    New-Event -SourceIdentifier '$evtSourceId' -Sender `$this -EventArguments `$args -MessageData `$args
                "
)
                $ScriptMethod[$registerMethodName] = & $RegisterMethod $evtSourceId
                $ScriptMethod[$unregisterMethodName] = & $unRegisterMethod $evtSourceId
            }
        }

        foreach ($tn in $TypeName) {
            $memberSetXml = ""
            if ($DebugBuild) {
                $updateSplat = [Ordered]@{
                    TypeName = $tn
                    Force = $true
                }
            }
            #region Construct PSStandardMembers
            if ($psBoundParameters.ContainsKey('SerializationDepth') -or
                $psBoundParameters.ContainsKey('IdProperty') -or
                $psBoundParameters.ContainsKey('DefaultDisplay') -or
                $psBoundParameters.ContainsKey('Reserializer')) {
                $defaultDisplayXml = if ($psBoundParameters.ContainsKey('DefaultDisplay')) {
                    if ($DebugBuild) {
                        Update-TypeData @updateSplat -DefaultDisplayPropertySet $DefaultDisplay
                    }
    $referencedProperties = "<Name>" + ($defaultDisplay -join "</Name>
                            <Name>"
) + "</Name>"
    " <PropertySet>
                        <Name>DefaultDisplayPropertySet</Name>
                        <ReferencedProperties>
                            $referencedProperties
                        </ReferencedProperties>
                    </PropertySet>
    "

                }
                $serializationDepthXml = if ($psBoundParameters.ContainsKey('SerializationDepth')) {
                    if ($DebugBuild) {
                        Update-TypeData @updateSplat -SerializationDepth $SerializationDepth
                    }
                    "
                    <NoteProperty>
                        <Name>SerializationDepth</Name>
                        <Value>$SerializationDepth</Value>
                    </NoteProperty>"

                } else {$null }

                $ReserializerXml = if ($psBoundParameters.ContainsKey('Reserializer'))  {
                    if ($DebugBuild) {
                        Update-TypeData @updateSplat -TargetTypeForDeserialization $Reserializer 
                    }
    "
                    <NoteProperty>
                        <Name>TargetTypeForDeserialization</Name>
                        <Value>$($Reserializer.FullName)</Value>
                    </NoteProperty>

    "

                } else { $null }

                $memberSetXml = "
                <MemberSet>
                    <Name>PSStandardMembers</Name>
                    <Members>
                        $defaultDisplayXml
                        $serializationDepthXml
                        $reserializerXml
                    </Members>
                </MemberSet>
                "

            }
            #endregion Construct PSStandardMembers

            #region PropertySetXml
            $propertySetXml  = if ($psBoundParameters.PropertySet) {

                foreach ($NameAndValue in $PropertySet.GetEnumerator() | Sort-Object Key) {                    
                    $referencedProperties = "<Name>" + ($NameAndValue.Value -join "</Name>
                        <Name>"
) + "</Name>"
                "<PropertySet>
                    <Name>$([Security.SecurityElement]::Escape($NameAndValue.Key))</Name>
                    <ReferencedProperties>
                        $referencedProperties
                    </ReferencedProperties>
                </PropertySet>"

                }
            } else {
                ""
            }
            #endregion



            #region Aliases
            $aliasPropertyXml = if ($psBoundParameters.AliasProperty) {
                foreach ($NameAndValue in $AliasProperty.GetEnumerator() | Sort-Object Key) {
                    $isHiddenChunk = if ($HideProperty -contains $NameAndValue.Key) {
                        'IsHidden="true"'
                    }
                    if ($DebugBuild) {                        
                        Update-TypeData @updateSplat -MemberType AliasProperty -MemberName $NameAndValue.Key -Value $NameAndValue.Value
                    }
                    "
                <AliasProperty $isHiddenChunk>
                    <Name>$([Security.SecurityElement]::Escape($NameAndValue.Key))</Name>
                    <ReferencedMemberName>$([Security.SecurityElement]::Escape($NameAndValue.Value))</ReferencedMemberName>
                </AliasProperty>"

                }
            } else {
                ""
            }
            #endregion Aliases
            $NotePropertyXml = if ($psBoundParameters.NoteProperty) {
                foreach ($NameAndValue in $NoteProperty.GetEnumerator() | Sort-Object Key) {
                    if ($DebugBuild) {                        
                        Update-TypeData @updateSplat -MemberType NoteProperty -MemberName $NameAndValue.Key -Value $NameAndValue.Value
                    }
                    $isHiddenChunk = if ($HideProperty -contains $NameAndValue.Key) {
                        'IsHidden="true"'
                    }
                    "
                <NoteProperty $isHiddenChunk>
                    <Name>$([Security.SecurityElement]::Escape($NameAndValue.Key))</Name>
                    <Value>$([Security.SecurityElement]::Escape($NameAndValue.Value))</Value>
                </NoteProperty>"

                }
            } else {
                ""
            }
            $scriptMethodXml = if ($ScriptMethod -and $ScriptMethod.Count) {                
                foreach ($methodNameAndCode in $ScriptMethod.GetEnumerator() | Sort-Object Key) {
                if ($DebugBuild) {                        
                    Update-TypeData @updateSplat -MemberType ScriptMethod -MemberName $methodNameAndCode.Key -Value $methodNameAndCode.Value
                }
                "<ScriptMethod>
                    <Name>$($methodNameAndCode.Key)</Name>
                    <Script>
                        $([Security.SecurityElement]::Escape($methodNameAndCode.Value))
                    </Script>
                </ScriptMethod>"

                }
            } else {
                ""
            }

            #region Script Property
            $scriptPropertyXml = if ($psBoundParameters.ScriptProperty) {
                foreach ($propertyNameAndCode in $ScriptProperty.GetEnumerator() | Sort-Object Key) {
                    $isHiddenChunk = if ($HideProperty -contains $propertyNameAndCode.Key) {
                        'IsHidden="true"'
                    }
                    $getScript, $setScript = $propertyNameAndCode.Value
                    if ($getScript -and $setScript) {
                        if ($DebugBuild) {
                            Update-TypeData @updateSplat -MemberType ScriptProperty -MemberName $propertyNameAndCode.Key -Value $getScript -SecondValue $setScript
                        }

                        "
                <ScriptProperty $isHiddenChunk>
                    <Name>$($propertyNameAndCode.Key)</Name>
                    <GetScriptBlock>
                        $([Security.SecurityElement]::Escape($getScript))
                    </GetScriptBlock>
                    <SetScriptBlock>
                        $([Security.SecurityElement]::Escape($setScript))
                    </SetScriptBlock>
                </ScriptProperty>"

                    } else {
                        if ($DebugBuild) {
                            Update-TypeData @updateSplat -MemberType ScriptProperty -MemberName $propertyNameAndCode.Key -Value $getScript
                        }
                        "
                <ScriptProperty $isHiddenChunk>
                    <Name>$($propertyNameAndCode.Key)</Name>
                    <GetScriptBlock>
                        $([Security.SecurityElement]::Escape($propertyNameAndCode.Value))
                    </GetScriptBlock>
                </ScriptProperty>"

                    }
                }
            }

            $innerXml = @($memberSetXml) + $propertySetXml + $aliasPropertyXml + $codePropertyXml + $codeMethodXml + $scriptMethodXml + $scriptPropertyXml + $NotePropertyXml

            $innerXml = ($innerXml  | Where-Object {$_} ) -join ([Environment]::NewLine)
            "
        <Type>
            <Name>$tn</Name>
            <Members>
                $innerXml
            </Members>
        </Type>"

        }

        if ($DebugBuild) {
            $myModule = $MyInvocation.MyCommand.ScriptBlock.Module
            if (-not $myModule.DebuggingTypeNames) {
                $myModule |
                    Add-Member NoteProperty DebuggingTypeNames @() -Force
            }
            $MyInvocation.MyCommand.ScriptBlock.Module.DebuggingTypeNames += $TypeName
        }
    }
}