しっぽを追いかけて

ぐるぐるしながら考えています

Unity と猫の話題が中心   掲載内容は個人の私見であり、所属組織の見解ではありません

共有ターゲットページのお作法は難しい

VisualStudio 2013 で「共有ターゲット コントラクト」の項目を追加するとお手軽に共有を受ける側のページコードが生成できます

受けたデータを加工して処理するように少しコードを加えて下記のようなコードを書いた場合・・・

/// <summary>
/// 他のアプリケーションがこのアプリケーションを介してコンテンツの共有を求めた場合に呼び出されます。
/// </summary>
/// <param name="e">Windows と連携して処理するために使用されるアクティベーション データ。</param>
public async void Activate(ShareTargetActivatedEventArgs e)
{
    this._shareOperation = e.ShareOperation;

    // ビュー モデルを使用して、共有されるコンテンツのメタデータを通信します
    var shareProperties = this._shareOperation.Data.Properties;
    var thumbnailImage = new BitmapImage();
    this.DefaultViewModel["Title"] = shareProperties.Title;
    this.DefaultViewModel["Description"] = shareProperties.Description;
    this.DefaultViewModel["Image"] = thumbnailImage;
    this.DefaultViewModel["Sharing"] = false;
    this.DefaultViewModel["ShowImage"] = false;
    this.DefaultViewModel["Comment"] = String.Empty;
    this.DefaultViewModel["Placeholder"] = "Add a comment";
    this.DefaultViewModel["SupportsComment"] = true;
    Window.Current.Content = this;
    Window.Current.Activate();

    // 何か時間のかかるデータ加工処理(ダミー)
    await Task.Delay(10000);

    // 共有されるコンテンツのイメージをバックグラウンドで更新します
    if (this._shareOperation.Data.Contains(StandardDataFormats.Bitmap))
    {
        var stream = await this._shareOperation.Data.GetBitmapAsync();
        thumbnailImage.SetSource(await stream.OpenReadAsync());
        this.DefaultViewModel["ShowImage"] = true;
    }
    else if(this._shareOperation.Data.Contains(StandardDataFormats.StorageItems))
    {
        var items = await this._shareOperation.Data.GetStorageItemsAsync();
                
        var firstFile = items.FirstOrDefault() as StorageFile;
        if(firstFile == null)
        {
            return;
        }

        var thumbnail = await Task.Run(async () =>
            {
                return await firstFile.GetThumbnailAsync(ThumbnailMode.PicturesView);
            });
                
        thumbnailImage.SetSource(thumbnail);
        this.DefaultViewModel["ShowImage"] = true;
    }
}

/// <summary>
/// ユーザーが [共有] をクリックしたときに呼び出されます。
/// </summary>
/// <param name="sender">共有を開始するときに使用される Button インスタンス。</param>
/// <param name="e">ボタンがどのようにクリックされたかを説明するイベント データ。</param>
private void ShareButton_Click(object sender, RoutedEventArgs e)
{
    this.DefaultViewModel["Sharing"] = true;
    this._shareOperation.ReportStarted();

    // TODO: this._shareOperation.Data を使用して共有シナリオに適した
    //       作業を実行します。通常は、カスタム ユーザー インターフェイス要素を介して
    //       このページに追加されたカスタム ユーザー インターフェイス要素を介して
    //       this.DefaultViewModel["Comment"]

    this._shareOperation.ReportCompleted();
}

これでも問題ないように見えますが、時間のかかる処理中に戻るボタンや画面外をクリックしてホステッドビューを閉じた後、もう一度共有チャームで開くと・・・アプリが落ちます;

共有中に待ち時間が発生する場合、ユーザーが「何かうまくいっていないのかな?」と思い上記のような操作をすることはあり得るので非常にまずいです

デバッグで確かめてみると

f:id:matatabi_ux:20140309093041p:plain

ウィンドウが閉じられている?!

・・・どうやら画面を開き直した際に前の共有画面のウィンドウが閉じられるようなのですが、そこでアプリ全体を巻き込んで例外が発生してしまうようなのです;

いろいろと調べていたところ、MSDN に下記のような記載を見つけました

