Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,21 @@ Conventions for contributors:

### Changed

- **Picker helpers moved to `Microsoft.Windows.Storage.Pickers` and renamed as non-hooks**
(issue #1374, spec 054 §8.3). Breaking: `UseFilePickerAsync` / `UseMultipleFilesPickerAsync`
/ `UseFolderPickerAsync` / `UseSaveFilePickerAsync` are now `PickFileAsync` /
`PickMultipleFilesAsync` / `PickFolderAsync` / `PickSaveFileAsync` (plus the new
`PickMultipleFoldersAsync`) on `RenderContext` and `Component`, and return path-based
`PickFileResult` / `PickFolderResult` instead of `StorageFile` / `StorageFolder`. Call them
directly from event handlers; the hook-rules analyzer no longer flags them. Option enums
come from `Microsoft.Windows.Storage.Pickers`; `SaveFilePickerOptions.FileTypeChoices` is an
ordered `IReadOnlyList<FileTypeChoice>` (order sets the default entry); `Title`,
`SuggestedFolder`, `SuggestedStartFolder`, `InitialFileTypeIndex`, open-picker
`FileTypeChoices`, `DefaultFileExtension` and `ShowOverwritePrompt` are new;
`FolderPickerOptions.FileTypeFilter` and `SaveFilePickerOptions.SuggestedSaveFile` are
removed. The dialog is owned by the window whose host rendered the component (previously
whichever window was most recently active, which with several windows picked the wrong one
and threw after any window closed), and the pickers now work in elevated apps.
- **The doc pipeline moved out of `mur`, and doc screenshots are captured with winapp's
Windows Graphics Capture.** `mur docs` is now `dotnet run --project tools/Reactor.DocPipeline --`
(`compile`, `check-tier`, `render-diagrams`, `new-diagram`; same options). It is a contributor
Expand Down
31 changes: 16 additions & 15 deletions docs/_pipeline/apps/windows/App.cs
Original file line number Diff line number Diff line change
Expand Up @@ -576,35 +576,36 @@ class PickerDemo : Component
public override Element Render()
{
// <snippet:pickers>
// Capture helpers during Render; invoke them only from UI-thread events.
var pickFile = UseFilePickerAsync;
var pickFiles = UseMultipleFilesPickerAsync;
var pickFolder = UseFolderPickerAsync;
var saveFile = UseSaveFilePickerAsync;

// Pickers are not hooks: call them directly from UI-thread event handlers.
return VStack(
Button("Open one...", async () =>
{
var file = await pickFile(new FilePickerOptions(FileTypeFilter: [".txt", ".md"]));
var file = await PickFileAsync(new FilePickerOptions(FileTypeFilter: [".txt", ".md"]));
if (file is null) return; // User cancelled.
var text = await File.ReadAllTextAsync(file.Path);
}),
Button("Open files...", async () =>
{
var files = await pickFiles(new FilePickerOptions(FileTypeFilter: [".txt", ".md"]));
var files = await PickMultipleFilesAsync(new FilePickerOptions
{
Title = "Choose notes",
FileTypeChoices = [new("Notes", [".txt", ".md"]), new("JSON", [".json"])],
});
if (files.Count == 0) return; // User cancelled.
}),
Button("Select folder...", async () =>
Button("Select folders...", async () =>
{
var folder = await pickFolder(new FolderPickerOptions());
if (folder is null) return;
var folders = await PickMultipleFoldersAsync(new FolderPickerOptions());
if (folders.Count == 0) return;
}),
Button("Save as...", async () =>
{
var file = await saveFile(new SaveFilePickerOptions(
new Dictionary<string, IReadOnlyList<string>> { ["Text"] = [".txt"] },
var target = await PickSaveFileAsync(new SaveFilePickerOptions(
[new("Text", [".txt"]), new("Markdown", [".md"])],
SuggestedFileName: "report"));
if (file is null) return;
// Write the desired content here; selecting a target is not a save.
if (target is null) return;
// Selecting a target is not a save: write the content yourself.
await File.WriteAllTextAsync(target.Path, "report body");
}));
// </snippet:pickers>
}
Expand Down
13 changes: 6 additions & 7 deletions docs/_pipeline/templates/hooks.md.dt
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,15 @@ lifecycle methods.
| <!-- ref:UseExternalStore --> | `TSnapshot` | Bridge subscribe/getSnapshot stores into Reactor and re-render only when the snapshot changes. |
| <!-- ref:UseResource --> | `AsyncValue<T>` | Cached async read (see [Async Resources](async-resources.md)). |
| <!-- ref:UsePersisted --> | `(T, Action<T>)` | `UseState` that survives unmount/remount within the process — in-memory, cleared on exit (see [Persistence](persistence.md)). |
| <!-- ref:UseFilePickerAsync --> / <!-- ref:UseMultipleFilesPickerAsync --> | `Task<StorageFile?>` / `Task<IReadOnlyList<StorageFile>>` | Open one or multiple files (see [Pickers](windows.md#pickers)). |
| <!-- ref:UseFolderPickerAsync --> / <!-- ref:UseSaveFilePickerAsync --> | `Task<StorageFolder?>` / `Task<StorageFile?>` | Select a folder or save-file target (see [Pickers](windows.md#pickers)). |

### Pattern: pick files from a click handler

Picker helpers are the exception to positional hook rules: they do not allocate
state slots and must run from UI-thread event handlers, not during `Render()`.
Cancellation returns `null` for single-file, folder, and save dialogs, or an empty
list for multiple files. See [Pickers](windows.md#pickers) for the full options and
the open/folder/save pattern:
The picker methods (`PickFileAsync`, `PickMultipleFilesAsync`, `PickFolderAsync`,
`PickMultipleFoldersAsync`, `PickSaveFileAsync`) are deliberately **not** hooks: they
reserve no state slot and must run from UI-thread event handlers, not during
`Render()`. Call them directly inside the handler. Cancellation returns `null` for
single-file, folder, and save dialogs, or an empty list for multiple selections. See
[Pickers](windows.md#pickers) for the full options and the open/folder/save pattern:

```csharp snippet="windows/pickers"
```
Expand Down
81 changes: 55 additions & 26 deletions docs/_pipeline/templates/windows.md.dt
Original file line number Diff line number Diff line change
Expand Up @@ -553,40 +553,69 @@ Caveats:

## Pickers

Picker helpers create native `Windows.Storage.Pickers` dialogs and initialize them with the owning
window HWND, so the picker is modal to the correct window without app code doing
HWND interop. Both `Component` and `RenderContext` expose the same operations:
Picker methods create `Microsoft.Windows.Storage.Pickers` dialogs owned by the
component's window, so each dialog is modal to the right window without any
HWND interop. They also work in elevated (administrator) apps, unlike the UWP
`Windows.Storage.Pickers` dialogs. `Component` and `RenderContext` expose the
same operations:

| Helper | Options | Cancellation |
| --- | --- | --- |
| <!-- ref:UseFilePickerAsync --> | `FilePickerOptions` | `null` |
| <!-- ref:UseMultipleFilesPickerAsync --> | `FilePickerOptions` | Empty `IReadOnlyList<StorageFile>` |
| <!-- ref:UseFolderPickerAsync --> | `FolderPickerOptions` | `null` |
| <!-- ref:UseSaveFilePickerAsync --> | `SaveFilePickerOptions` | `null` |

Open and folder options include file-type filters (null or empty means `"*"`),
suggested location, commit-button text, an optional view mode, and a settings
identifier for Windows to remember dialog state. Existing positional constructors
are unchanged; additional options are init properties. Save options require a
non-empty dictionary mapping display labels to extension lists, and support a
suggested file name, location, button text, settings identifier, and existing save
file. Windows validates extension syntax. The save helper only selects/creates
the target; the caller must write its contents.
| Method | Options | Returns | Cancellation |
| --- | --- | --- | --- |
| `PickFileAsync` | `FilePickerOptions` | `PickFileResult?` | `null` |
| `PickMultipleFilesAsync` | `FilePickerOptions` | `IReadOnlyList<PickFileResult>` | Empty list |
| `PickFolderAsync` | `FolderPickerOptions` | `PickFolderResult?` | `null` |
| `PickMultipleFoldersAsync` | `FolderPickerOptions` | `IReadOnlyList<PickFolderResult>` | Empty list |
| `PickSaveFileAsync` | `SaveFilePickerOptions` | `PickFileResult?` | `null` |

Each result exposes the chosen `Path`. Open the file with `System.IO`, or with
`StorageFile.GetFileFromPathAsync` when you need a `StorageFile`.

All three option records share a suggested start location, commit-button text,
a dialog `Title`, a `SettingsIdentifier` that Windows uses to remember dialog state,
`SuggestedFolder` (always open there), and `SuggestedStartFolder` (open there until
Windows remembers a location). Both folder properties take absolute paths. Open
and folder options add an optional `ViewMode`. File options add:

- `FileTypeFilter`: extensions such as `".txt"`. Null or empty shows all files.
- `FileTypeChoices`: an ordered list of `FileTypeChoice(label, extensions)` groups.
When non-empty, it replaces `FileTypeFilter`.
- `InitialFileTypeIndex`: the entry selected first. Null lets Windows choose.

Save options require at least one `FileTypeChoice`. The list order is the order
Windows shows, and the first entry is the default unless `InitialFileTypeIndex`
says otherwise. Labels must be unique and non-blank, and every choice needs at
least one extension. Save options also take a suggested file name,
`DefaultFileExtension`, and `ShowOverwritePrompt` (default `true`). The save
method only selects a target path; the caller must create and write the file.

Both `Windows.Storage.Pickers` and `Microsoft.Windows.Storage.Pickers` define
`PickerLocationId` and `PickerViewMode`, so import only the
`Microsoft.Windows.Storage.Pickers` namespace in files that build picker options.

```csharp snippet="windows/pickers"
```

Caveats:

- Unlike positional state hooks, these helpers open a dialog when called. Invoke
them from UI-thread event handlers, not during `Render()` or in `Task.Run`.
- Calling without an owning Reactor window throws `InvalidOperationException`.
- These methods are not hooks: they reserve no hook slot and open a dialog when
called. Call them from UI-thread event handlers, not during `Render()` or in
`Task.Run`. Their `Pick*` names keep the hook-rules analyzer quiet in handlers.
- The owner is the window whose host rendered the component (including nested and
virtualized-row components), not whichever window is active when the handler
runs. Once the component unmounts (including when its root is replaced) or its
window closes, the call throws `InvalidOperationException`.
- Option validation (missing or duplicate file-type choices) throws `ArgumentException`
before any dialog opens. Windows validates extension syntax and folder paths.
- Picker errors propagate to the caller; cancellation is not an error.
- Reactor never accepts arbitrary HWNDs; it always uses `UseWindow().NativeWindow`.
- Framework tests use an internal picker-service seam instead of opening modal dialogs.
- These helpers retain `Windows.Storage` results; they do not migrate to the
separate `Microsoft.Windows.Storage.Pickers` API or change its OS requirements.
- The native `FolderPicker` supports one folder at a time, not multi-selection.
- Content hosted through `ReactorHostControl` (or a `ReactorHost` you construct
yourself) has no owning `ReactorWindow`, so these methods throw
`InvalidOperationException` there. Construct the native picker from the window
that hosts the control instead, for example
`new Microsoft.Windows.Storage.Pickers.FileOpenPicker(hostWindow.AppWindow.Id)`.
Code that only has an element can use its
`XamlRoot.ContentIslandEnvironment.AppWindowId` (for example from
`UseElementRef<FrameworkElement>().Current?.XamlRoot`).

## WPF / UWP migration map

Expand All @@ -606,7 +635,7 @@ fields that were removed and what replaced them.
| Manual settings persistence | `.WithPersistence(id)` | Opt-in, one line. |
| `TaskbarItemInfo` | `TaskbarItem` | Facade over progress, overlay, description, thumb buttons. |
| WPF `JumpTask` / `JumpList` | `JumpListItem` / `JumpList` | Process-wide, not per-window; `UpdateAsync` replaces the whole list. |
| UWP/WinUI picker HWND setup | Open, multiple-file, folder, and save picker helpers | Owning HWND is wired automatically. |
| UWP/WinUI picker HWND setup | `PickFileAsync`, `PickMultipleFilesAsync`, `PickFolderAsync`, `PickMultipleFoldersAsync`, `PickSaveFileAsync` | Owner window is wired automatically; results are paths. |

## Finding and enumerating windows

Expand Down
44 changes: 22 additions & 22 deletions docs/guide/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,47 +36,47 @@ lifecycle methods.
| [UseExternalStore](reference/hooks/UseExternalStore.md) | `TSnapshot` | Bridge subscribe/getSnapshot stores into Reactor and re-render only when the snapshot changes. |
| [UseResource](reference/hooks/UseResource.md) | `AsyncValue<T>` | Cached async read (see [Async Resources](async-resources.md)). |
| [UsePersisted](reference/hooks/UsePersisted.md) | `(T, Action<T>)` | `UseState` that survives unmount/remount within the process — in-memory, cleared on exit (see [Persistence](persistence.md)). |
| [UseFilePickerAsync](reference/hooks/UseFilePickerAsync.md) / [UseMultipleFilesPickerAsync](reference/hooks/UseMultipleFilesPickerAsync.md) | `Task<StorageFile?>` / `Task<IReadOnlyList<StorageFile>>` | Open one or multiple files (see [Pickers](windows.md#pickers)). |
| [UseFolderPickerAsync](reference/hooks/UseFolderPickerAsync.md) / [UseSaveFilePickerAsync](reference/hooks/UseSaveFilePickerAsync.md) | `Task<StorageFolder?>` / `Task<StorageFile?>` | Select a folder or save-file target (see [Pickers](windows.md#pickers)). |

### Pattern: pick files from a click handler

Picker helpers are the exception to positional hook rules: they do not allocate
state slots and must run from UI-thread event handlers, not during `Render()`.
Cancellation returns `null` for single-file, folder, and save dialogs, or an empty
list for multiple files. See [Pickers](windows.md#pickers) for the full options and
the open/folder/save pattern:
The picker methods (`PickFileAsync`, `PickMultipleFilesAsync`, `PickFolderAsync`,
`PickMultipleFoldersAsync`, `PickSaveFileAsync`) are deliberately **not** hooks: they
reserve no state slot and must run from UI-thread event handlers, not during
`Render()`. Call them directly inside the handler. Cancellation returns `null` for
single-file, folder, and save dialogs, or an empty list for multiple selections. See
[Pickers](windows.md#pickers) for the full options and the open/folder/save pattern:

```csharp
// Capture helpers during Render; invoke them only from UI-thread events.
var pickFile = UseFilePickerAsync;
var pickFiles = UseMultipleFilesPickerAsync;
var pickFolder = UseFolderPickerAsync;
var saveFile = UseSaveFilePickerAsync;

// Pickers are not hooks: call them directly from UI-thread event handlers.
return VStack(
Button("Open one...", async () =>
{
var file = await pickFile(new FilePickerOptions(FileTypeFilter: [".txt", ".md"]));
var file = await PickFileAsync(new FilePickerOptions(FileTypeFilter: [".txt", ".md"]));
if (file is null) return; // User cancelled.
var text = await File.ReadAllTextAsync(file.Path);
}),
Button("Open files...", async () =>
{
var files = await pickFiles(new FilePickerOptions(FileTypeFilter: [".txt", ".md"]));
var files = await PickMultipleFilesAsync(new FilePickerOptions
{
Title = "Choose notes",
FileTypeChoices = [new("Notes", [".txt", ".md"]), new("JSON", [".json"])],
});
if (files.Count == 0) return; // User cancelled.
}),
Button("Select folder...", async () =>
Button("Select folders...", async () =>
{
var folder = await pickFolder(new FolderPickerOptions());
if (folder is null) return;
var folders = await PickMultipleFoldersAsync(new FolderPickerOptions());
if (folders.Count == 0) return;
}),
Button("Save as...", async () =>
{
var file = await saveFile(new SaveFilePickerOptions(
new Dictionary<string, IReadOnlyList<string>> { ["Text"] = [".txt"] },
var target = await PickSaveFileAsync(new SaveFilePickerOptions(
[new("Text", [".txt"]), new("Markdown", [".md"])],
SuggestedFileName: "report"));
if (file is null) return;
// Write the desired content here; selecting a target is not a save.
if (target is null) return;
// Selecting a target is not a save: write the content yourself.
await File.WriteAllTextAsync(target.Path, "report body");
}));
```

Expand Down
20 changes: 0 additions & 20 deletions docs/guide/reference/hooks/UseFilePickerAsync.md

This file was deleted.

20 changes: 0 additions & 20 deletions docs/guide/reference/hooks/UseFolderPickerAsync.md

This file was deleted.

20 changes: 0 additions & 20 deletions docs/guide/reference/hooks/UseMultipleFilesPickerAsync.md

This file was deleted.

20 changes: 0 additions & 20 deletions docs/guide/reference/hooks/UseSaveFilePickerAsync.md

This file was deleted.

Loading
Loading