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

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

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

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

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

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

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

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

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

続きを読む

“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を触るときは気を付ける。

Dictionary`2.Insertからぬるり

(゚д゚)ってなった事象。

System.NullReferenceException: オブジェクト参照がオブジェクト インスタンスに設定されていません。
  場所 System.Collections.Generic.Dictionary`2.Insert(TKey key, TValue value, Boolean add)
  場所 Inscribe.Communication.UserStreams.ConnectionManager.RefreshConnection(AccountInfo info)
  場所 Inscribe.Communication.UserStreams.ConnectionManager.b__2(AccountInfo i)

っていう例外が飛ぶんですね。なんでか知らないけど。

ConnectionManager.RefreshConnectionは単純にDictionaryの内容を弄り回してるだけなので、何が起きてるんだ 「Yフゥォォォォァァァァ—–!!!!!! ってなったところ。

ググってみたら答えがありました。

いつもいつもお世話になっているstackoverflowさん。

http://stackoverflow.com/questions/1320264/how-did-i-get-this-nullreferenceexception-error-here-right-after-the-constructor

Dictionary<>.Insert() will throw a NullReferenceException internally if the dictionary instance is modified from another thread during the insert operation.

要するに、きっちりDictionaryでSynchronizationが行われてないと死ぬよ、ってことですね。

lockなり、ReaderWriterLockSlimなりで同期取ってやるといいです。

.NET Framework 4 なら、System.Collections.Concurrent名前空間にConcurrentDictionaryっていうのもあります。こいつはスレッドセーフなので、あんまり変な事考えないで使うことができそうです。癖があるので、クリティカルなところだと自分でシンクロを実装した方が速いこともあるみたいですが。。

C#からUser Streamsに多重接続する時の注意点

User Streams 超便利ですね。これに対応していないTwitterクライアントとか考えられません。

ところで、クライアントの中には複数のアカウントが利用できるものがあります。折角なので、User Streamsは何本も張って使いたいですよね。(事実、TwitterのImplementation Suggestionsにも「複数のアカウントを扱う場合は複数のストリームを扱えるようにせいって書かれてます-> http://dev.twitter.com/pages/user_streams_suggestions )

そこで、何本も張ろうとすると、あることに気づきます。

3本目以降のストリームが必ずタイムアウトする。

これ、バグでもなんでもなく、ただ単にHTTP 1.1の仕様なんです。

Clients that use persistent connections SHOULD limit the number of
simultaneous connections that they maintain to a given server. A
single-user client SHOULD NOT maintain more than 2 connections with
any server or proxy. A proxy SHOULD use up to 2*N connections to
another server or proxy, where N is the number of simultaneously
active users. These guidelines are intended to improve HTTP response
times and avoid congestion.
http://www.ietf.org/rfc/rfc2616.txt

でも、そんなこと言ってる場合じゃないですね。Twitterは別なんです。廃人さんのサティスファクションを得るには、こんな制限そげぶしなければなりません。

で、ちょっと調べると、INTERNET_OPTION_MAX_CONNS_PER_SERVERとかSetInternetOptionsとかレジストリとか出てきて萎えますが、もちろんそんな手段に頼らなくても済みます。

こちらをご覧ください。

http://msdn.microsoft.com/ja-JP/library/system.net.servicepointmanager.defaultconnectionlimit.aspx

ServicePoint オブジェクトで許可される同時接続の最大数を取得または設定します。

まんまですね。

要するに、

ServicePointManger.DefaultConnectionLimit = (同時接続数上限);

をぶち込んでやれば解決です。たぶん、

ServicePointManager.Expect100Continue = false

って書いてるはずなので、その上か下あたりにでも書いとけばいいでしょう。

って感じのトラブルシューティング。たった1行だけど、僕とKrileには大きな進歩だったりします。

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とか使ってたよママン…

UACで昇格 by manifest in C#

アプリケーションの実行時にsuが欲しいときは、app.exe.manifestを使うといいらしい。

from http://blogs.msdn.com/tsmatsuz/archive/2006/11/01/windows-vista-uac.aspx

以下コピペ。WindowsApplication1は変更すること。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
   <assemblyIdentity version="1.0.0.0" processorArchitecture="X86" name="WindowsApplication1" type="win32"/>
      <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
      <security>
         <requestedPrivileges>
            <requestedExecutionLevel level="requireAdministrator"/>
         </requestedPrivileges>
      </security>
   </trustInfo>
</assembly>

 

で、VS2005とかだとあれこれしなきゃいけない。けど、VS2008からはもっと楽な方法がある。

①とりあえず作ったapp.exe.manifestをプロジェクトに参加させる

②プロジェクトのプロパティ画面を開く

③アイコンとマニフェスト→マニフェスト で app.exe.manifestを選択

以上。楽ですね。

カーソルをリソースからセットする。

Step 1.カーソルをリソースからロードする

なんというか、いろいろ迷いがち。

public Cursor(Type type,string resource); 

なんていうコンストラクタがあるけど、使ったら負け。

ストリームで返せって言ってるので、素直にMemoryStreamを使いましょう。 例:

var csor = new Cursor(new MemoryStream(Properties.Resources.csor)); 

ただし。

これで読むと、カーソルは白黒になります。

メモ Cursor クラスでは、アニメーション カーソル (.ani ファイル)、またはカラーのカーソルはサポートされていません。

http://msdn.microsoft.com/ja-jp/library/system.windows.forms.cursor%28VS.71%29.aspx

仕様です。仕様かーそーなのかー。

でも他のアプリケーションはバリバリカラーですね。どうしましょう。

Step 2.カーソルをカラーでリソースからロードする

カラーで読む方法は無いのかって?あるんです。

Icon icon = new Icon(@"orignal.ico");
Cursor cur = new Cursor(icon.Handle);
Cursor = cur;

ただし、ホットスポットは中心になります。なんとまぁ使えない

Step 3.最終奥義

困った時のWinAPI。

[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]  
extern public static IntPtr LoadImage(IntPtr Instance, String Name, uint Type, int xDesired, int yDesired, uint LoadOption);  
public static System.Windows.Forms.Cursor LoadCursor(String LoadCursorName)
{
         IntPtr ptr = LoadImage(IntPtr.Zero, LoadCursorName, 2, 0, 0, 0x10 | 0x40);
        if (ptr == IntPtr.Zero)
        {
            throw new Exception("カーソルのロードに失敗しました。(" + Marshal.GetLastWin32Error() + ")");
       }
       return new System.Windows.Forms.Cursor(ptr);  
}
  

これで、リソースを一旦ファイルにして読めばいいと思うよ!ほら、GetTempFileNameとかあるし!

[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] 
extern public static IntPtr LoadImage(IntPtr Instance, String Name, uint Type, int xDesired, int yDesired, uint LoadOption); 
public static System.Windows.Forms.Cursor LoadCursor(String LoadCursorName) 
{ 
       IntPtr ptr = LoadImage(IntPtr.Zero, LoadCursorName, 2, 0, 0, 0x10 | 0x40); 
      if (ptr == IntPtr.Zero) 
      { 
          throw new Exception("カーソルのロードに失敗しました。(" + Marshal.GetLastWin32Error() + ")"); 
     } 
     return new System.Windows.Forms.Cursor(ptr); 
} 

以上、備忘録。つかれた。

細かいところTypict 1.xのソースからパクったからバグあるかも。あと、Dispose関連は緩め。気をつけてね。(MemoryStreamに関してはCloseいらないみたいだけど)