かずきのBlog@hatena

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

Windows ストア アプリでスナップ中のアプリに共有をすると、コレクション操作でInvalidCastExceptionがおきる #win8dev_jp

ちょっと悩んだのでメモです。共有ターゲットコントラクトを実装していて、共有されたものをコレクションにためて保存するということをやっていたのですが、アプリケーションがスナップ表示中の時に共有ターゲットにしてデータを追加するとInvalidCastExceptionが起きて困ってました。エラーメッセージは以下のような感じです。

型 'System.Collections.Specialized.NotifyCollectionChangedEventHandler' の COM オブジェクトを
クラス型 'System.Collections.Specialized.NotifyCollectionChangedEventHandler' にキャストできません。
COM コンポーネントを表す型のインターフェイスを COM コンポーネントを表さない型にキャストすることはできません。
ただし、基になる COM コンポーネントがインターフェイスの IID の QueryInterface 呼び出しをサポートする場合は、
インターフェイスにキャストすることができます。

エラーが起きる画面イメージは以下のような感じ。

原因

スナップで表示している画面でListViewやGridViewなどのコントロールのItemsSourceにバインドしているコレクションを、共有ターゲットコントラクトの画面から操作していることが原因でした。もっと言うと、どうやら普通に起動したアプリケーションのUIスレッドと共有ターゲットコントラクトから起動された画面のUIスレッドは別みたいです。

結果として別スレッドでコレクションを操作したため、エラーになるというWPFSilverlightなどでおなじみのエラーパターンと同じでした。それにしても、何故InvalidCastExceptionなのだ!!

解決策

Dispatcher経由でコレクション操作をしましょう。例えば以下のようなヘルパークラスを作成しておきます。

namespace ShareInvalidCastExceptionApp
{
    using Windows.UI.Core;
    using Windows.UI.Xaml;

    static class DispatcherHelper
    {
        public static CoreDispatcher Dispatcher { get; private set; }

        public static bool InitializeIfNeeded(DependencyObject obj)
        {
            if (DispatcherHelper.Dispatcher != null)
            {
                // 初期化済み
                return false;
            }

            // 引数で渡されたオブジェクトのDispatcherを保持しておく
            DispatcherHelper.Dispatcher = obj.Dispatcher;
            return true;
        }
    }
}

そして、App.xaml.csのOnLaunchedでFrameあたりを使って初期化するようにします。

protected override void OnLaunched(LaunchActivatedEventArgs args)
{
    Frame rootFrame = Window.Current.Content as Frame;

    // ウィンドウに既にコンテンツが表示されている場合は、アプリケーションの初期化を繰り返さずに、
    // ウィンドウがアクティブであることだけを確認してください
    if (rootFrame == null)
    {
        // ナビゲーション コンテキストとして動作するフレームを作成し、最初のページに移動します
        rootFrame = new Frame();
        DispatcherHelper.InitializeIfNeeded(rootFrame); // ここらへんでDispatcherの初期化をしておく

次に、共有ターゲットコントラクトから起動された場合にもDispatcherHelperを初期化しておきます。これは、共有ターゲットコントラクトを追加したときにApp.xaml.csに作成されるOnShareTargetActivatedあたりに書くといいと思います。

/// <summary>
/// 共有操作のターゲットとしてアプリケーションがアクティブにされたときに呼び出されます。
/// </summary>
/// <param name="args">アクティベーション要求の詳細。</param>
protected override void OnShareTargetActivated(Windows.ApplicationModel.Activation.ShareTargetActivatedEventArgs args)
{
    var shareTargetPage = new ShareInvalidCastExceptionApp.ShareTargetPage();
    // 初期化しておく(rootFrameを使って既に初期化済みの場合は何もされない)
    DispatcherHelper.InitializeIfNeeded(shareTargetPage);
    shareTargetPage.Activate(args);
}

あとは、コレクション等の操作を行う時にはDispatcherHelper.Dispatcher経由で行うようにすると安全です。

/// <summary>
/// ユーザーが [共有] をクリックしたときに呼び出されます。
/// </summary>
/// <param name="sender">共有を開始するときに使用される Button インスタンス。</param>
/// <param name="e">ボタンがどのようにクリックされたかを説明するイベント データ。</param>
private async void ShareButton_Click(object sender, RoutedEventArgs e)
{
    this.DefaultViewModel["Sharing"] = true;
    this._shareOperation.ReportStarted();
    await DispatcherHelper.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
    {
        // コレクション等の操作は、Dispatcher経由で行う。
        // ここでのApp.Dataは、アプリケーションのMainPageでListViewにバインドしてるコレクション
        App.Data.Add(DateTimeOffset.Now);
    });
    this._shareOperation.ReportCompleted();
}

まとめ

WinRTはエラーメッセージがわかりにくい。

サンプルのダウンロード

こちらかどうぞ。
ShareInvalidCastExceptionApp