You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C#中如何结合事件回调控制方法执行顺序,等待回调完成再执行后续代码

问题

游戏开发中遇到一个问题:必须等待媒体播放器的MediaEnded事件回调执行完成后,才能运行后续代码。但当前逻辑是订阅事件后,后续代码会直接执行,完全不等回调触发。媒体时长在1秒到15秒之间。

有人建议将nextScreen这个Action塞进回调里,但这违反了SOLID的单一职责原则——回调不该知晓nextScreen的逻辑,这是HandleIncorrectTap方法的专属职责。另外,PlayWrongSoundWithCallBack里有声音开关判断,若开关关闭会直接返回,这也导致没法把nextScreen放进回调中。

核心问题代码

public void HandleIncorrectTap(Action nextScreen)
{
    AudioPlayerSource = IncorrectSound;
    ScreenShaker.ShakeScreen(View);
    PlayWrongSoundWithCallBack(AudioPlayer, IncorrectMediaEnededCallBack); // 当前实例中的EventHandler

    nextScreen.Invoke(); // 需要等待事件回调执行完再运行这行
}

更新补充代码

媒体播放与事件订阅逻辑

public void PlayMediaWithSoundWithCallBack(MediaElement mediaElement, EventHandler callBack)
{
    if (mediaElement == null)
        return;

    if (mediaElement.CurrentState == MediaElementState.Stopped)
    {
        Device.StartTimer(new TimeSpan(0, 0, 0, 0, 0), () =>
        {
            Device.BeginInvokeOnMainThread(() =>
            {
                if (mediaElement.Volume == 0)
                    mediaElement.Volume = 1;
                if (mediaElement.Speed == 0)
                    mediaElement.Speed = 1;
                mediaElement.Play();
            });

            return false;
        });
    }
    else if (mediaElement.CurrentState == MediaElementState.Playing)
    {
        Device.StartTimer(new TimeSpan(0, 0, 0, 0, 0), () =>
        {
            Device.BeginInvokeOnMainThread(() =>
            {
                mediaElement.Stop();
                mediaElement.Play();
            });

            return false;
        });
    }

    mediaElement.MediaEnded += callBack;
}

BaseViewModel中的基础方法

protected virtual void CorrectMediaEnededCallBack(object sender, EventArgs e)
{
}

protected virtual void IncorrectMediaEnededCallBack(object sender, EventArgs e)
{
}

protected virtual void EndMediaEnededCallBack(object sender, EventArgs e)
{
}

protected void RemoveAddedEvent(MediaElement mediaElement, EventHandler addedCallBackHandler)
{
    mediaElement.MediaEnded -= addedCallBackHandler;
}       

protected void PlayCorrectSoundWithCallBack(MediaElement mediaElement, EventHandler callBack)
{
    if (!Settings.IsCorrectSoundEnabled)
    {
        return; // 这也是没法把nextScreen放进回调的原因
    }
    else
    {
        SoundPlayer.PlayMediaWithSoundWithCallBack(mediaElement, callBack);
    }
}

protected void PlayWrongSoundWithCallBack(MediaElement mediaElement, EventHandler callBack)
{
    if (!Settings.IsIncorrectSoundEnabled)
    {
        return; // 这也是没法把nextScreen放进回调的原因
    }
    else
    {
        SoundPlayer.PlayMediaWithSoundWithCallBack(mediaElement, callBack);
    }
}
解决方案

用TaskCompletionSource将事件回调转换为可等待的Task,既不违反单一职责,又能处理声音开关关闭的场景。

步骤1:改造播放方法为异步返回Task

给SoundPlayer添加异步播放方法,自动处理事件订阅与取消:

public Task PlayMediaWithSoundAsync(MediaElement mediaElement)
{
    if (mediaElement == null)
        return Task.CompletedTask;

    var tcs = new TaskCompletionSource<bool>();

    EventHandler mediaEndedHandler = null;
    mediaEndedHandler = (sender, e) =>
    {
        mediaElement.MediaEnded -= mediaEndedHandler;
        tcs.TrySetResult(true);
    };

    mediaElement.MediaEnded += mediaEndedHandler;

    if (mediaElement.CurrentState == MediaElementState.Stopped)
    {
        Device.StartTimer(TimeSpan.Zero, () =>
        {
            Device.BeginInvokeOnMainThread(() =>
            {
                if (mediaElement.Volume == 0)
                    mediaElement.Volume = 1;
                if (mediaElement.Speed == 0)
                    mediaElement.Speed = 1;
                mediaElement.Play();
            });
            return false;
        });
    }
    else if (mediaElement.CurrentState == MediaElementState.Playing)
    {
        Device.StartTimer(TimeSpan.Zero, () =>
        {
            Device.BeginInvokeOnMainThread(() =>
            {
                mediaElement.Stop();
                mediaElement.Play();
            });
            return false;
        });
    }

    return tcs.Task;
}

步骤2:修改BaseViewModel中的播放方法

让它们返回Task,兼容声音开关判断:

protected async Task PlayCorrectSoundAsync(MediaElement mediaElement)
{
    if (!Settings.IsCorrectSoundEnabled)
        return;
    
    await SoundPlayer.PlayMediaWithSoundAsync(mediaElement);
}

protected async Task PlayWrongSoundAsync(MediaElement mediaElement)
{
    if (!Settings.IsIncorrectSoundEnabled)
        return;
    
    await SoundPlayer.PlayMediaWithSoundAsync(mediaElement);
}

步骤3:改造HandleIncorrectTap为异步方法

等待声音播放完成(或开关关闭直接返回)后,再执行后续逻辑:

public async void HandleIncorrectTap(Action nextScreen)
{
    AudioPlayerSource = IncorrectSound;
    ScreenShaker.ShakeScreen(View);
    await PlayWrongSoundAsync(AudioPlayer);

    nextScreen.Invoke(); // 现在会等待声音播放流程结束后再执行
}

方案优势

  • 遵循单一职责:播放逻辑与后续nextScreen逻辑完全分离,播放方法只负责媒体播放与等待结束,HandleIncorrectTap负责流程编排
  • 自动兼容开关场景:若声音开关关闭,播放方法直接返回已完成的Task,nextScreen会立即执行,符合预期
  • 避免内存泄漏:事件触发后自动取消订阅,不会残留无用的事件绑定

内容的提问来源于stack exchange,提问作者ThisQRequiresASpecialist

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 23:47:28