かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

非同期処理でWindowのClosingをキャンセルするかどうか決めたい

お昼にこんな話題が…。

もんもんと考えた結果以下のような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を返すメソッドを定義して、それをFunc>で返すプロパティを定義しておきます。

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>

実行するとこんな感じ。
f:id:okazuki:20130822215123j:plain
Windowを閉じようとするとこんな風になって
f:id:okazuki:20130822215201j:plain
キャンセルすると閉じない。
f:id:okazuki:20130822215236j:plain
OKすると、もちろん閉じる。


なんか、まだまだ良くできそうでもんもんとしてる上に実用するのか?と言われるとなんとなく自分ではしないような気がしつつメモ。