2014-10-06 24 views
2

我有一個操作列表和一個按鈕。正確的鏈式任務取決於任務狀態(已完成/出錯)

當用戶點擊按鈕時,執行的動作爲,按順序

每次操作完成時,它會設置一個標誌(更新UI),並繼續執行下一個操作。

  • 如果某個操作失敗,則所有其餘的操作將停止執行,並啓動一個錯誤例程。

  • 如果所有操作都成功,則啓動成功例程。

假設:每個動作的執行需要很長的時間,並具有UI線程

上由於每個動作都在UI線程上執行的,我用任務來強制要執行短暫的延遲,以允許用戶界面在繼續下一個動作之前進行更新。

我設法讓它工作(以某種方式)使用任務和鏈接在一起。

但我不確定這是否正確或最好的方法,並且如果有人可以檢查我的實現,我將不勝感激?

嘗試代碼:

  • 檢查所有項目,並運行:所有項目應變綠,成功味精盒

  • 取消選中一個項目,然後運行:選中的項目變爲紅色,錯誤msg框,其餘動作停止運行

Xaml:

<Window x:Class="Prototype.MainWindow" 
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
     xmlns:cv="clr-namespace:Prototype" 
     Title="MainWindow" Height="450" Width="450"> 
    <DockPanel x:Name="RootGrid" > 
     <!-- Run --> 
     <Button Content="Run" 
       Click="OnRun" 
       DockPanel.Dock="top" /> 

     <!-- Instructions --> 
     <TextBlock DockPanel.Dock="Top" 
        Text="Uncheck to simulate failure"/> 

     <!-- List of actions --> 
     <ItemsControl ItemsSource="{Binding Actions}"> 
      <ItemsControl.ItemTemplate> 
       <DataTemplate DataType="{x:Type cv:ActionVm}"> 
        <Grid x:Name="BgGrid"> 
         <CheckBox Content="Action" 
            IsChecked="{Binding IsSuccess,Mode=TwoWay}"/> 
        </Grid> 
        <DataTemplate.Triggers> 
         <!-- Success state --> 
         <DataTrigger Binding="{Binding State}" 
            Value="{x:Static cv:State.Success}"> 
          <Setter TargetName="BgGrid" 
            Property="Background" 
            Value="Green" /> 
         </DataTrigger> 

         <!-- Failure state --> 
         <DataTrigger Binding="{Binding State}" 
            Value="{x:Static cv:State.Failure}"> 
          <Setter TargetName="BgGrid" 
            Property="Background" 
            Value="Red" /> 
         </DataTrigger> 
        </DataTemplate.Triggers> 
       </DataTemplate> 
      </ItemsControl.ItemTemplate> 
     </ItemsControl> 
    </DockPanel> 
</Window> 

代碼後面:

using System; 
using System.Collections.ObjectModel; 
using System.ComponentModel; 
using System.Linq; 
using System.Runtime.CompilerServices; 
using System.Threading; 
using System.Threading.Tasks; 
using System.Windows; 
using System.Windows.Controls; 
using Prototype.Annotations; 

namespace Prototype 
{ 
    public partial class MainWindow 
    { 
     public MainViewModel Main { get; set; } 

     public MainWindow() 
     { 
      // Caller injects scheduler to use when executing action 
      Main = new MainViewModel(TaskScheduler.FromCurrentSynchronizationContext()); 
      InitializeComponent(); 
      DataContext = Main; 
     } 

     // User clicks on run 
     private void OnRun(object sender, RoutedEventArgs e) 
     { 
      Main.RunAll(); 
     } 
    } 

    public class MainViewModel 
    { 
     private TaskScheduler ActionScheduler { get; set; } 
     private TaskScheduler InternalUIScheduler { get; set; } 

     // List of actions 
     public ObservableCollection<ActionVm> Actions { get; set; } 

