-1

I have created a tray application for controlling some hardware components. How can I invoke the UI thread without a main form or control?

The tray app is started with Application.Run(new MyTrayApp()):

class MyTrayApp : ApplicationContext
{
    private NotifyIcon trayIcon;

    public MyTrayApp()
    {            
        trayIcon = new NotifyIcon()
        {
            Icon = Resources.app_icon,
            ContextMenu = new ContextMenu(new MenuItem[] {                
            new MenuItem("Exit", Exit)
        }),
            Visible = true
        };
        // context is still null here
        var context = SynchronizationContext.Current;
        // but I want to invoke UI thread in hardware events
        MyHardWareController controller= new MyHardWareController(context);
    }

    void Exit(object sender, EventArgs e)
    {
        // context is accessible here because this is a UI event
        // too late tho
        var context = SynchronizationContext.Current;
        trayIcon.Visible = false;
        Application.Exit();
    }
}
  1. Control.Invoke() is not available as there are no controls
  2. Searching suggests that SynchronizationContext.Current should be saved for later invoke but there is no ApplicationContext.Load() event...?
  3. I've noticed that MainForm is null in the whole cycle. I wonder how does SynchronizationContext initialized in this case?

Edit:

Just to add some background info on why I would like to invoke UI thread. It is because System.Threading.ThreadStateException will be thrown when attempt to access Windows resources such as Clipboard or SendKeys in another thread:

  HResult=0x80131520
  Message=Current thread must be set to single thread apartment (STA) mode before OLE calls can be made. Ensure that your Main function has STAThreadAttribute marked on it.
  Source=System.Windows.Forms
  StackTrace:
...

It's another can of worms but just for information:

  1. [STAThreadAttribute] is already set for Main function (no effect)
  2. Creating a new STA thread would result in anti-virus deleting my application upon compile

Thus Form.Invoke() or the equivalent to invoke main thread should be the easiest.

Edit 2:

Add a gist for reproducing the error: https://gist.github.com/jki21/eb950df7b88c06cc5c6d46f105335bbf

jack3694078
  • 993
  • 1
  • 9
  • 20
  • @Charlieface `ContextMenu` and `MenuItem` are `Component` which doesn't have a `Invoke()` method. It won't allow me to do various things such as `SendKey` in hardward event thread without `Invoke()`. Also the tray menu only serve to exit the application so capturing `SynchronizationContext` on the click event would be already too late. – jack3694078 Feb 26 '21 at 03:14
  • Sorry you're right, my bad. So you want to `SendKeys`, do you need to be on the UI thread for that? – Charlieface Feb 26 '21 at 03:25
  • If you need an STA thread, you will need to create one, or make sure the app is running with one. Why do you need a message pump, or an STA thread ? – TheGeneral Feb 26 '21 at 03:27
  • What do you need to update and how are these *updates* shown? The ApplicationContext's Form is not created here and it's not clear what updates what with what. – Jimi Feb 26 '21 at 03:37
  • Edited to add more background. Invoking main thread just seems to be the easiest option. – jack3694078 Feb 26 '21 at 03:39
  • That's probably because you have a Thread that is not defined as `[STAThread]`. Add that to whatever Thread uses the Clipboard or whatever else requires a single-threaded model. – Jimi Feb 26 '21 at 03:43
  • @Jimi Is it possible to mark a Thread `[STAThread]` after creation? It is created by the hardware library – jack3694078 Feb 26 '21 at 03:45
  • Change the apartment model? Why would you think about this? Mark the entry point here as `[STAThread]`. Then provide a SynchronizationContext on your ApplicationContext *ambient*. A `WindowsFormsSynchronizationContext` can be created if needed. Not sure what needs it, though, since - as mentioned - it's not clear what you're updating and with what / from where. – Jimi Feb 26 '21 at 03:54
  • The main thread is already marked `[STAThread]` but it doesn't seems to make the other thread do the same. Should I initialize a `WindowsFormsSynchronizationContext` in the main thread and pass that for later invoke? Anyway it goes backs to the same question that I can't get code run in the UI thread in the first place. – jack3694078 Feb 26 '21 at 04:02
  • If the entry point is *marked* as `[STAThread]`, then if you add `var context = new WindowsFormsSynchronizationContext();` in the Constructor of your `MyTrayApp`, then the `DestinationThread.ApartmentState` will be `STA`. Or, you could do something like this: [Adding MenuItems to Contextmenu for a TrayIcon in a Console app](https://stackoverflow.com/a/65048753/7444103). If it's not `STA`, something is missing. – Jimi Feb 26 '21 at 05:06
  • We need a minimal example that reproduces the observed behavior. Can you show how you start the other thread, and what code is run by the other thread that causes the program to fail? – Theodor Zoulias Feb 26 '21 at 06:12
  • 1
    Why not instantiate the `new MyHardwareController(context);` on the `Application.Idle event` ? – Loathing Feb 26 '21 at 06:22
  • Edited to add a gist. I will try the solutions from Jimi & Loathing. Thanks in advance! – jack3694078 Feb 26 '21 at 06:43

2 Answers2

3

Solved it with Application.Idle as mentioned by Loathing! Thanks everyone for your advice!

TrayApp:

class MyTrayApp: ApplicationContext {
  private MyHardwareController controller = null;

  public MyTrayApp() {
    Application.Idle += new EventHandler(this.OnApplicationIdle);
    // ...
  }

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

      controller = new MyHardwareController((f) => {
        Task.Factory.StartNew(
          () => {
            f();
          },
          CancellationToken.None,
          TaskCreationOptions.None,
          context);
      });
    }
  }
  // ...
}

MyHardwareController:

class MyHardwareController {
  private Action < Action > UIInvoke;

  public MyHardwareController(Action < Action > UIInvokeRef) {
    UIInvoke = UIInvokeRef;
  }

  void hardware_Event(object sender, EventArgs e) {
    // Invoke UI thread
    UIInvoke(() => Clipboard.SetText("I am in UI thread!"));
  }

}

jack3694078
  • 993
  • 1
  • 9
  • 20
  • Do you know if it's possible for the `Application.Idle` event to be raised for than once? – Theodor Zoulias Feb 26 '21 at 14:13
  • 1
    Yes it seems that `Application.Idle` will be fired multiple times. I have not investigated but I guess the application will go idle every time the UI thread invoke is finished. – jack3694078 Feb 26 '21 at 16:58
0

An alternative solution would be to create a dummy form (which will never be shown, but should be stored somewhere. You just have to access the Handle property of the Form to be able to invoke it from now on.

public static DummyForm Form { get; private set; }

static void Main(string[] args)
{
    Form = new DummyForm();
    _ = Form.Handle;
    Application.Run();
}

Now it is possible to invoke into the UI thread:

Form.Invoke((Action)(() => ...);
LionAM
  • 1,271
  • 10
  • 28