Skip to content

Add share sheet support - #275

Open
AkazaRenn wants to merge 21 commits into
dotMorten:mainfrom
AkazaRenn:share
Open

Add share sheet support#275
AkazaRenn wants to merge 21 commits into
dotMorten:mainfrom
AkazaRenn:share

Conversation

@AkazaRenn

@AkazaRenn AkazaRenn commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Related Issue (required)

Please link an existing issue for this PR.

If no issue exists yet, create one first before requesting review.

Closes #274

PR Type

  • Bug fix
  • Feature / Enhancement
  • Refactor / Internal
  • Documentation

Summary

  • Support share sheet (requires initialization first)

Validation

  • Tests added/updated where practical
  • Local build/tests/smoke checks run
  • Docs updated (if needed)

Notes

  • I don't have a strong knowledge about the repo, please let me know if you think any tests/docs should be updated.

@dotMorten dotMorten left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing work! This is a really cool addition, but see notes on where I think this should be put instead.

Comment thread src/WinUIEx/Share.cs Outdated
Comment thread src/WinUIEx/Share.cs Outdated

// IID of DataTransferManager, passed as the riid to GetForWindow:
Guid dtmIid = new(0xa5caee9b, 0x8708, 0x49d1, 0x8d, 0x36, 0x67, 0xd2, 0x5a, 0x8d, 0xa0, 0x0c);
_dtm = WinRT.MarshalInterface<DataTransferManager>.FromAbi(interop.GetForWindow(hWnd, dtmIid));

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be done on-the-fly in the extension method and cleaned up after instead?

@AkazaRenn AkazaRenn Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After ab23884, it is done on-the-fly, please see #275 (comment).

Comment thread src/WinUIExSample/Pages/Dialogs.xaml.cs Outdated
Comment thread src/WinUIEx/WindowExtensions.cs Outdated
}

dtm.DataRequested += handler;
window.ShowShareUI();

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could there be a case here where the dtm/handler goes out of scope and gets garbage collected before the event fires?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that won't happen because we are marshalling a COM object, and it is stable per window. Also, please correct me if I'm wrong, since we are keeping a reference of dtm in handler, it should not be recycled before the handler runs?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. Is there a case where the handler won't run, and thus the handler is now leaking? Or could multiple calls to this method in parallel cause the handler to trigger multiple times?

@AkazaRenn AkazaRenn Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For leaking, I think it has a risk of happening, though I'm not certain whether or how it can be triggered. The COM wrapper dtm and handlsr will be referencing each other until the event is fired for once. However, once the event is finally fired, the unregistration happens and they both get recycled. So the damage should be minimal.

As for multiple calls, I think it's safe, since each handler is designed to run only once and unregister itself.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually we could probably just remove this helper function if you feel really worried. Don't feel like it is something that must be in this project.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would the datarequested call just not happen if the user cancels the dialog? There's also the chance that window.ShowShareUI throws and again would cause it to never unhook. I could imagine that happens with a stale/closed window instance.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it happens fast enough maybe? As for windows closed while there are uninvoked handlers, yeah if that happens it would be a leak without any way to recover... Might just should remove it in this case?

Comment thread src/WinUIEx/HwndExtensions.cs
@dotMorten

Copy link
Copy Markdown
Owner

/api-diff

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

API changes

namespace WinUIEx
{
    public static class HwndExtensions
    {
        public DataTransferManager GetDataTransferManagerForWindow(nint hwnd);
        public ShowShareUIForWindow(nint hwnd);
    }
    public static class WindowExtensions
    {
        public DataTransferManager GetDataTransferManager(Window window);
        public Share(Window window, DataPackage data);
        public ShowShareUI(Window window);
    }
}

Generated with .NET Object Model Diagram Generator

@dotMorten

dotMorten commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Looking at the public APIs added, I get the need for public Share(Window window, DataPackage data);, since it has a datapackage. But what is the point of the other share calls that doesn't take a datapackage? What would you then be sharing?

And should the Share method also be on HwndExtensions?

@AkazaRenn

AkazaRenn commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

Looking at the public APIs added, I get the need for public Share(Window window, DataPackage data);, since it has a datapackage. But what is the point of the other share calls that doesn't take a datapackage? What would you then be sharing?

And should the Share method also be on HwndExtensions?

Actually the other two APIs are the one provided by Microsoft, public Share(Window window, DataPackage data); is created by me just to make things look nice.