     // Constructor 
     // Injected Scheduler to use when executing an action 
     public MainViewModel(TaskScheduler actionScheduler) 
     { 
      ActionScheduler = actionScheduler; 
      InternalUIScheduler = TaskScheduler.FromCurrentSynchronizationContext(); 

      Actions = new ObservableCollection<ActionVm>(); 
      Actions.Add(new ActionVm()); 
      Actions.Add(new ActionVm()); 
      Actions.Add(new ActionVm()); // Mock exception. 
      Actions.Add(new ActionVm()); 
      Actions.Add(new ActionVm()); 
     } 

     // Runs all actions 
     public void RunAll() 
     { 
      // Reset state 
      foreach(var action in Actions) action.State = State.Normal; 

      // Run 
      RunAction(); 
     } 

     // Recursively chain actions 
     private void RunAction(int index=0, Task task=null) 
     { 

      if (index < Actions.Count) 
      { 
       ActionVm actionVm = Actions[index]; 
       if (task == null) 
       { 
        // No task yet. Create new. 
        task = NewRunActionTask(actionVm); 
       } 
       else 
       { 
        // Continue with 
        task = ContinueRunActionTask(task, actionVm); 
       } 

       // Setup for next action (On completed) 
       // Continue with a sleep on another thread (to allow the UI to update) 
       task.ContinueWith(
        taskItem => { Thread.Sleep(10); } 
        , CancellationToken.None 
        , TaskContinuationOptions.AttachedToParent | TaskContinuationOptions.OnlyOnRanToCompletion 
        , TaskScheduler.Default) 

        .ContinueWith(
         taskItem => { RunAction(index + 1, taskItem); } 
         , CancellationToken.None 
         , TaskContinuationOptions.AttachedToParent | TaskContinuationOptions.OnlyOnRanToCompletion 
         , TaskScheduler.Default); 

       // Setup for error (on faulted) 
       task.ContinueWith(
        taskItem => 
        { 
         if (taskItem.Exception != null) 
         { 
          var exception = taskItem.Exception.Flatten(); 
          var msg = string.Join(Environment.NewLine, exception.InnerExceptions.Select(e => e.Message)); 
          MessageBox.Show("Error routine: " + msg); 
         } 
        } 
        , CancellationToken.None 
        , TaskContinuationOptions.AttachedToParent | TaskContinuationOptions.OnlyOnFaulted 
        , InternalUIScheduler); 
      } 
      else 
      { 
       // No more actions to run 
       Task.Factory.StartNew(() => 
       { 
        new TextBox(); // Mock final task on UI thread 
        MessageBox.Show("Success routine"); 
       } 
        , CancellationToken.None 
        , TaskCreationOptions.AttachedToParent 
        , InternalUIScheduler); 
      } 
     } 


     // Continue task to run action 
     private Task ContinueRunActionTask(Task task, ActionVm action) 
     { 
      task = task.ContinueWith(
       taskItem => action.Run() 
       , CancellationToken.None 
       , TaskContinuationOptions.AttachedToParent 
       , ActionScheduler); 
      return task; 
     } 

     // New task to run action 
     public Task NewRunActionTask(ActionVm action) 
     { 
      return Task.Factory.StartNew(
       action.Run 
       , CancellationToken.None 
       , TaskCreationOptions.AttachedToParent 
       , ActionScheduler); 
     } 
    } 

    public class ActionVm:INotifyPropertyChanged 
    { 
     // Flag to mock if the action executes successfully 
     public bool IsSuccess 
     { 
      get { return _isSuccess; } 
      set { _isSuccess = value; OnPropertyChanged();} 
     } 

     // Runs the action 
     public void Run() 
     { 
      if (!IsSuccess) 
      { 
       // Mock failure. 
       // Exceptions propagated back to caller. 

       // Update state (view) 
       State = State.Failure; 
       throw new Exception("Action failed"); 
      } 
      else 
      { 
       // Mock success 
       // Assumes that the action is always executed on the UI thread 
       new TextBox(); 
       Thread.Sleep(1000); 

       // Update state (view) 
       State = State.Success; 
      } 
     } 

     private State _state; 
     private bool _isSuccess = true; 

     // View affected by this property (via triggers) 
     public State State 
     { 
      get { return _state; } 
      set { _state = value; OnPropertyChanged(); } 
     } 

