microsoft / WindowsAppSDK

The Windows App SDK empowers all Windows desktop apps with modern Windows UI, APIs, and platform features, including back-compat support, shipped via NuGet.
https://docs.microsoft.com/windows/apps/windows-app-sdk/
MIT License
3.83k stars 321 forks source link

Simplify using UWP file pickers from desktop apps #1063

Open JaiganeshKumaran opened 3 years ago

JaiganeshKumaran commented 3 years ago

To use UWP file pickers such as file open picker or file save picker in desktop apps, you need to cast the object reference to IInitializeWithWindow and invoke its Initialize method. Although one can use Win32 file pickers, those lack some features like the ability to use picker contract apps. To simplify usage, I would suggest adding an overload for the constructor of FileOpenPicker and FileSavePicker to take a window handle. Since HWND can't be represented with the Windows Runtime type system, it can take an int64_t or the new WindowId structure instead.

angelazhangmsft commented 3 years ago

@Jaiganeshkumaran - as FYI in C#/.NET 5, we have just added support for several COM WinRT interop interfaces. Here are docs for more information: https://docs.microsoft.com/en-us/windows/apps/desktop/modernize/winrt-com-interop-csharp

With these new APIs, you can add the following code in a C#/WinUI3 app which simplifies the code:

var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
var folderPicker = new Windows.Storage.Pickers.FolderPicker();
WinRT.Interop.InitializeWithWindow.Initialize(folderPicker, hwnd);
JaiganeshKumaran commented 3 years ago

@Jaiganeshkumaran - as FYI in C#/.NET 5, we have just added support for several COM WinRT interop interfaces. Here are docs for more information: https://docs.microsoft.com/en-us/windows/apps/desktop/modernize/winrt-com-interop-csharp

With these new APIs, you can add the following code in a C#/WinUI3 app which simplifies the code:

var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
var folderPicker = new Windows.Storage.Pickers.FolderPicker();
WinRT.Interop.InitializeWithWindow.Initialize(folderPicker, hwnd);

Right but that's still a lot of work compared to an UWP app where you just construct a picker and use it. Also these helpers must be rewritten for each supported programming language so I think it's better to have a constructor overload that does the initialization with window automatically.

Scottj1s commented 3 years ago

Agreed that this interop pattern is cumbersome and not discoverable. Ideally, the constructor overload would flow from a metadata attribute, as was done for FastABI support. If a class was decorated with a CanInitializeWithWindowAttribute, a constructor (or factory method) could be generated to accept an HWND source object (i.e., implementing IWindowNative), from which the given class could then call IInitializeWithWindow.Initialize. That would accrue to all language projections, rather just C#/WinRT.

riverar commented 3 years ago

Ideally, you'd just internalize all this goop in the Window class, give it a HWND property, and everything else just falls into place. It's unfortunate how complicated this has all become over what appears to be some misguided attempt to make Window "pure".

Scottj1s commented 3 years ago

As @Jaiganeshkumaran points out, it stems from HWND not being a WinRT type. Such types require a backdoor (COM interop interface) to access. But we haven't done a good job of supporting language projections there, and have effectively dropped WinRT/COM interop support on the floor.

Scottj1s commented 3 years ago

Out API design team is going to investigate making this interop pattern simpler (hopefully nonexistent in most cases). Meantime, I've improved discoverability of the need for the pattern in C#/WinRT. Failure to call IInitializeWithWindow now presents a diagnostic that points to an interop code sample to copy: image

dotMorten commented 3 years ago

@Scottj1s The improved error message is good to see. I'm a big fan of help messages, instead of error messages. I had some comments here: https://github.com/microsoft/microsoft-ui-xaml/issues/4167#issuecomment-889553620 that touched on some of this and used the pickers as an example.

Scottj1s commented 3 years ago

Thanks @dotMorten, and for linking the related issue. Totally agree!

JaiganeshKumaran commented 1 year ago

I think C++/WinRT should do something like this:

template <typename T>
concept WindowLike = requires(T const& t)
{
    // Function resolved with ADL, add GetWindowHandle in your type's namespace. 
    { GetWindowHandle(t) } -> std::convertible_to<HWND>;
};

HWND GetWindowHandle(HWND value)
{
    return value;
}

namespace winrt::Microsoft::UI::Xaml
{
    HWND GetWindowHandle(Window const& window)
    {
        HWND result;
        check_hresult(window.as<IWindowNative>()->get_WindowHandle(&result));
        return result;
    }
}

namespace winrt::Microsoft::UI::Windowing
{
    HWND GetWindowHandle(AppWindow const& appWindow)
    {
        return GetWindowFromWindowId(appWindow.Id());
    }
}

namespace winrt::Windows::Storage::Pickers
{
    FileOpenPicker::FileOpenPicker(WindowLike auto const& windowLike)
    {
        check_hresult(try_as<IInitializeWithWindow>()->Initialize(GetWindowHandle(windowLike)));
    }

    // Repeat the same for other picker types.
}

Since the number of types using IInitializeWithWindow is not large and is documented, can manually add constructor overloads taking any type that satisfies WindowLike, basically needing to have a way to get HWND.