简体   繁体   中英

Run method on UI thread from another thread

I have a class which plays some music like this. It also saves the GUI thread id in a private int during construction:

public class MediaPlayer {

    public event EventHandler<Track> OnTrackComplete;
    private int GuiThreadId;

    public MediaPlayer(...){
      ...
      this.GuiThreadId = Thread.CurrentThread.ManagedThreadId;
    }

    public void Play(){
        Task t = Task.Factory.StartNew(() =>
        {

            //On Song complete
            FireOnTrackComplete();
        });
    }

    protected virtual void FireOnTrackComplete()
    {
        if (OnTrackComplete != null)
            OnTrackComplete(this, loadedTrack);
    }
}

Is it possible to call FireOnTrackComplete() on a Thread with a specific ID. In this case, the ID is stored in the this.GuiThreadId ?

Most of the solutions I have come across suggest I use invokes in my GUI code in methods which listen to the OnTrackComplete event handler. I want to avoid doing this. I want to do everything in the MediaPlayer class


Based on the accepted answer bellow this is how I changed my code

public class MediaPlayer { public event EventHandler<Track> OnTrackComplete; private SynchronizationContext callerCtx; public MediaPlayer(...){... callerCtx = System.Threading.SynchronizationContext.Current; } public void Play(){ Task t = Task.Factory.StartNew(() => { //On Song complete FireOnTrackComplete(); }); } protected virtual void FireOnTrackComplete() { Action e = () => { if (OnTrackComplete,= null) OnTrackComplete(this; loadedTrack); }; FireEvent(e). } //... Other events..; // protected virtual void FireEvent(Action e) { if (callerCtx == null) e(). else callerCtx,Post(new SendOrPostCallback((_) => e()); null); } }

The SynchronizationContext class was meant to solve this problem. Copy the value of its Current property in the constructor, use its Post() or Send() method later. This ensures your library will work with any GUI class library. Like this:

class MediaPlayer {
    public MediaPlayer() {
        callersCtx = System.Threading.SynchronizationContext.Current;
        //...
    }

    private void FireOnTrackComplete() {
        if (callersCtx == null) FireOnTrackCompleteImpl();
        else callersCtx.Post(new System.Threading.SendOrPostCallback((_) => FireOnTrackCompleteImpl()), null);
    }

    protected virtual void FireOnTrackCompleteImpl() {
        var handler = OnTrackComplete;
        if (handler != null) handler(this, loadedTrack);
    }

    private System.Threading.SynchronizationContext callersCtx;
}

Pass a reference to the main dispatcher (=GUI-Thread's dispatcher) and call Invoke on it directly with your callback code.

public class MediaPlayer {

    public event EventHandler<Track> OnTrackComplete;
    private Dispatcher { get; set; }

    public MediaPlayer(Dispatcher guiDispatcher){
        // Other code ...
        if(guiDispatcher == null) 
            throw new ArgumentNullException("guiDispatcher", "Cannot properly initialize media player, since no callback can be fired on GUI thread.");
        Dispatcher = guiDispatcher;
    }

    public void Play() {
        // Fire immediately on thread calling 'Play', since we'll forward exec. on gui thread anyway.
        FireOnTrackComplete(); 
    }

    protected virtual void FireOnTrackComplete()
    {
        // Pretending "loadedTrack" was set somewhere before.
        Dispatcher.Invoke(() => {
            if (OnTrackComplete != null)
                OnTrackComplete(this, loadedTrack);
        });
    }
}
// Somewhere in your initialization code
// ...
MediaPlayer player = new MediaPlayer(App.Current.Dispatcher); // If you use WPF. Don't know if this applies to WinForms too.
// ...

To be able to execute code on another thread, you must have a queue or message pump waiting for a new item to process.

This is already done in winforms and wpf via Control.Invoke and IDispatcher.Invoke . If you really want to avoid having the Control perform the listening, you'll have to pass the control into the MediaPlayer . It's really awkward, but there's a big complaint on SO that the first answer is "how about you stop doing that thing you're trying to do".. so here goes:

public class MediaPlayer {

    public event EventHandler<Track> OnTrackComplete;
    private int GuiThreadId;
    private readonly Control control;

    public MediaPlayer(..., Control control){
      ...
      this.GuiThreadId = Thread.CurrentThread.ManagedThreadId;
      this.contrl = control;
    }

    public void Play(){
        Task t = Task.Factory.StartNew(() =>
        {

            //On Song complete
            FireOnTrackComplete();
        });
    }

    protected virtual void FireOnTrackComplete()
    {
        var trackComplete = OnTrackComplete;
        if (onTrackComplete != null)
            this.control.Invoke((MethodInvoker) delegate {trackComplete(this, loadedTrack);});
    }
}

Apologies if there's a typo, I don't have everything in front of me to verify with; but this should get you what you're after.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM