Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: Add Application.OnUnload or OnExit #9165

Open
Al-Caron opened this issue Dec 13, 2023 · 4 comments
Open

Proposal: Add Application.OnUnload or OnExit #9165

Al-Caron opened this issue Dec 13, 2023 · 4 comments
Labels
feature proposal New feature proposal team-Reach Issue for the Reach team

Comments

@Al-Caron
Copy link

Al-Caron commented Dec 13, 2023

Proposal: Add virtual method for application exit in WINUI (C#)

Summary

Be able to hook on application exit just like the existing OnLaunched

Rationale

I'm using an existing code "C" code base using Interop (P/Invoke) calls from our existing system. I need to initialise the "library" on launch, but also need to "shutdown" that library properly when the application exits.
Right now, we're stuck to unload the library using the window close operation. But since we have multiple top level window, we need some weird code to make sure we perform the exit procedure on the "last window to close".

Would be nice to simply do that using an OnExit override on the Application class.

Note

Using WinUI in unpackaged C# application.

@Al-Caron Al-Caron added the feature proposal New feature proposal label Dec 13, 2023
@microsoft-github-policy-service microsoft-github-policy-service bot added the needs-triage Issue needs to be triaged by the area owners label Dec 13, 2023
@DarranRowe
Copy link

DarranRowe commented Dec 13, 2023

There is something that you are able to do now. This simulates the behaviour that you want. But it is okay to treat this as a work around. It requires overriding the Xaml generated main, but it is pretty quick and easy to do. I'll give instructions for both C++ and C# here.

First, override wWinMain/Main. You can find the generated version in a generated file named App.xaml.g.hpp or App.g.i.cs.

Screenshot 2023-12-13 230920

Screenshot 2023-12-13 230932

You can find the entry point function here surrounded by the DISABLE_XAML_GENERATED_MAIN preprocessor directive.

//C++
#ifndef DISABLE_XAML_GENERATED_MAIN
int __stdcall wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
{
    {
        void (WINAPI *pfnXamlCheckProcessRequirements)();
        auto module = ::LoadLibrary(L"Microsoft.ui.xaml.dll");
        if (module)
        {
            pfnXamlCheckProcessRequirements = reinterpret_cast<decltype(pfnXamlCheckProcessRequirements)>(GetProcAddress(module, "XamlCheckProcessRequirements"));
            if (pfnXamlCheckProcessRequirements)
            {
                (*pfnXamlCheckProcessRequirements)();
            }

            ::FreeLibrary(module);
        }
    }

    winrt::init_apartment(winrt::apartment_type::single_threaded);
    ::winrt::Microsoft::UI::Xaml::Application::Start(
        [](auto&&)
        {
            ::winrt::make<::winrt::Meh::implementation::App>();
        });

    return 0;
}
#endif
//C#
namespace CsMeh
{
#if !DISABLE_XAML_GENERATED_MAIN
    /// <summary>
    /// Program class
    /// </summary>
    public static class Program
    {
        [global::System.Runtime.InteropServices.DllImport("Microsoft.ui.xaml.dll")]
        private static extern void XamlCheckProcessRequirements();

        [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.UI.Xaml.Markup.Compiler"," 3.0.0.2309")]
        [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
        [global::System.STAThreadAttribute]
        static void Main(string[] args)
        {
            XamlCheckProcessRequirements();
            
            global::WinRT.ComWrappersSupport.InitializeComWrappers();
            global::Microsoft.UI.Xaml.Application.Start((p) => {
                var context = new global::Microsoft.UI.Dispatching.DispatcherQueueSynchronizationContext(global::Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread());
                global::System.Threading.SynchronizationContext.SetSynchronizationContext(context);
                new App();
            });
        }
    }
#endif
//Rest of the generated code
}

Not only does this allow you to disable the generated entry point, it also gives you a template to work with. Add DISABLE_XAML_GENERATED_MAIN as a C++ preprocessor definition or a C# conditional compilation symbol.
Then add a new source file and add your new main function.

//main.cpp
#include "pch.h"
#include "App.xaml.h"

struct winrt_apartment
{
	winrt_apartment()
	{
		winrt::init_apartment(winrt::apartment_type::single_threaded);
	}
	~winrt_apartment()
	{
		winrt::uninit_apartment();
	}
};

static winrt_apartment s_apartment_init;

void check_process_requirements()
{
	void (WINAPI * pfnXamlCheckProcessRequirements)();
	auto module = LoadLibraryW(L"Microsoft.ui.xaml.dll");
	if (module)
	{
		pfnXamlCheckProcessRequirements = reinterpret_cast<decltype(pfnXamlCheckProcessRequirements)>(GetProcAddress(module, "XamlCheckProcessRequirements"));
		if (pfnXamlCheckProcessRequirements)
		{
			(*pfnXamlCheckProcessRequirements)();
		}

		FreeLibrary(module);
	}
}

int APIENTRY wWinMain(_In_ HINSTANCE, _In_opt_ HINSTANCE, _In_ LPWSTR, _In_ int)
{
	check_process_requirements();
	winrt::Microsoft::UI::Xaml::Application::Start(
		[](auto &&)
		{
			winrt::make<winrt::Meh::implementation::App>();
		});

	return 0;
}
//Main.cs
using System;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using WinRT;

namespace CsMeh
{
    public static class Program
    {
        [DllImport("Microsoft.ui.xaml.dll")]
        private static extern void XamlCheckProcessRequirements();

        [STAThread]
        static void Main(string[] args)
        {
            XamlCheckProcessRequirements();

            ComWrappersSupport.InitializeComWrappers();
            Application.Start((p) => {
                var context = new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread());
                SynchronizationContext.SetSynchronizationContext(context);
                new App();
            });
        }
    }
}

I tidied the code up a little. You can build and run the application to make sure that it works.

Second. Add some cleanup functionality to App.

//App.xaml.h
namespace winrt::Meh::implementation
{
	struct App : AppT<App>
	{
		App();

		void OnLaunched(Microsoft::UI::Xaml::LaunchActivatedEventArgs const&);

		winrt::Microsoft::UI::Xaml::Window Window() const;

		void Cleanup();

	private:
		winrt::Microsoft::UI::Xaml::Window window{ nullptr };
	};
}
//App.xaml.cs
namespace CsMeh
{
    /// <summary>
    /// Provides application-specific behavior to supplement the default Application class.
    /// </summary>
    public partial class App : Application
    {
        /// <summary>
        /// Initializes the singleton application object.  This is the first line of authored code
        /// executed, and as such is the logical equivalent of main() or WinMain().
        /// </summary>
        public App()
        {
            this.InitializeComponent();
        }

        /// <summary>
        /// Invoked when the application is launched.
        /// </summary>
        /// <param name="args">Details about the launch request and process.</param>
        protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
        {
            m_window = new MainWindow();
            m_window.Activate();
        }

        public Window Window
        { get { return m_window; } }
        public void Cleanup()
        {
            m_window = null;
        }

        private Window m_window;
    }
}

Cleanup will be used to remove all resources used by App. In this example, I just use it to remove the reference to the application's window.

Finally, we take advantage of the fact that the callback passed to Start is a lambda. We add a variable to wWinMain/Main to hold our App reference, and then use the lambda capture to assign to the App instance.

//main.cpp
int APIENTRY wWinMain(_In_ HINSTANCE, _In_opt_ HINSTANCE, _In_ LPWSTR, _In_ int)
{
	check_process_requirements();
	winrt::Microsoft::UI::Xaml::Application app = nullptr;

	winrt::Microsoft::UI::Xaml::Application::Start(
		[&app](auto &&)
		{
			app = winrt::make<winrt::Meh::implementation::App>();
		});

	auto app_inst = app.as<winrt::Meh::implementation::App>();
	app_inst->Cleanup();
	app_inst = nullptr;
	app = nullptr;

	return 0;
}
//Main.cs
namespace CsMeh
{
    public static class Program
    {
        [DllImport("Microsoft.ui.xaml.dll")]
        private static extern void XamlCheckProcessRequirements();

        [STAThread]
        static void Main(string[] args)
        {
            XamlCheckProcessRequirements();
            App app = null;

            ComWrappersSupport.InitializeComWrappers();
            Application.Start((p) => {
                var context = new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread());
                SynchronizationContext.SetSynchronizationContext(context);
                app = new App();
            });

            app.Cleanup();
            app = null;
        }
    }
}

There is a little quirkiness with the C++ version due to the projection, but that is it.

@Al-Caron
Copy link
Author

Thank you for that !!!

It does fullfill my needs!!

Greatly appreaciated 👍

A real implementation of THAT solution inside the Application class would be nice to have eventually.

@JaiganeshKumaran
Copy link
Contributor

JaiganeshKumaran commented Dec 17, 2023

I need to initialise the "library" on launch, but also need to "shutdown" that library properly when the application exits.

Why not use a global object with destructor?

@bpulliam bpulliam removed the needs-triage Issue needs to be triaged by the area owners label Jan 12, 2024
@bpulliam bpulliam added the team-Reach Issue for the Reach team label May 3, 2024
@lolametro
Copy link

Another possible workaround:

public App()
{
    this.InitializeComponent();
    
    DispatcherQueue.GetForCurrentThread().ShutdownStarting +=  OnShutdownStarting;
    DispatcherQueue.GetForCurrentThread().ShutdownCompleted += OnShutdownCompleted;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature proposal New feature proposal team-Reach Issue for the Reach team
Projects
None yet
Development

No branches or pull requests

5 participants