2014-10-06 75 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