您不能直接在 Powershell 中创建 ParameterizedProperty 属性,但您可以通过允许 Powershell 将 PSObject 包装在具有访问器属性的对象周围来间接创建它们。然后,将此 PSObject 设置为要向其添加属性的对象的 NoteProperty。在 C# 中,我们谈论的是this[]
访问器。我编写了一个 Powershell 脚本,它创建了一个具有this[]
访问器的最小 .NET 对象。为了使其尽可能通用,我尝试复制 ScriptProperty 成员的功能,并添加了两个类型的属性ScriptBlock
- 一个用于Get
块,另一个用于Set
块。所以本质上,当用户设置this[]
访问器时,它调用Set
块,当用户从this[]
访问器,它调用Get
块。
以下模块,我称之为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;
}
请注意,我已经在 C# 代码中进行了条件编译,以使代码的行为类似于 Powershell 4 及更高版本中的正确 ScriptBlock,因此$this
会自动提供一个变量。否则,您必须确保调用每个脚本块中的第一个参数$this
。
以下是我的测试脚本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;
请注意,如果您使用的是 Powershell 4 之前的版本,则必须按照指示更改脚本块。