2012-03-23 12 views
6

Tengo una aplicación que tiene que hacer el siguiente tipo de cosas, de preferencia en el hilo de interfaz gráfica de usuario ya que es donde la mayor parte de la acción tiene lugar y no hay operaciones de marcha larga:¿Cómo se inicia una secuencia cronometrada de eventos en el hilo de la GUI en C#?

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

realizo podría utilizar un temporizador con una función OnTick estilo de máquina de estados, pero que parece engorroso:

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

Además me gustaría ser capaz de hacer que sea lo suficientemente genérico como para hacer algo así

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

¿Hay una manera idiomática de hacer este tipo de cosas? Dado todo el material de TPL, o Rx, o incluso las expresiones de cálculo en F #, asumiría que existe uno, pero no lo estoy encontrando.

+0

Lo interfaz gráfica de usuario está utilizando? Winforms, WPF o algo más? – svick

+0

WPF por ahora, pero me gustaría una solución (o al menos una técnica) que funcione en WinForms también. – lobsterism

+4

Barra lateral: un cambio que haría, en todas las soluciones sugeridas, es utilizar TimeSpan en lugar de enteros para la demora. 1000 podría significar un segundo, 1000 segundos, etc. TimeSpan es inherentemente claro y no ambiguo. –

Respuesta

1

Aquí está una manera de combinar "yield return" y el marco reactiva para darle una "asincronía de pobres". Básicamente le permite "esperar" cualquier IObservable. Aquí solo lo uso para temporizadores ya que eso es lo que le interesaba, pero puede tenerlo "aguardando" clics de botón (usando un Subject<Unit>) etc. antes de pasar al siguiente paso también.

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

Es una ligera modificación de la idea de Daniel AsyncIOPipe Earwicker: http://smellegantcode.wordpress.com/2008/12/05/asynchronous-sockets-with-yield-return-of-lambdas/ biblioteca

+0

Bien - ¡Me gusta que también puedas usar bloques de control! – lobsterism

8

Aquí hay un bosquejo de esto en 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 

que está en una aplicación de consola, pero sólo se puede llamar runOps sobre el hilo GUI. Vea también this blog.

Si está utilizando VS11/NetFx45/C# 5, se puede hacer una cosa similar con C# async/await y una List de TupleAction de delegados.

5

usando async CTP o .NET 4.5 (C# 5) es REALMENTE fácil usando un método asíncrono y el operador de espera. Esto se puede invocar directamente en el hilo de la interfaz de usuario y funcionará como se espera.

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

Si usabas beta de .Net 4.5, eso sería 'Task.Delay()'. – svick

0

Si puede utilizar el C# 4.5 hacerlo, ir con Firoso mensaje: es la mejor manera de lograr que en C#, exactamente lo asíncrono fue construido para.

Sin embargo, si no puede, puede haber algunas formas de hacerlo. Yo haría un gestor de "simple" para hacerlo:

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

Mira que este ejemplo es Winforms específica (utiliza Control.Invoke()). Necesitará una versión ligeramente diferente para WPF, que utiliza el asignador de subprocesos para lograr lo mismo. (Si la memoria no me falla, también puede utilizar Control.Dispatcher.Invoke(), pero tenga en cuenta que se trata de un control diferente)

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

Lo único que tiene que hacer para que este trabajo, es asegurarse de que su Func de devolver un valor (incluso si ese valor es Unit.Default, es decir, nada)

Editar: Aquí es cómo hacer una versión genérico:

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

Probablemente vale la pena mencionar para los no iniciados que esto es a través de [Reactive Extensions] (http://msdn.microsoft.com/en-us/data/gg577609) –

+3

No 'Do' sería un mejor operador que' Select' allí ? De esa forma no necesitarás preocuparte por el valor de retorno de las funciones. –

+2

@BryanAnderson Probablemente, pero me siento asqueroso cada vez que uso Do :) –

1

interesantes todas las diferentes respuestas . Aquí hay una opción de bricolaje simple que no depende de ninguna otra biblioteca, y no engancha los recursos de hilo innecesariamente.

Básicamente, para cada acción en su lista, crea una función onTick que ejecuta esa acción, luego llama recurrentemente a DoThings con las acciones y demoras restantes.

Aquí, ITimer es sólo un simple envoltorio alrededor DispatcherTimer (pero sería trabajar con un temporizador SWF también, o un temporizador simulacro para las pruebas unitarias), y DelayedAction es sólo una tupla con int Delay y 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; 
    } 
} 

Si no desea profundizar en F # o hacer referencia a Rx o usar .Net 4.5, esta es una solución viable simple.

Aquí hay un ejemplo de cómo probarlo:

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

Y aquí está el resto de clases para que se compile:

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