Controlling Some Controls
Picking up from previously, we’re next going to abstract how our controls are created. We’re going to Create a whole new Class right at the top of our file (but right below our using namespace
section)
# Factory for Creating any WinForms Control with Property Caching Class PSFormControlFactory { # Some overrides of Object to hide those methods hidden PSFormControlFactory() { } hidden static [bool] Equals([Object] $A, [Object] $B) { return [Object]::Equals($A,$B) } hidden static [bool] ReferenceEquals([Object] $A, [Object] $B) { return [Object]::ReferenceEquals($A,$B) } # Our static Cache of our Properties for each Type hidden static [HashTable] $PropertiesCache # Retrieves or creates a cached copy of the current Type's Properties hidden static [hashtable] GetPropertiesOrCache([type] $Type) { # Check if the Cache is created and create if not if ([PSFormControlFactory]::PropertiesCache -eq $null) { [PSFormControlFactory]::PropertiesCache = @{} } # Check if we've not cached this type's properties yet if (-not [PSFormControlFactory]::PropertiesCache.ContainsKey($Type)) { # Get All The Writable Properties, dump them into the Hashtable with Name being the Key, and Property Type being the value and Assign to the Cache [PSFormControlFactory]::PropertiesCache.$Type = $Type.GetProperties() | Where-Object { $_.CanWrite } | ForEach-Object -Begin {$HT = @{}} -Process { $HT[$_.Name]=$_.PropertyType } -End { $HT } } return [PSFormControlFactory]::PropertiesCache.$Type } # Creates a new Control or Component populating its writable properties static [MarshalByRefObject] Produce([type] $ControlType, [HashTable] $Properties) { # Create a new object from the type passed in $Control = $ControlType::new() # Get Properties for this type $ControlProperties = [PSFormControlFactory]::GetPropertiesOrCache($ControlType) # Loop through each passed in Key Value Pairs of Properties and Values foreach ($Name in $Properties.Keys) { # If we find the name in our keys, and we've passed in a value with the proper type assign it if ($Name -in $ControlProperties.Keys -and $Properties.$Name -is $ControlProperties.$Name) { $Control.$Name = $Properties.$Name } # Otherwise let user know they dropped in some bad values else { Write-Error -Message "$Name is not a valid writable property of $($ControlType.Name)." -Category WriteError -Exception RuntimeException -ErrorAction Inquire } } return $Control } # Calls its override to create a new Control or Component, then assigning it to its Parent static [MarshalByRefObject] Produce([type] $ControlType, [MarshalByRefObject] $Parent, [HashTable] $Properties) { # Call the Other Static overload to create our control $Control = [PSFormControlFactory]::Produce($ControlType,$Properties) # Determine what our children property is called $Children = switch ($true) { { $Parent -is [TreeNode] -or $Parent -is [TreeView] } { 'Nodes'; break; } { $Parent -is [Control] } { 'Controls'; break } } # Add our control to its parent $Parent.$Children.Add($Control) return $Control } }
Lot to unpack here but we’ve basically created a Controls Factory. It never needs to be instantiated into an object as everything we need is STATIC which basically means it exists ONCE in memory as a property, or method of the Class Type itself.
So lets go through the class a few pieces of a time
# Some overrides of Object to hide those methods hidden PSFormControlFactory() { } hidden static [bool] Equals([Object] $A, [Object] $B) { return [Object]::Equals($A,$B) } hidden static [bool] ReferenceEquals([Object] $A, [Object] $B) { return [Object]::ReferenceEquals($A,$B) }
This is more for my OCD than a hard requirement it hides from intellisense everything BUT the Produce
Methods.
# Our static Cache of our Properties for each Type hidden static [HashTable] $PropertiesCache
This little HashTable is going to be our cache of valid writable property names. Because it’s static it will only take up a single spot in memory and can be reused through as many method calls as are needed.
# Retrieves or creates a cached copy of the current Type's Properties hidden static [hashtable] GetPropertiesOrCache([type] $Type) { # Check if the Cache is created and create if not if ([PSFormControlFactory]::PropertiesCache -eq $null) { [PSFormControlFactory]::PropertiesCache = @{} } # Check if we've not cached this type's properties yet if (-not [PSFormControlFactory]::PropertiesCache.ContainsKey($Type)) { # Get All The Writable Properties, dump them into the Hashtable with Name being the Key, and Property Type being the value and Assign to the Cache [PSFormControlFactory]::PropertiesCache.$Type = $Type.GetProperties() | Where-Object { $_.CanWrite } | ForEach-Object -Begin {$HT = @{} } -Process { $HT[$_.Name]=$_.PropertyType } -End { $HT } } return [PSFormControlFactory]::PropertiesCache.$Type }
This static method first checks if we’ve actually created our Properties Cache, if not we make a new HashTable. Next we check if the Properties Cache does not already have the passed in Type cached. If not this mouthful of a line kicks in
# Get All The Writable Properties, dump them into the Hashtable with Name being the Key, and Property Type being the value and Assign to the Cache [PSFormControlFactory]::PropertiesCache.$Type = $Type.GetProperties() | Where-Object { $_.CanWrite } | ForEach-Object -Begin {$HT = @{} } -Process { $HT[$_.Name]=$_.PropertyType } -End { $HT }
This is actually pretty simple if we take it piece by piece
- We get to our static Cache Property with
[PSFormControlFactory]::PropertiesCache
- New HashTable members can be added via
[Key]=
OR via.Key=
. here we opt to use the literal type we have passed in for the Key to make it easier to look up later so now we have.$Type =
- Everything that follows we are assigning to the Value of our new Type Key
- First we query the Type for ALL its Properties (this is of course quite task intensive which is why we Cache it!) by calling
$Type.GetProperties()
- Then we filter those results to give us just the ones we can write to by piping to Where-Object by calling
| Where-Object { $_.CanWrite }
- Next we take the filtered results and Pipe it to
ForEach-Object
to create a new HashTable, populate it with each item Property Name being the Key, and its Type being the value. by calling| ForEach-Object -Begin {$HT = @{} } -Process { $HT[$_.Name]=$_.PropertyType } -End { $HT }
This is just an expanded version of ForEach-Object by specifying our ScriptBlocks like an advanced function. This allows it to create our$HT
, then Populate it, and finally return it as theValue
of the Type Key we’ve passed into our method.
- First we query the Type for ALL its Properties (this is of course quite task intensive which is why we Cache it!) by calling
Finally we return the cached copy of the Hashtable of that Type’s Properties
return [PSFormControlFactory]::PropertiesCache.$Type
So now we’ve got a handy speedy way to validate our writable properties. Next we have our overloaded Static Factory method Produce. The first Creates and returns a Control with the passed in properties set. The second calls the first and then assigns it as a child of the Parent object passed in.
First overload:
# Creates a new Control or Component populating its writable properties static [MarshalByRefObject] Produce([type] $ControlType, [HashTable] $Properties) { # Create a new object from the type passed in $Control = $ControlType::new() # Get Properties for this type $ControlProperties = [PSFormControlFactory]::GetPropertiesOrCache($ControlType) # Loop through each passed in Key Value Pairs of Properties and Values foreach ($Name in $Properties.Keys) { # If we find the name in our keys, and we've passed in a value with the proper type assign it if ($Name -in $ControlProperties.Keys -and $Properties.$Name -is $ControlProperties.$Name) { $Control.$Name = $Properties.$Name } # Otherwise let user know they dropped in some bad values else { Write-Error -Message "$Name is not a valid writable property of $($ControlType.Name)." -Category WriteError -Exception RuntimeException -ErrorAction Inquire } } return $Control }
Starting off we create our new Control because $ControlType
is a literal type we can call its new() method
# Create a new object from the type passed in $Control = $ControlType::new()
Next we call our static method passing in $ControlType
to get our cached list of writable properties
# Get Properties for this type $ControlProperties = [PSFormControlFactory]::GetPropertiesOrCache($ControlType)
Then we just loop through our passed in Properties Hashtable
# Loop through each passed in Key Value Pairs of Properties and Values foreach ($Name in $Properties.Keys) { # If we find the name in our keys, and we've passed in a value with the proper type assign it if ($Name -in $ControlProperties.Keys -and $Properties.$Name -is $ControlProperties.$Name) { $Control.$Name = $Properties.$Name } # Otherwise let user know they dropped in some bad values else { Write-Error -Message "$Name is not a valid writable property of $($ControlType.Name)." -Category WriteError -Exception RuntimeException -ErrorAction Inquire } }
We use the cached properties we collected to check first if each Key both is in the Control Properties’ Keys, AND its value’s type matches the type of its cached value for said Key
# If we find the name in our keys, and we've passed in a value with the proper type assign it if ($Name -in $ControlProperties.Keys -and $Properties.$Name -is $ControlProperties.$Name) { $Control.$Name = $Properties.$Name }
Because $Name
holds the string of the Property name and PowerShell lets us call properties even ones with non standard characters by surrounding them with either “” or ” e.g. $Obj."Some Spaced Property" = 10
our assignment happily assigns our value from our passed in properties to that property of our new $Control
object
$Control.$Name = $Properties.$Name
If for some reason we passed in a bad Key Value Pair we write an error so we see it in the console but otherwise we just ignore it.
# Otherwise let user know they dropped in some bad values else { Write-Error -Message "$Name is not a valid writable property of $($ControlType.Name)." -Category WriteError -Exception RuntimeException -ErrorAction Inquire }
Finally we return our newly created and populated Control
return $Control
Now we come to our other overload of Produce
this one takes in one additional property of another Control which will be the parent of the newly created Control
# Calls its override to create a new Control or Component, then assigning it to its Parent static [MarshalByRefObject] Produce([type] $ControlType, [MarshalByRefObject] $Parent, [HashTable] $Properties) { # Call the Other Static overload to create our control $Control = [PSFormControlFactory]::Produce($ControlType,$Properties) # Determine what our children property is called $Children = switch ($true) { { $Parent -is [TreeNode] -or $Parent -is [TreeView] } { 'Nodes'; break; } { $Parent -is [Control] } { 'Controls'; break } } # Add our control to its parent $Parent.$Children.Add($Control) return $Control }
First we simply call our Overload to create our Control since that logic already exists.
# Call the Other Static overload to create our control $Control = [PSFormControlFactory]::Produce($ControlType,$Properties)
Next we determine what property name defines the children of our Parent. There may be more that have different property names then Controls but we’d just add in another {$Parent -is [Type]} { 'SomeName'; break; }
line to our switch if we discover another one besides [TreeView]
, and [TreeNode]
.
# Determine what our children property is called $Children = switch ($true) { { $Parent -is [TreeNode] -or $Parent -is [TreeView] } { 'Nodes'; break; } { $Parent -is [Control] } { 'Controls'; break } }
Almost done, we call the Add method of our Parent Controls Children Property. Powershell once again happily uses $Children
‘s string value to pull up the proper property.
# Add our control to its parent $Parent.$Children.Add($Control)
Finally we return our new Control now a child of some parent.
return $Control
That’s it for our little Factory.
I’d probably put it in the same dot sourced file our PSForm Class from Part 2 went into, that or the Module it went into
Next post we’ll finally be starting to create our [ObjectForm]
class by converting the code from our original Function based Form, using all the groundwork we just laid in these last 3 posts.