【问题标题】:Correct way to chain Tasks depending on task status (completed/faulted)根据任务状态(已完成/失败)链接任务的正确方法
【发布时间】:2014-10-07 07:59:01
【问题描述】:

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

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

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

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

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

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

因为每个动作都是在 UI 线程上执行的,所以我使用 Tasks 来强制进行短暂的延迟,以允许 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 将被锁定,在所有操作完成之前,没有可见的反馈表明操作的状态已更改。

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

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

【问题讨论】:

  • 为什么任务要在UI线程上执行?假设他们必须这样做,这是 Application.DoEvents 的情况。
  • 这些操作涉及调用方法和访问仅在 UI 线程上可用的属性。 ActionVm 的实际实现是我无法控制的。但是,我的代码和调用者之间的代码契约要求在 UI 线程上执行 ActionVm.Run() 才能正确。
  • 因为这是工作代码,你应该会发现你在codereview.stackexchange.com得到更好的响应
  • DoEvents 的情况从未stackoverflow.com/questions/11352301/…
  • 如果您在 UI 线程上执行需要很长时间的任务,您的 UI 将无法使用永远在 UI 线程上执行需要很长时间的任务。如果您有那么多需要链接的任务,我建议您使用一系列任务,然后一个一个地运行它们。

标签: c# wpf task-parallel-library


【解决方案1】:

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

【讨论】:

  • ActionVm.Run() 的内部是一个黑盒。 Thread.Sleep(1000) 模拟/模拟在 UI 线程上执行的 1 秒操作。假设我唯一可以修改的类是 MainViewModel,是否仍然可以使用 async/await?
【解决方案2】:

假设您需要在 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));
}

这与您现有的构造基本相同。 await 从 .NET 4.0 开始可用。

与 UI 事件轮询方法相比,我更喜欢 Task.Delay 版本。而且我更喜欢轮询你现在使用的纠结代码。因为很难测试,所以很难让它没有错误。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-02-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-09-27
    • 1970-01-01
    相关资源
    最近更新 更多