2008-09-30 11 views
13

Hinweis: Der Code in dieser Frage ist Teil von deSleeper, wenn Sie die vollständige Quelle möchten.Asynchrone WPF-Befehle

Eines der Dinge, die ich von Befehlen wollte, war ein gebackenes Design für asynchrone Operationen. Ich wollte, dass die Schaltfläche während der Ausführung des Befehls deaktiviert und nach Abschluss wieder ausgeführt wurde. Ich wollte, dass die eigentliche Arbeit in einem ThreadPool-Arbeitselement ausgeführt wird. Und schließlich wollte ich eine Möglichkeit haben, mit Fehlern umzugehen, die während der asynchronen Verarbeitung aufgetreten sind.

Meine Lösung war ein AsyncCommand:

public abstract class AsyncCommand : ICommand 
{ 
    public event EventHandler CanExecuteChanged; 
    public event EventHandler ExecutionStarting; 
    public event EventHandler<AsyncCommandCompleteEventArgs> ExecutionComplete; 

    public abstract string Text { get; } 
    private bool _isExecuting; 
    public bool IsExecuting 
    { 
     get { return _isExecuting; } 
     private set 
     { 
      _isExecuting = value; 
      if (CanExecuteChanged != null) 
       CanExecuteChanged(this, EventArgs.Empty); 
     } 
    } 

    protected abstract void OnExecute(object parameter); 

    public void Execute(object parameter) 
    { 
     try 
     { 
      IsExecuting = true; 
      if (ExecutionStarting != null) 
       ExecutionStarting(this, EventArgs.Empty); 

      var dispatcher = Dispatcher.CurrentDispatcher; 
      ThreadPool.QueueUserWorkItem(
       obj => 
       { 
        try 
        { 
         OnExecute(parameter); 
         if (ExecutionComplete != null) 
          dispatcher.Invoke(DispatcherPriority.Normal, 
           ExecutionComplete, this, 
           new AsyncCommandCompleteEventArgs(null)); 
        } 
        catch (Exception ex) 
        { 
         if (ExecutionComplete != null) 
          dispatcher.Invoke(DispatcherPriority.Normal, 
           ExecutionComplete, this, 
           new AsyncCommandCompleteEventArgs(ex)); 
        } 
        finally 
        { 
         dispatcher.Invoke(DispatcherPriority.Normal, 
          new Action(() => IsExecuting = false)); 
        } 
       }); 
     } 
     catch (Exception ex) 
     { 
      IsExecuting = false; 
      if (ExecutionComplete != null) 
       ExecutionComplete(this, new AsyncCommandCompleteEventArgs(ex)); 
     } 
    } 

    public virtual bool CanExecute(object parameter) 
    { 
     return !IsExecuting; 
    } 
} 

so ist die Frage: Ist das alles nötig? Ich habe bemerkt, eingebaute asynchrone Unterstützung für die Datenbindung, also warum Befehlsausführung nicht? Vielleicht hängt es mit der Parameterfrage zusammen, die meine nächste Frage ist.

+0

Eines der Dinge, die hier ein Problem ist, dass das normale Design für CanExecute ist, wo command- spezifische Logik ist da. Z.B. UndoCommand überprüft möglicherweise, ob ein UndoStack vorhanden ist. Die einzige Logik, die Sie hier haben, ist, ob sie bereits ausgeführt wird oder nicht. –

+0

Beachten Sie, dass CanExecute virtuell ist. In diesem Muster überschreibe ich es und rufe die Basis zuerst auf, um eine befehlsspezifische Logik bereitzustellen. – nedruod

Antwort

4

Ich war in der Lage, die ursprüngliche Probe zu verfeinern und habe ein paar Tipps für alle anderen, die in ähnliche Situationen geraten.

Zunächst überlegen, ob BackgroundWorker die Bedürfnisse erfüllen wird. Ich benutze immer noch häufig AsyncCommand, um die automatische Deaktivierungsfunktion zu erhalten, aber wenn viele Dinge mit BackgroundWorker erledigt werden könnten.

Aber durch Background Einwickeln, bietet AsyncCommand Befehl wie Funktionalität mit asynchronem Verhalten (Ich habe auch ein blog entry on this topic)

public abstract class AsyncCommand : ICommand 
{ 
    public event EventHandler CanExecuteChanged; 
    public event EventHandler RunWorkerStarting; 
    public event RunWorkerCompletedEventHandler RunWorkerCompleted; 

    public abstract string Text { get; } 
    private bool _isExecuting; 
    public bool IsExecuting 
    { 
     get { return _isExecuting; } 
     private set 
     { 
      _isExecuting = value; 
      if (CanExecuteChanged != null) 
       CanExecuteChanged(this, EventArgs.Empty); 
     } 
    } 

    protected abstract void OnExecute(object parameter); 

    public void Execute(object parameter) 
    { 
     try 
     { 
      onRunWorkerStarting(); 

      var worker = new BackgroundWorker(); 
      worker.DoWork += ((sender, e) => OnExecute(e.Argument)); 
      worker.RunWorkerCompleted += ((sender, e) => onRunWorkerCompleted(e)); 
      worker.RunWorkerAsync(parameter); 
     } 
     catch (Exception ex) 
     { 
      onRunWorkerCompleted(new RunWorkerCompletedEventArgs(null, ex, true)); 
     } 
    } 

    private void onRunWorkerStarting() 
    { 
     IsExecuting = true; 
     if (RunWorkerStarting != null) 
      RunWorkerStarting(this, EventArgs.Empty); 
    } 

    private void onRunWorkerCompleted(RunWorkerCompletedEventArgs e) 
    { 
     IsExecuting = false; 
     if (RunWorkerCompleted != null) 
      RunWorkerCompleted(this, e); 
    } 

    public virtual bool CanExecute(object parameter) 
    { 
     return !IsExecuting; 
    } 
} 
+0

Die Verwendung der BackgroundWorker-Klasse sollte für Sie in Ordnung sein. Beachten Sie jedoch, dass diese Klasse ursprünglich für WinForms erstellt wurde. Warum nutzen Sie dafür nicht die Aufgabenbibliothek? – UrbanEsc

+0

Ein guter aktualisierter Vorschlag .. die Antwort ist jedoch ziemlich einfach .. zu der Zeit, als die Aufgabenbibliothek nicht existierte. – nedruod

+2

Im Fall von TPL würde ich den BackgroundWorker durch solche Codezeilen ersetzen "Task.Factory.StartNew (() => OnExecute (Parameter)). ContinueWith (Aufgabe => OnExecuteCompleted (task.Exception), TaskScheduler.FromCurrentSynchronizationContext()) ; ' – Luke

1

Da ich in Ihrer anderen Frage geantwortet habe, möchten Sie wahrscheinlich immer noch synchron dazu binden und dann die Befehle asynchron starten. Auf diese Weise vermeiden Sie die Probleme, die Sie jetzt haben.