根据任务状态链接任务的正确方法(已完成/出现故障)

时间:2014-10-06 15:49:17

标签: c# wpf task-parallel-library

我有一个动作列表和一个按钮。

当用户点击该按钮时,操作 按顺序执行

每次操作完成时,它都会设置一个标志(更新UI),然后继续执行下一个操作。

  • 如果某个操作失败,所有剩余的操作都会停止执行,并启动错误例程。

  • 如果所有操作都成功,则启动成功例程。

假设:每个动作的执行需要很长时间,并且必须在UI线程上执行

因为每个操作都是在UI线程上执行的,所以我使用“任务”强制执行短暂延迟,以允许UI在继续执行下一个操作之前进行更新。

我设法让它(以某种方式)使用任务并将它们链接在一起工作。

但我不确定这是正确的还是最好的方法,如果有人可以审核我的实施,我会很感激吗?

尝试代码:

  • 检查所有项目并运行:所有项目应变为绿色,成功消息框

  • 取消选中一项并运行:未选中的项目变为红色,错误信息框,剩余的操作停止运行

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线程上是一个耗时的操作,完成后,将自动设置其内部State属性(视图有界)。

我可以修改/控制的唯一类是MainViewModel(运行每个任务,然后是成功/失败例程)。

如果我所做的只是一个foreach-Run(),那么UI将被锁定,没有可见的反馈,即行动&#39;状态会发生变化,直到所有操作都完成。

因此,我试图在执行动作之间添加非UI延迟,以允许对ActionVm.State的视图绑定至少在下一次阻塞运行之前重绘。

ActionVms是长时间运行的操作,会阻止UI线程。这是正常工作所必需的。我至少尝试做的是向用户提供一些视觉反馈,告知事情仍在运行。

2 个答案:

答案 0 :(得分:0)

假设您执行的操作只需要在短时间内访问UI(因此大部分时间花费在可以在任何线程上执行的计算),那么您可以使用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编译器。

答案 1 :(得分:-1)

假设您需要在UI线程上运行此工作,您所能做的就是不时处理事件。你这样做的方式有效,但同样的事情可以通过yielding to the event loop regularly来实现。这样做通常足以让UI看起来很敏感。我认为每隔10毫秒调用它将是一个很好的目标间隔。

通过轮询处理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。