AvaloniaUI / Avalonia

Develop Desktop, Embedded, Mobile and WebAssembly apps with C# and XAML. The most popular .NET UI client technology
https://avaloniaui.net
MIT License
25.71k stars 2.22k forks source link

Allow `IStorageProvider` to be used without an Avalonia window #13242

Open TomEdwardsEnscape opened 1 year ago

TomEdwardsEnscape commented 1 year ago

I am working on a plugin for an existing host application, and I want to open a file select dialog over a native window of the host application. But I cannot create a file select dialog without first creating an Avalonia window. For ownership to work properly, I also have to show that window.

Having looked at the various implementations of IStorageProvider, they each have an IWindowImpl constructor parameter. But the only use of this object is reading the native window handle. I already have a native handle for the window I want to open the file select dialog over, but because there is no associated Avalonia window object, I can't use it.

I'm not sure if a window handle is even required. What if I want to open a file select dialog in response to a click on a system tray icon, for example?

Describe the solution you'd like

I'd like to be able to generate an IStorageProvider with just an IntPtr representing the intended owner window's native handle. Ideally, IntPtr.Zero would be an acceptable argument.

Describe alternatives you've considered

We currently work around the issue like this:

var window = new Window
{
    Width = 0,
    Height = 0,
    Opacity = 0,
    ShowInTaskbar = false,
    SystemDecorations = SystemDecorations.None,
};

SetWindowOwner(window, hostWindowHandle); // platform-specific magic

window.Show();
window.Hide();

IStorageFile? targetFile;
try
{
    targetFile = await window.StorageProvider.SaveFilePickerAsync(...);
}
finally
{
    window.Close();
}

This works, but is silly.

timunie commented 1 year ago

don't know if that's supported for all platforms 🤔 But I understand the idea behind it.

robloo commented 1 year ago

Relates to:

Avalonia needs to be smarter about selecting the default Window just like WPF does.

Separately a lot of services, like IStorageProvider are tied to the top level and the window concepts. Honestly, this probably shouldn't have been done and we should just be able to get services without references the top level or the window. Just the application itself:

maxkatz6 commented 1 year ago

I don't think you need to open a window, unless it's a managed dialog.

A general idea was to ship Avalonia.DesktopExtensions package, where we would add some APIs like global services. Which also should force [SupportedOn(Windows | macOS | Linux)] .NET attributes.

Avalonia needs to be smarter about selecting the default Window just like WPF does.

WPF is not smarter here really, it does basically the same what @TomEdwardsEnscape did. It always sets a parent on the file dialogs. But if a developer hasn't specify a parent, it tries to use a global active window, or if there is non - it uses pre-created window handle - Application.cs#L1909-L1922 called from CommonDialog.cs#L91. And even then, if this shared fake window wasn't created for some reason - it will create a new window later.

To continue that, WPF does the same for system parameters as well - by creating a fake window and fetching parameters from it, like DPI settings.

robloo commented 1 year ago

It always sets a parent on the file dialogs. But if a developer hasn't specify a parent, it tries to use a global active window, or if there is non - it uses pre-created window handle - Application.cs#L1909-L1922 called from CommonDialog.cs#L91. And even then, if this shared fake window wasn't created for some reason - it will create a new window later.

That is much smarter logic ;) It allows the parent to be optional and then deduces what needs to happen behind the scenes if no parent is provided.

That said I don't think this needs to be desktop specific at all. The TopLevel is registered in mobile applications too. Avalonia can deduce what to use automatically.