2012-03-23 7 views
6

Ho un app che ha a che fare il seguente tipo di cose, preferibilmente sul filo GUI in quanto è lì che la maggior parte dell'azione è in atto e non ci sono ops a lungo in esecuzione:Come si avvia una sequenza temporale di eventi sul thread della GUI in C#?

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

mi rendo conto potrei usare un timer con una funzione onTick state stile macchina, ma che sembra ingombrante:

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; 
     } 
    } 

Inoltre mi piacerebbe essere in grado di rendere sufficientemente generico per fare qualcosa di simile

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

Esiste un modo idiomatico per fare questo genere di cose? Dato tutto il materiale di TPL, o Rx, o anche le espressioni di calcolo in F #, suppongo che ne esista uno, ma non lo trovo.

+0

Cosa GUI stai usando? Winforms, WPF o qualcos'altro? – svick

+0

WPF per ora, ma mi piacerebbe una soluzione (o almeno una tecnica) che funzionasse anche su WinForms. – lobsterism

+4

Sidebar: una modifica che apporterei - attraverso tutte le soluzioni suggerite - consiste nell'utilizzare TimeSpan anziché integer per il ritardo. 1000 potrebbe significare un secondo, 1000 secondi, ecc. TimeSpan è intrinsecamente chiaro e non ambiguo. –

risposta

1

Ecco un modo per combinare "dei rendimenti" e il quadro reattivo per darti un "povero uomo asincrono". In pratica ti consente di "attendere" qualsiasi IObservable. Qui lo uso solo per i timer poiché è quello che ti interessa, ma puoi farlo "attendere" i clic del pulsante (usando un Subject<Unit>) ecc prima di passare alla prossima cosa.

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() {} 
} 

E 'una leggera modifica dell'idea AsyncIOPipe di Daniel Earwicker: http://smellegantcode.wordpress.com/2008/12/05/asynchronous-sockets-with-yield-return-of-lambdas/ biblioteca

+0

Bello - Mi piace il fatto che puoi usare anche i blocchi di controllo! – lobsterism

8

Ecco uno schizzo di questo in 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 

che è in una console app, ma si può chiamare runOps sul filo GUI. Vedi anche this blog.

Se stai usando VS11/NetFx45/C# 5, si può fare una cosa simile con C# async/await e un List di Tuple di Action delegati.

5

utilizzando il CTP asincrono o .NET 4.5 (C# 5) è REALMENTE facile utilizzando un metodo asincrono e l'operatore di attesa. Questo può essere chiamato direttamente sul thread dell'interfaccia utente e funzionerà come previsto.

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

Se si stesse utilizzando la versione beta di .Net 4.5, sarebbe 'Task.Delay()'. – svick

0

Se è possibile utilizzare il C# 4.5 per farlo, andare con Firoso postale: è il modo migliore realizzare questo in C#, esattamente ciò che è stato costruito per asincrona.

Tuttavia, se non è possibile, potrebbero esserci alcuni modi per farlo. Farei un manager "semplice" per farlo:

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(); 
    } 
} 

Attenzione che questo esempio è WinForms specifica (usa Control.Invoke()). Avrai bisogno di una versione leggermente diversa per WPF, che usa il dispatcher di thread per ottenere la stessa cosa. (Se la memoria non mi manca, è anche possibile utilizzare Control.Dispatcher.Invoke(), ma tenete a mente che si tratta di un controllo diverso)

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

L'unica cosa che dovete fare per rendere questo lavoro, è assicurarsi che il Func di restituire un valore (anche se tale valore è Unit.Default, cioè niente)

Edit: Ecco come fare una versione generica:

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

Probabilmente vale la pena menzionare per chi non lo sapesse che ciò avviene tramite [Reactive Extensions] (http://msdn.microsoft.com/en-us/data/gg577609) –

+3

Non sarebbe "Do" essere un operatore migliore di "Seleziona" lì ? In questo modo non dovrai preoccuparti del valore di ritorno delle funzioni. –

+2

@BryanAnderson Probabilmente, ma mi sento disgustoso ogni volta che uso Do :) –

1

interessanti tutte le diverse risposte . Ecco una semplice opzione fai-da-te che non dipende da altre librerie e non utilizza risorse thread inutilmente.

In pratica, per ogni azione nell'elenco, crea una funzione onTick che esegue tale azione, quindi chiama in modo ricorsivo DoThings con le azioni rimanenti e i ritardi.

Qui, ITimer è solo un semplice involucro intorno DispatcherTimer (ma sarebbe lavorare con un timer SWF pure, o un timer finto per unit testing), e DelayedAction è solo una tupla con int Delay e Action 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; 
    } 
} 

Se non vuoi approfondire F # o fare riferimento a Rx o usare .Net 4.5 questa è una soluzione semplice e praticabile.

Ecco un esempio di come testarlo:

[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); 
    } 
} 

Ed ecco le altre classi per farlo compilare:

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; } 
    } 
} 
Problemi correlati