0

I have a tray app with an event handler. Whenever the event is triggered I want to inform the user via a corresponding popup image about a status. This image should appear for around 500 ms in the center of the screen for which I need a form with a picturebox.

I tried to display the form via new & close and via show & hide but both are not working as expected. Either the form is showing and hiding itself only once at the start (when I create and show it via the constructor of the context class) but not for further event triggers (I can see the forms boundaries but its grey and hanging) or the form is not showing at all (when I only create and show it via delegate from the event handler to a method of the context class) or it is only seen for half a millisecond or I get a thread error (even when I do not use any additional threads) when the second trigger happens.

I am really lost here and do not know which would be the correct approach.

UPDATE & SOLUTION:

As I use a tray app (starting with an ApplicationContext instead of a Form) the UI thread has to be handled manually, see How to invoke UI thread in Winform application without a form or control.

As explained in the top answer, you need to add a method for the Application.Idle event where you put the instanciation of your controller/handler in order to prevent duplicate instanciations and thereby a deadlock of your UI thread.

For the instanciation and within the controller/handler class you need to add a UI invoke reference of the type Action<Action> which can be used for any UI manipulations as those are directly executed in the UI thread.

tar
  • 156
  • 2
  • 13
  • Do you have a WPF app there? Do you run `MyContext` to start the application? What is the relation between `MyContext` and `MyHandler`? Can you show it? Did you check the `ManagedThreadId` of the Dispatcher's Thread? -- Note that you're not disposing the Timer (which should not be exposed as a public Field, at all. You should have instead public methods that allow to start, stop and dispose the Timer, or add the Timer to the Form's Components in the Constructor) -- Unrelated: in the Form, you should not subscribe to events, but override `OnLoad()` and `OnShown()` instead. – Jimi Apr 13 '23 at 16:47
  • It is a classic WinForms desktop tray app in .NET Framework. No WPF is used and I use `Application.Run(new MyContext())` to start the app. I've updated the coding according to your suggestions and got rid of the call to MyContext in MyHandler. The form is now inside MyHandler. I am not sure if I really need any Dispatcher and I am confused of the threading error which occurs with and without the Dispatcher. I am also not sure the Timer is the best strategy as the form seems to hang which confuses me even further as I have no Thread.Sleep() included. – tar Apr 13 '23 at 17:46
  • Why do you keep on changing the code? Are you still *experimenting* stuff? Well, don't do that. Also, now it's not clear where the handler gets the App context from. Anyway, the Dispatcher is a WindowsBase class, used in WPF. It looks like you picked code from different sources. Why invoking? If you invoke from `OnEvent`, you may pick up a different Thread Context that way, that's why I suggested to check the `ManagedThreadId` value. What calls `OnEvent`? -- Did you decorate the `Main` entry point with `[STAThread]`? – Jimi Apr 13 '23 at 18:03
  • On the one hand you want know every detail, on the other I should not change/update the code ;) Yes, I use STAThread - the whole code is working except the fricken form. – tar Apr 13 '23 at 18:12
  • And ye, of course I am testing as I want to find out how it could work. I now got rid of the Timer and the Dispatcher and used a Thread instead where I open the form and close it after Thread.Sleep() but the form is not correctly shown (it stays grey). Geez, what is the issue... – tar Apr 13 '23 at 18:20

1 Answers1

0

Final code fitting to the explanation in the OP under UPDATE & SOLUTION:

Program.cs is the main entry point of our tray app:

using System;
using System.Windows.Forms;

namespace Demo {
  static class Program {
    [STAThread]
    static void Main() {
      Application.EnableVisualStyles();
      Application.SetCompatibleTextRenderingDefault(false);
      Application.Run(new MyContext()); // we start with context instead of form
    }
  }
}

MyContext.cs prevents duplicate instanciation via Application.Idle event method and has an Update() method where it uses a handler value to update some UI elements:

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;    

namespace Demo {
  public class MyContext : ApplicationContext {
    private MyHandler  _Handler  = null;
    private NotifyIcon _TrayIcon = null;

    public MyContext() {
      // constructor is within the OnApplicationIdle method
      // due to UI thread handling and preventing duplicates when having events
      Application.ApplicationExit += new EventHandler(OnExit);
      Application.Idle            += new EventHandler(OnIdle);
    }