クイックスタート: 共有コンテンツの受信 (C#/VB/C++ と XAML を使った Windows ストア アプリ) (Windows)

ただし、ターゲット アプリが ReportStarted の前に ReportDataRetrieved を呼び出すことができる場合があります。たとえば、アプリがアクティブ化ハンドラーのタスクの一部としてデータを受信できるが、ユーザーが [共有] ボタンをクリックするまで ReportStarted を呼び出さない場合です。

これを読む限り、本来は ReportStarted の後に ReportDataRetrieved を呼ぶものですが、共有コントラクト ターゲット のテンプレートでは Activated で短時間にデータ取得が完了するので簡略化しているだけのように見えます

というわけで

Window Activated → 時間のかかる処理&画面表示切り替え →(共有ボタン押下)→ ReportStarted → データの処理 → ReportCompleted

上記の処理の流れを

Window Activated → ReportStarted → 時間のかかる処理&画面表示切り替え → ReportDataRetrieved →(共有ボタン押下)→ データの処理 → ReportCompleted

このように変更したのが次のコードです

/// <summary>
/// 他のアプリケーションがこのアプリケーションを介してコンテンツの共有を求めた場合に呼び出されます。
/// </summary>
/// <param name="e">Windows と連携して処理するために使用されるアクティベーション データ。</param>
public async void Activate(ShareTargetActivatedEventArgs e)
{
    this._shareOperation = e.ShareOperation;

    // ビュー モデルを使用して、共有されるコンテンツのメタデータを通信します
    var shareProperties = this._shareOperation.Data.Properties;
    var thumbnailImage = new BitmapImage();
    this.DefaultViewModel["Title"] = shareProperties.Title;
    this.DefaultViewModel["Description"] = shareProperties.Description;
    this.DefaultViewModel["Image"] = thumbnailImage;
    this.DefaultViewModel["Sharing"] = false;
    this.DefaultViewModel["ShowImage"] = false;
    this.DefaultViewModel["Comment"] = String.Empty;
    this.DefaultViewModel["Placeholder"] = "Add a comment";
    this.DefaultViewModel["SupportsComment"] = true;
    Window.Current.Content = this;
    Window.Current.Activate();

    // 共有操作の開始を OS に通知
    this._shareOperation.ReportStarted();

    // 画面を閉じた場合の処理を追加
    Window.Current.VisibilityChanged += this.OnVisibilityChanged;
    Window.Current.Closed += this.OnClosed;

    // 何か時間のかかるデータ加工処理(ダミー)
    await Task.Delay(10000);

    if (!Window.Current.Visible)
    {
        Window.Current.VisibilityChanged -= this.OnVisibilityChanged;
        this._shareOperation.ReportCompleted();
        return;
    }

    // 共有されるコンテンツのイメージをバックグラウンドで更新します
    if (this._shareOperation.Data.Contains(StandardDataFormats.Bitmap))
    {
        var stream = await this._shareOperation.Data.GetBitmapAsync();
        thumbnailImage.SetSource(await stream.OpenReadAsync());
        this.DefaultViewModel["ShowImage"] = true;
    }
    else if (this._shareOperation.Data.Contains(StandardDataFormats.StorageItems))
    {
        var items = await this._shareOperation.Data.GetStorageItemsAsync();

        var firstFile = items.FirstOrDefault() as StorageFile;
        if (firstFile == null)
        {
            this._shareOperation.ReportCompleted();
            return;
        }

        var thumbnail = await Task.Run(async () =>
            {
                return await firstFile.GetThumbnailAsync(ThumbnailMode.PicturesView);
            });

        thumbnailImage.SetSource(thumbnail);
        this.DefaultViewModel["ShowImage"] = true;
    }

    // 共有データの取得完了を OS に通知
    this._shareOperation.ReportDataRetrieved();
}
/// <summary>
/// ウィンドウクローズイベントハンドラ
/// </summary>
/// <param name="sender">イベント発行者</param>
/// <param name="e">イベント引数</param>
private void OnClosed(object sender, CoreWindowEventArgs e)
{
    // ReportCompleted で例外発生しないよう1回目のクローズ後処理をスキップする
    Window.Current.Closed -= this.OnClosed;
    e.Handled = true;
}

/// <summary>
/// ウィンドウの表示状態変更イベントハンドラ
/// </summary>
/// <param name="sender">イベント発行者</param>
/// <param name="e">イベント引数</param>
private void OnVisibilityChanged(object sender, VisibilityChangedEventArgs e)
{
    // 画面が閉じられたら共有を正常終了させる
    if (!Window.Current.Visible)
    {
        Window.Current.VisibilityChanged -= this.OnVisibilityChanged;
        this._shareOperation.ReportCompleted();
    }
}

共有ボタン押下後に行っていた ShareOperation.ReportStarted() の共有開始通知を時間のかかる処理の前に行い、画面が閉じられた際の処理を追加すれば大丈夫でした

データの取得&表示が終わったら ShareOperation.ReportDataRetrieved() を実行していますが、こちらは呼び出さなくても変化はありませんでした・・・何をやっているのかは不明ですが定義上実行しておいた方がよいとは思います