Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SystemEvents.TimeChanged not firing (UPDATED!)

What I want

I want to do something whenever the timezone in Windows is changed.

What I have so far

For this purpose I have implemented the event SystemEvents.TimeChanged as follows:

In the constructor:

SystemEvents.TimeChanged += SystemEvents_TimeChanged;

The event body:

void SystemEvents_TimeChanged(object sender, EventArgs e)
    {
        MessageBox.Show("Test1");}

What is the problem

The event is not fired when the time or the time zone is changed in Windows.

What I have tried

When I write the above code in a clean WinForms application, everything works as expected. But not in my application, which of cause contains a lot of other code. I do not see that I have any other events, which should block the firing of the above event.

My question is

Does anyone have an idea what could cause the above code to not be firing in my application, but work as expected when I create a new project/application only containing the above code?

UPDATE 1

It happens because I show a splash screen in a separate thread before calling

Application.Run(new FormMain());

Then SystemEvents sticks to the thread created by the splash screen, even this thread is terminated when the application has loaded.

The question is now if there is a way to tell SystemEvents that is should now use the "correct" UI thread when the application has loaded?

like image 624
JohnSaps Avatar asked Aug 26 '19 23:08

JohnSaps


2 Answers

This answer regarding to question UPDATE 1 part, hence the provided code example in the original question is working.

I have spent some time to figure this it out. Hence I do not have full overview of your code I have improvised 2 WinForms for this solution (one as splash and the other as main), of course this is just example to illustrate the concept.

From what I understand you when you start your software, it starts with splash part as separate thread and when splash is done, than FormMain start afterwards. You can do something better, use ApplicationContext. You make your own context class that is extended from ApplicationContext and in that class you declare your Splash and FormMain with their respective own logic. Now in your case you need to make sure FormMain starts at some point after Splash or some thing like that (I do not know how your software works/flow).

In context class you create the methods to subscribe and unsubscribe to SystemEvents.TimeChanged so you can listen to time changing. I have for demonstration purpose also created a BindingList to demonstrate time changes.

Now lets show some code:

public static void Main()
{
    // use own created context
    MainApplicationContext context = new MainApplicationContext();
    Application.Run(context);
}

// just quick way to demonstrate how we collect time changes
public static BindingList<string> Logs { get; private set; }

private class MainApplicationContext : ApplicationContext
{
    private int _formCount;

    public MainApplicationContext()
    {
        Logs = new BindingList<string>();

        _formCount = 0;

        // splash screen
        var splash = new FormSplash();
        splash.Closed += OnFormClosed;
        splash.Load += OnFormOpening;
        splash.Closing += OnFormClosing;
        _formCount++;
        splash.Show();

        // For demo, make some logic that close splash when program loaded.
        Thread.Sleep(2000);

        var main = new FormMain();
        main.Closed += OnFormClosed;
        main.Load += OnFormOpening;
        main.Closing += OnFormClosing;
        _formCount++;

        splash.Close();
        main.Show();
    }

    private void OnFormOpening(object sender, EventArgs e)
    {
        SystemEvents.TimeChanged += SystemEvents_TimeChanged;
    }

    private void OnFormClosing(object sender, CancelEventArgs e)
    {
        SystemEvents.TimeChanged -= SystemEvents_TimeChanged;
    }

    private void OnFormClosed(object sender, EventArgs e)
    {
        _formCount--;
        if (_formCount == 0)
        {
            ExitThread();
        }
    }

    private void SystemEvents_TimeChanged(object sender, EventArgs e)
    {
        var text = $"TimeChanged, Time changed; it is now {DateTime.Now.ToLongTimeString()}";
        Logs.Add(text);
    }
}

Now in our FormMain, create listbox call it LogListBox:

public FormMain()
{
    InitializeComponent();
    Load += ListChanged;
}

// this keep list of time changes events updated if changed this could be log or some thing else.
private void ListChanged(object sender, EventArgs e)
{
    LogListBox.DataSource = Program.Logs;
}

And here how it works: enter image description here

Documentation

  • https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.application.run?view=netframework-4.8
  • https://learn.microsoft.com/en-us/dotnet/api/microsoft.win32.systemevents?view=netframework-4.8
like image 128
Maytham Avatar answered Nov 02 '22 19:11

Maytham


Before showing the splash screen in a separate thread, you can call the method bellow from the main thread, to associate this thread with the system thread that listens for system events.

private static void InitializeSystemEvents()
{
    var timerId = SystemEvents.CreateTimer(1);
    SystemEvents.KillTimer(timerId);
}

This way you will be able to subscribe later to SystemEvents from your main Form, which runs in the main thread.


Update: the root of the problem is that the system thread that listens for system events is killed when the splash screen is closed. So another way to solve this problem would be to keep the splash screen open (but hidden) during the whole lifetime of the application.

like image 36
Theodor Zoulias Avatar answered Nov 02 '22 18:11

Theodor Zoulias