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.