3

I have a list of actions, and a button.

When the user clicks on the button, the actions are executed in order.

Each time an action completes, it sets a flag (updates the UI), and continue to the next action.

  • If an action fails, all remaining actions stop executing, and an error routine is started.

  • If all actions succeeded, a success routine is started.

Assumption: Each action's execution takes a long time, and has to be executed on the UI thread

Because each action is executed on the UI thread, I'm using Tasks to force a short delay, to allow the UI to update before moving on to the next action.

I've managed to get it to work (somehow) using Tasks and chaining them together.

But I'm not sure if this is correct or the best method, and would appreciate it if someone can review my implementation?

Try the code:

  • Check all items and run: All items should turn green, success msg box

  • Uncheck an item and run: unchecked item turns red, error msg box, remaining actions stop running

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>

Code behind:

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
    }

}

[update 1]

Just to clarify, in the sample code, ActionVm is assumed to be a blackbox. Its Run() method is assumed to be a time-consuming operation on the UI thread, and when completed, will automatically set its internal State property (view bounded).

The only class that I can modify/have control over is the MainViewModel (runs each task, followed by success/failure routines).

If all I did was a foreach-Run(), the UI will be locked, with no visible feedback that actions' states are changed until all actions are completed.

Hence, I'm trying to add a non-UI delay between executing Actions to allow the view binding to ActionVm.State to at least redraw before the next blocking run.

ActionVms are long-running operations that will block the UI thread. This is necessary for it to work correctly. The least I'm trying to do is to provide some visual feedback to the user that things are still running.

jayars
  • 1,347
  • 3
  • 13
  • 25
  • Why do the tasks *have* to be executed on the UI thread? Assuming they have to, this is a case for Application.DoEvents. – usr Oct 06 '14 at 16:00
  • The actions involve calling methods and accessing properties that are only available on the UI thread. The actual implementation of ActionVm is out of my control. However, the code contract between my code and the caller requires ActionVm.Run() to be executed on the UI thread to be correct. – jayars Oct 06 '14 at 16:17
  • Since this is working code, you should find you get better response on http://codereview.stackexchange.com – Hogan Oct 06 '14 at 16:35
  • There is *never* a case for `DoEvents`. http://stackoverflow.com/questions/11352301/how-to-use-doevents-without-being-evil – Peter Ritchie Oct 06 '14 at 16:37
  • If you perform tasks that take a long time on the UI thread, your UI will be unusable *never* perform tasks that take a long time on the UI thread. If you have that many tasks that require chaining, I'd suggest a queue of tasks and just run them one by one. – Peter Ritchie Oct 06 '14 at 16:39

2 Answers2

0

Assuming that the actions you're executing only need to access the UI for short periods of time (so most of the time is spend doing calculations that can be executed on any thread), then you can write those actions using async-await. Something like:

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;
};

And then execute it like this:

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);
}

Since I'm using async-await in the code above, you'll need a C# 5.0 compiler for this.

svick
  • 236,525
  • 50
  • 385
  • 514
  • The internals of ActionVm.Run() is a blackbox. The Thread.Sleep(1000) mocks/simulates 1-second-worth of operations executed on the UI thread. Assuming the only class that I can modify is MainViewModel, is it still possible to use async/await? – jayars Oct 06 '14 at 23:31
-1

Assuming that you need to run this work on the UI thread all you can do is process events from time to time. Your way of doing that works but the same thing can be achieved by yielding to the event loop regularly. Do this often enough to have the UI seem responsive. I think calling it every 10ms would be a good target interval.

Processing UI events by polling has serious drawbacks. There is good discussion on the WinForms equivalent DoEvents that mostly applies to WPF. As there is no way to avoid running work on the UI thread in your case it is appropriate to use it. On the plus side it is very easy to use and untangles your code.

Your existing approach can be improved:

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

This achieves basically the same thing that your existing construct does. await is available starting with .NET 4.0.

I'd prefer the Task.Delay version over the UI event polling approach. And I'd prefer the polling over the tangled code that you are using right now. It will be very hard to make it bug free because it is hard to test.

Community
  • 1
  • 1
usr
  • 168,620
  • 35
  • 240
  • 369