Skip to content

Instantly share code, notes, and snippets.

@tmatz
Last active April 1, 2019 09:13
Show Gist options
  • Save tmatz/13a5617299cf72324a329f8dff63749c to your computer and use it in GitHub Desktop.
Save tmatz/13a5617299cf72324a329f8dff63749c to your computer and use it in GitHub Desktop.
use Task like a Promise
using System;
using System.Threading;
using System.Threading.Tasks;
namespace WpfAsyncCommandTest
{
public static class TaskEx
{
/// <summary>
/// 非同期処理を実行するアクションです。
/// 非同期処理が完了したら <paramref name="tcs"/> 引数の
/// <see cref="TaskCompletionSource{}.SetResult"/> メソッドで完了を通知します。
/// <para>
/// 非同期処理は必ず値を返す必要があります。
/// 返すべき値が無いときは <see cref="Unit.Value"/> を返すようにします。
/// </para>
/// </summary>
/// <typeparam name="R">非同期処理の結果の型</typeparam>
/// <param name="tcs">非同期処理の完了を通知</param>
public delegate void AsyncAction<R>(TaskCompletionSource<R> tcs);
/// <summary>
/// 非同期処理 <paramref name="action"/> を開始し、
/// 完了を通知する <see cref="Task{}"/> を返します。
/// </summary>
/// <typeparam name="TResult"></typeparam>
/// <param name="action"></param>
/// <returns></returns>
public static Task<TResult> RunAsync<TResult>(
AsyncAction<TResult> action)
{
var tcs = new TaskCompletionSource<TResult>();
try
{
action(tcs);
}
catch (Exception ex)
{
tcs.SetException(ex);
}
return tcs.Task;
}
/// <summary>
/// 現在の同期コンテキスト(≒スレッド)でタスクを非同期に継続します。
/// </summary>
/// <typeparam name="T">入力の型</typeparam>
/// <typeparam name="R">結果の型</typeparam>
/// <param name="task"></param>
/// <param name="token"></param>
/// <param name="then"></param>
/// <returns></returns>
public static Task<R> Then<T, R>(
this Task<T> task,
CancellationToken token,
Func<T, R> then)
=> task.Map(token, then);
/// <summary>
/// 現在の同期コンテキスト(≒スレッド)でタスクを非同期に継続します。
/// </summary>
/// <typeparam name="T">入力の型</typeparam>
/// <typeparam name="R">結果の型</typeparam>
/// <param name="task"></param>
/// <param name="token"></param>
/// <param name="then"></param>
/// <returns></returns>
public static Task<R> Then<T, R>(
this Task<T> task,
CancellationToken token,
Func<T, Task<R>> then)
=> task
.Bind(token, then);
/// <summary>
/// 現在の同期コンテキスト(≒スレッド)でタスクを非同期に継続します。
/// <see cref="action"/> はパイプラインの結果に影響を与えず、入力がそのまま出力されます。
/// </summary>
/// <typeparam name="T">入出力の型</typeparam>
/// <param name="task"></param>
/// <param name="token"></param>
/// <param name="action"></param>
/// <returns></returns>
public static Task<T> Tap<T>(
this Task<T> task,
CancellationToken token,
Action<T> action)
=> task.Map(token, t => { action(t); return t; });
/// <summary>
/// 現在の同期コンテキスト(≒スレッド)でタスクを非同期に継続します。
/// <see cref="func"/> はパイプラインの結果に影響を与えず、入力がそのまま出力されます。
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="task"></param>
/// <param name="token"></param>
/// <param name="func"></param>
/// <returns></returns>
public static Task<T> Tap<T>(
this Task<T> task,
CancellationToken token,
Func<T, Task> func)
=> task
.Bind(token, t => func(t).ToUnit())
.Map(token, _ => task.Result);
/// <summary>
/// 結果を持たない <see cref="Task"/> を Task&lt;Unit&gt; に変換します。
/// </summary>
/// <param name="task"></param>
/// <returns></returns>
public static Task<Unit> ToUnit(this Task task)
=> Continuation(
task,
CancellationToken.None,
() => Unit.Value);
/// <summary>
/// 現在の同期コンテキスト(≒スレッド)でタスクを非同期に継続します。
/// </summary>
/// <typeparam name="T">入力の型</typeparam>
/// <typeparam name="R">結果の型</typeparam>
/// <param name="task"></param>
/// <param name="token"></param>
/// <param name="then"></param>
/// <returns></returns>
public static Task<R> Map<T, R>(
this Task<T> task,
CancellationToken token,
Func<T, R> then)
=> Continuation(
task,
token,
() => then(task.Result));
/// <summary>
/// 現在の同期コンテキスト(≒スレッド)でタスクを非同期に完了します。
/// </summary>
/// <typeparam name="T">入力の型</typeparam>
/// <param name="task"></param>
/// <param name="resolve"></param>
/// <param name="reject"></param>
/// <returns></returns>
public static Task Done<T>(
this Task<T> task,
Action<T> resolve,
Action<Exception> reject)
=> task.ContinueWith(
t =>
{
if (t.IsFaulted)
{
reject(t.Exception.Flatten());
}
else if (t.IsCanceled)
{
reject(new TaskCanceledException());
}
else
{
try
{
resolve(t.Result);
}
catch (Exception ex)
{
reject(ex);
}
}
return Unit.Value;
},
CancellationToken.None,
TaskContinuationOptions.None,
TaskScheduler.FromCurrentSynchronizationContext());
/// <summary>
/// 現在の同期コンテキスト(≒スレッド)でタスクを非同期に継続します。
/// </summary>
/// <typeparam name="T">入力の型</typeparam>
/// <typeparam name="R">結果の型</typeparam>
/// <param name="task"></param>
/// <param name="token"></param>
/// <param name="then"></param>
/// <returns></returns>
public static Task<R> Bind<T, R>(
this Task<T> task,
CancellationToken token,
Func<T, Task<R>> then)
=> task
.Map(token, then)
.Unwrap();
/// <summary>
/// 現在の同期コンテキスト(≒スレッド)でタスクを非同期に継続します。
/// </summary>
/// <typeparam name="T">入力の型</typeparam>
/// <typeparam name="R">結果の型</typeparam>
/// <param name="task"></param>
/// <param name="token"></param>
/// <param name="then"></param>
/// <returns></returns>
public static Task<R> Select<T, R>(
this Task<T> task,
CancellationToken token,
Func<T, R> then)
=> task
.Map(token, then);
/// <summary>
/// 現在の同期コンテキスト(≒スレッド)でタスクを非同期に継続します。
/// </summary>
/// <typeparam name="T">入力の型</typeparam>
/// <typeparam name="R">結果の型</typeparam>
/// <param name="task"></param>
/// <param name="token"></param>
/// <param name="selector"></param>
/// <returns></returns>
public static Task<R> SelectMany<T, R>(
this Task<T> task,
CancellationToken token,
Func<T, Task<R>> selector)
=> task
.Bind(token, selector);
/// <summary>
/// 現在の同期コンテキスト(≒スレッド)でタスクを非同期に継続します。
/// </summary>
/// <typeparam name="T">入力の型</typeparam>
/// <typeparam name="U">中間の型</typeparam>
/// <typeparam name="R">結果の型</typeparam>
/// <param name="task"></param>
/// <param name="token"></param>
/// <param name="binder"></param>
/// <param name="then"></param>
/// <returns></returns>
public static Task<R> SelectMany<T, U, R>(
this Task<T> task,
CancellationToken token,
Func<T, Task<U>> binder,
Func<T, U, Task<R>> then)
=> task
.Bind(token, binder)
.Bind(token, u => then(task.Result, u));
/// <summary>
/// 現在の同期コンテキスト(≒スレッド)でタスクを非同期に継続します。
/// <para>
/// キャンセル、および例外は後続のタスクに伝搬します。
/// </para>
/// </summary>
/// <typeparam name="R">結果の型</typeparam>
/// <param name="task"></param>
/// <param name="token"></param>
/// <param name="then"></param>
/// <returns></returns>
private static Task<R> Continuation<R>(
Task task,
CancellationToken token,
Func<R> then)
{
var tcs = new TaskCompletionSource<R>();
task.ContinueWith(
t =>
{
if (t.IsFaulted)
{
tcs.SetException(t.Exception.InnerExceptions);
}
else if (t.IsCanceled)
{
tcs.SetCanceled();
}
else if (token.IsCancellationRequested)
{
tcs.SetCanceled();
}
else
{
try
{
tcs.SetResult(then());
}
catch (Exception ex)
{
tcs.SetException(ex);
}
}
},
CancellationToken.None,
TaskContinuationOptions.None,
TaskScheduler.FromCurrentSynchronizationContext());
return tcs.Task;
}
}
}
using Prism.Commands;
using System;
using System.Linq.Expressions;
using System.Threading.Tasks;
namespace WpfAsyncCommandTest
{
/// <summary>
/// 非同期コマンドの実行中、次のコマンド実行を禁止します。
/// </summary>
public sealed class MutexDelegateCommand : DelegateCommandBase
{
private static readonly Func<bool> TrueFunc = new Func<bool>(() => true);
private readonly Action<CompletedCallback> _execute;
private readonly Func<bool> _canExecute;
private bool _isExecuting;
/// <summary>
/// 非同期処理の完了を通知するためのコールバック関数です。
/// </summary>
/// <param name="exception"></param>
public delegate void CompletedCallback(Exception exception = null);
/// <summary>
/// コンストラクタです。
/// <para>
/// <paramref name="execute"/> の引数には、
/// コマンドの処理が完了したときに呼び出すコールバック関数が与えられます。
/// </para>
/// </summary>
/// <param name="execute">コマンドを非同期に実行</param>
public MutexDelegateCommand(Action<CompletedCallback> execute)
: this(execute, TrueFunc)
{
}
/// <summary>
/// コンストラクタです。
/// <para>
/// <paramref name="execute"/> の引数には、
/// コマンドの処理が完了したときに呼び出すコールバック関数が与えられます。
/// </para>
/// </summary>
/// <param name="execute">コマンドを非同期に実行</param>
/// <param name="canExecute">コマンドが実行可能か</param>
public MutexDelegateCommand(Action<CompletedCallback> execute, Func<bool> canExecute)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute ?? throw new ArgumentNullException(nameof(canExecute));
}
/// <summary>
/// コンストラクタです。
/// </summary>
/// <param name="execute">コマンドを非同期に実行</param>
public MutexDelegateCommand(Func<Task> execute)
: this(execute, TrueFunc)
{
}
/// <summary>
/// コンストラクタです。
/// </summary>
/// <param name="execute">コマンドを非同期に実行</param>
/// <param name="canExecute">コマンドが実行可能か</param>
public MutexDelegateCommand(Func<Task> execute, Func<bool> canExecute)
{
if (execute == null)
{
throw new ArgumentNullException(nameof(execute));
}
void wrappedExecute(CompletedCallback completed)
{
try
{
execute().ContinueWith(task =>
{
completed(task.IsFaulted ? task.Exception : null);
}, TaskScheduler.FromCurrentSynchronizationContext());
}
catch (Exception ex)
{
completed(ex);
}
}
_execute = wrappedExecute;
_canExecute = canExecute ?? throw new ArgumentNullException(nameof(canExecute));
}
/// <summary>
/// コマンドを実行中かを示します。
/// </summary>
public bool IsExecuting
{
get => _isExecuting;
private set
{
if (_isExecuting != value)
{
_isExecuting = value;
RaiseCanExecuteChanged();
}
}
}
/// <summary>
/// コマンドを実行します。
/// </summary>
/// <param name="parameter">パラメータ</param>
protected override void Execute(object parameter)
{
if (IsExecuting)
{
throw new InvalidOperationException("command is already executing.");
}
IsExecuting = true;
try
{
_execute(ex =>
{
IsExecuting = false;
if (ex is TaskCanceledException _)
{
System.Diagnostics.Debug.WriteLine($"command canceled {ex}.");
}
else if (ex != null)
{
throw new Exception("command threw exception.", ex);
}
});
// TODO: タイムアウトを設定する
}
catch
{
IsExecuting = false;
throw;
}
}
/// <summary>
/// コマンドが実行可能かを評価します。
/// </summary>
/// <param name="parameter">パラメータ</param>
/// <returns>
/// コマンドが実行可能なら true を返します。
/// コマンド実行中は false を返します。
/// </returns>
protected override bool CanExecute(object parameter)
{
if (IsExecuting)
{
return false;
}
return _canExecute();
}
/// <summary>
/// プロパティが変化したら <see cref="DelegateCommandBase.CanExecuteChanged"/> イベントを発行します。
/// </summary>
/// <typeparam name="T">プロパティの型</typeparam>
/// <param name="propertyExpression">プロパティを返す式木</param>
/// <returns></returns>
public MutexDelegateCommand ObservesProperty<T>(Expression<Func<T>> propertyExpression)
{
ObservesPropertyInternal(propertyExpression);
return this;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment