Reputation: 193
How can I implement a get/set property with PowerShell class? Please have a look on my example below:
Class TestObject
{
[DateTime]$StartTimestamp = (Get-Date)
[DateTime]$EndTimestamp = (Get-Date).AddHours(2)
[TimeSpan] $TotalDuration {
get {
return ($this.EndTimestamp - $this.StartTimestamp)
}
}
hidden [string] $_name = 'Andreas'
[string] $Name {
get {
return $this._name
}
set {
$this._name = $value
}
}
}
New-Object TestObject
Upvotes: 15
Views: 15177
Reputation: 389
You can use Add-Member
, but you must do it for every new object everytime. But you can update type at once with Update-TypeData
that based on Extended Type System.
I recomend to use it in static constructor, because it will be invoked once before creating first object of your type.
Update-TypeData
can add a property to class with parameter -MemberType
ScriptProperty or CodeProperty. Parameter -Value
is mandatory and it must be property getter. Parameter -SecondValue
isn't mandatory and it must be property setter.
-MemberType ScriptProperty
class TestObject1 {
# hidden property
hidden [string]$_name
TestObject1([string]$name) {
$this._name = $name
}
# static constructor or type initializer
static TestObject1() {
# add new property with getter and setter
[TestObject1] | Update-TypeData -MemberType ScriptProperty -MemberName Name -Value {
return $this._name
} -SecondValue {
param([string]$value)
if (![string]::IsNullOrEmpty($value)) {
$this._name = $value
}
}
}
}
Example:
PS> $to1 = [TestObject1]::new('test')
PS> $to1
Name
----
test
PS> $to1.Name = ''
PS> $to1
Name
----
test
# Name in members but not _name
PS> $to1 | Get-Member
TypeName: TestObject1
Name MemberType Definition
---- ---------- ----------
Equals Method bool Equals(System.Object obj)
GetHashCode Method int GetHashCode()
GetType Method type GetType()
ToString Method string ToString()
Name ScriptProperty System.Object Name {get=...
# Intellisense on <Ctrl>+Space
PS> $to1.Name
Name Equals GetHashCode GetType ToString
System.Object Name {get=
return $this._name
;set=
param([string]$value)
if (![string]::IsNullOrEmpty($value)) {
$this._name = $value
}
;}
-MemberType CodeProperty
class TestObject2 {
# hidden property
hidden [string]$_name
TestObject2([string]$name) {
$this._name = $name
}
# getter method
hidden static [string] getName([psobject]$obj) {
return $obj._name
}
# setter method
hidden static [void] setName([psobject]$obj, [string]$value) {
if (![string]::IsNullOrEmpty($value)) {
$obj._name = $value
}
}
# static constructor or type initializer
static TestObject2() {
$getName = [TestObject2].GetMethod('getName')
$setName = [TestObject2].GetMethod('setName')
# static constructor or type initializer
[TestObject2] | Update-TypeData -MemberType CodeProperty -MemberName Name -Value $getName -SecondValue $setName
}
}
IMPORTANT: getter and setter must be public static and get [psobject]
in first argument, it will be $this
. But this parameter cannot be named $this
.
By the way hidden
attribute don't make that methods private but it removes they from Intellisense and Get-Member
result.
Example:
PS> $to2 = [TestObject2]::new('test')
PS> $to2
Name
----
test
PS> $to2.Name = ''
PS> $to2
Name
----
test
# Name in members but not _name
PS> $to2 | Get-Member
TypeName: TestObject2
Name MemberType Definition
---- ---------- ----------
Name CodeProperty System.String Name{get=getName;set=setName;}
Equals Method bool Equals(System.Object obj)
GetHashCode Method int GetHashCode()
GetType Method type GetType()
ToString Method string ToString()
# Intellisense on <Ctrl>+Space
PS> $to2.Name
Name Test Equals GetHashCode GetType ToString
System.String Name{get=getName;set=setName;}
property | ScriptProperty | CodeProperty |
---|---|---|
Code size | smaller | bigger |
Property type | [System.Object] |
exactly [string] |
Intellisense | full scriptblock | only method names |
Constraints | no constraints | see IMPORTANT |
Both variants are good and your can use one of these you want.
This solution can help your for creating classes with custom property get/set or property with only get. And this solution is optimized for only one type changing.
Upvotes: 1
Reputation: 11
The best solution I have found sofar is using 'Update-TypeData'.
see about_Classes_Properties Update-TypeData
The following files are a little sample how to uese it. Init.psq
#-------------------------------------------------------------------------------
# (c) public domain
function InitMyClass
{
<#
.SYNOPSIS
Static constuctor for MyClass.
#>
[OutputType( [void] )]
[CmdletBinding()]
param ()
#-------------------------------------------------------------------------------
# Add a new Property.
# You need to use a string. [MyClass].Name will not work.
Update-TypeData `
-TypeName 'MyClass' `
-MemberName 'Help' `
-MemberType ScriptProperty `
-Value { return GetHelpMyClass $this } `
-SecondValue { param( [string]$psHelp )
SetHelpMyClass $this $psHelp }
#-------------------------------------------------------------------------------
# Just fpr the fun of it.
Remove-Item Function:InitMyClass
} # function InitMyClass
#-------------------------------------------------------------------------------
return
#------------------------------------- EOF -------------------------------------
Func.ps1
#-------------------------------------------------------------------------------
# (c) public domain
function GetHelpMyClass
{
<#
.SYNOPSIS
Static constuctor for MyClass.
#>
[OutputType( [string] )]
[CmdletBinding()]
param
(
[Parameter( Mandatory = $true )]
[MyClass]
$pcThis
)
#-------------------------------------------------------------------------------
# Do what ever you want and return the value.
return ('Help: ' + $pcThis.msHelp )
} # function InitMyClass
function SetHelpMyClass
{
<#
.SYNOPSIS
Does somthing and may store it in the instance.
.NOTES
Parameter may have alll attributes.
#>
[OutputType( [void] )]
[CmdletBinding()]
param
(
[Parameter( Mandatory = $true )]
[MyClass]
$pcThis,
[Parameter( Mandatory = $true )]
[AllowEmptyString()]
[string]
$psNewHelp
)
#-------------------------------------------------------------------------------
# Do whatever you want and store the value.
if( [string]::IsNullOrEmpty( $psNewHelp ))
{
$pcThis.msHelp = 'No help.'
return
} # if( [string]::IsNullOrEmpty( $psNewHelp ))
$pcThis.msHelp = $psNewHelp
} # function InitMyClass
#-------------------------------------------------------------------------------
return
#------------------------------------- EOF -------------------------------------
Class.ps1
#-------------------------------------------------------------------------------
# (c) public domain
#-------------------------------------------------------------------------------
# Avoid double definition of the class
# To debug initiator issues break on the first access to the class (next
# statement) and step into.
if( [MyClass]::mbExist )
{ return }
Write-Error 'If this point is reached the class has an issue.'
#-------------------------------------------------------------------------------
Class MyClass
{
#-------------------------------------------------------------------------------
# visible properties
#-------------------------------------------------------------------------------
# Hiddden property for the Get/Set functions.
# Is not neccesssary but for this sample.
hidden [string]$msHelp
#-------------------------------------------------------------------------------
# Hidden static
# Avoid double definition of the class.
hidden static [bool]$mbExist = $true
#-------------------------------------------------------------------------------
# Constructor
MyClass()
{
SetHelpMyClass $this 'No help given'
} # MyClass()
MyClass(
[string]$psNewHelp
)
{
SetHelpMyClass $this $psNewHelp
} # MyClass( [string] )
#-------------------------------------------------------------------------------
# Static Constructor
static MyClass()
{
InitMyClass
} # static MyClass()
} # Class MyClass
#-------------------------------------------------------------------------------
return
#------------------------------------- EOF -------------------------------------
Test.ps1
#-------------------------------------------------------------------------------
# (c) public domain
#-------------------------------------------------------------------------------
# Get the class
. .\Init.ps1
. .\Func.ps1
. .\Class.ps1
#-------------------------------------------------------------------------------
# Test the class
Write-Output '#'*80
Write-Output '[MyClass]$gcHelp = [MyClass]::new()'
[MyClass]$gcHelp = [MyClass]::new()
$gcHelp.msHelp
$gcHelp.Help
Write-Output ( '#' * 80 )
Write-Output '$gcHelp.Help = 'there is help''
$gcHelp.Help = 'there is help'
$gcHelp.msHelp
$gcHelp.Help
Write-Output ( '#' * 80 )
Write-Output '$gcHelp.Help = $null'
$gcHelp.Help = $null
$gcHelp.msHelp
$gcHelp.Help
Write-Output ( '#' * 80 )
Write-Output '$gcHelp.msHelp = 'there is another help''
$gcHelp.msHelp = 'there is another help'
$gcHelp.msHelp
$gcHelp.Help
Write-Output ( '#' * 80 )
Write-Output 'Write-Output '#'*80'
$gcHelp.msHelp = $null
$gcHelp.msHelp
$gcHelp.Help
#------------------------------------- EOF -------------------------------------
Upvotes: 1
Reputation: 2643
Here's how I went about it
[string]$BaseCodeSignUrl; # Getter defined in __class_init__. Declaration allows intellisense to pick up property
[string]$PostJobUrl; # Getter defined in __class_init__. Declaration allows intellisense to pick up property
[hashtable]$Headers; # Getter defined in __class_init__. Declaration allows intellisense to pick up property
[string]$ReqJobProgressUrl; # Getter defined in __class_init__. Declaration allows intellisense to pick up property
# Powershell lacks a way to add get/set properties. This is a workaround
hidden $__class_init__ = $(Invoke-Command -InputObject $this -NoNewScope -ScriptBlock {
$this | Add-Member -MemberType ScriptProperty -Name 'BaseCodeSignUrl' -Force -Value {
if ($this.Production) { [CodeSign]::CodeSignAPIUrl } else { [CodeSign]::CodeSignTestAPIUrl }
}
$this | Add-Member -MemberType ScriptProperty -Name 'PostJobUrl' -Force -Value {
"$($this.BaseCodeSignUrl)/Post?v=$([CodeSign]::ServiceApiVersion)"
}
$this | Add-Member -MemberType ScriptProperty -Name 'Headers' -Force -Value {
@{
_ExpireInMinutes=[CodeSign]::Timeout.Minutes;
_CodeSigningKey=$this.Key;
_JobId=$this.JobId;
_Debug=$this.Dbg;
_Token=$this.Token;
}
}
$this | Add-Member -MemberType ScriptProperty -Name 'ReqJobProgressUrl' -Force -Value {
"$($this.BaseCodeSignUrl)Get?jobId=$($this.JobId)"
}
});
Upvotes: 1
Reputation: 4194
You can use Add-Member ScriptProperty
to achieve a kind of getter and setter:
class c {
hidden $_p = $($this | Add-Member ScriptProperty 'p' `
{
# get
"getter $($this._p)"
}`
{
# set
param ( $arg )
$this._p = "setter $arg"
}
)
}
Newing it up invokes the initializer for $_p
which adds scriptproperty p
:
PS C:\> $c = [c]::new()
And using property p
yields the following:
PS C:\>$c.p = 'arg value'
PS C:\>$c.p
getter setter arg value
This technique has some pitfalls which are mostly related to how verbose and error-prone the Add-Member
line is. To avoid those pitfalls, I implemented Accessor
which you can find here.
Using Accessor
instead of Add-Member
does an amount of error-checking and simplifies the original class implementation to this:
class c {
hidden $_p = $(Accessor $this {
get {
"getter $($this._p)"
}
set {
param ( $arg )
$this._p = "setter $arg"
}
})
}
Upvotes: 16