PowerShell Custom Object Formatting

On this article I'm going to touch a field of PowerShell development that most are not aware of. That is formatting. Every PowerShell user has used the "Format-Table" and "Format-List" cmdlets or even the "Format-Custom" to display data in a manner that suits them. What happens though when you have objects that you'll like to format a bit differently or set the default view for? Then its time to use formatting.

To start off, we're going to create a custom module. Pick a name for the module - mine is going to be "TestModule" - and create a directory with that name. Then create the module manifest using the "New-ModuleManifest" cmdlet and a script file with the ".psm1" extension. You should end up with a structure like the following:

PS C:\> New-Item -ItemType dir -Name TestModule

    Directory: C:\Users\cpolydorou\Desktop

Mode        LastWriteTime  Length Name
----        -------------  ------ ----
d----- 19/7/2018 11:26 πμ         TestModule

PS C:\> New-ModuleManifest -Path .\TestModule\TestModule.psd1

PS C:\> New-Item -ItemType File -Path .\TestModule\TestModule.psm1

  Directory: C:\Users\cpolydorou\Desktop\TestModule

Mode        LastWriteTime Length Name
----        ------------- ------ ----
-a---- 19/7/2018 11:26 πμ      0 TestModule.psm1

PS C:\> Get-ChildItem -Path .\TestModule
    Directory: C:\Users\cpolydorou\Desktop\TestModule

Mode        LastWriteTime Length Name
----        ------------- ------ ----
-a---- 19/7/2018 11:26 πμ   7740 TestModule.psd1
-a---- 19/7/2018 11:26 πμ      0 TestModule.psm1

PS C:\>

At this point, there are no functions in this module, to add one we have to define it in the script file and then configure the manifest accordingly.

Below is a function that returns a custom object:

Function Test-Function
{
    $props = @{
                ID = "0001"
                FirstName = "John"
                LastName = "Smith"
                Email = "jsmith@domain.com"
             }

    New-Object -TypeName PSObject -Property $props
}

Now that the function is defined , we have to update the module manifest to include the script file and export the function. Open the file, locate the "NestedModules" directive, uncomment it and add the "TestModule.psm1". Don't forget to add the name of the function to the "FunctionsToExport".

# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
NestedModules = @('TestModule.psm1')

# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
FunctionsToExport = @("Test-Function")

The module is not located in any of the predefined module locations so we have to import it using the path of the manifest file:

PS C:\> Import-Module .\TestModule\TestModule.psd1
PS C:\> get-module

