かずきのBlog@hatena

日本マイクロソフトに勤めています。XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

非同期処理で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すると、もちろん閉じる。


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