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. If the type is deserialized, this is the depth of subpropeties
    # that will be stored. For instance, a serialization depth of 3 would storage an object, it's
    # subproperties, and those objects' subproperties. You can use the serialization depth
    # to minimize the overhead of moving objects back and forth across the remoting boundary,
    # or to ensure that you capture the correct information.
    [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,

    # Will hide any properties in the list from a display
    [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"
}
"@
)
    }

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

            #region Construct PSStandardMembers
            if ($psBoundParameters.ContainsKey('SerializationDepth') -or
                $psBoundParameters.ContainsKey('IdProperty') -or
                $psBoundParameters.ContainsKey('DefaultDisplay') -or
                $psBoundParameters.ContainsKey('Reserializer')) {
                $defaultDisplayXml = if ($psBoundParameters.ContainsKey('DefaultDisplay')) {
    $referencedProperties = "<Name>" + ($defaultDisplay -join "</Name>
                            <Name>"
) + "</Name>"
    " <PropertySet>
                        <Name>DefaultDisplayPropertySet</Name>
                        <ReferencedProperties>
                            $referencedProperties
                        </ReferencedProperties>
                    </PropertySet>
    "

                }
                $serializationDepthXml = if ($psBoundParameters.ContainsKey('SerializationDepth')) {
                    "
                    <NoteProperty>
                        <Name>SerializationDepth</Name>
                        <Value>$SerializationDepth</Value>
                    </NoteProperty>"

                } else {$null }

                $ReserializerXml = if ($psBoundParameters.ContainsKey('Reserializer'))  {
    "
                    <NoteProperty>
                        <Name>TargetTypeForDeserialization</Name>
                        <Value>$Reserializer</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"'
                    }
                    "
                <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) {
                    $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) {                                "
                <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) {
                        "
                <ScriptProperty $isHiddenChunk>
                    <Name>$($propertyNameAndCode.Key)</Name>
                    <GetScriptBlock>
                        $([Security.SecurityElement]::Escape($getScript))
                    </GetScriptBlock>
                    <SetScriptBlock>
                        $([Security.SecurityElement]::Escape($setScript))
                    </SetScriptBlock>
                </ScriptProperty>"

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

        }
    }

}