开发者

Is it possible to extend indexers in PowerShell?

开发者 https://www.devze.com 2022-12-30 23:36 出处:网络
PowerShell\'s type extension facility is neat, but I haven\'t yet figured out the way -- if one exists -- to extend an indexer. I\'ve tried to add a ScriptProperty for the indexer property (Chars in t

PowerShell's type extension facility is neat, but I haven't yet figured out the way -- if one exists -- to extend an indexer. I've tried to add a ScriptProperty for the indexer property (Chars in the case of System.String) and a ScriptMethod for the getter (get_Chars), but neither approach seems fruitful. Is it at all possible, or am I wasting my time? :)

[Edit] Apparently the proper member type is ParameterizedProperty, but when I try that, I get:

Add-Member : Cannot add a member with type "ParameterizedProperty". Specify a different 
type for the MemberTypes parameter.

At line:1 char:11
+ Add-Member <<<<  -Me开发者_Go百科mberType ParameterizedProperty -Name Item -InputObject $string { "x" }
+ CategoryInfo          : InvalidOperation: (:) [Add-Member], InvalidOperationException
+ FullyQualifiedErrorId : CannotAddMemberType,Microsoft.PowerShell.Commands.AddMemberCommand


I'm going to conclude that the error message I'm getting is the final word on the matter. Also, on further reflection it has become obvious that the sort of extending I was hoping for is not supported by this mechanism anyway. :-)


You can't create ParameterizedProperty properties directly in Powershell, but you are able to indirectly create them by allowing Powershell to wrap a PSObject around an object that has an accessor property. You then make this PSObject a NoteProperty on the object to which you want to add the property. In C#, we are talking about a this[] accessor. I have written a Powershell script which creates a minimal .NET object which has as this[] accessor. In order to make this as generic as possible, I have tried to copy what the ScriptProperty member does, and I have added two properties of type ScriptBlock - one for a Get block, and another for a Set block. So essentially, when the user sets the this[] accessor, it calls the Set block, and when the user retrieves from the this[] accessor, it calls the Get block.

The following module, I have called PSObjectWrappers.psm1:

<#
    .SUMMARY
    Creates a new ParameterizedPropertyAccessor object.

    .DESCRIPTION
    Instantiates and returns an object compiled on the fly which provides some plumbing which allows a user to call a new Parameterized
    Property, which looks as if it is created on the parent object. In fact, a NoteProperty is created on the parent object which retrieves
    an instance of ParameterizedPropertyAccessor, which has a this[] accessor which Powershell wraps in a ParameterizedProperty object.
    When the this[] accessor is retrieved, it tries to retrieve a value via a Get script block. When the this[] accessor is updated, this 
    triggers a Set script block.

    .NOTES
    No actual variable value state is stored by this object.
    The C# code is conditionally compiled to take advantage of new functionality in Powershell 4. Before this version, the first parameter
    in the Set and Get script blocks must be "[PSObject] $this". From this version, the $this parameter is automatically created for the user.
#>
Function New-ParameterizedPropertyAccessor
{
    Param(
        # Contains the object on which the "ParameterizedProperty" will be added.
        [Parameter(Mandatory = $true, Position = 0)]
        [PSObject] $Parent,
        # The name of the parameterized property.
        [Parameter(Mandatory = $true, Position = 1)]
        [string] $Name,
        # Script block which will be called when the property is retrieved.
        # First parameter must be $this. Second parameter must be $key.
        [Parameter(Mandatory = $true, Position = 2)]
        [scriptblock] $Get,
        # Script block which will be called when the property is set.
        # First parameter must be $this. Second parameter must be $key. Third parameter must be $value.
        [Parameter(Mandatory = $true, Position = 3)]
        [scriptblock] $Set
    );

    # Note. You *MUST* ensure the next line starts at position 1 on the line. Likewise, the last line of the code *MUST*
    # start at position 1 on the line.

$csharpCode = @'
    using System;
    using System.Collections.Generic;
    using System.Management.Automation;

    public class ParameterizedPropertyAccessor
    {
        private PSObject _parentPsObject;
        private ScriptBlock _getBlock;
        private ScriptBlock _setBlock;

        public ParameterizedPropertyAccessor(PSObject parentPsObject, string propertyName, ScriptBlock getBlock, ScriptBlock setBlock)
        {
            _parentPsObject = parentPsObject;

            PSVariable psVariable = new PSVariable(propertyName, this, ScopedItemOptions.ReadOnly);
            PSVariableProperty psVariableProperty = new PSVariableProperty(psVariable);
            _parentPsObject.Properties.Add(psVariableProperty);

            _getBlock = getBlock;
            _setBlock = setBlock;
        }

        public object this[object key]
        {
            get
            {
#if WITH_CONTEXT
                return _getBlock.InvokeWithContext(null, new List<PSVariable> { new PSVariable("this", _parentPsObject) }, new object[] { key });
#else
                return _getBlock.Invoke(new object[] { _parentPsObject, key });
#endif
            }
            set
            {
#if WITH_CONTEXT
                _setBlock.InvokeWithContext(null, new List<PSVariable> { new PSVariable("this", _parentPsObject) }, new object[] { key, value });
#else
                _setBlock.Invoke(new object[] { _parentPsObject, key, value });
#endif
            }
        }
    }
'@;

    <#
    The version of the ScriptBlock object in Powershell 4 and above allows us to create automatically declared
    context variables. In this case, we are providing a $this object, like you would get if we were using a
    ScriptMethod or ScriptProperty member script. If we are using this version, then set the WITH_CONTEXT symbol 
    to conditionally compile a version of the C# code above which takes advantage of this.
    #>
    If ($PSVersionTable.PSVersion.Major -ge 4)
    {
        $compilerParameters = New-Object System.CodeDom.Compiler.CompilerParameters;
        $compilerParameters.CompilerOptions = "/define:WITH_CONTEXT";
        $compilerParameters.ReferencedAssemblies.Add( "System.dll" );
        $compilerParameters.ReferencedAssemblies.Add( "System.Core.dll" );
        $compilerParameters.ReferencedAssemblies.Add( ([PSObject].Assembly.Location) );
    }
    # Compiles the C# code in-memory and allows us to instantiate it.
    Add-Type -TypeDefinition $csharpCode -CompilerParameters $compilerParameters;

    # Instantiates the object.
    New-Object ParameterizedPropertyAccessor -ArgumentList $Parent,$Name,$Get,$Set;
}

