PowerShell 5 classes (As of Win10 Tech Preview 9926)

It has taken me a little while to work out the new class feature in PowerShell 5 preview. Each preview so far has seemed to slightly change the way classes work, so if you are using a later preview or the actual release some syntax might change slightly between now and then.

So what are classes and why would you want them? The easiest way I can think to describe them is that the objects you have been using so far in PowerShell are cookies, and classes are the cookie cutters. They are a structured way to define an object type that you can the use to define your objects in a more structured way.

Take the System.ServiceProcess.ServiceController object type for example, this is the object type you get when you run Get-Service. If you run Get-Service and pipe it to Get-Member you get a range of properties and methods, if you create your own custom object by using a hash table you will get a range of properties but there is no easy way to add your own methods. Also the ServiceController object is always the same, no matter what service you are looking at. They all have the same properties and methods, this is because they all came from the same class.

In the following example I will make a class that you could use to define objects that can later be used to build a html report.

So how do you define a class? firstly you do a very similar thing to how you would already create functions, give it a name:

Class ReportTable
{
}

Next you need to define what properties the object will have, in this example I want a Title to refer to the object as and use as a header in the report, a Description for my report, an Object like a service object I can add to the report as a table of data, an Order so I can control where in the report each object goes and an EntryType so that I can make any errors or warnings stand out more, and finally an As property so that we can structure the object as a table or a list.

You will notice that you can still cast each property’s type, for example Int or String, and you can provide other parameter properties like validatesets the same way you can do in functions.

Class ReportTable
{
 [String]$Title
 [String]$Description
 [ValidateSet("Error","Warning","Information")][String]$EntryType
 $Object
 [ValidateRange(0,100)][Int]$Order
 [ValidateSet("Table","List")][String]$As
}

You can now create objects based on this class by casting a new variable to the class name you specified, note that you need to run this in a single run instance which I will explain later in this article.

$newVar = [ReportTable]::new()

You can then manipulate the data in the object by referring to its properties, for example:

$newVar.Title = 'Hello World'

You will notice that whenever you create an instance of this class as a new object it will have no data, if you want to be able to add data on creation you will need to define a constructor. In this example I have created a constructor that accepts values for all properties. You need to use the syntax $This.property to refer to properties of the class.

Class ReportTable
{
   [String]$Title
   [String]$Description
   [ValidateSet("Error","Warning","Information")][String]$EntryType
   $Object
   [ValidateRange(0,100)][Int]$Order
   [ValidateSet("Table","List")][String]$As

   ReportTable([string]$newTitle, [String]$newDescription,[String]$newEntryType,$newObject,[Int]$newOrder,[String]$newAs)
   {
      $this.Title = $newTitle;
      $this.Description = $newDescription;
      $this.EntryType = $newEntryType;
      $this.Object = $newObject;
      $this.Order = $newOrder;
      $this.As = $newAs;
   }
}

To create an object based on this new constructor you can do the following

$obj = Get-Service *bits* | select DiplayName, Status
$newVar = [ReportTable]::new('Bits Service','This is the current status of the Background Intelligent Transfer Service','Information',$obj,'1','Table')

But sometimes you might not want to specify that it will be in a table, but use table as a default. In this case you can create multiple constructors like follows:

Class ReportTable
{
   [String]$Title
   [String]$Description
   [ValidateSet("Error","Warning","Information")][String]$EntryType
   $Object
   [ValidateRange(0,100)][Int]$Order
   [ValidateSet("Table","List")][String]$As

   ReportTable([string]$newTitle, [String]$newDescription,[String]$newEntryType,$newObject,[Int]$newOrder,[String]$newAs)
   {
      $this.Title = $newTitle;
      $this.Description = $newDescription;
      $this.EntryType = $newEntryType;
      $this.Object = $newObject;
      $this.Order = $newOrder;
      $this.As = $newAs;
   }
   ReportTable([string]$_title, [String]$_description,[String]$_entryType,[Psobject]$_object,[Int]$_order)
   {
      $this.Title = $_title;
      $this.Description = $_description;
      $this.EntryType = $_entryType; 
      $this.Object = $_object;
      $this.Order = $_order;
      $this.As = "Table";
   }
}

