Merge pull request #1806 from rmcrackan/rmcrackan/1804-win-folder-picker-custom-paths

#1804 - Fix Windows folder picker crash on custom paths
This commit is contained in:
rmcrackan
2026-05-12 09:04:18 -04:00
committed by GitHub
6 changed files with 205 additions and 8 deletions

View File

@@ -0,0 +1,87 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace FileManager;
/// <summary>
/// Normalizes stored paths for OS folder picker APIs that are stricter than <see cref="Directory"/> (for example WinForms <c>FolderBrowserDialog</c> / shell <c>SHCreateItemFromParsingName</c>).
/// </summary>
public static class FolderPickerInitialPath
{
private const string WinLongPathPrefix = @"\\?\";
private const string WinLongUncPrefix = @"\\?\UNC\";
/// <summary>
/// Returns a directory path suitable as a folder picker's starting location, or <c>null</c> to let the OS use its default.
/// Verifies the directory exists using the same path rules as <see cref="LongPath"/> before returning a shell-oriented string.
/// </summary>
public static string? GetExistingDirectoryOrNull(string? path)
{
if (string.IsNullOrWhiteSpace(path))
return null;
path = path.Trim();
try
{
LongPath longPath = path;
if (!Directory.Exists(longPath))
return null;
var forPicker = ToOsFolderPickerPath(longPath.Path);
if (string.IsNullOrWhiteSpace(forPicker))
return null;
if (!Directory.Exists(forPicker))
return null;
try
{
var full = Path.GetFullPath(forPicker);
if (Directory.Exists(full))
return full;
}
catch (ArgumentException)
{
// Path.GetFullPath rejects some edge cases; fall through
}
catch (NotSupportedException)
{
}
catch (PathTooLongException)
{
}
return forPicker;
}
catch (ArgumentException)
{
return null;
}
catch (NotSupportedException)
{
return null;
}
catch (PathTooLongException)
{
return null;
}
}
private static string? ToOsFolderPickerPath(string absolutePathFromLongPath)
{
if (string.IsNullOrWhiteSpace(absolutePathFromLongPath))
return null;
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return absolutePathFromLongPath;
if (absolutePathFromLongPath.StartsWith(WinLongUncPrefix, StringComparison.OrdinalIgnoreCase))
return @"\\" + absolutePathFromLongPath.Substring(WinLongUncPrefix.Length);
if (absolutePathFromLongPath.StartsWith(WinLongPathPrefix, StringComparison.Ordinal))
return absolutePathFromLongPath.Substring(WinLongPathPrefix.Length);
return absolutePathFromLongPath;
}
}

View File

@@ -3,6 +3,7 @@ using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using Dinah.Core;
using FileManager;
using LibationFileManager;
using ReactiveUI;
using System.Collections.Generic;
@@ -124,12 +125,34 @@ public partial class DirectoryOrCustomSelectControl : UserControl
public async void Browse_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
var window = this.GetParentWindow();
if (window is null)
return;
var options = new FolderPickerOpenOptions
{
AllowMultiple = false
};
var selectedFolders = await this.GetParentWindow().StorageProvider.OpenFolderPickerAsync(options);
var start = FolderPickerInitialPath.GetExistingDirectoryOrNull(tboxCustomDirPath.Text);
if (start is not null)
{
var loc = await window.StorageProvider.TryGetFolderFromPathAsync(start);
if (loc is not null)
options.SuggestedStartLocation = loc;
}
IReadOnlyList<IStorageFolder> selectedFolders;
try
{
selectedFolders = await window.StorageProvider.OpenFolderPickerAsync(options);
}
catch
{
options.SuggestedStartLocation = null;
selectedFolders = await window.StorageProvider.OpenFolderPickerAsync(options);
}
Directory = selectedFolders.SingleOrDefault()?.TryGetLocalPath() ?? Directory;
}
}

View File

