根据任务状态(已完成/出现故障)来连锁任务的正确方式

本文关键字:任务 方式 状态 已完成 故障 | 更新日期: 2023-09-27 18:25:45

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

当用户单击按钮时,操作将按顺序执行

每次操作完成时,它都会设置一个标志(更新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(因此大部分时间都花在了可以在任何线程上执行的计算上),那么您可以使用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编译器。

假设您需要在UI线程上运行这项工作,您所能做的就是不时地处理事件。你的方法是有效的,但同样的事情也可以通过定期向事件循环屈服来实现。经常这样做,使UI看起来响应迅速。我认为每10毫秒一次是一个很好的目标间隔。

通过轮询处理UI事件有严重的缺点。关于WinForms等价的DoEvents(主要适用于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版本。我更喜欢轮询,而不是你现在使用的复杂代码。很难让它没有bug,因为它很难测试。