Windowsエクスプローラーに存在しないファイルをドロップする（Part3）

# はじめに
[Windowsエクスプローラーに存在しないファイルをドロップする（Part2）](https://qiita.com/miswil/items/49025648cc65fc7ae6e0)の続きです。
# What/How
この課題に対するソリューションは複数あり、やりたいことが複雑になるほど実装も複雑になります。複雑になるほど汎用ソリューションとして使えますが、保守性を考えると適切なレベルで妥協するほうがよいでしょう。
それぞれ内容が全く異なるため記事を分けます。簡単な順に
1. ファイルは1つだけで、ファイルのプロパティ情報（ファイル名・サイズ等）はすでに分かっており、ファイルのデータもすでに手元にある→[Part1](https://qiita.com/miswil/items/b97dd4914a335fa21790)
1. ファイルは複数あり、ファイルのプロパティ情報はすでに分かっており、ファイルのデータもすでに手元にある。未定義動作が起きる可能性を許容する。→[Part2](https://qiita.com/miswil/items/49025648cc65fc7ae6e0)
1. ファイルは複数あり、ファイルのプロパティ情報はすでに分かっており、ファイルのデータもすでに手元にある。定義済み動作のみ許容する。→**本記事**
1. ファイルは複数あり、ファイルのプロパティ情報は取得に時間がかかり、ファイルのデータも取得に時間がかかる。デスクトップアプリを応答不能にしたくない。→[Part4](https://qiita.com/miswil/items/39dfdffde6df0d2a384c)
1. エクスプローラーにドロップするなら、せっかくだしドラッグ中のイメージとかテキストとかも表示したい。↓こういうの→[ドラッグドロップ時にイメージを表示する](https://qiita.com/miswil/items/d635469e8df023b58026)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1247638/3531c1ff-3d22-29d1-a778-f02a0c5784ad.png)
1. いっそのことドロップ先のフォルダーがわかれば一番手っ取り早い。→[Windowsエクスプローラーへファイルドロップした際、ドロップ先のフォルダーを取得する](https://qiita.com/miswil/items/028da61ec3d02775b38e)

# 実装
[Part2](https://qiita.com/miswil/items/49025648cc65fc7ae6e0)では未定義動作になる理由として、.NETがWin32 APIのパラメータを捨ててしまうためと説明しました。そこで今回はWin32 API対応コードを書いてパラメータを直接受け取れるようにします。
## `System.Runtime.InteropServices.ComTypes.IDataObject`
[`System.Runtime.InteropServices.ComTypes.IDataObject`](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.comtypes.idataobject?view=net-8.0&viewFallbackFrom=windowsdesktop-8.0)インタフェースはWin32 APIにおけるデータ交換（ドラッグ&ドロップ、クリップボードコピー）操作に使われます。より詳しく説明すれば"COMインタフェース"と呼ばれるもので、プロセス間でデータを共有できているのはCOMの仕組みのおかげです[^1][^2]。WinFormsで使われる`System.Windows.Forms.DataObject`やWPFの`System.Windows.DataObject`はいずれもこのインタフェースを実装しているので、別プロセスで実行環境も異なるWindowsエクスプローラーとデータをやり取りできるようになっています。`System.Windows.Forms.DataObject`や`System.Windows.DataObject`ではWin32のアンマネージドな領域をうまくラップしてマネージドコードから使いやすくしてくれますが、その副作用で一部のパラメータが捨てられてしまいます。そこで、`System.Runtime.InteropServices.ComTypes.IDataObject`インタフェースを直接実装することで生のパラメータを受け取れるようにするのが今回の趣旨です。
とはいえ、本格的に`System.Runtime.InteropServices.ComTypes.IDataObject`インタフェースを実装することはしません。せっかく`System.Windows.Forms.DataObject`や`System.Windows.DataObject`で既に実装されているのですから、ほとんどそれを再利用します。デザインパターンにおけるProxyです。

```csharp
class MyDataObject : System.Runtime.InteropServices.ComTypes.IDataObject
{
    private DataObject dataObject;
```

:::note
[Part2](https://qiita.com/miswil/items/49025648cc65fc7ae6e0)ではWinFormsのほうは継承で新しい`IDataObject`実装を作ることができましたが、今回はそれはできずWPFと同じようにProxyパターンで実装する必要があります。`System.Runtime.InteropServices.ComTypes.IDataObject`インタフェースを実装しているメソッドがvirtualではなくoverrideできないためです。
:::
:::note warn
ここでWPFでのみ注意が必要な点として、新しく実装するクラスは`System.Windows.IDataObject`インタフェースを実装してはいけません。`System.Runtime.InteropServices.ComTypes.IDataObject`インタフェースのみ実装してください。
```csharp
// NG!!!
class MyDataObject : System.Windows.IDataObject, System.Runtime.InteropServices.ComTypes.IDataObject
{
    private DataObject dataObject;
```
これは、両方のインタフェースが実装されている場合には、`System.Windows.IDataObject`インタフェースの実装を優先して呼び出すという.NETの仕様によるものです。ドキュメントなどに明記がないものではありますが、[実際の`DataObject`](https://github.com/dotnet/wpf/blob/v8.0.1/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/dataobject.cs#L75)がそう動作しています。今後この動作はたぶん変わらないでしょう（変わると互換性が著しく失われるので）。
:::
## `GetData`メソッドと`GetDataHere`メソッド
`System.Runtime.InteropServices.ComTypes.IDataObject`インタフェースのデータ取得メソッドを実装します。ここでFileContentsを取得しようとしているときには、どのファイルかを示す`lindex`パラメータを受け取れるようになります。
```csharp
void IComDataObject.GetData(ref FORMATETC format, out STGMEDIUM medium)
{
    if (DataFormats.GetDataFormat(format.cfFormat).Name == "FileContents" &&
        this.fileContents is not null)
    {
        this.dataObject.SetData("FileContents", this.fileContents[format.lindex]);
    }
    ((IComDataObject)this.dataObject).GetData(ref format, out medium);
}
void IComDataObject.GetDataHere(ref FORMATETC format, ref STGMEDIUM medium)
{
    if (DataFormats.GetDataFormat(format.cfFormat).Name == "FileContents" &&
        this.fileContents is not null)
    {
        this.dataObject.SetData("FileContents", this.fileContents[format.lindex]);
    }
    ((IComDataObject)this.dataObject).GetDataHere(ref format, ref medium);
}
```
`lindex`の値によって、FileContentsに設定する`MemoryStream`を切り替えてやります。あとは元の`DataObject`実装に丸投げしてやるだけです。
## ドラッグ&ドロップの開始
`System.Runtime.InteropServices.ComTypes.IDataObject`インタフェースを実装したクラスが出来上がったら、そのオブジェクトを**直接**使ってドラッグ&ドロップを開始します。
```csharp
var myData = new MyDataObject();
DragDrop.DoDragDrop((Border)sender, myData, DragDropEffects.Copy);
```

# 解説
ソースの説明は適宜していたので、さらに詳細な実装の話をします。`System.Runtime.InteropServices.ComTypes.IDataObject`インタフェースの扱いはWPF, WinFormsそれぞれに独特で、詳細を知らないと期待通りに動作しない場合があります。独自実装などを加える際にはどのインタフェースを実装しどうやって使うのかをよく注意してください。
## WPFの場合
WPFでドラッグ&ドロップを開始するときには`DragDrop.DoDragDrop(DependencyObject dragSource, object data, DragDropEffects allowedEffects)`staticメソッドを呼び出しますが、このメソッドは第二引数で渡された`data`が`System.Windows.DataObject`ではない場合に、`System.Windows.DataObject`でラップする動作をします[^3]。
```csharp
var data = new DataObject(myData);
```
`System.Windows.DataObject`はコンストラクタ第一引数で渡された引数が`System.Windows.IDataObject`インタフェースを実装する場合には、そのインタフェース経由で`System.Windows.IDataObject`インタフェースメソッド、および`System.Runtime.InteropServices.ComTypes.IDataObject`インタフェースメソッドへの呼び出しをディスパッチします。あるいは、`System.Runtime.InteropServices.ComTypes.IDataObject`インタフェースを実装している場合にはそのインタフェース経由で`System.Windows.IDataObject`インタフェースメソッド、および`System.Runtime.InteropServices.ComTypes.IDataObject`インタフェースメソッドへの呼び出しをディスパッチします[^4]。インタフェースが一致しない部分はデータ変換を行い、また一部のメソッドはNotImplementedとして扱います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1247638/4662aa25-0d17-798b-84a2-68b5a96fc8dc.png)
これが、`System.Runtime.InteropServices.ComTypes.IDataObject`インタフェースと`System.Windows.IDataObject`インタフェースを同時に実装してはいけない理由です。`System.Windows.IDataObject`インタフェースを実装している場合、そのインタフェース経由で処理がディスパッチされるため、`System.Runtime.InteropServices.ComTypes.IDataObject`インタフェース実装は使われなくなってしまいます。
## WinFormsの場合
WinFormsでは逆の動きになります。ドラッグ&ドロップを開始するときに呼び出す`Control::DoDragDrop`メソッドでは、`System.Runtime.InteropServices.ComTypes.IDataObject`インタフェースはそのまま使われます[^5]。そのためWinFormsでは`System.Runtime.InteropServices.ComTypes.IDataObject`インタフェースと`System.Windows.Forms.IDataObject`インタフェースを同時に実装したオブジェクトで`Control::DoDragDrop`メソッドを呼び出した場合、`System.Runtime.InteropServices.ComTypes.IDataObject`インタフェースメソッドのみ使われ`System.Windows.Forms.IDataObject`インタフェースの実装は生かされません。
一方、`System.Windows.Forms.DataObject`のコンストラクタに引数を渡した場合の動きはWPFと同様になります。各インタフェースメソッドから内部オブジェクトに処理をディスパッチします[^6]。
# ソースコード全体
上記のコードそのままではなくさらにブラッシュアップしていますが、https://github.com/miswil/DropFiles/tree/master/DropMultipleFilesCom にあります。
# 所感
だんだん怪しいにおいがし始めてきました。フレームワークの内部実装まで気にし始めるくらいなら、素直にWin32を直接使ったほうがきれいな気さえします。

[^1]:[コンポーネント オブジェクト モデル (COM)](https://learn.microsoft.com/ja-jp/windows/win32/com/component-object-model--com--portal)
[^2]:[Windows-Based プログラムでの COM の使用](https://learn.microsoft.com/ja-jp/windows/win32/learnwin32/module-2--using-com-in-your-windows-program)
[^3]:[DragDrop.cs](https://github.com/dotnet/wpf/blob/v8.0.1/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/DragDrop.cs#L412)
[^4]:[WPF dataobject.cs](https://github.com/dotnet/wpf/blob/v8.0.1/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/dataobject.cs#L68)
[^5]:[Control.cs](https://github.com/dotnet/winforms/blob/v8.0.1/src/System.Windows.Forms/src/System/Windows/Forms/Control.cs#L5208)
[^6]:[WinForms DataObject.cs](https://github.com/dotnet/winforms/blob/v8.0.1/src/System.Windows.Forms/src/System/Windows/Forms/DataObject.cs#L104)