お昼にこんな話題が…。
ユーザー レスポンスを非同期処理に見立てて await する手法だと Window.Closing をキャンセルできない問題、常に e.Cancel = true; しておいて、レスポンスで OK 来たら Application.Shutdown(); してしまえというアレな策が
もんもんと考えた結果以下のようなBehaviorが出来ました。実用に耐えうるか…?
using System; using System.ComponentModel; using System.Threading.Tasks; using System.Windows; using System.Windows.Interactivity; namespace WpfApplication4 { public class AsyncClosingBehavior : Behavior<Window> { // ClosingAsyncActionにnullが設定された場合に使用する値 private static readonly Func<Task<bool>> FalseAction = () => Task.FromResult(false); // closingをCancelするかどうか private bool cancelClosing = true; // 現在closingイベントの処理中かどうか private bool processing; /// <summary> /// WindowのClosingイベントでキャンセルするかどうかを確認するための非同期処理を行うデリゲート /// </summary> public Func<Task<bool>> ClosingAsyncAction { get { return (Func<Task<bool>>)GetValue(ClosingAsyncActionProperty); } set { SetValue(ClosingAsyncActionProperty, value); } } public static readonly DependencyProperty ClosingAsyncActionProperty = DependencyProperty.Register( "ClosingAsyncAction", typeof(Func<Task<bool>>), typeof(AsyncClosingBehavior), new PropertyMetadata(FalseAction, null, CoerceClosingAsyncAction)); // nullだったらFalseAction private static object CoerceClosingAsyncAction(DependencyObject d, object baseValue) { return baseValue ?? FalseAction; } protected override void OnAttached() { base.OnAttached(); this.AssociatedObject.Closing += this.WindowClosing; } protected override void OnDetaching() { this.Cleanup(); base.OnDetaching(); } // イベントの後片付け private void Cleanup() { this.AssociatedObject.Closing -= this.WindowClosing; } private async void WindowClosing(object sender, CancelEventArgs e) { e.Cancel = this.cancelClosing; // Windowを閉じる場合は何もしない if (!this.cancelClosing) { return; } // Closingの処理中の場合は何もしない if (this.processing) { return; } this.processing = true; // Closingイベント中にCloseすると例外が出るので一旦Closingイベントを確実に抜けて続きをやる await this.Dispatcher.InvokeAsync(async () => { try { // 非同期呼び出しでクローズをキャンセルするかどうか問い合わせる this.cancelClosing = await this.ClosingAsyncAction(); if (!this.cancelClosing) { // キャンセルしない場合は閉じる this.Cleanup(); this.AssociatedObject.Close(); } } finally { this.processing = false; } }); } } }
使う側はこんな風にTask
using System; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; namespace WpfApplication4 { /// <summary> /// MainWindow.xaml の相互作用ロジック /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } public Func<Task<bool>> ClosingAsyncAction { get { return this.ClosingAsync; } } private Task<bool> ClosingAsync() { var currentContent = this.Content; var source = new TaskCompletionSource<bool>(); var panel = new StackPanel(); var confirmMessage = new TextBlock { Text = "本当に閉じるの?" }; panel.Children.Add(confirmMessage); var ok = new Button { Content = "OK" }; var cancel = new Button { Content = "Cancel" }; panel.Children.Add(ok); panel.Children.Add(cancel); ok.Click += (_, __) => { this.Content = currentContent; source.SetResult(false); }; cancel.Click += (_, __) => { this.Content = currentContent; source.SetResult(true); }; this.Content = panel; return source.Task; } } }
んで、Windowにビヘイビアをこんな風においておきます。
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" xmlns:local="clr-namespace:WpfApplication4" x:Class="WpfApplication4.MainWindow" Title="MainWindow" Height="350" Width="525" Name="window"> <i:Interaction.Behaviors> <local:AsyncClosingBehavior ClosingAsyncAction="{Binding ClosingAsyncAction, ElementName=window}"/> </i:Interaction.Behaviors> <Grid> <TextBlock HorizontalAlignment="Left" Margin="43,39,0,0" TextWrapping="Wrap" VerticalAlignment="Top"><Run Language="ja-jp" Text="着任"/></TextBlock> </Grid> </Window>
実行するとこんな感じ。
Windowを閉じようとするとこんな風になって
キャンセルすると閉じない。
OKすると、もちろん閉じる。
なんか、まだまだ良くできそうでもんもんとしてる上に実用するのか?と言われるとなんとなく自分ではしないような気がしつつメモ。