Can advanced functions be added to custom objects?

This topic contains 0 replies, has 1 voice, and was last updated by Profile photo of Forums Archives Forums Archives 5 years, 3 months ago.

  • Author
    Posts
  • #5893

    by willsteele at 2012-08-15 01:08:45

    I am working on a script in which I want to use a custom object. The plan is to create the central object, and, add some key advanced functions as if they were methods. Can I do this? And, if so, how?

    by JeffH at 2012-08-15 05:48:51

    There are a few ways you could do this. The easiest might be to use Add-Member.


    $obj=new-object psobject -Property @{
    Name=$env:username
    Computer=$env:computername
    Day=(Get-Date).DayOfWeek
    }

    $m1={get-service | where {$_.status -eq "stopped"}}
    $m2={Param([string]$log="System") Get-Eventlog -logname $log -newest 10}

    $obj | Add-Member -MemberType ScriptMethod -Name GetRunning -Value $m1
    $obj | Add-Member -MemberType ScriptMethod -Name GetLastLogs -Value $m2

    $obj

    by yooakim at 2012-08-15 06:04:01

    Nice example, it's so powerful! Thanks!

    by DexterPOSH at 2012-08-15 06:52:27

    Nicely explained.
    Till now I have been adding just the NoteProperty to Custom Objects, but now time to try this.

    by willsteele at 2012-08-15 07:10:35

    Thanks Jeff. That helps. The Get-Help shows examples with the $this object. I didn't connect the ability to simply call methods independent of the object itself.

    by poshoholic at 2012-08-15 08:13:33

    Here's another really fun technique that allows you to create an object with both public and private methods. You can also define properties as read-only using this technique. It was fun to write, and it reminds me of why I love PowerShell. Don't be scared, this is the advanced scripting forum ;).

    Anyhow, here's the script, with comments explaining what's going on:

    $userClass = {
    # Use the param block to define the properties for "constructor". The entire script block is
    # the constructor, defining properties and methods on the objects dynamically.
    [CmdletBinding()]
    param(
    [Parameter(Position=0,Mandatory=$true)]
    [ValidateNotNullOrEmpty()]
    [ValidateScript({$_ -eq $_.Trim()})]
    [System.String]
    $FirstName,

    [Parameter(Position=1,Mandatory=$true)]
    [ValidateNotNullOrEmpty()]
    [ValidateScript({$_ -eq $_.Trim()})]
    [System.String]
    $LastName
    )

    # First turn off default exporting so that you can control what is public and what is private.
    Export-ModuleMember

    # Here we define our constructor parameters as read-only public variables
    Set-Variable -Name FirstName -Option ReadOnly
    Set-Variable -Name LastName -Option ReadOnly
    Export-ModuleMember -Variable FirstName,LastName

    # We also need to calculate a read-only public FullName variable based on those values.
    Set-Variable -Name FullName -Option ReadOnly -Value "$FirstName $LastName"
    Export-ModuleMember -Variable FullName

    # Now lets set up an internal private method to do some work for us
    function UpdateEmailAddress {
    [CmdletBinding()]
    param()
    $FirstName.ToLower().SubString(0,1) + ($LastName -replace '[^a-z]').ToLower().SubString(0,[System.Math]::Min($LastName.Length,7)) + '@poshoholicstudios.com'
    }

    # Now we can create a public EmailAddress variable that is defined by using the private function as part of the "constructor".
    Set-Variable -Name EmailAddress -Option ReadOnly -Value (UpdateEmailAddress)
    Export-ModuleMember -Variable EmailAddress

    # But what if they get married and change their last name? We can use a public method for that
    # since the public property is read-only
    function ChangeLastName {
    [CmdletBinding()]
    [OutputType([System.Void])]
    param(
    [Parameter(Position=0,Mandatory=$true)]
    [ValidateNotNullOrEmpty()]
    [ValidateScript({$_ -eq $_.Trim()})]
    [System.String]
    $NewLastName
    )
    Set-Variable -Scope script -Name LastName -Value $NewLastName -Force
    Set-Variable -Scope script -Name EmailAddress -Value (UpdateEmailAddress) -Force
    }
    Export-ModuleMember -Function ChangeLastName
    }

    # Create our object
    $me = New-Module -AsCustomObject -ScriptBlock $userClass -ArgumentList Kirk,Munro

    # Look at the properties it has.
    $me

    # Look at the public properties and methods it has. Note that this approach automatically
    # defines NoteProperty and ScriptMethod members. The key difference here is control over
    # visibility of the members and methods, plus being able to create read-only properties.
    # Note that the UpdateEmailAddress method is not displayed because it is private.
    $me | Get-Member

    # Now let's try changing FirstName (that's read-only, remember?). This will not work.
    $me.FirstName = 'Poshoholic'

    # Here's the email address before we change the last name
    $me.EmailAddress

    # Now we can change the last name.
    $me.ChangeLastName('Poshoholic')

    # And here's the email address after we change the last name
    $me.EmailAddress

    Is that fun or what? 😀

    by willsteele at 2012-08-15 10:09:16

    Excellent post Kirk. Thanks for the example. As a sidenote, that is one thing I find missing (and am excited to see this forum provide) is in depth, advanced samples.

    Back to the main thread, and this may well be a separate thread, but, I am implementing a system based on a central XML document. I will be using the XML document (in memory) as the object itself. This way, I can tap into built-in XML object model and manipulate it as an object. In that case, how would I add custom methods to this central object? In short, how would I extend the XML Document class as loaded in memory? If this is better in a separate thread, I can post sample code to target the discussion.

    by jmiller76 at 2012-08-15 10:56:12

    Will likes to point out this type of forum conversation, because he knows I'll spend a part of my lunch learning and adding my own $0.02. Thanks will I was going to do "work".


    function new-Person {
    # Use the param block to define the properties for "constructor". The entire script block is
    # the constructor, defining properties and methods on the objects dynamically.
    [CmdletBinding()]
    param(
    [Parameter(Position=0,Mandatory=$true)]
    [ValidateNotNullOrEmpty()]
    [ValidateScript({$_ -eq $_.Trim()})]
    [System.String]
    $FirstName,

    [Parameter(Position=1,Mandatory=$true)]
    [ValidateNotNullOrEmpty()]
    [ValidateScript({$_ -eq $_.Trim()})]
    [System.String]
    $LastName
    )
    new-object PSObject -Property @{"FirstName"=$FirstName;"LastName"=$LastName} |
    Add-Member -MemberType ScriptProperty -Name "EmailAddress" {
    $this.FirstName.ToLower().SubString(0,1) + ($this.LastName -replace '[^a-z]').ToLower().SubString(0,[System.Math]::Min($this.LastName.Length,7)) + '@poshoholicstudios.com'
    } -PassThru |
    Add-Member -MemberType ScriptProperty -Name "FullName" {
    "$($this.FirstName) $($this.LastName)"
    } -PassThru |
    Add-Member -MemberType ScriptMethod -Name "ChangeLastName" {
    [CmdletBinding()]
    [OutputType([System.Void])]
    param(
    [Parameter(Position=0,Mandatory=$true)]
    [ValidateNotNullOrEmpty()]
    [ValidateScript({$_ -eq $_.Trim()})]
    [System.String]
    $NewLastName
    )
    $this.LastName=$NewLastName
    } -PassThru
    }

    # Create our object
    $me2 = new-Person "Kirk" "Munro"

    # Look at the properties it has.
    $me2

    $me2 | Get-Member

    # We lose the ReadOnly Property Aspect
    #$me.FirstName = 'Poshoholic'

    # Here's the email address before we change the last name
    $me2.EmailAddress

    # Now we can change the last name.
    #$me2.LastName = 'Poshoholic'
    $me2.ChangeLastName('Poshoholic')

    # And here's the email address after we change the last name
    $me2.EmailAddress

    I like the flexability that kirk added to things with the readonly properties and the new-module. But if this restriction isn't needed add-member can handle some of the other needs using $this to refer back to itself.

    by poshoholic at 2012-08-15 10:59:26

    Will, I think it depends on what you want your extensions to do. If you just want one object to access the xml and to invoke methods that do things with the xml, then you could have the xml object be a public property on a custom object using New-Object -AsCustomObject like I showed earlier, with public methods to do work against the xml object. In that case you have one object, not two, but it wraps it up together nicely. If you're talking about having extensions for specific parts of an xml document, either the document itself or nodes in the document, you might want to look at yet another extension method: using ps1xml files to define type extensions for object types. That way you can go about using xml the way you already do natively in PowerShell, but you would automatically get extensions defined in a types.ps1xml file once it is loaded in just the right locations.

    Make sense?

    by willsteele at 2012-08-15 11:03:47

    Yeah, it makes sense. These are new questions and new answers, so, I am thinking through them on the fly. Nonetheless, seeing examples such as yours gives me a new context with which to consider those internal dialogues. Let me chew on it a bit. Gracias.

    by psdk at 2012-08-15 11:04:55

    Thank you for this very useful introduction Kirk!

    by poshoholic at 2012-08-15 11:11:00

    [quote="jmiller76"]I like the flexability that kirk added to things with the readonly properties and the new-module. But if this restriction isn't needed add-member can handle some of the other needs using $this to refer back to itself.[/quote]

    I completely agree with you. There are many depths you can go to with custom objects. From Select-Object to New-Object with Add-Member to New-Module -AsCustomObject to Types.ps1xml files with Update-TypeData to Add-Type. It's about need, comfort-level and style. The best part of this flexibility is that you can do so much from PowerShell itself without diving into a compiler with C# or some other .NET language. I have used some of these types together, such as pairing New-Module -AsCustomObject with Add-Member so that I could add ScriptProperty members to my object, because they are like fields in .NET with getters and setters, and when you see those in Get-Member you can actually identify if a given property is read-only or not (as opposed to using New-Module -AsCustomObject with Set-Variable -Option Read-Only, which doesn't tell the user if a property is read-only or not). The example you shared back is also a good example because it encapsulates the object creation. Regardless of the approach you take, trying to encapsulate the object creation in a subroutine is a great idea so that you separate functionality from object definitions and can give yourself the equivalent of a constructor that does type checking and validation of the arguments.

    One last thought on all of this: regardless of which approach you take, don't forget to always give your object a custom type names. Type names are important in PowerShell, and they are used by many very useful features, so please, regardless of which approach you take, give your custom objects a type name so that the type name can be used in OutputType attributes, parameter type names, ps1xml files for type and format extensions, and so on. They also show up in Get-Member, and are useful in identifying what you're working with. If you don't know how to do this, it's really easy. Just add a new type to the custom object type hierarchy like this:

    $myObject.PSTypeNames.Insert(0,'My.Custom.Type.Name')

    by willsteele at 2012-08-15 11:14:49

    [quote="poshoholic"]Just add a new type to the custom object type hierarchy like this:

    $myObject.PSTypeNames.Insert(0,'My.Custom.Type.Name')[/quote]

    I tried adding one of these the other day, but, I think I had a type accelerator in mind. How would I add a custom namespace I can access like a static class? I.e., with the [Really.Cool.Class.UserClass]::ChangeLastName syntax. Also, when I tried referencing the new type name I didn't see it with .GetType(). What was I overlooking?

    by jmiller76 at 2012-08-15 11:28:34

    Kirk,

    Making my day, not only did you show some good use of the AsCustomObject functionality, but the PSTypeName is the first thing I have added to my PowerShell Notes.TXT file in over two months. I will have to update some old code too.

    – Josh

    by poshoholic at 2012-08-15 11:58:44

    Will: For static members, you're going to have to use Add-Type and declare the objects in C#. There's no support that I know of in PowerShell for declaring types with static members other than that.

    Here's a simple example that I have used before numerous times. It creates a modal (top-most) message box, ensuring that the window doesn't get lost behind PSISE, PowerGUI, PowerSE, etc.

    #region Define the Win32WindowClass

    if (-not ('PowerShellTypeExtensions.Win32Window' -as [System.Type])) {
    $cSharpCode = @'
    using System;

    namespace PowerShellTypeExtensions {
    public class Win32Window : System.Windows.Forms.IWin32Window
    {
    public static Win32Window CurrentWindow {
    get {
    return new Win32Window(System.Diagnostics.Process.GetCurrentProcess().MainWindowHandle);
    }
    }

    public Win32Window(IntPtr handle) {
    _hwnd = handle;
    }

    public IntPtr Handle {
    get {
    return _hwnd;
    }
    }

    private IntPtr _hwnd;
    }
    }
    '@

    Add-Type -ReferencedAssemblies System.Windows.Forms -TypeDefinition $cSharpCode
    }

    #endregion

    #region Now let's create a message box from a modern PowerShell editor that makes sure that the message box is modal (displayed on top of) the editor.

    #Note the use of the static member on the custom type in the first parameter of the call to Show.

    $dialogResult = [System.Windows.Forms.MessageBox]::Show(
    [PowerShellTypeExtensions.Win32Window]::CurrentWindow,
    'This is a modal MessageBox window. It is top-most, so you can''t click on the window it loads over.',
    'Modal Message Box',
    [System.Windows.Forms.MessageBoxButtons]::OK,
    [System.Windows.Forms.MessageBoxIcon]::Information
    )

    #endregion

    Also Will, for your second question, if you set a custom type name using the PSTypeNames.Insert trick, that is an ETS (Extended Type System) type name. It doesn't change the name of the actual type of the object. But ETS type names are good enough for Get-Member, OutputType, type and format ps1xml files, etc, because PowerShell leverages the ETS type system. GetType() will still tell you it's a good 'ole PSObject under the covers. Given this is the case, if you want to manually check the type of an object that leverages ETS for its type name, you need to do something like this:

    if ($myObject.PSTypeNames -contains 'My.Custom.Type.Name') {...}

    Josh: Thanks for the positive feedback. PowerShell extensions have been an area that I have focused on pretty hard core for 5 years now, and I'm happy to share that knowledge with others.

    by willsteele at 2012-08-15 19:06:09

    Back to the original thread question. I wrote the following as a simple starting point for some of my "Server" object definition. I am trying to work off the pattern you laid out. My main question is how to basically associate the main "Server" object with my XML. I initially thought about using a .Load method, but, I seem to be putting the cart before the horse. As I think through it, I see the server (which is really just some XML) being loaded first, then, all these features (methods, fields, etc) being added later. Here is my first stab at just getting something running:

    $server_definition = {
    param(
    $ComputerName = $env:ComputerName,
    $InstallationFolder = (Join-Path -Path $env:SystemDrive -ChildPath 'IssueServer')
    )

    # Clear module members
    Export-ModuleMember

    # Set variables
    Set-Variable -Name ComputerName -Option ReadOnly -Value $ComputerName
    Set-Variable -Name InstallationFolder -Option ReadOnly -Value $InstallationFolder
    if($ComputerName -eq $env:COMPUTERNAME)
    {
    Set-Variable -Name ServerSource `
    -Option ReadOnly `
    -Value (Join-Path -Path $InstallationFolder -ChildPath 'Server.xml')
    }
    else
    {
    Set-Variable -Name ServerSource `
    -Option ReadOnly `
    -Value (Join-Path -Path "\\$ComputerName\$($InstallationFolder -replace ':','$')" -ChildPath 'Server.xml')
    }

    Export-ModuleMember -Variable *

    # Define object methods
    #region Server Management

    function Load
    {
    param(

    )

    $this | Add-Member -Name Xml -MemberType NoteProperty -Value ([Xml](Get-Content -Path $ServerSource -Encoding UTF8))
    }
    Export-ModuleMember -Function Load
    #endregion
    }
    $server = New-Module -AsCustomObject -ScriptBlock $server_definition -ArgumentList '192.168.0.1','d:\test'

    I tried calling to $this, but, it seems there must be a better way to refer to the current object in this "constructor" than to call $this and pipe to Add-Member. Thoughts? In essence, I want to store my object in an XML file, and, using this approach, append "features", such as methods, to provide a management object via the shell. I can expand on the idea, but, the main question here is how to add the XML...as a member or as the primary object itself.

    by poshoholic at 2012-08-15 22:28:28

    I see two issues here:

    1. As mentioned on the other thread, $this only applies in ScriptProperty and ScriptMethod script blocks. It doesn't apply inside of script blocks used for calls to New-Module -AsCustomObject.
    2. What it looks like you really want to do is, instead of defining a Load method, simply assign your xml to some internal variable like this:

    $Xml = [Xml](Get-Content -Path $ServerSource -Encoding UTF8)

    Since that's inside of the "constructor" (the script block used with New-Module -AsCustomObject calls acts like a constructor) the Xml property will be assigned the xml representation of the contents of $ServerSource at the time that you create an instance of the object (i.e. when you call New-Module -AsCustomObject). That will give you a custom object with the Xml property already loaded. You don't need to use a reference to the object itself, since simply assigning a variable inside of that script block automatically sets up the association during the New-Module -AsCustomObject call.

    It's late, so I may not have explained this as clearly as I could have, but I wanted to leave you with something to think about. I'll check back tomorrow, see where you're at with this then.

    by willsteele at 2012-08-16 09:47:06

    Seems better to tack follow up questions on this thread since there is a key concept (the use of New-Module) not used outside this post. Anyway, continuing with the discussion of that notion, I have two questions:
    1) When I want to debug/explore an internal member (something set to Private) how can I explore that object/field/member? If I were writing a normal function, I would embed a | Get-Member call. Since it's hidden, doesn't seem like the object will export Get-Member results.
    2) Can function defaults be used when functions are defined with defaults inside the module? For instance, in my Xml blocks thread, I wrote a function to gen up XML chunks. If I use that as in internal function, it would seem I need to pass all arguments from to object's external method (the one exposed through Export-ModuleMember). In other words, I can't envision how to only pass arguments to the fields without default values on the internal function definition.

    by poshoholic at 2012-08-16 10:37:29

    More great questions. 🙂

    1. You can use Write-* calls inside of your function definitions or in the module script block itself to give you on-demand debugging information. Write-Debug and Write-Verbose would be two worth considering. You could be lazy and use Write-Host for your own testing, then removing it afterwards, but if you use Write-Debug and Write-Verbose you can leave them there and unless someone has set $DebugPreference or $VerbosePreference to 'Continue', they won't see them. This is the approach I recommend if you want some debugging/troubleshooting information in your functions/methods through simple invocation. You can also set breakpoints in your functions though, and PowerShell will find them. This allows you to actually step through your functions in a debugger, even when you're calling them as methods on a custom object, which is really cool because nothing beats finding bugs in script like using a debugger!

    2. You can use default property values for parameters in your functions. For example, in the sample I posted earlier on this thread, I can assign a default value of 'Joe' to the NewLastName parameter in the ChangeLastName advanced function. When you do this, if you invoke ChangeLastName as a method on your custom object with no parameters (e.g. $me.ChangeLastName()), it still works because it has a default value. If you invoke it with a last name parameter (e.g. $me.ChangeLastName('Poshoholic'), that works too, because it has a parameter for that. If you're going to use default values though, I recommend putting any parameter that is optional (i.e. any parameter that has a default value) at the end so that you can invoke it with the least amount of information as possible.

You must be logged in to reply to this topic.