Augustin Ziegler
Augustin Ziegler

Reputation: 193

Powershell class implement get set property

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

Answers (4)

groser
groser

Reputation: 389

Use Update-TypeData

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.

  1. With -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
      }
    ;}
  1. With -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;}

Pros and Cons

property ScriptProperty CodeProperty
Code size smaller bigger
Property type [System.Object] exactly [string]
Intellisense full scriptblock only method names
Constraints no constraints see IMPORTANT

Result

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

DiWat
DiWat

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

Derek Ziemba
Derek Ziemba

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

alx9r
alx9r

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

Related Questions