Note that I have done so conditional compilation in the C# code to make the code behave like a proper ScriptBlock in Powershell 4 and above, so a $this variable is automatically provided. Otherwise, you must ensure that the first parameter in each script block is called $this.

The following is my test script, Test-PPA.ps1:

<#
    .SYNOPSIS
    Test script for the ParameterizedPropertyAccessor object.
#>



<#
    .SYNOPSIS
    Create a new PSCustomObject which will contain a NoteProperty called Item accessed like a ParameterizedProperty.
#>
Function New-TestPPA
{
    # Instantiate our test object.
    $testPPA = New-Object -TypeName PSCustomObject;

    # Create a new instance of our PPA object, added to our test object, providing it Get and Set script blocks.
    # Note that currently the scripts are set up for Powershell 4 and above. If you are using a version of Powershell
    # previous to this, comment out the current Param() values, and uncomment the alternate Param() values.
    $ppa = New-ParameterizedPropertyAccessor -Parent $testPPA -Name Item -Get `
    {
        Param(
            <#
            [Parameter(Mandatory = $true, Position = 0)]
            [PSObject] $this,
            [Parameter(Mandatory = $true, Position = 1)]
            [string] $Key
            #>
            [Parameter(Mandatory = $true, Position = 0)]
            [string] $Key
        )
        $this._ht[$Key];
    } -Set {
        Param(
            <#
            [Parameter(Mandatory = $true, Position = 0)]
            [PSObject] $this,
            [Parameter(Mandatory = $true, Position = 1)]
            [string] $Key,
            [Parameter(Mandatory = $true, Position = 2)]
            [string] $Value
            #>
            [Parameter(Mandatory = $true, Position = 0)]
            [string] $Key,
            [Parameter(Mandatory = $true, Position = 1)]
            [string] $Value
        )
        $this._ht[$Key] = $Value;
    };

    # Add a HashTable <_ht> used as our backing store. Note that this could be any keyed collection type object.
    $testPPA | Add-Member -MemberType NoteProperty -Name _ht -Value @{} -PassThru;
}


[string] $scriptDir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent;
Import-Module $scriptDir\PSObjectWrappers.psm1;

# Create test object.
$testPPA = New-TestPPA;

# Note that "Item" property is actually a NoteProperty of type ParameterizedPropertyAccessor.
Write-Host "Type '`$testPPA | gm' to see Item NoteProperty.";

# Note that it is the ParameterizedPropertyAccessor object retrieved that has a ParameterizedProperty.
# Also note that Powershell has named this property "Item".
Write-Host "Type '`$testPPA.Item | gm' to see Item ParameterizedProperty";

# Step through what happens when we "set" the "parameterized" Item property.
# Note that this is actually retrieving the Item NoteProperty, and then setting its default accessor, which calls
# the 'Set' ScriptBlock.

Write-Host "";
Write-Host "Setting Name value";
Write-Host "... to 'Mark'."
$testPPA.Item["Name"] = "Mark";

# Step through what happens when we "get" the "parameterized" Item property.
# Note that this is actually retrieving the Item NoteProperty, and then retrieving its default accessor, which calls
# the 'Get' ScriptBlock.

Write-Host "";
Write-Host "Retrieving Name value:";
$temp = $testPPA.Item["Name"];
Write-Host $temp;

Note that you will have to change the script blocks, as indicated, if you are using versions previous to Powershell 4.

0

精彩评论

暂无评论...
验证码 换一张
取 消