     public event PropertyChangedEventHandler PropertyChanged; 

     [NotifyPropertyChangedInvocator] 
     protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) 
     { 
      PropertyChangedEventHandler handler = PropertyChanged; 
      if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName)); 
     } 
    } 

    public enum State 
    { 
     Normal, 
     Success, 
     Failure 
    } 

} 

[更新1]

只是爲了澄清,在示例代碼,ActionVm被假定爲一個黑箱。它的Run()方法假定在UI線程上是一個耗時的操作,並且在完成時會自動設置其內部狀態屬性(視圖有界)。

我可以修改/控制的唯一類是MainViewModel(運行每個任務,然後是成功/失敗例程)。

如果我所做的只是一個foreach-Run(),UI將被鎖定,並且沒有可見的反饋,即在所有操作完成之前,操作的狀態會發生更改。

因此,我試圖在執行Actions之間添加一個非UI延遲,以允許視圖綁定到ActionVm.State,以便在下一次阻塞運行之前至少重新繪製。

ActionVms是長時間運行的操作,將阻塞UI線程。這是正確工作所必需的。至少我試圖做的是提供一些視覺反饋給用戶,仍然在運行。

+0

爲什麼任務*有*要在UI線程上執行?假設他們必須這樣做,Application.DoEvents就是這種情況。 – usr 2014-10-06 16:00:54

+0

這些操作涉及調用方法和訪問僅在UI線程上可用的屬性。 ActionVm的實際執行超出了我的控制範圍。但是,我的代碼和調用者之間的代碼契約需要在UI線程上執行ActionVm.Run()以正確執行。 – jayars 2014-10-06 16:17:39

+0

由於這是工作代碼,因此您應該在http://codereview.stackexchange.com上找到更好的響應。 – Hogan 2014-10-06 16:35:36

回答

-1

假設您需要在UI線程上運行此項工作,您所能做的就是不時處理事件。你的方式可行,但yielding to the event loop regularly可以達到同樣的效果。這樣做經常足以讓UI看起來很敏感。我認爲每10ms調用一次會是一個很好的目標區間。

通過輪詢處理UI事件有嚴重的缺點。 There is good discussion on the WinForms equivalent DoEvents that mostly applies to WPF。由於無法避免在您的情況下在UI線程上運行工作,因此可以使用它。從好的一面來看,它很容易使用和解開你的代碼。

您現有的方法可以改進:

var myActions = ...; 
foreach (var item in myActions) { 
item.Run(); //run on UI thread 
await Task.Delay(TimeSpan.FromMilliseconds(10)); 
} 

這基本上實現了現有的結構做同樣的事情。從.NET 4.0開始可以使用await

我更喜歡Task.Delay版本的UI事件輪詢方法。而且我更傾向於對現在使用的糾結代碼進行輪詢。由於它很難測試,因此很難使其無bug。

0

假設您正在執行的操作只需要短時間訪問用戶界面(因此大部分時間用於執行可在任何線程上執行的計算),那麼您可以使用async - await。喜歡的東西:

Func<Task> action1 = async() => 
{ 
    // start on the UI thread 
    new TextBox(); 

    // execute expensive computation on a background thread, 
    // so the UI stays responsive 
    await Task.Run(() => Thread.Sleep(1000)); 

    // back on the UI thread 
    State = State.Success; 
}; 

,然後執行這樣的:

var actions = new[] { action1 }; 

try 
{ 
    foreach (var action in actions) 
    { 
     await action(); 
    } 

    MessageBox.Show("Success routine"); 
} 
catch (Exception ex) 
{ 
    MessageBox.Show("Error routine: " + ex.Message); 
} 

由於我使用async - 在上面的代碼await,你需要一個C#5.0編譯器這一點。

+0

ActionVm.Run()的內部是一個黑盒子。 Thread.Sleep(1000)模擬/模擬在UI線程上執行的1秒鐘的操作。假設我可以修改的唯一類是MainViewModel,是否仍可以使用async/await? – jayars 2014-10-06 23:31:47

+0

不是這樣,不。 – svick 2014-10-06 23:32:43