16. Files, storage, drag/drop, and clipboard

Goal

Why this matters

Prerequisites

1. Storage provider fundamentals

All pickers live on TopLevel.StorageProvider (Window, control, etc.). The storage provider is an abstraction over native dialogs and sandbox rules.

var topLevel = TopLevel.GetTopLevel(control);
if (topLevel?.StorageProvider is { } storage)
{
    // storage.OpenFilePickerAsync(...)
}

If StorageProvider is null, ensure the control is attached (e.g., call after Loaded/Opened).

IStorageProvider exposes capability flags such as CanOpen, CanSave, and CanPickFolder. Check them before presenting commands so sandboxed targets (browser/mobile) can hide unsupported options. Dialog methods accept option records (FilePickerOpenOptions, FolderPickerOpenOptions, etc.) that describe filters, suggested locations, and tokens for continuing previous sessions.

1.1 Service abstraction for MVVM

public interface IFileDialogService
{
    Task<IReadOnlyList<IStorageFile>> OpenFilesAsync(FilePickerOpenOptions options);
    Task<IStorageFile?> SaveFileAsync(FilePickerSaveOptions options);
    Task<IStorageFolder?> PickFolderAsync(FolderPickerOpenOptions options);
}

public sealed class FileDialogService : IFileDialogService
{
    private readonly TopLevel _topLevel;
    public FileDialogService(TopLevel topLevel) => _topLevel = topLevel;

    public Task<IReadOnlyList<IStorageFile>> OpenFilesAsync(FilePickerOpenOptions options)
        => _topLevel.StorageProvider?.OpenFilePickerAsync(options) ?? Task.FromResult<IReadOnlyList<IStorageFile>>(Array.Empty<IStorageFile>());

    public Task<IStorageFile?> SaveFileAsync(FilePickerSaveOptions options)
        => _topLevel.StorageProvider?.SaveFilePickerAsync(options) ?? Task.FromResult<IStorageFile?>(null);

    public async Task<IStorageFolder?> PickFolderAsync(FolderPickerOpenOptions options)
    {
        if (_topLevel.StorageProvider is null)
            return null;
        var folders = await _topLevel.StorageProvider.OpenFolderPickerAsync(options);
        return folders.FirstOrDefault();
    }
}

Register the service per window (in DI) so view models request dialogs via IFileDialogService without touching UI types.

1.2 Launching files and URIs

TopLevel.Launcher gives access to ILauncher, which opens files, folders, or URIs using the platform shell (Finder, Explorer, default browser, etc.). Combine it with storage results to let users reveal files after saving.

var topLevel = TopLevel.GetTopLevel(control);
if (topLevel?.Launcher is { } launcher && file is not null)
{
    await launcher.LaunchFileAsync(file);
    await launcher.LaunchUriAsync(new Uri("https://docs.avaloniaui.net"));
}

Return values indicate whether the launch succeeded; fall back to in-app viewers when it returns false.

2. Opening files (async streams)

public async Task<string?> ReadTextFileAsync(IStorageFile file, CancellationToken ct)
{
    await using var stream = await file.OpenReadAsync();
    using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
    return await reader.ReadToEndAsync(ct);
}

2.1 Remote or sandboxed locations

On Android/iOS/Browser the returned stream might be virtual (no direct file path). Always rely on stream APIs; avoid LocalPath if Path is null.

2.2 File type filters

var options = new FilePickerOpenOptions
{
    Title = "Open images",
    AllowMultiple = true,
    SuggestedStartLocation = await storage.TryGetWellKnownFolderAsync(WellKnownFolder.Pictures),
    FileTypeFilter = new[]
    {
        new FilePickerFileType("Images")
        {
            Patterns = new[] { "*.png", "*.jpg", "*.jpeg", "*.webp", "*.gif" }
        }
    }
};

TryGetWellKnownFolderAsync returns common directories when supported (desktop/mobile). Source: WellKnownFolder.cs.

3. Saving files

var saveOptions = new FilePickerSaveOptions
{
    Title = "Export report",
    SuggestedFileName = $"report-{DateTime.UtcNow:yyyyMMdd}.csv",
    DefaultExtension = "csv",
    FileTypeChoices = new[]
    {
        new FilePickerFileType("CSV") { Patterns = new[] { "*.csv" } },
        new FilePickerFileType("All files") { Patterns = new[] { "*" } }
    }
};

var file = await _dialogService.SaveFileAsync(saveOptions);
if (file is not null)
{
    await using var stream = await file.OpenWriteAsync();
    await using var writer = new StreamWriter(stream, Encoding.UTF8, leaveOpen: false);
    await writer.WriteLineAsync("Id,Name,Email");
    foreach (var row in rows)
        await writer.WriteLineAsync($"{row.Id},{row.Name},{row.Email}");
}

4. Enumerating folders

var folder = await storage.TryGetFolderFromPathAsync(new Uri("file:///C:/Logs"));
if (folder is not null)
{
    await foreach (var item in folder.GetItemsAsync())
    {
        switch (item)
        {
            case IStorageFile file:
                // Process file
                break;
            case IStorageFolder subfolder:
                // Recurse or display
                break;
        }
    }
}

GetItemsAsync() returns an async sequence; iterate with await foreach on .NET 7+. Use GetFilesAsync/GetFoldersAsync to filter.

5. Bookmarks and persisted access

Some platforms revoke file permissions when your app suspends. If an IStorageItem reports CanBookmark, call SaveBookmarkAsync() and store the returned string (e.g., in preferences). Later, reopen it via IStorageProvider.OpenFileBookmarkAsync/OpenFolderBookmarkAsync.

var bookmarks = new Dictionary<string, string>();