ModuleType Version Name                            ExportedCommands
---------- ------- ----                            ----------------
Manifest   3.1.0.0 Microsoft.PowerShell.Management {Add-Computer, Add-Content, Checkpoint-Computer, Clear-Con...
Manifest   3.1.0.0 Microsoft.PowerShell.Utility    {Add-Member, Add-Type, Clear-Variable, Compare-Object...}
Script     1.2     PSReadline                      {Get-PSReadlineKeyHandler, Get-PSReadlineOption, Remove-PS...
Manifest   1.0     TestModule                      Test-Function

PS C:\> Test-Function

Email     : jsmith@domain.com
ID        : 0001
FirstName : John
LastName  : Smith

PS C:\>

As shown above, the function is available in the module and when called, returns a custom object.

Before starting with the formatting, let's see a couple of ways to create custom objects. The first way is by using a hashtable with the properties of the object just like we did in the test function above. Although this is pretty straight forward, the type of the object is not well defined, thus we cannot link formatting to the object.

To create a custom object type, we have to define a class. PowerShell 5 and later provides the ability to create classes directly in the scripts:

class CustomObjectType1
{
    [string]$ID
    [string]$FirstName
    [string]$LastName
    [string]$Email
}

In previous versions of PowerShell, you can define a custom type using the "Add-Type" cmdlet:

Add-Type @'
namespace TestModule
{
    public class CustomObjectType2
    {
        public string ID;
        public string FirstName;
        public string LastName;
        public string Email;
    }
}
'@

Add the above to the script file of the module to make the types available.

Now let's create a couple of functions that return objects based on the above types. To create the objects, use the New-PSObject cmdlet just like below:

PS C:\> New-Object -TypeName CustomObjectType1

ID FirstName LastName Email
-- --------- -------- -----

PS C:\> New-Object -TypeName TestModule.CustomObjectType2

ID FirstName LastName Email
-- --------- -------- -----

PS C:\>

The objects are empty but the access level of the properties is public so you can update them. I've added two functions in the module, one for each type:

Function Test-Function1
{
    $obj = New-Object -TypeName CustomObjectType1

    $obj.ID = "0002"
    $obj.FirstName = "Nick"
    $obj.LastName = "Johnson"
    $obj.Email = "N.Johnson@domain.com"

    Write-Output $obj
}

Function Test-Function2
{
    $obj = New-Object -TypeName TestModule.CustomObjectType2

    $obj.ID = "0003"
    $obj.FirstName = "Claire"
    $obj.LastName = "Williams"
    $obj.Email = "C.Williams@domain.com"

    Write-Output $obj
}

Don't forget to update the "FunctionsToExport" in the manifest file to make them available!

We're done with the types and the functions in the module, I'ts time to start working on the formatting. The output of the above cmdlets will be formatted in a table view:

PS C:\> Test-Function1

ID   FirstName    LastName Email
--   ---------    -------- -----
0002 Nick         Johnson  N.Johnson@domain.com

PS C:\> Test-Function2

ID   FirstName LastName Email
--   --------- -------- -----
0003 Claire    Williams C.Williams@domain.com

PS C:\>

To update the formatting of objects in the module, you have to create a formatting file. Formatting files are xml files (with the .ps1.xml extension) that contain information on how to format the objects, the default view type and the properties to display by default. The syntax is not too difficult, let's take a look at an example.

<?xml version="1.0" encoding="utf-16"?>
<Configuration>
<ViewDefinitions>
<View>
  <Name>CustomObjectType1-View1</Name>
  <ViewSelectedBy>
    <TypeName>CustomObjectType1</TypeName>
  </ViewSelectedBy>
  <TableControl>
    <TableRowEntries>
      <TableRowEntry>
        <TableColumnItems>
          <TableColumnItem>           
           <PropertyName>ID</PropertyName>
          </TableColumnItem>
          <TableColumnItem>
            <PropertyName>FirstName</PropertyName>
          </TableColumnItem>
          <TableColumnItem>
            <PropertyName>LastName</PropertyName>
          </TableColumnItem>
        </TableColumnItems>
      </TableRowEntry>
    </TableRowEntries>
    <AutoSize/>
  </TableControl>
</View>
</ViewDefinitions>
</Configuration>

A table view for the "CustomObjectType1" type is defined here, with the columns ID, FirstName and LastName. I've chosen to omit the Email property from the view just to make the output simpler:

PS C:\> Test-Function1

ID   FirstName LastName
--   --------- --------
0002 Nick      Johnson

PS C:\>

Adding a list view is equally easy:

<View>
  <Name>CustomObjectType1-View2</Name>
  <ViewSelectedBy>
    <TypeName>CustomObjectType1</TypeName>
  </ViewSelectedBy>
  <ListControl>
    <ListEntries>
      <ListEntry>
        <ListItems>
          <ListItem>
            <Label>ID</Label>
            <PropertyName>ID</PropertyName>
          </ListItem>
          <ListItem>
            <Label>First Name</Label>
            <PropertyName>FirstName</PropertyName>
          </ListItem>
          <ListItem>
            <Label>Surname</Label>
            <PropertyName>LastName</PropertyName>
          </ListItem>
        </ListItems>
      </ListEntry>
    </ListEntries>
  </ListControl>
</View>

The Email property is also omitted in this view, and labels are defined for the properties. The output when selecting the list view is going to be:

PS C:\> Test-Function1 | Format-List

ID         : 0002
First Name : Nick
Surname    : Johnson

PS C:\>

Formatting objects can greatly enhance the experience for your modules and their functions, since it will make it easier for the users to get the information they need by presenting them the right set of properties in the right view.

Views are not limited to selecting properties and labels, but can also do things like setting maximum width on columns, grouping and displaying calculated properties. More information is available on the PowerShell docs!

Popular posts from this blog

Domain Controller Machine Password Reset

Configuring a Certificate on Exchange Receive Connector

Running Multiple NGINX Ingress Controllers in AKS