DataTransferManager GetDataTransferManager(Window window); like said, returns the COM object DataTransferManager for ShowShareUI(Window window);

ShowShareUI(Window window); tells the system to show the share sheet for this window, but it does not provide what to be shared. The data to be shared will be later set in the event handler of dtm.DataRequested.

It is actually how Microsoft expects us to use the API: https://learn.microsoft.com/en-us/windows/apps/develop/windows-integration/integrate-sharesheet-send#implement-share-for-desktop-apps-winui-3-wpf-winforms

private void OnDataRequested(DataRequestedEventArgs args)
{
    DataRequest request = args.Request;
    DataPackage data = request.Data;

    data.Properties.Title = "Share from my desktop app";
    data.SetText("Shared content");

    // For URLs:
    // data.SetWebLink(new Uri("https://example.com"));

    // For files:
    // var item = await StorageFile.GetFileFromPathAsync(filePath);
    // data.SetStorageItems(new[] { item });
}

// In your Share button handler:
private void ShareButton_Click()
{
    var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
    var interop = DataTransferManager.As<IDataTransferManagerInterop>();
    interop.ShowShareUIForWindow(hWnd);
}

I'm adding void Share(Window window, DataPackage data); just to make the process look nicer, like you can forget about the event handling and just bring up the share sheet with the data in one call.

@dotMorten

Copy link
Copy Markdown
Owner

Thank you for the quick follow ups. I'm gonna do a little more research on this one and fully understand the share contracts. The thing that has me slightly concerned with the helper method, is the up-front creation of data that might not ever be requested if the user cancels the share UI. In the example it's a cheap call, but that could also be very heavy and large data objects, which leads me to think this should probably be more of a delegate parameter to return the data on-demand.
It also seems to me there's quite a lot more advanced scenarios you can do that this wouldn't cover and you'd have to use the manager instead. Of course I realize this simple share method covers common cases and is convenient, but I don't really like that once it gets more advanced, it isn't just an extra parameter, but a completely different set of APIs and flow to your code.

The obvious solution is to not provide the helper, and just expose the manager, but then there really isn't much left, and I do like how simple the Share method is. So bottom line, let me think about it some more and play with it a little, and see if I can't come up with something that satisfies everything and gets the best of both worlds.

I definitely like what you're adding here and appreciate all the time you put into it. I want to try and move this forward.

@AkazaRenn

Copy link
Copy Markdown
Contributor Author

Thank you for the quick follow ups. I'm gonna do a little more research on this one and fully understand the share contracts. The thing that has me slightly concerned with the helper method, is the up-front creation of data that might not ever be requested if the user cancels the share UI. In the example it's a cheap call, but that could also be very heavy and large data objects, which leads me to think this should probably be more of a delegate parameter to return the data on-demand. It also seems to me there's quite a lot more advanced scenarios you can do that this wouldn't cover and you'd have to use the manager instead. Of course I realize this simple share method covers common cases and is convenient, but I don't really like that once it gets more advanced, it isn't just an extra parameter, but a completely different set of APIs and flow to your code.

The obvious solution is to not provide the helper, and just expose the manager, but then there really isn't much left, and I do like how simple the Share method is. So bottom line, let me think about it some more and play with it a little, and see if I can't come up with something that satisfies everything and gets the best of both worlds.

I definitely like what you're adding here and appreciate all the time you put into it. I want to try and move this forward.

No worries at all!

@dotMorten

dotMorten commented Aug 1, 2026

Copy link
Copy Markdown
Owner

A well-designed one-shot Share API could cover normal and advanced usage without needing to jump to GetDataTransferManager. Perhaps something like:

void Share(this Window window, DataPackage data);
void Share(this Window window, Action<DataRequestedEventArgs> onDataRequested);
void Share(this Window window, Func<DataRequest, Task> onDataRequestedAsync);

The callback overload should receive  DataRequestedEventArgs (or at least DataRequest), not return a  DataPackage : it runs when the OS requests data, supports every normal format through args.Request.Data, and callers can use GetDeferral() for async work, or we do the last example with an async overload that can acquire and complete that deferral internally. That's probably cleaner than the second overload.

window.Share(async request =>
{
    var file = await StorageFile.GetFileFromPathAsync(filePath);
    request.Data.Properties.Title = "Monthly report";
    request.Data.SetStorageItems([file]);
});

@dotMorten dotMorten self-assigned this Aug 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support share sheet

2 participants