if (file.CanBookmark)
{
    var bookmarkId = await file.SaveBookmarkAsync();
    if (!string.IsNullOrEmpty(bookmarkId))
        bookmarks[file.Path.ToString()] = bookmarkId;
}

var restored = await storage.OpenFileBookmarkAsync(bookmarkId);

Keep bookmarks updated when users revoke access. iOS and Android can throw when bookmarks expire—wrap calls in try/catch and ask users to reselect the folder. Desktop platforms typically return standard file paths, but bookmarks still help retain portal-granted access (e.g., Flatpak).

IStorageItem.GetBasicPropertiesAsync() exposes metadata (size, modified time) without opening streams—use it when building file browsers.

6. Platform notes

Platform Storage provider Considerations
Windows/macOS/Linux Native dialogs; file system access Standard read/write. Some Linux desktops require portals (Flatpak/Snap).
Android/iOS Native pickers; sandboxed URIs Streams may be content URIs; persist permissions if needed.
Browser (WASM) File System Access API Requires user gestures; may return handles that expire when page reloads.

Wrap storage calls in try/catch to handle permission denials or canceled dialogs gracefully.

7. Drag-and-drop: receiving data

<Border AllowDrop="True"
        DragOver="OnDragOver"
        Drop="OnDrop"
        Background="#111827" Padding="12">
  <TextBlock Text="Drop files or text" Foreground="#CBD5F5"/>
</Border>
private void OnDragOver(object? sender, DragEventArgs e)
{
    if (e.Data.Contains(DataFormats.Files) || e.Data.Contains(DataFormats.Text))
        e.DragEffects = DragDropEffects.Copy;
    else
        e.DragEffects = DragDropEffects.None;
}

private async void OnDrop(object? sender, DragEventArgs e)
{
    var files = await e.Data.GetFilesAsync();
    if (files is not null)
    {
        foreach (var item in files.OfType<IStorageFile>())
        {
            await using var stream = await item.OpenReadAsync();
            // import
        }
        return;
    }

    if (e.Data.Contains(DataFormats.Text))
    {
        var text = await e.Data.GetTextAsync();
        // handle text
    }
}

7.1 Initiating drag-and-drop

private async void DragSource_PointerPressed(object? sender, PointerPressedEventArgs e)
{
    if (sender is not Control control)
        return;

    var data = new DataObject();
    data.Set(DataFormats.Text, "Example text");

    var effects = await DragDrop.DoDragDrop(e, data, DragDropEffects.Copy | DragDropEffects.Move);
    if (effects.HasFlag(DragDropEffects.Move))
    {
        // remove item
    }
}

DataObject supports multiple formats (text, files, custom types). For custom data, both source and target must agree on a format string.

7.2 Custom visuals and adorners

Wrap your layout in an AdornerDecorator and render drop cues while a drag is in progress. Toggle overlays in DragEnter/DragLeave handlers to show hit targets or counts.

private void OnDragEnter(object? sender, DragEventArgs e)
{
    _dropOverlay.IsVisible = true;
}

private void OnDragLeave(object? sender, RoutedEventArgs e)
{
    _dropOverlay.IsVisible = false;
}

You can also inspect e.DragEffects to switch icons (copy vs move) or reject unsupported formats with a custom message. For complex scenarios create a lightweight Window as a drag adorner so the pointer stays responsive on multi-monitor setups.

8. Clipboard operations

public interface IClipboardService
{
    Task SetTextAsync(string text);
    Task<string?> GetTextAsync();
    Task SetDataObjectAsync(IDataObject dataObject);
    Task<IReadOnlyList<string>> GetFormatsAsync();
}

public sealed class ClipboardService : IClipboardService
{
    private readonly TopLevel _topLevel;
    public ClipboardService(TopLevel topLevel) => _topLevel = topLevel;

    public Task SetTextAsync(string text) => _topLevel.Clipboard?.SetTextAsync(text) ?? Task.CompletedTask;
    public Task<string?> GetTextAsync() => _topLevel.Clipboard?.GetTextAsync() ?? Task.FromResult<string?>(null);
    public Task SetDataObjectAsync(IDataObject dataObject) => _topLevel.Clipboard?.SetDataObjectAsync(dataObject) ?? Task.CompletedTask;
    public Task<IReadOnlyList<string>> GetFormatsAsync() => _topLevel.Clipboard?.GetFormatsAsync() ?? Task.FromResult<IReadOnlyList<string>>(Array.Empty<string>());
}

8.1 Multi-format clipboard payload

var dataObject = new DataObject();
dataObject.Set(DataFormats.Text, "Plain text");
dataObject.Set("text/html", "<strong>Bold</strong>");
dataObject.Set("application/x-myapp-item", myItemId);

await clipboardService.SetDataObjectAsync(dataObject);
var formats = await clipboardService.GetFormatsAsync();

Browser restrictions: clipboard APIs require user gesture and may only allow text formats.

9. Error handling & async patterns

var progress = new Progress<int>(value => ImportProgress = value);
await _importService.ImportAsync(file, progress, cancellationToken);

10. Diagnostics

11. Practice exercises

  1. Implement IFileDialogService and expose commands for Open, Save, and Pick Folder; update the UI with results.
  2. Build a file manager pane that enumerates folders asynchronously, persists bookmarks for sandboxed platforms, and mirrors changes via drag/drop.
  3. Create a clipboard history panel that stores the last N text snippets using the IClipboard service.
  4. Add drag support from a list to the OS shell (export files) with a custom adorner overlay showing the item count.
  5. Implement cancellation for long-running file imports and confirm resources are disposed when canceled.

Look under the hood (source bookmarks)

Check yourself

What's next