    new public void Dispose() {
      _TrayIcon.Visible = false;
     Application.Exit();
    }    

    private void OnExit(object sender, EventArgs e) {
      Dispose();
    }

    private void OnIdle(object sender, EventArgs e) {
      // prevent duplicate initialization on each Idle event
      if (_Handler == null) {
        var context = TaskScheduler.FromCurrentSynchronizationContext();

        _Handler = new MyHandler(
          (f) => {                      // 1st parameter of MyHandler constructor
            Task.Factory.StartNew(
              () => {
                f();
              },
              CancellationToken.None,
              TaskCreationOptions.None,
              context);
          },
          this                          // 2nd parameter of MyHandler constructor
        );

        _TrayIcon     = new NotifyIcon() {
          ContextMenu = new ContextMenu(new MenuItem[] {
            new MenuItem("Toggle Something", ToggleSomething),
            new MenuItem("-"),          
            new MenuItem("Exit",             OnExit)
          }),
          Text        = "My wonderful app",
          Visible     = true
        };

        _TrayIcon.MouseClick += new MouseEventHandler(_TrayIcon_Click);

        Update();                       // Handler is used and form is shown
      }
    }

    public void Update() {
     bool value = _Handler.GetValue();

      // tray icon is updated
      _TrayIcon.Icon = value ? path.to.icon.when.true
                             : path.to.icon.when.false;

      // form is shown and closed by itself after a particular amount of time 
      MyForm form = new MyForm(value);
      form.Show();
    }

    private void _TrayIcon_Click(object sender, MouseEventArgs e) {
      if (e.Button == MouseButtons.Left) {
        ToggleSomething(sender, e);
      }
    }

    private void ToggleSomething(object sender, EventArgs e) {
      _Handler.ToggleValue();
    }

    // ...
  }
}

MyHandler.cs needs a UI invoke reference by which it can directly call into the UI thread and thereby manipulate UI elements:

using System;
using System.Collections.Generic;
using System.Linq;
// ...

namespace Demo {
  public class MyHandler {
    private          MyContext      _Context     = null;
    private readonly Action<Action> _UIInvokeRef = null;
    private          bool           _Value       = false;

    public MyHandler(Action<Action> uIInvokeRef, MyContext context) {
      _Context          = context;
      _UIInvokeRef      = uIInvokeRef;

      // ...
      Something something.OnSomething += Something_OnSomething; // an event that is triggered by something outside (e.g. a library that reacts to a system device)
    }

    private void Something_OnSomething(Data data) {
      _Value = data.Value > 10 ? true : false;  // data has been changed and value is set

      // ...

      _UIInvokeRef(() => {                      // UI thread is used
        _Context.Update();                      // update tray icon and show form
      });
    }

    // ...

    public bool GetValue() {
      return _Value;
    }

    public void ToggleValue() {
      _Value = !_Value;

      // can also be used to manipulate a system device (e.g.)
      // in order to trigger the Something_OnSomething event
      // which then updates the UI elements
    }
  }
}

MyForm.cs uses a timer by which it can close itself:

using System;
using System.Windows.Forms;

namespace Demo {
  public partial class MyForm : Form {
    private System.Windows.Forms.Timer _Timer = null;

    public FormImage(bool value) {
      InitializeComponent();

      pbx.Image = value ? path.to.picture.when.true
                        : path.to.picture.when.false;
    }

    protected override void OnLoad(EventArgs e) {
      base.OnLoad(e);

      this.FormBorderStyle = FormBorderStyle.None;
      this.StartPosition   = FormStartPosition.CenterScreen;
      this.ShowInTaskbar   = false;
      this.TopLevel        = true;
    }

    protected override void OnShown(EventArgs e) {
      base.OnShown(e);

      _Timer = new System.Windows.Forms.Timer();
      _Timer.Interval = 500;                       // intervall until timer tick event is called
      _Timer.Tick += new EventHandler(Timer_Tick); // timer tick event is registered
      _Timer.Start();                              // timer is started
    }

    private void Timer_Tick(object sender, EventArgs e) {
      _Timer.Stop();                               // timer is stopped
      _Timer.Dispose();                            // timer is discarded
      this.Close();                                // form is closed by itself
    }
  }
}

This works even when the handler event (system trigger) Something_OnSomething is called faster again than the form timer event Timer_Tick can close the form.

tar
  • 156
  • 2
  • 13