@@ -3,9 +3,11 @@ using Avalonia.Controls;
using Avalonia.Platform.Storage;
using DataLayer;
using Dinah.Core;
using FileManager;
using LibationFileManager;
using LibationUiBase;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
@@ -53,10 +55,28 @@ public partial class LocateAudiobooksDialog : DialogWindow
{
Title = "Select the folder to search for audiobooks",
AllowMultiple = false,
SuggestedStartLocation = await StorageProvider.TryGetFolderFromPathAsync(Configuration.Instance.Books?.PathWithoutPrefix ?? "")
};
var selectedFolder = (await StorageProvider.OpenFolderPickerAsync(folderPicker))?.SingleOrDefault()?.TryGetLocalPath();
var start = FolderPickerInitialPath.GetExistingDirectoryOrNull(Configuration.Instance.Books?.Path ?? "");
if (start is not null)
{
var loc = await StorageProvider.TryGetFolderFromPathAsync(start);
if (loc is not null)
folderPicker.SuggestedStartLocation = loc;
}
IReadOnlyList<IStorageFolder> picked;
try
{
picked = await StorageProvider.OpenFolderPickerAsync(folderPicker);
}
catch
{
folderPicker.SuggestedStartLocation = null;
picked = await StorageProvider.OpenFolderPickerAsync(folderPicker);
}
var selectedFolder = picked?.SingleOrDefault()?.TryGetLocalPath();
if (selectedFolder is null || !Directory.Exists(selectedFolder))
{

View File

@@ -1,6 +1,8 @@
using LibationFileManager;
using FileManager;
using LibationFileManager;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows.Forms;
namespace LibationWinForms.Dialogs;
@@ -81,9 +83,21 @@ public partial class DirectoryOrCustomSelectControl : UserControl
using var dialog = new FolderBrowserDialog
{
Description = string.IsNullOrWhiteSpace(dirSearchTitle) ? "Search" : $"Search for {dirSearchTitle}",
SelectedPath = this.customTb.Text
};
dialog.ShowDialog();
var initial = FolderPickerInitialPath.GetExistingDirectoryOrNull(this.customTb.Text);
if (initial is not null)
dialog.SelectedPath = initial;
try
{
dialog.ShowDialog();
}
catch (Win32Exception)
{
dialog.SelectedPath = string.Empty;
dialog.ShowDialog();
}
if (!string.IsNullOrWhiteSpace(dialog.SelectedPath))
this.customTb.Text = dialog.SelectedPath;
}

View File

@@ -1,8 +1,10 @@
using DataLayer;
using Dinah.Core;
using FileManager;
using LibationFileManager;
using LibationUiBase;
using System;
using System.ComponentModel;
using System.IO;
using System.Threading;
using System.Windows.Forms;
@@ -44,10 +46,21 @@ public partial class LocateAudiobooksDialog : Form
{
Description = "Select the folder to search for audiobooks",
UseDescriptionForTitle = true,
InitialDirectory = Configuration.Instance.Books?.Path ?? string.Empty
};
var initial = FolderPickerInitialPath.GetExistingDirectoryOrNull(Configuration.Instance.Books?.Path ?? "");
if (initial is not null)
fbd.InitialDirectory = initial;
var result = fbd.ShowDialog(this);
DialogResult result;
try
{
result = fbd.ShowDialog(this);
}
catch (Win32Exception)
{
fbd.InitialDirectory = string.Empty;
result = fbd.ShowDialog(this);
}
if (result != DialogResult.OK || !Directory.Exists(fbd.SelectedPath))
{
DialogResult = result;

View File

@@ -0,0 +1,40 @@
using System;
using System.IO;
using FileManager;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace FileManager.Tests;
[TestClass]
public class FolderPickerInitialPathTests
{
[TestMethod]
public void GetExistingDirectoryOrNull_NullOrEmpty_ReturnsNull()
{
Assert.IsNull(FolderPickerInitialPath.GetExistingDirectoryOrNull(null));
Assert.IsNull(FolderPickerInitialPath.GetExistingDirectoryOrNull(""));
Assert.IsNull(FolderPickerInitialPath.GetExistingDirectoryOrNull(" "));
}
[TestMethod]
public void GetExistingDirectoryOrNull_MissingPath_ReturnsNull()
{
Assert.IsNull(FolderPickerInitialPath.GetExistingDirectoryOrNull(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"), "nope")));
}
[TestMethod]
public void GetExistingDirectoryOrNull_ExistingTempDir_ReturnsFullPath()
{
var dir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "LibationTest_" + Guid.NewGuid().ToString("N"))).FullName;
try
{
var result = FolderPickerInitialPath.GetExistingDirectoryOrNull(dir);
Assert.IsNotNull(result);
Assert.IsTrue(Directory.Exists(result));
}
finally
{
try { Directory.Delete(dir, recursive: true); } catch { /* ignore */ }
}
}
}