如何在C#中的GUI线程上启动一系列定时事件



我有一个应用程序,它必须做以下类型的事情,最好是在GUI线程上,因为这是大多数操作发生的地方,并且没有长时间运行的操作:

Wait 1000
FuncA()
Wait 2000
FuncB()
Wait 1000
FuncC()

我意识到我可以使用带有状态机风格的OnTick功能的计时器,但这似乎很麻烦:

    int _state;
    void OnTick(object sender, EventArgs e) {
        switch (_state) {
            case 0:
                FuncA();
                _timer.Interval = TimeSpan.FromSeconds(2);
                _state = 1;
                break;
            case 1:
                FuncB();
                _timer.Interval = TimeSpan.FromSeconds(1);
                _state = 2;
                break;
            case 2:
                FuncC();
                _timer.IsEnabled = false;
                _state = 0;
        }
    }

另外,我希望能够使它足够通用,可以做一些类似的事情

RunSequenceOnGuiThread(new Sequence {
    {1000, FuncA}
    {2000, FuncB}
    {1000, FuncC}};

做这种事有惯用的方法吗?给定所有的TPL东西,或者Rx,甚至F#中的计算表达式,我会假设存在一个,但我找不到

Observable.Concat(
        Observer.Timer(1000).Select(_ => Func1()),
        Observer.Timer(2000).Select(_ => Func2()),
        Observer.Timer(1000).Select(_ => Func3()))
    .Repeat()
    .Subscribe();

要实现这一点,你唯一需要做的就是确保你的Func返回一个值(即使这个值是Unit.Default,即什么都没有)

编辑:以下是如何制作通用版本:

IObservable<Unit> CreateRepeatingTimerSequence(IEnumerable<Tuple<int, Func<Unit>>> actions)
{
    return Observable.Concat(
        actions.Select(x => 
            Observable.Timer(x.Item1).Select(_ => x.Item2())))
        .Repeat();
}

这是F#中的一个草图:

let f() = printfn "f"
let g() = printfn "g"
let h() = printfn "h"
let ops = [
    1000, f
    2000, g
    1000, h
    ]
let runOps ops =
    async {
        for time, op in ops do
            do! Async.Sleep(time)
            op()
    } |> Async.StartImmediate 
runOps ops
System.Console.ReadKey() |> ignore

这在控制台应用程序中,但您可以在GUI线程上调用runOps。另请参阅此博客。

如果使用的是VS11/NetFx45/C#5,则可以使用C#async/awaitTupleListAction委托执行类似的操作。

使用异步CTP或.NET 4.5(C#5),使用异步方法和等待运算符非常容易。这可以直接在UI线程上调用,它将按预期工作。

    public async void ExecuteStuff()
    {
        await TaskEx.Delay(1000);
        FuncA();
        await TaskEx.Delay(2000);
        FuncB();
        await TaskEx.Delay(1000);
        FuncC();
    }

这里有一种将"yield return"和反应式框架结合起来的方法,为您提供一个"穷人的异步"。基本上可以让您"等待"任何可观察的对象。在这里,我只是把它用于计时器,因为这是你感兴趣的,但在继续下一件事之前,你可以让它"等待"按钮点击(使用Subject<Unit>)等。

public sealed partial class Form1 : Form {
    readonly Executor _executor = new Executor();
    public Form1() {
        InitializeComponent();
        _executor.Run(CreateAsyncHandler());
    }
    IEnumerable<IObservable<Unit>> CreateAsyncHandler() {
        while (true) {
            var i = 0;
            Text = (++i).ToString();
            yield return WaitTimer(500);
            Text = (++i).ToString();
            yield return WaitTimer(500);
            Text = (++i).ToString();
            yield return WaitTimer(500);
            Text = (++i).ToString();
        }
    }
    IObservable<Unit> WaitTimer(double ms) {
        return Observable.Timer(TimeSpan.FromMilliseconds(ms), new ControlScheduler(this)).Select(_ => Unit.Default);
    }
}
public sealed class Executor {
    IEnumerator<IObservable<Unit>> _observables;
    IDisposable _subscription = new NullDisposable();
    public void Run(IEnumerable<IObservable<Unit>> actions) {
        _observables = (actions ?? new IObservable<Unit>[0]).Concat(new[] {Observable.Never<Unit>()}).GetEnumerator();
        Continue();
    }
    void Continue() {
        _subscription.Dispose();
        _observables.MoveNext();
        _subscription = _observables.Current.Subscribe(_ => Continue());
    }
    public void Stop() {
        Run(null);
    }
}
sealed class NullDisposable : IDisposable {
    public void Dispose() {}
}

这是对Daniel Earwicker的AsyncIOPipe想法的轻微修改:http://smellegantcode.wordpress.com/2008/12/05/asynchronous-sockets-with-yield-return-of-lambdas/

所有不同的反应都很有趣。这里有一个简单的DIY选项,它不依赖于任何其他库,也不会不必要地占用线程资源。

基本上,对于列表中的每个操作,它都会创建一个onTick函数来执行该操作,然后用剩余的操作和延迟递归地调用DoThings。

这里,ITimer只是DispatcherTimer的一个简单包装器(但它也可以与SWF定时器或用于单元测试的模拟定时器一起使用),而DelayedAction只是一个带有int DelayAction action 的元组

public static class TimerEx {
    public static void DoThings(this ITimer timer, IEnumerable<DelayedAction> actions) {
        timer.DoThings(actions.GetEnumerator());
    }
    static void DoThings(this ITimer timer, IEnumerator<DelayedAction> actions) {
        if (!actions.MoveNext())
            return;
        var first = actions.Current;
        Action onTick = null;
        onTick = () => {
            timer.IsEnabled = false;
            first.Action();
            // ReSharper disable AccessToModifiedClosure
            timer.Tick -= onTick;
            // ReSharper restore AccessToModifiedClosure
            onTick = null;
            timer.DoThings(actions);
        };
        timer.Tick += onTick;
        timer.Interval = first.Delay;
        timer.IsEnabled = true;
    }
}

如果你不想深入研究F#或参考Rx或使用.Net 4.5,这是一个简单可行的解决方案。

下面是一个如何测试它的例子:

[TestClass]
public sealed class TimerExTest {
    [TestMethod]
    public void Delayed_actions_should_be_scheduled_correctly() {
        var timer = new MockTimer();
        var i = 0;
        var action = new DelayedAction(0, () => ++i);
        timer.DoThings(new[] {action, action});
        Assert.AreEqual(0, i);
        timer.OnTick();
        Assert.AreEqual(1, i);
        timer.OnTick();
        Assert.AreEqual(2, i);
        timer.OnTick();
        Assert.AreEqual(2, i);
    }
}

以下是使其编译的其他类:

public interface ITimer {
    bool IsEnabled { set; }
    double Interval { set; }
    event Action Tick;
}
public sealed class Timer : ITimer {
    readonly DispatcherTimer _timer;
    public Timer() {
        _timer = new DispatcherTimer();
        _timer.Tick += (sender, e) => OnTick();
    }
    public double Interval {
        set { _timer.Interval = TimeSpan.FromMilliseconds(value); }
    }
    public event Action Tick;
    public bool IsEnabled {
        set { _timer.IsEnabled = value; }
    }
    void OnTick() {
        var handler = Tick;
        if (handler != null) {
            handler();
        }
    }
}
public sealed class MockTimer : ITimer {
    public event Action Tick;
    public bool IsEnabled { private get; set; }
    public double Interval { set { } }
    public void OnTick() {
        if (IsEnabled) {
            var handler = Tick;
            if (handler != null) {
                handler();
            }
        }
    }
}

public sealed class DelayedAction {
    readonly Action _action;
    readonly int _delay;
    public DelayedAction(int delay, Action action) {
        _delay = delay;
        _action = action;
    }
    public Action Action {
        get { return _action; }
    }
    public int Delay {
        get { return _delay; }
    }
}

如果您可以使用C#4.5来实现这一点,请参阅Firoso文章:这是在C#中实现这一目标的最佳方式,正是Async的目的。

然而,如果你做不到,可能有一些方法可以做到。我会做一个"简单"的经理来做到这一点:

public partial class Form1 : Form
{
    private TimedEventsManager _timedEventsManager;
    public Form1()
    {
        InitializeComponent();
    }
    private void Form1_Load(object sender, EventArgs e)
    {
        _timedEventsManager 
            = new TimedEventsManager(this,
                new TimedEvent(1000, () => textBox1.Text += "Firstn"),
                new TimedEvent(5000, () => textBox1.Text += "Secondn"),
                new TimedEvent(2000, () => textBox1.Text += "Thirdn")
            );
    }
    private void button1_Click(object sender, EventArgs e)
    {
        _timedEventsManager.Start();
    }
}
public class TimedEvent
{
    public int Interval { get; set; }
    public Action Action { get; set; }
    public TimedEvent(int interval, Action func)
    {
        Interval = interval;
        Action = func;
    }
}
public class TimedEventsManager
{
    private readonly Control _control;
    private readonly Action _chain;
    public TimedEventsManager(Control control, params TimedEvent[] timedEvents)
    {
        _control = control;
        Action current = null;
        // Create a method chain, beginning by the last and attaching it 
        // the previous.
        for (var i = timedEvents.Length - 1; i >= 0; i--)
        {
            var i1 = i;
            var next = current;
            current = () =>
                          {
                              Thread.Sleep(timedEvents[i1].Interval);
                               // MUST run it on the UI thread!
                              _control.Invoke(new Action(() => timedEvents[i1].Action()));
                              if (next != null) next();
                          };
        }
        _chain = current;
    }
    public void Start()
    {
        new Thread(new ThreadStart(_chain)).Start();
    }
}

请注意,此示例是特定于Winforms的(使用Control.Invoke())。您将需要一个稍微不同的WPF版本,它使用线程分配器来实现相同的功能。(如果我的内存没有故障,您也可以使用Control.Dispatcher.Invoke(),但请记住这是一个不同的控件)

相关内容

  • 没有找到相关文章

最新更新