3

I have a Windows Forms project in C#. In this project there is a WaveOut device that's played back in separate thread. This playback thread periodically needs to call a UI thread method and pass it some data (an array holding the audio information that is being passed to the soundcard). The passAudio method calls the connected EventHandler periodically.

Right now, the waveout device (WaveOutPlayer.cs) has an EventHandler:

public class WaveOutPlayer : IDisposable
{
    public event EventHandler<AudioEventArgs> BufferSwapped;
    ...
    private void passAudio(byte[] pAudiodata)
    {
        AudioEventArgs args = new AudioEventArgs();
        args.Data = pAudiodata;
        args.WaveFormat = ws.Format;
        if (BufferSwapped != null)
        {
            BufferSwapped.Invoke(this, args);
        }
    }
}

And the Windows Form instance connects to this EventHandler:

private void Start()
{
    WaveStream Audio = new WaveStream("sine440hz_16bit_stereo.wav");
    WaveOutPlayer wp = new WaveOutPlayer(audio, 0);
    wp.BufferSize = 8192; // testing
    wp.Repeat = false; // 'true' not implemented yet
    wp.BufferSwapped += Wp_BufferSwapped;
}

private void Wp_BufferSwapped(object sender, AudioEventArgs e)
{
    // The audio buffer data can be found in the event args.
    // So analyze this Audio and manipulate some of the forms' controls 
    // accordingly.

    this.labelForAmplitude.Text = "some value";
}

However, this results in an exception because the Wp_BufferSwapped-Method actually belongs to the playback thread and thus may not manipulate the label's text.

Now: How can I solve this problem without making the Windows Form's code more difficult? The reason for this is that I want to let my students (high school) do some cool stuff with arrays and simple user interfaces. But at this point they only have a very basic understanding of user interfaces work. They do not know anything about stuff like BeginInvoke or MethodInvoker, yet. So I want to give them the WaveOutPlayer in form of a DLL - and they only need to deal with the Windows Form. Is there a solution for this particular Kind of problem?

AudioGuy
  • 413
  • 5
  • 18
  • you can send `this.labelForAmplitude.Text` as a `ref`to `Wp_BufferSwapped-Method` – Usman lqbal Dec 05 '17 at 11:08
  • Possible duplicate of [How do I update the GUI from another thread in C#?](https://stackoverflow.com/questions/661561/how-do-i-update-the-gui-from-another-thread-in-c) – PaulF Dec 05 '17 at 11:08
  • @Usmanlqbal That would be a solution if there were only one or two labels to be changed. However, I would like to let the students decide which labels or picture boxes to change. – AudioGuy Dec 05 '17 at 11:12
  • One could imagine overriding the `add` and `remove` handlers for the event and capturing a `SynchronizationContext` during the add and keeping each delegate and context together for use when dispatching the event. However, I'm just making this up at the moment so cannot provide a robust implementation. Also, of course, there's the risk that the students will learn, incorrectly, that it's safe to update UI from event handlers which you'll then have to somehow undo. Sometimes too much handholding may backfire. – Damien_The_Unbeliever Dec 05 '17 at 11:19
  • Capture current SynchronizationContext in constructor of WaveOutPlayer and invoke event handler on that context. – Evk Dec 05 '17 at 11:21
  • @Evk I'm not sure the WaveOutPlayer should be responsible for where it's events get run. It should be up to the handlers to make that kind of decision. – bornfromanegg Dec 05 '17 at 11:24
  • A number of suggestions for 'automating' the handling of the UI thread are suggested here: https://stackoverflow.com/questions/2367718/automating-the-invokerequired-code-pattern – LordWilmore Dec 05 '17 at 11:25
  • @bornfromanegg how's that? You can decide which handlers to run on that context and which not if you'd like to, capturing it in constructor does not oblige you to use it everywhere. – Evk Dec 05 '17 at 11:26
  • @Evk It's a valid solution to the actual question, but it seems like a violation of SRP to me, and more complicated than simply marshalling the handler to the UI thread. Of course, the OP doesn't want to do that, but I do wonder if it would be more complicated than what he's trying to avoid. – bornfromanegg Dec 05 '17 at 11:35
  • Have you thought about implementing the solution as async/await? Making SwapBuffer a async Method? – quadroid Dec 05 '17 at 11:35
  • @bornfromanegg there are quite some built-in classes which do exactly that. For example, `Progress` class. On the other hand, this `Progress` creates quite a lot of confusion because of this, so maybe you are right. – Evk Dec 05 '17 at 11:44
  • I am ok with whatever solution as long as the Windows Form class remains simple without any complicated method calls, delegates or something like that ;-) – AudioGuy Dec 05 '17 at 11:52
  • Ah, you're right. I actually read "Capture current SynchronizationContext in constructor" as "Pass the current SynchronizationContext to the constructor" in your initial comment. I see what you mean now. – bornfromanegg Dec 05 '17 at 11:53
  • @AudioGuy I think the link posted by LordWilmore is what you want. – bornfromanegg Dec 05 '17 at 11:53
  • Maybe it is simpler to just copy the data that's passed to the Windows Form and then set a Timer to periodically update the labels based on what was copied before. I think I need to lock the array for each timer tick? – AudioGuy Dec 05 '17 at 11:54
  • @AudioGuy If you are teaching CS, you may also want to check out the [cseducators.se] community. – Ben I. Dec 21 '17 at 22:32
  • @BenI. thanks! I'll go and have a look at it. – AudioGuy Dec 24 '17 at 12:09

1 Answers1

1

You can capture current SynchronizationContext in constructor and then Post your event handler invocation to it, like this:

public class WaveOutPlayer {
    private readonly SynchronizationContext _context;
    public WaveOutPlayer() {
        // capture
        _context = SynchronizationContext.Current;
    }

    public event EventHandler<AudioEventArgs> BufferSwapped;

    private void passAudio(byte[] pAudioData) {
        var args = new AudioEventArgs();
        args.Data = pAudioData;
        var handler = BufferSwapped;
        if (handler != null) {
            if (_context != null)
                // post
                _context.Post(_ => handler(this, args), null);
            else
                handler(this, args);
        }
    }
}

By doing this you do not introduce dependency on winforms to your WaveOutPlayer while at the same time no compilcated actions are required from WinForms part, event handlers are just invoked on UI thread. Note that Post here will be analog to Control.BeginInvoke. If you want analog of Control.Invoke - use Send instead.

Evk
  • 98,527
  • 8
  • 141
  • 191