You will notice that in the second constructor it does not expect a table to be specified and that it has table hard coded. Note that if you specify a constructor you will no longer be able to create a empty version of the object. If you still want to be able to do this you need to specify an empty constructor like follows:

Class ReportTable
{
   [String]$Title
   [String]$Description
   [ValidateSet("Error","Warning","Information")][String]$EntryType
   $Object
   [ValidateRange(0,100)][Int]$Order
   [ValidateSet("Table","List")][String]$As

   ReportTable([string]$newTitle, [String]$newDescription,[String]$newEntryType,$newObject,[Int]$newOrder,[String]$newAs)
   {
      $this.Title = $newTitle;
      $this.Description = $newDescription;
      $this.EntryType = $newEntryType;
      $this.Object = $newObject;
      $this.Order = $newOrder;
      $this.As = $newAs;
   }
   ReportTable([string]$_title, [String]$_description,[String]$_entryType,[Psobject]$_object,[Int]$_order)
   {
      $this.Title = $_title;
      $this.Description = $_description;
      $this.EntryType = $_entryType; 
      $this.Object = $_object;
      $this.Order = $_order;
      $this.As = "Table";
   }
   ReportTable(){}
}

Now that we have a class with some properties attached we can add methods. Methods are useful because they can be used to manipulate the object you created at any stage further along in your code. In this example I will make a method that will convert the $object stored in our class into html. We do this by using the void keyword.

Class ReportTable
{
   [String]$Title
   [String]$Description
   [ValidateSet("Error","Warning","Information")][String]$EntryType
   $Object
   [ValidateRange(0,100)][Int]$Order
   [ValidateSet("Table","List")][String]$As

   ReportTable([string]$newTitle, [String]$newDescription,[String]$newEntryType,$newObject,[Int]$newOrder,[String]$newAs)
   {
      $this.Title = $newTitle;
      $this.Description = $newDescription;
      $this.EntryType = $newEntryType;
      $this.Object = $newObject;
      $this.Order = $newOrder;
      $this.As = $newAs;
   }
   ReportTable([string]$_title, [String]$_description,[String]$_entryType,[Psobject]$_object,[Int]$_order)
   {
      $this.Title = $_title;
      $this.Description = $_description;
      $this.EntryType = $_entryType; 
      $this.Object = $_object;
      $this.Order = $_order;
      $this.As = "Table";
   }
   ReportTable(){}

   [void]ToHtml()
   {
      $this.Object = ConvertTo-Html -InputObject $this.Object -Fragment
   }
}

We can call this method using the dot notation as follows:

$obj = Get-Service *bits* | select DiplayName, Status
$newVar = [ReportTable]::new('Bits Service','This is the current status of the Background Intelligent Transfer Service','Information',$obj,'1','Table')
$newVar.ToHtml()

Now there is a string stored on the $object property which contains a html version of the original service object.

We can also pass data into methods like in the following example, here we will be able to add or subtract a specified value from the order property.

Class ReportTable
{
   [String]$Title
   [String]$Description
   [ValidateSet("Error","Warning","Information")][String]$EntryType
   $Object
   [ValidateRange(0,100)][Int]$Order
   [ValidateSet("Table","List")][String]$As

   ReportTable([string]$newTitle, [String]$newDescription,[String]$newEntryType,$newObject,[Int]$newOrder,[String]$newAs)
   {
      $this.Title = $newTitle;
      $this.Description = $newDescription;
      $this.EntryType = $newEntryType;
      $this.Object = $newObject;
      $this.Order = $newOrder;
      $this.As = $newAs;
   }
   ReportTable([string]$_title, [String]$_description,[String]$_entryType,[Psobject]$_object,[Int]$_order)
   {
      $this.Title = $_title;
      $this.Description = $_description;
      $this.EntryType = $_entryType; 
      $this.Object = $_object;
      $this.Order = $_order;
      $this.As = "Table";
   }
   ReportTable(){}

   [void]ToHtml()
   {
      $this.Object = ConvertTo-Html -InputObject $this.Object -Fragment
   }
   [void]AddOrder([int]$_add)
   {
      $this.Order += $_add;
   }
   [void]SubtractOrder([int]$_subtract)
   {
      $this.Order -= $_subtract;
   }
}

Now if we specify a integer in the AddOrder method the order property will increment by the specified value.

It is worth noting that the validaterange will still apply to the order value even after the object is created. So if you try to add 100 to the order value when it was previously 1 then that would total 101, which is outside the range of (0, 100) and you will get the validation error displayed.

You can also overload the methods like we did with constructors, in this example I will add 2 more methods to add or subtract ‘1’ from the order if you call $newVar.AddOrder() and don’t specify a value.

Class ReportTable
{
   [String]$Title
   [String]$Description
   [ValidateSet("Error","Warning","Information")][String]$EntryType
   $Object
   [ValidateRange(0,100)][Int]$Order
   [ValidateSet("Table","List")][String]$As

   ReportTable([string]$newTitle, [String]$newDescription,[String]$newEntryType,$newObject,[Int]$newOrder,[String]$newAs)
   {
      $this.Title = $newTitle;
      $this.Description = $newDescription;
      $this.EntryType = $newEntryType;
      $this.Object = $newObject;
      $this.Order = $newOrder;
      $this.As = $newAs;
   }
   ReportTable([string]$_title, [String]$_description,[String]$_entryType,[Psobject]$_object,[Int]$_order)
   {
      $this.Title = $_title;
      $this.Description = $_description;
      $this.EntryType = $_entryType; 
      $this.Object = $_object;
      $this.Order = $_order;
      $this.As = "Table";
   }
   ReportTable(){}

   [void]ToHtml()
   {
      $this.Object = ConvertTo-Html -InputObject $this.Object -Fragment
   }
   [void]AddOrder([int]$_add)
   {
      $this.Order += $_add;
   }
   [void]SubtractOrder([int]$_subtract)
   {
      $this.Order -= $_subtract;
   }
   [void]AddOrder()
   {
      $this.Order += 1;
   }
   [void]SubtractOrder()
   {
      $this.Order -= 1;
   }
}

Earlier I mentioned that you needed to refer to the class in the same run instance for it to work. Let me try to explain this further: If you define a class in ISE and then press F5 and then try to create an object using this class in the interactive shell pane it will throw an error saying the class does not exist.

This is because the class definition only lasts as long as the currently running script. In the above case you would need to define the class in the script pane and then also refer to it in the last few lines of the script pane and run the whole script at once.

After you create the object you can change all the properties and call the methods from the class but you can not create any new objects from that class.

Think of it this way, a class is the cookie cutter and the object is the cookie, after you run the script the cookie cutter is destroyed but the cookie remains.

To get around this for testing you can put the class in a function and then call the function to create new instances of the class like below:

Function New-ReportTable{
   Class ReportTable
   {
      [String]$Title
      [String]$Description
      [ValidateSet("Error","Warning","Information")][String]$EntryType
      $Object
      [ValidateRange(0,100)][Int]$Order
      [ValidateSet("Table","List")][String]$As

      ReportTable([string]$newTitle, [String]$newDescription,[String]$newEntryType,$newObject,[Int]$newOrder,[String]$newAs)
      {
         $this.Title = $newTitle;
         $this.Description = $newDescription;
         $this.EntryType = $newEntryType;
         $this.Object = $newObject;
         $this.Order = $newOrder;
         $this.As = $newAs;
      }
      ReportTable([string]$_title, [String]$_description,[String]$_entryType,[Psobject]$_object,[Int]$_order)
      {
         $this.Title = $_title;
         $this.Description = $_description;
         $this.EntryType = $_entryType; 
         $this.Object = $_object;
         $this.Order = $_order;
         $this.As = "Table";
      }
      ReportTable(){}

      [void]ToHtml()
      {
         $this.Object = ConvertTo-Html -InputObject $this.Object -Fragment
      }
      [void]AddOrder([int]$_add)
      {
         $this.Order += $_add;
      }
      [void]SubtractOrder([int]$_subtract)
      {
         $this.Order -= $_subtract;
      }
      [void]AddOrder()
      {
        $this.Order += 1;
      }
      [void]SubtractOrder()
      {
         $this.Order -= 1;
     }
   }
   return [ReportTable]::new()
}

You can then make new empty versions of this class by casting the function into a new variable.

$A = New-ReportTable

Hopefully this has all made sense, just remember that some, all or none of this might change in any future release as PowerShell 5 is still in preview, but hopefully this should start you off in how to work with classes.