ドラッグしてドロップ on WPF

勢い良く手を上げたはよいけど何を書けばよくわからなくなったXAML Advent Calendar 19日目を執筆させていただきます、かーのと申します。よろしくお願いします。

みなさん、XAMLライフ、満喫していますか!

XAML Advent Calendar ですが、ぼくはWPF以外ほとんど触ったことがないので
おもにWPFのお話をしていきます!

今日はWPFにおけるドラッグアンドドロップの実装について、です!

ドラッグアンドドロップの難しさ

ドラッグアンドドロップ、アイコンを移動させてゴミ箱へ投げたり、
要素を移動して並び替えたり、用途は様々。

しかし、いざ実装しようとするとこれがなかなか曲者です。
移動の検知は?移動先は?ドロップの可否は?ドロップ時の処理は?

というわけで、ドラッグアンドドロップの仕組みを見ながら、せっかくのXAMLなので、今回は
「ビヘイビアベース」でのドラッグアンドドロップの実装をしたいと思います。
そっちの方がMVVMと親和性高いしね!

続きを読む

Krile StarryEyes をリリースしました。

大変永らくお待たせしました。

本日 10/31 12:00(GMT)、Krile StarryEyesを一般公開しました。

http://krile.starwing.net/

フルスクラッチで開発された、Windows向けのフルスペック新世代Twitterクライアント(自称)です。

2.9.0ということで、まだプレビュー版です。バグがたくさん残っているとは思いますが、未実装項目もありますが、何卒よろしくお願いします。

続きを読む

“DependencyObject-Context”例外についての仮説。

注:個人的なメモです。8割憶測です。

System.ArgumentException: 指定された DependencyObject は、この Freezable のコンテキストではありません。

この例外は、Krile Mystique のレンダリング時に不定期に発生するもので、スタックトレースを見ても

