かずきのBlog@hatena

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

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