2012-03-23 67 views
6

我已經得到了需要做以下類型的東西,最好是在GUI線程,因爲這是大多數的行動正在進行,並沒有長期運行OPS的應用程序:如何在C#中的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#中的計算表達式,我都會假設存在一個,但我沒有找到它。

+0

什麼GUI庫您使用? Winforms,WPF或其他東西? – svick 2012-03-23 19:58:51

+0

現在WPF,但我想要一個解決方案(或至少一種技術),以及WinForms的工作。 – lobsterism 2012-03-23 20:04:23

+4

補充工具欄:我所做的一個改變 - 跨所有建議的解決方案 - 是使用TimeSpan而不是整數來實現延遲。 1000可能意味着一秒,1000秒等。TimeSpan本質上是清晰明確的。 – 2012-03-23 22:14:23

回答

1

下面就以「產量回報」和無功框架相結合的方式給你一個「窮人的異步」。基本上讓你「等待」任何IObservable。在這裏,我只是將它用於計時器,因爲這是您感興趣的內容,但在繼續下一步之前,您可以讓它「等待」按鈕點擊(使用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() {} 
} 

它丹尼爾·埃裏克的AsyncIOPipe想法稍作修改:http://smellegantcode.wordpress.com/2008/12/05/asynchronous-sockets-with-yield-return-of-lambdas/

+0

不錯 - 我喜歡你可以在那裏使用控制塊! – lobsterism 2012-04-18 18:19:53

8

下面是這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。另見this blog

如果您使用VS11/NetFx45/C#5,你可以用C#async/await類似的事情和的Action代表Tuple一個List

5

使用異步CTP或.NET 4.5(C#5),使用異步方法和await操作符非常簡單。這可以直接在UI線程上調用,它將按預期工作。

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

如果您使用.Net 4.5的測試版,那會是'Task.Delay()'。 – svick 2012-03-23 20:00:04

0

如果你可以使用C#4.5做了,去Firoso後:這是最好的方式完成,在C#中,正是異步是爲建造。

但是,如果你不能,可能有一些方法可以做到這一點。我願意做一個「簡單」的經理做:

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 += "First\n"), 
       new TimedEvent(5000,() => textBox1.Text += "Second\n"), 
       new TimedEvent(2000,() => textBox1.Text += "Third\n") 
      ); 

    } 

    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(),但請記住,這是一個不同的控制)

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

你唯一要做的事情,使這個工作,是確保你的函數功能的返回值(即使這個值是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(); 
} 
+4

可能值得一提的是,這是通過[Reactive Extensions](http://msdn.microsoft.com/en-us/data/gg577609) – 2012-03-23 22:07:50

+3

這不是'是'比'Select'更好的操作符?這樣你就不用擔心函數的返回值。 – 2012-03-23 22:41:58

+2

@BryanAnderson也許,但我覺得每次我使用Do :) – 2012-03-23 22:50:35

1

有趣所有的不同反應。這是一個簡單的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; } 
    } 
}