Be sure to check out my Scripting4Crypto initiative. It’s a fun way to get into using cryptocurrencies all while getting your PowerShell needs met.
Introduction
When working with the ProjectWise SDK from PowerShell, there are times when the functionality you need is available through the underlying ProjectWise API, but is not exposed directly through the PowerShell wrapper you are using.
One example is retrieving detailed information about the immediate child folders of a ProjectWise folder.
ProjectWise provides the aaApi_SelectChildProjects() function for selecting the child projects of a folder. Once those projects have been selected, individual project properties can be retrieved using functions such as:
aaApi_GetProjectNumericProperty()aaApi_GetProjectStringProperty()aaApi_GetProjectGuidProperty()
The goal of this example is to create a reusable PowerShell function that accepts a ProjectWise folder ID and returns the complete ProjectWise project information for its immediate child folders.
The resulting objects can then be used just like normal PowerShell objects.
The Problem
The ProjectWise SDK provides the ability to select the child projects associated with a folder:
[pwwrapper]::aaApi_SelectChildProjects($FolderID)
The function returns the number of projects selected. The selected projects can then be accessed by index.
The challenge is that there are many different properties associated with a ProjectWise project. Rather than writing a large amount of repetitive code every time project information is needed, we can create a reusable PowerShell class and a generic function for populating it.
There is also an additional complication.
The ProjectWise SDK function:
aaApi_GetProjectGuidProperty
returns an LPCGUID, which is a native pointer type. The PowerShell wrapper does not provide a convenient managed .NET representation of this function.
To handle that, we can create a small C# wrapper around the native ProjectWise API.
Creating a C# Wrapper for aaApi_GetProjectGuidProperty
PowerShell allows us to compile C# code at runtime using Add-Type.
The following code imports aaApi_GetProjectGuidProperty from dmscli.dll:
Add-Type -TypeDefinition @"using System;using System.Runtime.InteropServices;public class DMSProjectFunctions{ [DllImport("dmscli.dll", EntryPoint = "aaApi_GetProjectGuidProperty", CharSet = CharSet.Unicode)] private static extern IntPtr intPtr_aaApi_GetProjectGuidProperty( int lPropertyId, int lIdxRow); public static Guid aaApi_GetProjectGuidProperty( int PropertyId, int lIndex) { return (Guid)Marshal.PtrToStructure( intPtr_aaApi_GetProjectGuidProperty(PropertyId, lIndex), Type.GetType("System.Guid")); }}"@ -Language CSharp
The native API returns a pointer to a GUID. Marshal.PtrToStructure() allows that native value to be converted into a .NET Guid.
This gives us a PowerShell-callable method:
[DMSProjectFunctions]::aaApi_GetProjectGuidProperty($PWProperty, $Index)
This technique is particularly useful when a ProjectWise SDK function exists in the native API but is not exposed by the PowerShell wrapper.
Creating a ProjectWise Project Class
Next, we need an object to hold the information returned by ProjectWise.
A PowerShell class provides a convenient way to define the structure:
class ProjectWiseProject { [int]$ID [int]$VersionNO [int]$ManagerID [int]$StorageID [int]$CreatorID [int]$UpdaterID [int]$WorkflowID [int]$StateID [int]$Type [int]$ArchiveID [int]$IsParent [string]$Name [string]$Desc [string]$Code [string]$Version [string]$CreateTime [string]$UpdateTime [int]$EnvironmentID [int]$ParentID [int]$Access [guid]$ProjGUID [guid]$PprjGuid [int]$WSpaceProfID [int]$ComponentClassID [int]$Flags [int]$ComponentInstanceID}
The advantage of using a class rather than returning a collection of unrelated values is that every returned project has the same predictable structure.
For example:
$Project.ID$Project.Name$Project.ParentID$Project.ProjGUID
can all be accessed directly.
Defining the ProjectWise Properties
The next step is determining how each property should be retrieved.
ProjectWise provides different SDK functions depending on the property’s underlying data type.
For this example, the properties are categorized as:
- Numeric
- String
- GUID
A hashtable is used to define this information:
$ProjectProperties = @{ ID = 'Numeric' VersionNo = 'Numeric' ManagerID = 'Numeric' StorageID = 'Numeric' CreatorID = 'Numeric' UpdaterID = 'Numeric' WorkflowID = 'Numeric' StateID = 'Numeric' Type = 'Numeric' ArchiveID = 'Numeric' IsParent = 'Numeric' Name = 'String' Desc = 'String' Code = 'String' Version = 'String' CreateTime = 'String' UpdateTime = 'String' EnvironmentID = 'Numeric' ParentID = 'Numeric' Access = 'Numeric' ProjGuid = 'guid' PprjGuid = 'guid' WSpaceProfID = 'Numeric' ComponentClassId = 'Numeric' Flags = 'Numeric' ComponentInstanceId = 'Numeric'}
This allows the property retrieval code to be generic rather than having to explicitly call the appropriate ProjectWise API function for every property.
Creating Get-PWProject
Now that we have the class and property definitions, we can create a function that retrieves a single selected ProjectWise project.
FUNCTION Get-PWProject { param ( [int]$Index ) $Project = [ProjectWiseProject]::new() foreach ($Property in $ProjectProperties.GetEnumerator()) { $PropertyName = $Property.Key $PropertyType = $Property.Value $PWProperty = [pwwrapper+projectproperty]::$PropertyName switch ($PropertyType) { 'Numeric' { $Value = [pwwrapper]::aaApi_GetProjectNumericProperty( $PWProperty, $Index ) } 'String' { $Value = [pwwrapper]::aaApi_GetProjectStringProperty( $PWProperty, $Index ) } 'Guid' { $Value = [DMSProjectFunctions]::aaApi_GetProjectGuidProperty( $PWProperty, $Index ) } } $Project.$PropertyName = $Value } return $Project} # end FUNCTION Get-PWProject...
There are a couple of interesting things happening here.
First, this line dynamically retrieves the corresponding ProjectWise property constant:
$PWProperty = [pwwrapper+projectproperty]::$PropertyName
For example, when $PropertyName is ID, PowerShell effectively retrieves:
[pwwrapper+projectproperty]::ID
When it is ParentID, it retrieves:
[pwwrapper+projectproperty]::ParentID
This allows the same code to process every property in the hashtable.
The appropriate ProjectWise API function is then selected based on the property’s type.
Getting the Immediate Child Folders
With the supporting pieces in place, we can create the primary function:
FUNCTION Get-MyPWImmediateChildFolders {
The function accepts a single parameter:
[int] $FolderID
The parameter validation also verifies that the supplied ID represents an existing ProjectWise folder:
[ValidateNotNullOrEmpty()][ValidateScript({ Get-PWFolders -FolderID $_ -JustOne})]
This is useful because it prevents the ProjectWise API call from being made with an invalid folder ID.
Selecting the Child Projects
The actual ProjectWise SDK call is straightforward:
[int]$projectCount = [pwwrapper]::aaApi_SelectChildProjects($FolderID)
The returned value represents the number of child projects selected.
If no projects are returned, the function stops processing:
if($projectCount -lt 1){ throw "Failed to select child projects."}
We then create a strongly typed generic list:
$ImmediateChildFolders = [System.Collections.Generic.List[ProjectWiseProject]]::new()
Using a generic list works well here because we know exactly what type of object the function will return.
Retrieving Each Project
The selected projects can be accessed by their index.
for ($i = 0; $i -lt $projectCount; $i++) { $Project = Get-PWProject -Index $i $ImmediateChildFolders.Add($Project)}
For each selected project, Get-PWProject retrieves all of the properties defined in $ProjectProperties.
The completed list is then returned:
return $ImmediateChildFolders
The result is therefore a collection of ProjectWiseProject objects rather than a simple list of folder IDs.
Putting It All Together
The complete function contains standard PowerShell BEGIN, PROCESS, and END blocks.
The BEGIN block records the start time:
$StartTime = Get-Date
The PROCESS block performs the ProjectWise operation, and the END block reports the elapsed time:
$EndTime = Get-DateWrite-Verbose -Message "[END] It took $($EndTime - $StartTime) to complete the process."
This is particularly helpful when working with larger ProjectWise environments where SDK calls may take longer than expected.
The function can then be called using:
[int] $ProjectID = 3966$results = Get-MyPWImmediateChildFolders ` -FolderID $ProjectID ` -Verbose
The number of returned folders can be displayed with:
Write-Host "$($results.Count) immediate child folders found." ` -ForegroundColor Cyan
Working with the Results
Because the function returns ProjectWiseProject objects, the results can be inspected just like any other PowerShell object.
For example:
$results | Select-Object ID, Name, ParentID
Or, to retrieve a specific project:
$results[0]
Individual properties can also be accessed directly:
$results[0].ID$results[0].Name$results[0].ParentID$results[0].ProjGUID
This makes the function useful as a building block for more complex ProjectWise automation.
For example, the returned collection could subsequently be used to:
- Find a child folder by name.
- Build a ProjectWise folder hierarchy.
- Locate Work Areas.
- Compare ProjectWise folder structures.
- Retrieve environment or workflow information.
- Process multiple levels of a folder hierarchy.
- Export ProjectWise folder information to CSV or JSON.
- Use the returned project IDs in subsequent ProjectWise SDK calls.
Why Return the Entire Project?
At first glance, returning all of the project properties may seem unnecessary if the immediate goal is simply to obtain the folder ID and name.
However, retrieving the complete project object makes the function more reusable.
Instead of creating another function later to retrieve a property’s information, the caller already has access to it.
For example, if a future requirement needs the ProjectWise environment ID, there is no need to modify the function:
$results | Select-Object ID, Name, EnvironmentID
Likewise, GUID information is already available:
$results | Select-Object ID, Name, ProjGUID, PprjGuid
This approach effectively creates a reusable PowerShell representation of the ProjectWise project data returned by the SDK.
One Important Distinction: Immediate Children
The function uses:
aaApi_SelectChildProjects($FolderID)
The intent here is to retrieve the immediate children of the supplied folder.
For example, if the hierarchy looks like this:
Project A│├── Folder B│ ├── Folder D│ └── Folder E│└── Folder C └── Folder F
Calling:
Get-MyPWImmediateChildFolders -FolderID <Project A ID>
returns:
Folder BFolder C
It does not recursively return:
Folder DFolder EFolder F
This distinction is important when building ProjectWise hierarchy-processing functions. A recursive function can be built on top of this function if deeper levels are required.
Conclusion
The ProjectWise SDK provides a powerful set of native APIs, but working with those APIs from PowerShell sometimes requires a little additional infrastructure.
This example demonstrates how several techniques can be combined:
- Use the ProjectWise PowerShell wrapper for the functions it exposes.
- Use
Add-Typeand C# when a native SDK function is not directly available. - Create a PowerShell class to represent ProjectWise project data.
- Use a property definition hashtable to avoid repetitive API calls.
- Return strongly typed objects that can be reused by other PowerShell functions.
- Build small, focused functions that can serve as building blocks for more complex automation.
The result is a reusable function that turns a ProjectWise folder ID into a collection of rich ProjectWise project objects representing its immediate child folders.
Once this pattern is established, it can be extended considerably. Functions for recursively walking a ProjectWise hierarchy, locating specific folders, identifying Work Areas, or exporting complete ProjectWise folder structures can all be built on top of the same foundation.
Experiment with it and have fun.
Hopefully, you find this useful. Please let me know if you have any questions or comments. If you like this post, please click the Like button at the bottom of the page. And thank you for checking out my blog.
