Table of Contents

File and folder dialogs

Dapplo.Windows.Dialogs shows the Windows file open, file save and folder picker dialogs (the Common Item Dialog, IFileOpenDialog / IFileSaveDialog) through COM. It needs neither Windows Forms nor WPF, so it also works in console applications and other UI frameworks.

dotnet add package Dapplo.Windows.Dialogs

Namespace: Dapplo.Windows.Dialogs.

The simple API

FileDialog has one method per dialog. Each returns null when the user cancels and throws on real errors.

// Every FileDialog method returns null when the user cancels
string path = FileDialog.PickFileToOpen(
    title: "Open Document",
    initialDirectory: Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
    filters: new[] { ("Text files", "*.txt"), ("All files", "*.*") });
if (path != null)
{
    Console.WriteLine(File.ReadAllText(path));
}

string savePath = FileDialog.PickFileToSave(
    title: "Save Screenshot",
    suggestedFileName: "screenshot.png",
    filters: new[] { ("PNG Image", "*.png"), ("JPEG Image", "*.jpg") },
    defaultExtension: "png");

IReadOnlyList<string> files = FileDialog.PickFilesToOpen(filters: new[] { ("Images", "*.png;*.jpg;*.bmp") });

string folder = FileDialog.PickFolder(title: "Select Output Folder");

The builders

For more options use FileOpenDialogBuilder, FileSaveDialogBuilder and FolderPickerBuilder. FileDialog uses them too. ShowDialog returns a FileDialogResult: WasCancelled, SelectedPath and, for multiple selection, SelectedPaths. Pass the handle of your window to ShowDialog to make the dialog modal for it.

FileDialogResult result = new FileOpenDialogBuilder()
    .WithTitle("Open Configuration")
    .WithInitialDirectory(@"C:\ProgramData\MyApp")
    .AddFilter("Config files", "*.json;*.xml")
    .AddFilter("All files", "*.*")
    .WithDefaultExtension("json")
    // The dialog is modal for this window
    .ShowDialog(ownerWindowHandle);

if (result.WasCancelled)
{
    return;
}
string configPath = result.SelectedPath;
FileDialogResult result = new FileOpenDialogBuilder()
    .WithTitle("Import Images")
    .AddFilter("Images", "*.png;*.jpg;*.bmp;*.gif")
    .AllowMultipleSelection()
    .ShowDialog();

if (!result.WasCancelled)
{
    foreach (string file in result.SelectedPaths)
    {
        Console.WriteLine(file);
    }
}
FileDialogResult result = new FileSaveDialogBuilder()
    .WithTitle("Export Report")
    .WithSuggestedFileName("report.pdf")
    .WithDefaultExtension("pdf")
    .AddFilter("PDF files", "*.pdf")
    .ShowDialog();

if (!result.WasCancelled)
{
    Console.WriteLine($"Export to {result.SelectedPath}");
}
FileDialogResult result = new FolderPickerBuilder()
    .WithTitle("Select Backup Location")
    .WithInitialDirectory(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments))
    .ShowDialog();

if (!result.WasCancelled)
{
    Console.WriteLine($"Backup to {result.SelectedPath}");
}

AddPlace adds a folder to the navigation pane of this dialog:

string projects = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Projects");

FileDialogResult result = new FileOpenDialogBuilder()
    .WithTitle("Open Project")
    .AddFilter("Project files", "*.proj")
    // Pinned at the top of the navigation pane, only for this dialog
    .AddPlace(projects, atTop: true)
    .ShowDialog();
Option How
File types AddFilter("Images", "*.png;*.jpg"), the first filter is selected
Default extension WithDefaultExtension("png"), without the dot
Start folder WithInitialDirectory(path); without it Windows uses the folder the user used last
File name FileSaveDialogBuilder.WithSuggestedFileName("report.pdf")
Several files FileOpenDialogBuilder.AllowMultipleSelection()
Owner window ShowDialog(ownerHandle)

Errors and threading

A cancel is not an error: WasCancelled is true (or the FileDialog method returns null). Real failures throw a COMException.

FileDialogResult result;
try
{
    result = new FileOpenDialogBuilder().AddFilter("All files", "*.*").ShowDialog();
}
catch (COMException ex)
{
    // Not a cancel: something went wrong, e.g. a shell extension failed
    Console.WriteLine($"The dialog failed: 0x{ex.ErrorCode:X8}");
    return;
}

if (result.WasCancelled)
{
    // Not an error, the user pressed Cancel or Escape
    return;
}
Console.WriteLine(result.SelectedPath);

The dialog must be shown on an STA thread. The UI threads of Windows Forms and WPF are STA. A console application needs [STAThread] on Main, other threads need a dedicated STA thread:

// The dialog needs an STA thread. UI threads of WinForms and WPF are STA, a console application's Main needs [STAThread],
// from other threads use a dedicated STA thread:
string path = null;
var thread = new Thread(() => path = FileDialog.PickFileToOpen());
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
thread.Join();

On any other thread ShowDialog (and the FileDialog methods) throw an InvalidOperationException which says so, before any COM object is created. Thread pool threads (Task.Run, await continuations without a UI SynchronizationContext) are always MTA:

try
{
    // Task.Run uses a thread pool thread, which is MTA: the builder throws before any COM object is created
    await Task.Run(() => new FolderPickerBuilder().ShowDialog());
}
catch (InvalidOperationException ex)
{
    // "FolderPickerBuilder.ShowDialog must be called from an STA thread, but the current thread (...) is MTA. ..."
    Console.WriteLine(ex.Message);
}

Not covered

The builders don't expose dialog events (IFileDialog.Advise), client GUIDs for separate "last folder" memory, custom button labels, or save-dialog property stores. For those, use the COM interfaces directly.