場所 System.Windows.Freezable.RemoveContextInformation(DependencyObjectcontext, DependencyProperty property)
  場所 System.Windows.Freezable.RemoveInheritanceContext(DependencyObjectcontext, DependencyProperty property)
  場所 System.Windows.DependencyObject.RemoveSelfAsInheritanceContext(DependencyObject doValue, DependencyProperty dp)
  場所 System.Windows.DependencyObject.UpdateEffectiveValue(EntryIndex entryIndex, DependencyProperty dp, PropertyMetadata metadata, EffectiveValueEntry oldEntry, EffectiveValueEntry& newEntry, Boolean coerceWithDeferredReference, Boolean coerceWithCurrentValue, OperationType operationType)
  場所 System.Windows.DependencyObject.SetValueCommon(DependencyProperty dp, Object value, PropertyMetadata metadata, Boolean coerceWithDeferredReference, Boolean coerceWithCurrentValue, OperationType operationType, Boolean isInternal)
  場所 System.Windows.DependencyObject.SetCurrentValue(DependencyProperty dp, Object value)
  場所 System.Windows.Controls.Image.MeasureArrangeHelper(Size inputSize)
  場所 System.Windows.Controls.Image.MeasureOverride(Size constraint)
  場所 System.Windows.FrameworkElement.MeasureCore(Size availableSize)
  場所 System.Windows.UIElement.Measure(Size availableSize)
  場所 Mystique.Views.CustomPanels.FillPanel.MeasureOverride(Size availableSize)
  場所 System.Windows.FrameworkElement.MeasureCore(Size availableSize)
  場所 System.Windows.UIElement.Measure(Size availableSize)
  場所 System.Windows.Controls.Grid.MeasureCell(Int32 cell, Boolean forceInfinityV)
  場所 System.Windows.Controls.Grid.MeasureCellsGroup(Int32 cellsHead, Size referenceSize, Boolean ignoreDesiredSizeU, Boolean forceInfinityV)
  場所 System.Windows.Controls.Grid.MeasureOverride(Size constraint)
  場所 System.Windows.FrameworkElement.MeasureCore(Size availableSize)
  場所 System.Windows.UIElement.Measure(Size availableSize)
  場所 MS.Internal.Helper.MeasureElementWithSingleChild(UIElement element, Size constraint)
  場所 System.Windows.Controls.ContentPresenter.MeasureOverride(Size constraint)
  場所 System.Windows.FrameworkElement.MeasureCore(Size availableSize)
  場所 System.Windows.UIElement.Measure(Size availableSize)
  場所 System.Windows.Controls.Border.MeasureOverride(Size constraint)
  場所 System.Windows.FrameworkElement.MeasureCore(Size availableSize)
  場所 System.Windows.UIElement.Measure(Size availableSize)
  場所 System.Windows.Controls.Control.MeasureOverride(Size constraint)
  場所 System.Windows.FrameworkElement.MeasureCore(Size availableSize)
  場所 System.Windows.UIElement.Measure(Size availableSize)
  場所 MS.Internal.Helper.MeasureElementWithSingleChild(UIElement element, Size constraint)
  場所 System.Windows.Controls.ContentPresenter.MeasureOverride(Size constraint)
  場所 System.Windows.FrameworkElement.MeasureCore(Size availableSize)
  場所 System.Windows.UIElement.Measure(Size availableSize)
  場所 System.Windows.Controls.Border.MeasureOverride(Size constraint)
  場所 System.Windows.FrameworkElement.MeasureCore(Size availableSize)
  場所 System.Windows.UIElement.Measure(Size availableSize)
  場所 System.Windows.Controls.Control.MeasureOverride(Size constraint)
  場所 System.Windows.FrameworkElement.MeasureCore(Size availableSize)
  場所 System.Windows.UIElement.Measure(Size availableSize)
  場所 System.Windows.Controls.VirtualizingStackPanel.MeasureOverride(Size constraint)
  場所 System.Windows.FrameworkElement.MeasureCore(Size availableSize)
  場所 System.Windows.UIElement.Measure(Size availableSize)
  場所 System.Windows.ContextLayoutManager.UpdateLayout()
  場所 System.Windows.ContextLayoutManager.UpdateLayoutCallback(Object arg)
  場所 System.Windows.Media.MediaContext.FireInvokeOnRenderCallbacks()
  場所 System.Windows.Media.MediaContext.RenderMessageHandlerCore(Object resizedCompositionTarget)
  場所 System.Windows.Media.MediaContext.RenderMessageHandler(Object resizedCompositionTarget)
  場所 System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
  場所 MS.Internal.Threading.ExceptionFilterHelper.TryCatchWhen(Object source, Delegate method, Object args, Int32 numArgs, Delegate catchHandler)
  場所 System.Windows.Threading.Dispatcher.WrappedInvoke(Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
  場所 System.Windows.Threading.DispatcherOperation.InvokeImpl()
  場所 System.Threading.ExecutionContext.runTryCode(Object userData)
  場所 System.Runtime.CompilerServices.RuntimeHelpers.ExecuteCodeWithGuaranteedCleanup(TryCode code, CleanupCode backoutCode, Object userData)
  場所 System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean ignoreSyncCtx)
  場所 System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
  場所 System.Windows.Threading.DispatcherOperation.Invoke()
  場所 System.Windows.Threading.Dispatcher.ProcessQueue()
  場所 System.Windows.Threading.Dispatcher.WndProcHook(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
  場所 MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
  場所 MS.Win32.HwndSubclass.DispatcherCallbackOperation(Object o)
  場所 System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
  場所 MS.Internal.Threading.ExceptionFilterHelper.TryCatchWhen(Object source, Delegate method, Object args, Int32 numArgs, Delegate catchHandler)
  場所 System.Windows.Threading.Dispatcher.WrappedInvoke(Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
  場所 System.Windows.Threading.Dispatcher.InvokeImpl(DispatcherPriority priority, TimeSpan timeout, Delegate method, Object args, Int32 numArgs)
  場所 MS.Win32.HwndSubclass.SubclassWndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam)
  場所 MS.Win32.UnsafeNativeMethods.DispatchMessage(MSG& msg)
  場所 System.Windows.Threading.Dispatcher.PushFrameImpl(DispatcherFrame frame)
  場所 System.Windows.Application.RunInternal(Window window)
  場所 System.Windows.Application.Run()
  場所 Mystique.App.Main()

と、要するにどうしたらいいんだろうかと頭を抱えてしまう代物だった。

最終的に内部で利用している画像キャッシュに起因するものだと分かったのだが、最近またこの例外が再燃した。これらの顛末と、そこからの僕の勝手な推測をメモっておく。

初期の問題

Krileの画像キャッシュは、平たく言えば Dictionary<Uri, BitmapImage> となっている。BitmapImageはFreezeしてしまえばスレッドセーフとして扱えるので、バックグラウンドスレッドで画像の取得とフリーズを行いこれをキャッシュに格納し、必要に応じてBitmapImageを取り出すという動作をしていた。(参照: http://msdn.microsoft.com/ja-jp/library/system.windows.freezable(v=VS.80).aspx )

画像の取得では当初は単に

return cache[uri];

としていたのだが、これではこの例外が不定期に送出されていた。そこで、

var img = cache[uri].Clone();
img.Freeze();
return img;

という実装に変更したところ、同様の例外はぱったり送出されなくなった。

というわけで、クローンしないとこの例外が飛ぶということを理解した。

最近の問題

画像の表示処理が処理のボトルネックとなっていることが分かったので、画像の表示処理を高速化したいと考えた。

種々の対策を講じたのだが、先述したClone部分にもメスを入れた。

http://msdn.microsoft.com/ja-jp/library/system.windows.freezable.getasfrozen.aspx

によると、

Freezable を固定できることを検証するには、このメソッドを呼び出す前に CanFreeze プロパティを確認する必要があります。 このメソッドを使用するのは、Clone を使用してコピーを作成してから、Freeze メソッドを使用して、作成したコピーを固定するのと同じです。

GetAsFrozen メソッドおよび GetCurrentValueAsFrozen メソッドでは、既に固定されている Freezable サブオブジェクトを複製せずに、それらを参照によってコピーするだけなので、コピー パフォーマンスが向上します。

とのことで、まさにうってつけだと感じ、先述した実装を

return (BitmapImage)cache[url].GetAsFrozen();

に変更した。

すると、また例外が送出されるようになった。

つまり

まず、初期の問題から考える。

初期の問題は、「Cloneしていない」ことによって例外が送出されていたと思われる。

また、この例外は常に

System.Windows.Freezable.RemoveContextInformation(DependencyObject context, DependencyProperty property)

から送出されている。

メソッドの具体的な動作が分からないが、このFreezableに付加していたContextInformationを取り除こうとしていることが分かる。そして、そんなコンテキストは無いということで例外が飛んだのだろう。完全に異常系で処理されているので、「本来コンテキストはあって然るべきだが、それが失われている」状態になっているということが分かる。

この手の問題で一番ありがちなのがスレッドセーフ関連だが、スタックトレースを見れば分かるとおり、このメソッドはディスパッチャスレッドから呼ばれているため、マルチスレッド絡みの問題は生じない。

残る可能性としては、Krileの構造に由来する例外だということ。Krileの構造上、BitmapImageのImageへの束縛と解放が短時間に大量に行われるという動作があり、これが悪影響を及ぼしているのではないか。

BitmapImageはCloneされず、同じURIに対しては同じインスタンスを返すようにしていたので、大量のImageから一つのインスタンスへの参照が張られている状態になる。この状態で動作している時に何らかの理由でContextInformationが破損した状態になり、これにより例外が送出、と考えると辻褄が合う。

子は複数の親を持つ場合に想定外の動作をする、ということなのだろうか?

とりあえずこれ以上は内部の実装の問題になってしまうので、「BitmapImageに限っては」複数の親に所持される場合、個々の親に対してCloneしてやる必要があるようだ。
※他のFreezableやDependencyObjectなどについては不明

最近の問題は明らかにCloneからGetAsFrozenメソッドへ切り替えたことが問題であるが、これは

GetAsFrozen メソッドおよび GetCurrentValueAsFrozen メソッドでは、既に固定されている Freezable サブオブジェクトを複製せずに、それらを参照によってコピーするだけなので、コピー パフォーマンスが向上します。

という点によるものだろう。複製によって例外送出を回避したのだから、「複製せず」なら例外が飛ぶ。

そんな感じのメモ。ImageCacheStorageを触るときは気を付ける。

MVVMおぼえがき

自分の知ってるMVVMについての知識をまとめるつもりが、なんだか変な方向に行きました。

あと、以下に書いてある内容は正しいとは限りません。あくまで僕の考え方を自分の為にまとめたものです。プログラムの組み方なんて星の数ほどあるじゃないですかという。

WPFを使ってると、どうしても「Model-View-ViewModel パターン」、通称「MVVMパターン」を採用せざるを得なくなります。このMVVMパターン、定義は人によっていろいろなんですが、今回はただ単にViewとViewModelが存在する状態ってことにしといてください。そんなんMVVMじゃねーよって人はぜひ定義を教えてください。

MVVMパターンでプログラムを作成されたことがある人なら分かると思いますが、何も無しでMVVMパターンを実践しようとすると死にます。普通に死にます。大切な事なので二回言いました。それほどまでに正しく実装するのが割と難しいんですが、幸いなことに色々なライブラリやインフラストラクチャがリリースされています。たとえば、MVVM LightやPrismは有名どころですね。あとは最近僕が使ってるやつですが、Livetっていうのもあります。探せばたぶんいっぱい出てくると思います。

まぁインフラは使いましょう。問題はそこから、WinFormsやWPF(Without MVVM)でやってたことをどうやればいいのっていう。

Why MVVM?

なぜ今までのUIモデルを捨て、MVVMパターンを採用するのでしょう。答えは簡単、そのほうが作りやすいからです。少々回りくどくはなりますが、バグが少なくパフォーマンスが高くメンテナンスしやすいプログラムが作成できます。さらに、MVVMはWPFのためのパターンです。WPFの持つ表現力を最大限まで引き出すことができます。

MVVMは、MVPやMVCなどの三層アーキテクチャモデルの親戚です。すでに成熟したMVPやMVCではなく、何故MVVMパターンという新しいモデルを用いるのかと考える方がいるかと思いますが、それは誤解です。MVVMでも基本は変わりません。単純に、実現の方法が違うだけです。BindingというWPFの機能を活用すれば、より自然な三層アーキテクチャモデルを構築可能だということです。三層アーキテクチャ + Binding = MVVMだと考えると良いと思います。僕もそう考えています。

MVVMインフラストラクチャにはWPFという明確なターゲットがあるため、しばしばWPF固有の知識が盛り込まれますが、これはMVVMパターンをより簡単に実装可能にするためのもので、これらを活用しなければMVVMではないということはありません。ただ、車輪の再発明だと思いますので、できれば頼った方が良いとは思います。

要するに、肩肘張ってMVVMパターンを実装する必要はありません、ということです。あなたがMainWindowViewModel.csを作った瞬間、それはMVVMパターンです。(だと思います)

MVVM – Model View ViewModel

Viewは、画面です。勝手にできたり消えたりします。
ユーザーになにかを見せたり、ユーザーから入力を受け取ったりするのが仕事なので、このあたりはとても得意です。ですが、状態に応じて表示を変えたり、ユーザーの入力に応じて何かしたりっていうのは限定的な機能しかありません。

ViewModelは、画面とデータをくっつけるものです。
データをViewに送るために整形したり、ユーザーの操作を解釈したりするのがお仕事です。ちょうどViewに欠けている部分がまとまって固まってるようなものです。

Modelは、データです。なんでもいいです。

WinFormsでプログラムを作成されたことがある方なら、Viewが *.Designer.cs、ViewModelが*.csみたいなものかーって考えられる方もいらっしゃるかと思うんですが、ちょっと違います。Viewはもっと高度なことができます。見た目を司るのがView、全体的な動作を司るのがViewModelって考えるといいかもしれません。

生存期間

ViewとViewModelとModelはそれぞれ生存期間が違います。Modelはそれぞれでしょう。ViewModelは、参照してる間は生きてます。いずれにしても、ModelとViewModelの寿命についてはプログラマがコントロール可能です。ところが、Viewについては勝手にできたり勝手に消えたり、消えたと思ったらまた作られたりを繰り返しています。プログラマはコントロールできません。

たとえば、リストボックスを考えてみてください。10000件のデータがあるけど、そのうちの100件しか描画範囲内に無いなら、その100件だけあればいいですね。このとき、Viewは本当に100件分しか存在しません。(※実際のところはいろいろ変わってくるのですが、今回はそういうことにしといてください。)
リストをスクロールしたら、スクロールして新しく表れてきた分のViewを作って、隠れてしまった分のViewを削除すれば、メモリ領域を無駄に使うことなくリストを表示できますね。(※)
WPFは、この動作をいろんなところで自動的に行います。この一連の動作はプログラマからは隠されていて、いつできたか、いつ消されるかを知ることは基本的にできないと思った方が良いです。

さて、Viewを作るときには当然それの種となるものが必要です。見た目はXAMLのDataTemplateでなんとかなると思いますが、表示するデータはどうするかというと、ここでViewModelを使います。ViewModelが、前のViewの状態を覚えていて、新しく作られるViewにその情報を提供することで、ユーザーにViewが出来たり消えたりしていることを意識させないようにすることができます。←重要

データバインディング

Viewは当然どこかでViewModelへの参照を得ないといけません。また、各コントロールはViewModelのどのデータを参照すればいいかを指定されないとどれを表示すればいいんだか困ります。この時に使われるのが、データバインディングシステムです。

WPFの各コントロールには「DataContext」というプロパティが存在します。このDataContextはobject型のプロパティですから、いろんなデータを渡すことができます。WPFのコントロールの中にはコントロールを内包できるものがあります(WindowやGridなんかは代表例ですね)。そんな親コントロールのDataContextを設定した時、子コントロールのDataContextも自動的にそれを継承します。Windowの中にTextBoxを配置している時に、WindowのDataContextに何かセットすると、TextBoxのDataContextもそれになります。実のところ、これはViewModelへの参照そのものです。DataContextにViewModelのインスタンスをセットしてやればすべて上手く行きます。

コントロールでどのデータを表示するのかを決めるのがいわゆる「データバインディング」です。たとえば、<TextBlock Text=”{Binding SomeValue}“ />としたとき、このテキストブロックはViewModelのSomeValueプロパティの内容を取得して表示します。ViewModelの参照は先述の通り、DataContextを使います。

ところで、DataContextへViewModelのインスタンスを渡す方法も必要になります。考え付くのは、

  • XAMLでインスタンスを生成する
    <Window.DataContext>
        <vm:WindowViewModel />
    </Window.DataContext>
    のような感じですね。
  • コードビハインドでインスタンスを生成する
  • コードビハインドをViewModelにする(!!)
    Window.DataContext={Binding}
    で設定することができますが、これには実は問題があります。後でまた。
  • インフラの機能に頼る
    特に画面遷移の場面で多いのですが、インフラによっては、ViewModelを指定してViewを表示することができるものがあります。

とまぁいろいろあります。

このデータバインディングの機能を利用してViewとViewModelを結びつけますが、参照可能なのはView→ViewModel だけであることに注意してください。ViewModelはViewを直接操作することはできません。

遠回りな、理不尽な実装に見えますが、これは非常に理にかなっています。次で説明しますね。

バインディングとパフォーマンス

Viewが出来たり消えたりするということと、ViewModelとViewはBindingでくっつくということは分かったかと思いますが、Bindingで参照可能なのはView→ViewModelの一方通行だけです。どうしてこんなことになっているのでしょうか?

例えば、ViewModelにBindingControlsプロパティか何かを用意して、Viewがバインドされたらここに参照を追加、バインド解除されたらここから参照を削除という方法を考えてみましょう。(View->ViewModelは多数張ることができますので、必然的にコレクションとなります。)これならViewModelからもバインドされているViewにアクセス可能です。なぜWPFはこのような実装をしなかったのでしょう。

先述した通り、Viewはめまぐるしい勢いで生成され、削除されます。生成されるたびに通知、削除されるたびに通知、しかもバインディングの数だけ。パフォーマンスは期待できません。すべてのWPFアプリケーションの足枷となってしまいます。そもそも、Viewの削除はGCによって行われていると推測できます。ファイナライザスレッドを使ってこの処理を行わなければいけないのは、パフォーマンスに重大な悪影響を与えます。そのため、ただ単に参照を保持するだけで済む View->ViewModelの一方通行となっています。これならパフォーマンスを高く保つことができます。

メモリリーク

View←ViewModelの参照は持つことが基本的にできません。しかし、どうしてもViewに情報を渡さなければいけない局面がいくつか存在します。たとえば、特定のコントロールにフォーカスを遷移する時。たとえば、メッセージボックスを出す時。たとえば、ダイアログを出して画面を遷移する時。これはどのようにすれば解決できるでしょうか。

コードビハインドでDataContextの変更をリッスンし、変更されたらDataContextにViewを渡す。場合によってはうまく行くでしょう。しかし、Viewの寿命は一般的にViewModelより短いです。このとき、どのようなことが起こるでしょう?

Viewが破棄されると、ガベージコレクションの対象となります。しかし、ViewのインスタンスはまだViewModelから参照されている状態になります。これでは、ガベージコレクションは行えません。

この現象は、ViewがViewModelのイベントをリッスンしている時も同じです。イベントはデリゲートのコレクションを保持し、発行された時に通知して回ります。デリゲートはViewのインスタンスを保持していますから、結局Viewのガベージコレクションが行えません。先述した「コードビハインドをViewModelとして使う」という解決策の問題点もまた同じです。コードビハインドは実行時にViewと結合されます。ViewModelへの参照がそのままViewの参照と同一となり、結局メモリリークを引き起こします。

コレクションされるべきViewがいつまでもコレクションされずメモリを圧迫し続ける、これはまさにメモリリークです。この現象の根本的な原因は、寿命の短いViewの参照をViewより寿命が長いViewModelが持っているという点にあります。

知識がある方なら、WeakReferenceによって解決することを思いつくでしょう。しかし、全てのView←ViewModel参照においてWeakReferenceを間違いなく実装するのは骨が折れます。そこで、各MVVMインフラストラクチャの出番となります。多くのMVVMインフラストラクチャは、この機能を実装しています。それも、簡単に実装が行える形で。たとえば、ViewModelの中にメッセンジャーとよばれる機構を組み込んだり。この機能についての詳細は各インフラストラクチャのヘルプを参照してください。

なぜ「メッセンジャー」を用いるのか。それは、View←ViewModelの参照が持てないから、です。

統一された手法を用いる意味

MVVMパターンでインフラストラクチャを用いる利点について説明しましたが、これらは場所が限定的であれば、例示したような解決策でもうまく行きます。たとえば、アプリケーションの開始から終了まで一貫して破棄されないViewであれば、メモリリークの問題は発生しません。

ですが、「このViewは破棄されるからMessenger」「このViewは破棄されないからイベント」というように一々考慮していてはバグの温床となりますし、メンテナンス性も大きく下がります。それならば、どこでも統一された、安全な手法を用いてアプリケーションを構築した方が良いと思いませんか?メンテナンスもしやすく、バグも生み出しにくくなります。

さぁ、WPF With MVVM!

INotifyPropertyChangedの働きだとかまだいろいろありますが、後は使ってみれば分かると思います。というか、別の人が詳しくまとめてると思います!

throw new 筆();

VisualTreeHelperのHitTestは、割と適当。

VisualTreeHelperのHitTestは、Collapsedとかあんまり考慮しないみたいです。

流石に作成時からCollapsedな要素はヒットしませんが、そうじゃないときは最大値に対してヒットしてるような。

位置に関しては正確なので、うまいこと処理してやる必要があります。StackPanelとかWrapPanelだったら、最後にヒットした要素を採用する、とか。

 

小一時間はまったのでメモ。

DependencyObjectの親ウィンドウを取得したい

UserControlとかが配置されてるWindowを取得したいって時、たまーにあったりします。

そんなときは、

Window Window.GetWindow(DependencyObject dependencyObject);

で、いけるっぽいです。( http://msdn.microsoft.com/ja-jp/library/system.windows.window.getwindow(VS.80).aspx )

わざわざVisualTreeHelperとかLogicalTreeHelperとか使ってたよママン…