Search code examples
c#c++winui-3xaml-islands

Can native Win32 exe show a WinUI3 Page?


I need to show a WinUI3 page from a native Win32 exe. I cannot make modifications to the Win32 exe. The architecture is like this:

  1. native Win32 .exe -> links to a C++/CLI dll with a known native C++ interface
  2. C++/CLI dll -> references and makes calls into a C# .NET8 Class Library
  3. C# .NET8 Class Library -> contains WinUI3 pages & controls.

(We also use the C# .NET8 class library's pages from a different WinUI3 exe as well)

(Also, we cannot change the native win32 exe. It is not under our control)

We've had this architecture for generations of MS technologies and always found a way to make it work with WinForms and then with WPF. But now we have WinUI3...

I'm not finding any examples other than https://www.codeproject.com/Articles/5359576/WinUI3-in-Existing-Win32-Applications and when I clone the example repo, it does not work (perhaps out of date due to the various betas that WinUI went through?)

Can this be achieved? If so, any advice on examples or documentation of the relevant calls that need to be made to set this up?

Thanks!


Solution

  • Here is sample code for a regular Win32 desktop application which knows nothing about WinUI3 (note: WinAppSDK 1.6 needs to be installed for the sample to work but if it's not, you'll be automatically pointed to it). It's main message loop is completely standard:

    Win32 app

    Click on "Show WinUI3 Window menu item" to add WinUI3 content to it:

    Win32 app hosting WinUI3

    This uses the "Xaml Islands" technology, through the DesktopWindowXamlSource class. You can refer to official documentation here Host WinRT XAML controls in desktop apps (XAML Islands). It's talking about UWP, but the general principles are the same.

    Click on it and a WinUI3 ContentDialog is shown:

    Win32 app hosting ContentDialog

    Full code is available at https://github.com/smourier/Win32ShowsWinUI3

    Here's the C++ code portion to host .NET Core directly from C++, inspired from the official .NET Hosting Sample:

    hostfxr_initialize_for_dotnet_command_line_fn init_for_cmd_line_fptr = nullptr;
    hostfxr_initialize_for_runtime_config_fn init_for_config_fptr = nullptr;
    hostfxr_get_runtime_delegate_fn get_delegate_fptr = nullptr;
    hostfxr_run_app_fn run_app_fptr = nullptr;
    hostfxr_close_fn close_fptr = nullptr;
    component_entry_point_fn _showWindowFn = nullptr;
    
    bool load_hostfxr()
    {
      char_t buffer[2048];
      auto buffer_size = sizeof(buffer) / sizeof(char_t);
      auto rc = get_hostfxr_path(buffer, &buffer_size, nullptr);
      if (rc != 0)
        return false;
    
      auto lib = LoadLibrary(buffer);
      if (!lib)
        return false;
    
      init_for_config_fptr = (hostfxr_initialize_for_runtime_config_fn)GetProcAddress(lib, "hostfxr_initialize_for_runtime_config");
      get_delegate_fptr = (hostfxr_get_runtime_delegate_fn)GetProcAddress(lib, "hostfxr_get_runtime_delegate");
      close_fptr = (hostfxr_close_fn)GetProcAddress(lib, "hostfxr_close");
    
      return (init_for_config_fptr && get_delegate_fptr && close_fptr);
    }
    
    load_assembly_and_get_function_pointer_fn get_dotnet_load_assembly(const char_t* config_path)
    {
      void* load_assembly_and_get_function_pointer = nullptr;
      hostfxr_handle cxt = nullptr;
      auto rc = init_for_config_fptr(config_path, nullptr, &cxt);
      if (rc != 0 || !cxt)
      {
        close_fptr(cxt);
        return nullptr;
      }
    
      rc = get_delegate_fptr(cxt, hdt_load_assembly_and_get_function_pointer, &load_assembly_and_get_function_pointer);
      close_fptr(cxt);
      return (load_assembly_and_get_function_pointer_fn)load_assembly_and_get_function_pointer;
    }
    
    int ShowWinUI3Window(HWND hwnd)
    {
      if (!_showWindowFn)
      {
        auto loaded = load_hostfxr();
        if (!loaded)
        {
          MessageBox(nullptr, L"Error: cannot load .NET Core.", L"Win32ShowsWinUI3", 0);
          return 0;
        }
    
        auto filePath = wil::GetModuleFileNameW();
        PathCchRemoveFileSpec(filePath.get(), lstrlen(filePath.get()));
    
        // load the WinUI3ClassLibrary runtime config 
        // note the .NET library must declare <EnableDynamicLoading>true</EnableDynamicLoading> to generate such a file
        wil::unique_cotaskmem_string rtPath;
        PathAllocCombine(filePath.get(), L"WinUI3ClassLibrary.runtimeconfig.json", 0, &rtPath);
    
        auto load_assembly_and_get_function_pointer = get_dotnet_load_assembly(rtPath.get());
        if (!load_assembly_and_get_function_pointer)
        {
          MessageBox(nullptr, L"Error: cannot load 'WinUI3ClassLibrary' assembly", L"Win32ShowsWinUI3", 0);
          return 0;
        }
    
        // load the WinUI3ClassLibrary.dll
        wil::unique_cotaskmem_string dllPath;
        PathAllocCombine(filePath.get(), L"WinUI3ClassLibrary.dll", 0, &dllPath);
    
        auto hr = load_assembly_and_get_function_pointer(
          dllPath.get(),
          L"WinUI3ClassLibrary.SampleWindow, WinUI3ClassLibrary",
          L"ShowWindow",
          nullptr,
          nullptr,
          (void**)&_showWindowFn);
    
        if (!_showWindowFn)
        {
          MessageBox(nullptr, L"Error: cannot load 'ShowWindow' function.", L"Win32ShowsWinUI3", 0);
          return 0;
        }
      }
    
      return _showWindowFn((void*)hwnd, sizeof(void*));
    }
    

    Here's the code in .NET library that enables WinUI3 in the hosting application (this library has been created from the standard WinUI3 class library), using the Bootstrap Class to initialize the WinAppSDK:

    private static DummyApp? _app;
    
    // this is called by the Win32 app (see hosting.cpp)
    // in this sample, args is the parent HWND, sizeBytes is unused
    public static int ShowWindow(nint args, int sizeBytes)
    {
        // ask for WinAppSDK 1.6
        if (!Bootstrap.TryInitialize(0x00010006, string.Empty, new PackageVersion(), Bootstrap.InitializeOptions.OnNoMatch_ShowUI, out var hr))
            return hr;
    
        if (_app == null)
        {
            // comment this line if you don't want WinUI3 styles
            _app = new DummyApp();
            DispatcherQueueController.CreateOnCurrentThread();
        }
    
        var _source = new DesktopWindowXamlSource();
        _source.Initialize(Win32Interop.GetWindowIdFromWindow(args));
    
        var button = new Button()
        {
            HorizontalAlignment = HorizontalAlignment.Center,
            Content = "Click me!",
        };
    
        var grid = new Grid() { Background = new SolidColorBrush(Colors.LightBlue) };
        grid.Children.Add(button);
    
        button.Click += async (s, e) =>
        {
            var contentDialog = new ContentDialog()
            {
                XamlRoot = grid.XamlRoot,
                Title = "Information",
                Content = "Hello from WinUI 3!",
                CloseButtonText = "OK"
            };
            await contentDialog.ShowAsync();
            _source.Dispose();
        };
    
        _source.Content = grid;
        return 0;
    }
    
    // this is needed for proper XAML support with WinUI3 styles
    private sealed class DummyApp : Application, IXamlMetadataProvider
    {
        private readonly XamlControlsXamlMetaDataProvider provider = new();
        private readonly IXamlMetadataProvider _myLibProvider;
    
        public DummyApp()
        {
            // find the generated IXamlMetadataProvider for this lib
            var type = GetType().Assembly.GetTypes().First(t => typeof(IXamlMetadataProvider).IsAssignableFrom(t) && t.GetCustomAttribute<GeneratedCodeAttribute>() != null);
            _myLibProvider = (IXamlMetadataProvider)Activator.CreateInstance(type)!;
        }
    
        public IXamlType GetXamlType(Type type)
        {
            var ret = provider.GetXamlType(type);
            ret ??= _myLibProvider.GetXamlType(type);
            return ret;
        }
    
        public IXamlType GetXamlType(string fullName)
        {
            var ret = provider.GetXamlType(fullName);
            ret ??= _myLibProvider.GetXamlType(fullName);
            return ret;
        }
    
        public XmlnsDefinition[] GetXmlnsDefinitions()
        {
            var ret = provider.GetXmlnsDefinitions();
            ret ??= _myLibProvider.GetXmlnsDefinitions();
            return ret;
        }
    
        protected override void OnLaunched(LaunchActivatedEventArgs args)
        {
            Resources.MergedDictionaries.Add(new XamlControlsResources());
            base.OnLaunched(args);
        }
    }
    

    Note: you can also use WinUI3 "full" windows, but you can't run them w/o Application.Start, something like this just after Bootstrap.TryInitialize:

    Application.Start(p =>
    {
        var window = new SampleWindow();
        window.Activate();
    });
    

    However, there are issues when these windows are closed to reopen them (not sure if they are not bugs... but we're not really in a WinUI3 "app"). DesktopWindowXamlSource is really the sanctified way.

    Note if you don't use the DummyApp class, the WinUI3 style will use the hosting Win32 style:

    Win32 app hosting WinUI3 unstyles