Last active
April 1, 2019 09:13
-
-
Save tmatz/13a5617299cf72324a329f8dff63749c to your computer and use it in GitHub Desktop.
use Task like a Promise
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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<Unit> に変換します。 | |
/// </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; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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