Add FFU.Common and FFUUI.Core module manifests and shared UI functions

- Created module manifest for FFU.Common with initial version 0.0.1.
- Created module manifest for FFUUI.Core with initial version 0.0.1.
- Implemented shared UI functions in FFUUI.Shared.psm1, including:
  - Update-ListViewItemStatus: Updates the status of items in a ListView.
  - Update-OverallProgress: Updates a progress bar and status label.
  - Invoke-ProgressUpdate: Enqueues progress updates to the UI thread.
  - Add-SortableColumn: Adds sortable columns to a ListView.
  - Add-SelectableGridViewColumn: Adds a selectable column with a "Select All" checkbox.
  - Update-SelectAllHeaderCheckBoxState: Updates the state of the header checkbox.
  - Invoke-ListViewSort: Sorts ListView items based on specified properties.
  - Show-ModernFolderPicker: Displays a modern folder picker dialog.
This commit is contained in:
rbalsleyMSFT
2025-06-11 20:50:51 -07:00
parent e4201aebff
commit 9282b4231e
10 changed files with 1406 additions and 1345 deletions
+618
View File
@@ -0,0 +1,618 @@
# Function to update status of a specific item in a ListView
function Update-ListViewItemStatus {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[object]$WindowObject, # Changed type to [object]
[Parameter(Mandatory)]
[object]$ListView, # Changed type to [object]
[Parameter(Mandatory)]
[string]$IdentifierProperty,
[Parameter(Mandatory)]
[string]$IdentifierValue,
[Parameter(Mandatory)]
[string]$StatusProperty,
[Parameter(Mandatory)]
[string]$StatusValue
)
# Ensure we are in UI mode and objects are of correct WPF types
if ($WindowObject -is [System.Windows.Window] -and $ListView -is [System.Windows.Controls.ListView]) {
# Directly update UI elements as this function is now called on the UI thread
try {
$itemToUpdate = $ListView.Items | Where-Object { $_.$IdentifierProperty -eq $IdentifierValue } | Select-Object -First 1
if ($null -ne $itemToUpdate) {
$itemToUpdate.$StatusProperty = $StatusValue
$ListView.Items.Refresh() # Refresh the view to show the change
}
else {
# Log if item not found (for debugging)
WriteLog "Update-ListViewItemStatus: Item with $IdentifierProperty '$IdentifierValue' not found in ListView."
}
}
catch {
WriteLog "Update-ListViewItemStatus: Error updating ListView: $($_.Exception.Message)"
}
} # End of if ($WindowObject -is [System.Windows.Window]...)
else {
# Log if called in non-UI mode or with incorrect types (should not happen if Invoke-ParallelProcessing $isUiMode is correct)
WriteLog "Update-ListViewItemStatus: Skipped UI update for $IdentifierValue due to non-UI mode or incorrect object types."
}
}
# Function to update overall progress bar and status text label
function Update-OverallProgress {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[object]$WindowObject, # Changed type to [object]
[Parameter(Mandatory)]
[int]$CompletedCount,
[Parameter(Mandatory)]
[int]$TotalCount,
[Parameter(Mandatory)]
[string]$StatusText,
[Parameter(Mandatory)]
[string]$ProgressBarName,
[Parameter(Mandatory)]
[string]$StatusLabelName
)
# Ensure we are in UI mode and WindowObject is of correct WPF type
if ($WindowObject -is [System.Windows.Window]) {
# Directly update UI elements as this function is now called on the UI thread
try {
# Find controls by name using the $WindowObject
$pb = $WindowObject.FindName($ProgressBarName)
$lbl = $WindowObject.FindName($StatusLabelName)
if ($null -eq $pb) {
WriteLog "Update-OverallProgress: ProgressBar '$ProgressBarName' not found."
return
}
if ($null -eq $lbl) {
WriteLog "Update-OverallProgress: StatusLabel '$StatusLabelName' not found."
return
}
# Update the progress bar
if ($TotalCount -gt 0) {
$percentComplete = ($CompletedCount / $TotalCount) * 100
$pb.Value = $percentComplete
}
else {
$pb.Value = 0
}
# Update the status label
$lbl.Text = $StatusText
}
catch {
WriteLog "Update-OverallProgress: Error updating progress: $($_.Exception.Message)"
}
} # End of if ($WindowObject -is [System.Windows.Window])
else {
# Log if called in non-UI mode or with incorrect types
WriteLog "Update-OverallProgress: Skipped UI update ($StatusText) due to non-UI mode or incorrect WindowObject type."
}
}
# Helper function to enqueue progress updates to the UI thread
function Invoke-ProgressUpdate {
param(
[Parameter(Mandatory)]
[System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue,
[Parameter(Mandatory)]
[string]$Identifier,
[Parameter(Mandatory)]
[string]$Status
)
$ProgressQueue.Enqueue(@{ Identifier = $Identifier; Status = $Status })
}
# Add a function to create a sortable list view
function Add-SortableColumn {
param(
[System.Windows.Controls.GridView]$gridView,
[string]$header,
[string]$binding,
[int]$width = 'Auto',
[bool]$isCheckbox = $false,
[System.Windows.HorizontalAlignment]$headerHorizontalAlignment = [System.Windows.HorizontalAlignment]::Stretch
)
$column = New-Object System.Windows.Controls.GridViewColumn
$commonPadding = New-Object System.Windows.Thickness(5, 2, 5, 2)
$headerControl = New-Object System.Windows.Controls.GridViewColumnHeader
$headerControl.Tag = $binding # Used for sorting
if ($isCheckbox) {
# Cell template for a column of checkboxes
$cellTemplate = New-Object System.Windows.DataTemplate
$gridFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.Grid])
$checkBoxFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.CheckBox])
$checkBoxFactory.SetBinding([System.Windows.Controls.CheckBox]::IsCheckedProperty, (New-Object System.Windows.Data.Binding("IsSelected")))
$checkBoxFactory.SetValue([System.Windows.FrameworkElement]::HorizontalAlignmentProperty, [System.Windows.HorizontalAlignment]::Center)
$checkBoxFactory.SetValue([System.Windows.FrameworkElement]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Center)
$checkBoxFactory.AddHandler([System.Windows.Controls.CheckBox]::ClickEvent, [System.Windows.RoutedEventHandler] {
param($eventSourceLocal, $eventArgsLocal)
# Sync logic would be needed here if this column had a header checkbox
})
$gridFactory.AppendChild($checkBoxFactory)
$cellTemplate.VisualTree = $gridFactory
$column.CellTemplate = $cellTemplate
}
else {
# For regular text columns
$headerControl.HorizontalContentAlignment = $headerHorizontalAlignment
$headerControl.Content = $header
$headerTextElementFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.TextBlock])
$headerTextElementFactory.SetValue([System.Windows.Controls.TextBlock]::TextProperty, $header)
$headerTextBlockPadding = New-Object System.Windows.Thickness($commonPadding.Left, $commonPadding.Top, $commonPadding.Right, $commonPadding.Bottom)
$headerTextElementFactory.SetValue([System.Windows.Controls.TextBlock]::PaddingProperty, $headerTextBlockPadding)
$headerTextElementFactory.SetValue([System.Windows.FrameworkElement]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Center)
$headerDataTemplate = New-Object System.Windows.DataTemplate
$headerDataTemplate.VisualTree = $headerTextElementFactory
$headerControl.ContentTemplate = $headerDataTemplate
$cellTemplate = New-Object System.Windows.DataTemplate
$textBlockFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.TextBlock])
$textBlockFactory.SetBinding([System.Windows.Controls.TextBlock]::TextProperty, (New-Object System.Windows.Data.Binding($binding)))
# Adjust left padding to 0 for cell text to align with header text
$cellTextBlockPadding = New-Object System.Windows.Thickness(0, $commonPadding.Top, $commonPadding.Right, $commonPadding.Bottom)
$textBlockFactory.SetValue([System.Windows.Controls.TextBlock]::PaddingProperty, $cellTextBlockPadding)
$textBlockFactory.SetValue([System.Windows.FrameworkElement]::HorizontalAlignmentProperty, [System.Windows.HorizontalAlignment]::Left)
$textBlockFactory.SetValue([System.Windows.FrameworkElement]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Center)
$cellTemplate.VisualTree = $textBlockFactory
$column.CellTemplate = $cellTemplate
}
$column.Header = $headerControl
if ($width -ne 'Auto') {
$column.Width = $width
}
$gridView.Columns.Add($column)
}
# Function to add a selectable GridViewColumn with a "Select All" header CheckBox
function Add-SelectableGridViewColumn {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[System.Windows.Controls.ListView]$ListView,
[Parameter(Mandatory)]
[string]$HeaderCheckBoxScriptVariableName,
[Parameter(Mandatory)]
[double]$ColumnWidth,
[string]$IsSelectedPropertyName = "IsSelected"
)
# Ensure the ListView has a GridView
if ($null -eq $ListView.View -or -not ($ListView.View -is [System.Windows.Controls.GridView])) {
WriteLog "Add-SelectableGridViewColumn: ListView '$($ListView.Name)' does not have a GridView or View is null. Cannot add column."
return
}
$gridView = $ListView.View
# Create the "Select All" CheckBox for the header
$headerCheckBox = New-Object System.Windows.Controls.CheckBox
$headerCheckBox.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Center
# MODIFICATION: Store the actual ListView object in the header's Tag
$headerTagObject = [PSCustomObject]@{
PropertyName = $IsSelectedPropertyName
ListViewControl = $ListView # Store the object itself
}
$headerCheckBox.Tag = $headerTagObject
$headerCheckBox.Add_Checked({
param($senderCheckBoxLocal, $eventArgsCheckedLocal)
$tagData = $senderCheckBoxLocal.Tag
$localPropertyName = $tagData.PropertyName
$actualListView = $tagData.ListViewControl # Get the control directly from the tag
$collectionToUpdate = if ($null -ne $actualListView.ItemsSource) { $actualListView.ItemsSource } else { $actualListView.Items }
if ($null -ne $collectionToUpdate) {
foreach ($item in $collectionToUpdate) { $item.$($localPropertyName) = $true }
$actualListView.Items.Refresh()
}
})
$headerCheckBox.Add_Unchecked({
param($senderCheckBoxLocal, $eventArgsUncheckedLocal)
if ($senderCheckBoxLocal.IsChecked -eq $false) {
$tagData = $senderCheckBoxLocal.Tag
$localPropertyName = $tagData.PropertyName
$actualListView = $tagData.ListViewControl # Get the control directly from the tag
$collectionToUpdate = if ($null -ne $actualListView.ItemsSource) { $actualListView.ItemsSource } else { $actualListView.Items }
if ($null -ne $collectionToUpdate) {
foreach ($item in $collectionToUpdate) { $item.$($localPropertyName) = $false }
$actualListView.Items.Refresh()
}
}
})
Set-Variable -Name $HeaderCheckBoxScriptVariableName -Value $headerCheckBox -Scope Script -Force
WriteLog "Add-SelectableGridViewColumn: Stored header checkbox in script variable '$HeaderCheckBoxScriptVariableName'."
$selectableColumn = New-Object System.Windows.Controls.GridViewColumn
$selectableColumn.Header = $headerCheckBox
$selectableColumn.Width = $ColumnWidth
$cellTemplate = New-Object System.Windows.DataTemplate
$borderFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.Border])
$borderFactory.SetValue([System.Windows.FrameworkElement]::HorizontalAlignmentProperty, [System.Windows.HorizontalAlignment]::Stretch)
$borderFactory.SetValue([System.Windows.FrameworkElement]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Stretch)
$checkBoxFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.CheckBox])
$checkBoxFactory.SetBinding([System.Windows.Controls.CheckBox]::IsCheckedProperty, (New-Object System.Windows.Data.Binding($IsSelectedPropertyName)))
$checkBoxFactory.SetValue([System.Windows.FrameworkElement]::HorizontalAlignmentProperty, [System.Windows.HorizontalAlignment]::Center)
$checkBoxFactory.SetValue([System.Windows.FrameworkElement]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Center)
# MODIFICATION: Store the actual ListView object in the item checkbox's Tag
$tagObject = [PSCustomObject]@{
HeaderCheckboxName = $HeaderCheckBoxScriptVariableName
ListViewControl = $ListView # Store the object itself
}
$checkBoxFactory.SetValue([System.Windows.FrameworkElement]::TagProperty, $tagObject)
$checkBoxFactory.AddHandler([System.Windows.Controls.CheckBox]::ClickEvent, [System.Windows.RoutedEventHandler] {
param($eventSourceLocal, $eventArgsLocal)
$itemCheckBox = $eventSourceLocal -as [System.Windows.Controls.CheckBox]
$tagData = $itemCheckBox.Tag
$headerCheckboxNameFromTag = $tagData.HeaderCheckboxName
$targetListView = $tagData.ListViewControl # Get the control directly from the tag
WriteLog "Add-SelectableGridViewColumn: Item Click. ListView: '$($targetListView.Name)', HeaderChkName: '$headerCheckboxNameFromTag'"
$headerChk = Get-Variable -Name $headerCheckboxNameFromTag -Scope Script -ValueOnly -ErrorAction SilentlyContinue
if ($null -ne $headerChk) {
Update-SelectAllHeaderCheckBoxState -ListView $targetListView -HeaderCheckBox $headerChk
}
else {
WriteLog "Add-SelectableGridViewColumn: Error - Could not retrieve script variable for header checkbox named '$headerCheckboxNameFromTag'."
}
})
$borderFactory.AppendChild($checkBoxFactory)
$cellTemplate.VisualTree = $borderFactory
$selectableColumn.CellTemplate = $cellTemplate
$gridView.Columns.Insert(0, $selectableColumn)
WriteLog "Add-SelectableGridViewColumn: Successfully added selectable column to '$($ListView.Name)'."
}
# Function to update the IsChecked state of a "Select All" header CheckBox
function Update-SelectAllHeaderCheckBoxState {
param(
[Parameter(Mandatory)]
[System.Windows.Controls.ListView]$ListView,
[Parameter(Mandatory)]
[System.Windows.Controls.CheckBox]$HeaderCheckBox
)
$collectionToInspect = $null
if ($null -ne $ListView.ItemsSource) {
$collectionToInspect = @($ListView.ItemsSource)
}
elseif ($ListView.HasItems) {
# Check if Items collection has items and ItemsSource is null
$collectionToInspect = @($ListView.Items)
}
# If no items to inspect (either ItemsSource was null and Items was empty, or ItemsSource was empty)
if ($null -eq $collectionToInspect -or $collectionToInspect.Count -eq 0) {
$HeaderCheckBox.IsChecked = $false
return
}
$selectedCount = ($collectionToInspect | Where-Object { $_.IsSelected }).Count
$totalItemCount = $collectionToInspect.Count # Get the total count from the collection being inspected
if ($totalItemCount -eq 0) {
# Handle empty list case specifically
$HeaderCheckBox.IsChecked = $false
}
elseif ($selectedCount -eq $totalItemCount) {
$HeaderCheckBox.IsChecked = $true
}
elseif ($selectedCount -eq 0) {
$HeaderCheckBox.IsChecked = $false
}
else {
# Indeterminate state
$HeaderCheckBox.IsChecked = $null
}
}
# Function to sort ListView items
function Invoke-ListViewSort {
param(
[System.Windows.Controls.ListView]$listView,
[string]$property,
[PSCustomObject]$State
)
# Toggle sort direction if clicking the same column
if ($State.Flags.lastSortProperty -eq $property) {
$State.Flags.lastSortAscending = -not $State.Flags.lastSortAscending
}
else {
$State.Flags.lastSortAscending = $true
}
$State.Flags.lastSortProperty = $property
# Get items from ItemsSource or Items collection
$currentItemsSource = $listView.ItemsSource
$itemsToSort = @()
if ($null -ne $currentItemsSource) {
$itemsToSort = @($currentItemsSource)
}
else {
$itemsToSort = @($listView.Items)
}
if ($itemsToSort.Count -eq 0) {
return
}
$selectedItems = @($itemsToSort | Where-Object { $_.IsSelected })
$unselectedItems = @($itemsToSort | Where-Object { -not $_.IsSelected })
# Define the primary sort criterion
$primarySortDefinition = @{
Expression = {
$val = $_.$property
if ($null -eq $val) { '' } else { $val }
}
Ascending = $State.Flags.lastSortAscending
}
$sortCriteria = [System.Collections.Generic.List[hashtable]]::new()
$sortCriteria.Add($primarySortDefinition)
# Determine secondary sort property based on the ListView
$secondarySortPropertyName = $null
if ($listView.Name -eq 'lstDriverModels') {
$secondarySortPropertyName = "Model"
}
elseif ($listView.Name -eq 'lstWingetResults') {
$secondarySortPropertyName = "Name"
}
elseif ($listView.Name -eq 'lstAppsScriptVariables') {
if ($property -eq "Key") {
$secondarySortPropertyName = "Value"
}
elseif ($property -eq "Value") {
$secondarySortPropertyName = "Key"
}
else {
# Default secondary sort for IsSelected or other properties
$secondarySortPropertyName = "Key"
}
}
if ($null -ne $secondarySortPropertyName -and $property -ne $secondarySortPropertyName) {
$itemsHaveSecondaryProperty = $false
if ($unselectedItems.Count -gt 0) {
if ($null -ne $unselectedItems[0].PSObject.Properties[$secondarySortPropertyName]) {
$itemsHaveSecondaryProperty = $true
}
}
elseif ($selectedItems.Count -gt 0) {
if ($null -ne $selectedItems[0].PSObject.Properties[$secondarySortPropertyName]) {
$itemsHaveSecondaryProperty = $true
}
}
if ($itemsHaveSecondaryProperty) {
# Create a scriptblock for the secondary sort expression dynamically
$expressionScriptBlock = [scriptblock]::Create("`$_.$secondarySortPropertyName")
$secondarySortDefinition = @{
Expression = {
$val = Invoke-Command -ScriptBlock $expressionScriptBlock -ArgumentList $_
if ($null -eq $val) { '' } else { $val }
}
Ascending = $true # Secondary sort always ascending
}
$sortCriteria.Add($secondarySortDefinition)
}
}
$sortedUnselected = $unselectedItems | Sort-Object -Property $sortCriteria.ToArray()
# Ensure $sortedUnselected is not null before attempting to add its range
if ($null -eq $sortedUnselected) {
$sortedUnselected = @()
}
# Combine sorted items: selected items first, then sorted unselected items
$newSortedList = [System.Collections.Generic.List[object]]::new()
$newSortedList.AddRange($selectedItems)
$newSortedList.AddRange($sortedUnselected)
# Set the new sorted list as the ItemsSource
# Try nulling out ItemsSource first to force a more complete refresh
$listView.ItemsSource = $null
$listView.ItemsSource = $newSortedList.ToArray()
}
# --------------------------------------------------------------------------
# SECTION: Modern Folder Picker
# --------------------------------------------------------------------------
# 1) Define a C# class that uses the correct GUIDs for IFileDialog, IFileOpenDialog, and FileOpenDialog,
# while omitting conflicting "GetResults/GetSelectedItems" from IFileDialog.
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
public static class ModernFolderBrowser
{
// Flags for IFileDialog
[Flags]
private enum FileDialogOptions : uint
{
OverwritePrompt = 0x00000002,
StrictFileTypes = 0x00000004,
NoChangeDir = 0x00000008,
PickFolders = 0x00000020,
ForceFileSystem = 0x00000040,
AllNonStorageItems = 0x00000080,
NoValidate = 0x00000100,
AllowMultiSelect = 0x00000200,
PathMustExist = 0x00000800,
FileMustExist = 0x00001000,
CreatePrompt = 0x00002000,
ShareAware = 0x00004000,
NoReadOnlyReturn = 0x00008000,
NoTestFileCreate = 0x00010000,
DontAddToRecent = 0x02000000,
ForceShowHidden = 0x10000000
}
// IFileDialog (GUID from Windows SDK)
// - Omitting GetResults / GetSelectedItems to avoid overshadow.
[ComImport]
[Guid("42F85136-DB7E-439C-85F1-E4075D135FC8")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IFileDialog
{
[PreserveSig]
int Show(IntPtr parent);
void SetFileTypes(uint cFileTypes, IntPtr rgFilterSpec);
void SetFileTypeIndex(uint iFileType);
void GetFileTypeIndex(out uint piFileType);
void Advise(IntPtr pfde, out uint pdwCookie);
void Unadvise(uint dwCookie);
void SetOptions(FileDialogOptions fos);
void GetOptions(out FileDialogOptions pfos);
void SetDefaultFolder(IShellItem psi);
void SetFolder(IShellItem psi);
void GetFolder(out IShellItem ppsi);
void GetCurrentSelection(out IShellItem ppsi);
void SetFileName([MarshalAs(UnmanagedType.LPWStr)] string pszName);
void GetFileName(out IntPtr pszName);
void SetTitle([MarshalAs(UnmanagedType.LPWStr)] string pszTitle);
void SetOkButtonLabel([MarshalAs(UnmanagedType.LPWStr)] string pszText);
void SetFileNameLabel([MarshalAs(UnmanagedType.LPWStr)] string pszLabel);
void GetResult(out IShellItem ppsi);
void AddPlace(IShellItem psi, int fdap);
void SetDefaultExtension([MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension);
void Close(int hr);
void SetClientGuid(ref Guid guid);
void ClearClientData();
void SetFilter(IntPtr pFilter);
// NOTE: We intentionally do NOT define GetResults and GetSelectedItems here,
// because they cause overshadow warnings in IFileOpenDialog.
}
// IFileOpenDialog extends IFileDialog by adding 2 new methods with the same name,
// which otherwise cause overshadow warnings. We'll define them only here.
[ComImport]
[Guid("D57C7288-D4AD-4768-BE02-9D969532D960")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IFileOpenDialog : IFileDialog
{
// These two come after the parent's vtable:
void GetResults(out IntPtr ppenum);
void GetSelectedItems(out IntPtr ppsai);
}
// The coclass for creating an IFileOpenDialog
[ComImport]
[Guid("DC1C5A9C-E88A-4DDE-A5A1-60F82A20AEF7")]
private class FileOpenDialog
{
}
// IShellItem
[ComImport]
[Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IShellItem
{
void BindToHandler(IntPtr pbc, ref Guid bhid, ref Guid riid, out IntPtr ppv);
void GetParent(out IShellItem ppsi);
void GetDisplayName(uint sigdnName, out IntPtr ppszName);
void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs);
void Compare(IShellItem psi, uint hint, out int piOrder);
}
private const uint SIGDN_FILESYSPATH = 0x80058000;
public static string ShowDialog(string title, IntPtr parentHandle)
{
// Create COM dialog instance
IFileOpenDialog dialog = (IFileOpenDialog)(new FileOpenDialog());
// Get current options
FileDialogOptions opts;
dialog.GetOptions(out opts);
// Add flags for picking folders
opts |= FileDialogOptions.PickFolders | FileDialogOptions.PathMustExist | FileDialogOptions.ForceFileSystem;
dialog.SetOptions(opts);
// Set title
if (!string.IsNullOrEmpty(title))
{
dialog.SetTitle(title);
}
// Show the dialog
int hr = dialog.Show(parentHandle);
// 0 = S_OK. 1 or 0x800704C7 often means user canceled. Return null if so.
if (hr != 0)
{
if ((uint)hr == 0x800704C7 || hr == 1)
{
return null; // Canceled
}
else
{
Marshal.ThrowExceptionForHR(hr);
}
}
// Retrieve the selection (IShellItem)
IShellItem shellItem;
dialog.GetResult(out shellItem);
if (shellItem == null) return null;
// Convert to file system path
IntPtr pszPath = IntPtr.Zero;
shellItem.GetDisplayName(SIGDN_FILESYSPATH, out pszPath);
if (pszPath == IntPtr.Zero) return null;
string folderPath = Marshal.PtrToStringAuto(pszPath);
Marshal.FreeCoTaskMem(pszPath);
return folderPath;
}
}
"@ -Language CSharp
# 2) Define a PowerShell function that invokes our C# wrapper
function Show-ModernFolderPicker {
param(
[string]$Title = "Select a folder"
)
# For a simple test, pass IntPtr.Zero as the parent window handle
return [ModernFolderBrowser]::ShowDialog($Title, [IntPtr]::Zero)
}
Export-ModuleMember -Function *