MAUI でドラッグ&ドロップしたい!


[.NET MAUI Advent Calendar 2024](https://qiita.com/advent-calendar/2024/dotnetmaui) 1日目の記事です。

## TL;DR;

これ: https://learn.microsoft.com/dotnet/maui/fundamentals/gestures/drag-and-drop#drag-and-drop-between-applications

## やりたいこと

アプリケーションにファイルを投げ込んで、そのファイルを表示させたいと思ったことはありますか?

`MAUI Drag and Drop` とかで調べてみると、アプリケーション内の画像をドラッグアンドドロップで移動するチュートリアルが出てきますが、それではなく**アプリケーションの外からファイルを読み込みたい!!**という内容です。

![スクリーンショット 2024-12-01 012821.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2769460/9586a72e-bfdd-2b32-edab-846d4edf15a9.png)

この資料では、使い方のみ解説しています。詳細は[公式ドキュメント](https://learn.microsoft.com/dotnet/maui/fundamentals/gestures/drag-and-drop#drag-and-drop-between-applications)をご覧ください。

(.NET 8 以上で動きます)

## 動かす

テンプレートの MAUI アプリケーションを作成します。

### UI 部分

画像とラベル部分を以下のように変更します。

<!--
![スクリーンショット 2024-12-01 012935.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2769460/9cfc47bc-9138-0bba-809a-521fc4a8752a.png)
![スクリーンショット 2024-12-01 014521.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2769460/1760c1c7-7a79-84a7-b7cc-ff756e09fe36.png)
-->

```diff_xml
  <Image
+     x:Name="image"
      Source="dotnet_bot.png"
      HeightRequest="185"
      Aspect="AspectFit"
-     SemanticPropeties.Description="dot net bot in a race car number eight" />
+     SemanticPropeties.Description="dot net bot in a race car number eight">
+     <Image.GestureRecognizers>
+         <DropGestureRecognizer Drop="OnDropGestureRecognizerDrop" />
+     </Image.GestureRecognizers>
+ </Image>

  ...

  <Label
      Text="Hello, World!"
+     x:Name="path"
      ... />
```

ドラッグアンドドロップは `GestureRecognizers` を利用します。

今回は、画像部分にはドロップされた画像を、テキスト部分にはドロップされたファイルのパスを表示するため、`x:Name` で識別子をつけておきます。

### 処理部分

コード (C#) 側は、以下のようにします。

<!--
![unnamed.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2769460/de6ce7f7-8901-b2e2-78ba-64a54c37ed72.png)
-->

```cs
#if WINDOWS
using Windows.ApplicationModel.DataTransfer;
using Windows.Storage;
#elif IOS || MACCATALYST
using UIKit;
using Foundation;
using System.Diagnostics;
#endif

async void OnDropGestureRecognizerDrop(object? sender, DropEventArgs e)
{
    var filePaths = new List<string>();

    #if WINDOWS
    if (e.PlatformArgs is not null && e.PlatformArgs.DragEventArgs.DataView.Contains(StandardDataFormats.StorageItems))
    {
        var items = await e.PlatformArgs.DragEventArgs.DataView.GetStorageItemsAsync();
        if (items.Any())
        {
            foreach (var item in items)
            {
                if (item is StorageFile file)
                    filePaths.Add(item.Path);
            }
        }
    }
    #elif IOS || MACCATALYST
    var session = e.PlatformArgs?.DropSession;
    if (session == null)
        return;

    foreach (UIDragItem item in session.Items)
    {
        var result = await LoadItemAsync(item.ItemProvider, item.ItemProvider.RegisteredTypeIdentifiers.ToList());
        if (result is not null)
            filePaths.Add(result.FileUrl?.Path!);
    }
    foreach (var item in filePaths)
    {
        Debug.WriteLine($"Path: {item}");
    }

    static async Task<LoadInPlaceResult?> LoadItemAsync(NSItemProvider itemProvider, List<string> typeIdentifiers)
    {
        if (typeIdentifiers is null || typeIdentifiers.Count == 0)
            return null;

        var typeIdent = typeIdentifiers.First();

        if (itemProvider.HasItemConformingTo(typeIdent))
            return await itemProvider.LoadInPlaceFileRepresentationAsync(typeIdent);

        typeIdentifiers.Remove(typeIdent);
        return await LoadItemAsync(itemProvider, typeIdentifiers);
    }
    #endif

    // string filePath = filePaths.FirstOrDefault();

    // Process the dropped file
    path.Text = filePaths.FirstOrDefault();
    image.Source = filePaths.FirstOrDefault();
}
```

([公式ドキュメント](https://learn.microsoft.com/dotnet/maui/fundamentals/gestures/drag-and-drop#drag-and-drop-between-applications)のものを改変)

コードを見てわかるとおり、Windows と iOS + macOS で異なる処理をしてファイルを読み込んでいます。(Android は非対応)

複数のファイルがドロップされることがあるため、List として管理 (`var filePaths = new List<string>();`) し、表示の際には1番目のファイル (`filePaths.FirstOrDefault()`) を利用しています。

### 結果

このサンプルを動かすと以下のようになります。

![スクリーンショット 2024-12-01 012821.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2769460/9586a72e-bfdd-2b32-edab-846d4edf15a9.png)

(画像ファイルをドロップする)

![スクリーンショット 2024-12-01 014653.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2769460/9a4d94ee-e654-a205-ecf6-d719bad36047.png)

(画像が変更され、画像のパスが表示される)

以上です。

先程の

```cs
path.Text = filePaths.FirstOrDefault();
image.Source = filePaths.FirstOrDefault();
```

だった部分を別の処理に変えることで、ドロップされたファイルを簡単に操作できますよ!

### その他

何故かよくわからないけど、Intellisence があんまうまく効いていないみたいです。

![スクリーンショット 2024-12-01 014609.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2769460/daa9e177-7293-3ed4-3596-2b93cfd621ea.png)

(XAML のほうで `<DropGestureRecognizer Drop="OnDropGestureRecognizerDrop" />` と設定しているのに、`OnDropGestureRecognizerDrop` が使われていないという警告が出ています。現在の .NET 9 でも直っていないです)

## 最後に

この方法を見つけるのに半年以上かかりました…

もともと .NET 9 での新機能 ([タイトルバーのカスタマイズ](https://learn.microsoft.com/dotnet/maui/whats-new/dotnet-9?view=net-maui-9.0#titlebar-for-windows)とか) で書こうと思っていたのですが、試す時間がなくてやめました。結構便利そうなので今度試してみたいですね。