WPFの添付プロパティでファイルドラッグ&ドロップをMVVMに実装する方法

C#

WPFでファイルを読み込む処理を作成します。今回はコードビハインドを使わない方法 を試してみたいと思います。そのために、依存関係プロパティの一種である 添付プロパティ(Attached Properties) を利用します。

実行環境

以下の環境で動作確認を行っています。

oswindows11(4cpu、16GB)
.NET8.0.304
IDEVisual Studio 2022 Community
CommunityToolkit.Mvvm8.4.0
MaterialDesignThemes5.2.1

記事のコードは、解説をわかりやすくするために、エラーチェックなど一部の処理を省略しています。

MVVMの概要を理解している前提で記載しています。

MVVM ToolkitやMaterialDesignThemesは必須ではありません。

コードビハインドでの実装

まずは簡単にコードビハインドでの実装を確認します。ファイルを背景色付き個所にドラッグアンドドロップすることで、そのファイルのパスを下部領域に表示するシンプルなアプリです。

今回は ListBox に対してファイルのドロップ機能を実装します。AllowDrop を True に設定すると、そのコントロールをドラッグアンドドロップ操作の対象にできます。
さらに Drop イベントを追加し、ファイルをドロップした際の処理をコードビハインドに記述します。

<ListBox AllowDrop="True" Drop="FileDrop_Drop" Margin="3" Background="LightGreen"
            HorizontalAlignment="Stretch" VerticalAlignment="Center">
    <TextBlock Text="コードビハインド"  Foreground="Black"/>
</ListBox>
<TextBlock Text="ここにファイルパスを表示" Margin="3" x:Name="FilePath" Grid.Row="1" Foreground="Black"/>

コードビハインド側の実装は以下のとおりです。

		private void FileDrop_Drop(object sender, DragEventArgs e)
		{
			var files = (string[])e.Data.GetData(DataFormats.FileDrop);
			FilePath.Text = files[0];
		}

添付プロパティの利用

次に、添付プロパティを利用した方法を見ていきます。
TextBlock などのコントロールに対して、カスタムプロパティを追加するために添付プロパティを利用します。これを利用して、ファイルドロップ時の処理をMVVMパターンで記述できるようにします。

実装にあたり、以下の記事を参考にしました。

FileDropインターフェースの作成

MVVMで記述するため、実際の処理(ドロップされたファイルパスを TextBlock に設定する処理)は 「FileDropVM.cs」 に書きたいです。そのために、まずはインターフェースを作成し、それを FileDropVM に実装します。

public interface IFileDrop
{
	void OnFilesDropped(string[] files);
}

具体的な処理は以下になります。

internal partial class FileDropVM: ObservableObject, IFileDrop
{

	[ObservableProperty]
	private string? _attached = "ここにファイルパスを表示";

	public void OnFilesDropped(string[] files) => Attached = files[0];

}

添付プロパティの作成

それでは、添付プロパティ自体の実装を見ていきます。

添付プロパティはstatic readonlyとして定義します。また、命名規約としては「XXXXProperty」という形で命名します。

RegisterAttached の引数は以下の順で指定します。

  1. プロパティ名
  2. プロパティの型
  3. プロパティを定義しているクラス
  4. メタデータ(既定値やコールバックなど)

ここではFileDropというbool型のプロパティを作成しています。Trueが設定された際にファイルドロップ機能を有効にします。

メタデータには様々な設定ができますが、重要なのはプロパティ変更コールバック(Property-changed callbacks)です。これはFileDropプロパティの値(True/False)が変更された場合に呼び出されるメソッドを指定します(後述)。第一引数はFileDropプロパティのデフォルト値です。また、8、10行目はFileDropプロパティのgetter、setterです。

public class FileDropPropertyExtension : UIElement
{
	public static readonly DependencyProperty FileDropProperty = DependencyProperty.RegisterAttached(
		"FileDrop", typeof(bool), typeof(FileDropPropertyExtension),
		new FrameworkPropertyMetadata(false, OnPropertyChanged)
	);

	public static bool GetFileDrop(UIElement element) 
    => (bool)element.GetValue(FileDropProperty);
	public static void SetFileDrop(UIElement element, bool value) 
    => element.SetValue(FileDropProperty, value);

}

つづいてOnPropertyChanged の実装を見ていきます。

OnPropertyChanged は添付プロパティの値が変更されたときに呼び出されます。
ここで AllowDrop を有効化し、Drop イベントを追加します。false の場合は逆に解除します。これはコードビハインドでコントロールに直接設定したものと同様の設定です。

	private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
	{
		if (d is not FrameworkElement fe) throw new InvalidOperationException();

		if ((bool)e.NewValue)
		{
			fe.AllowDrop = true;
			fe.Drop += OnDrop;
		}
		else
		{
			fe.AllowDrop = false;
			fe.Drop -= OnDrop;
		}
	}

最後にOnDropイベントを実装します。

OnDrop では DataContext を取り出し、IFileDrop にキャストして処理を実行します。
これにより、コードビハインドではなくVM側で処理できます。

	private static void OnDrop(object sender, DragEventArgs e)
	{
		var dataContext = ((Control)sender).DataContext;

		if (dataContext is not IFileDrop fd)
			return;

		if (e.Data.GetData(DataFormats.FileDrop) is not string[] files)
			return;

		fd.OnFilesDropped(files);
	}

VMクラスはIFileDropインターフェースを継承させているので、問題なくキャストできます。

最後に、添付プロパティをViewで利用します。
まずは xmlns に添付プロパティを定義した名前空間を追加し、コントロールにプロパティを設定します。「クラス名.プロパティ名」という形で利用します。

<Window x:Class="xxxxxxxx"
        (略)
        xmlns:e="clr-namespace:projectname.directory.directory"
        Title="FileDrop" Height="450" Width="800">

    <Grid>
        (略)
        <ListBox Margin="3" Background="LightBlue"  e:FileDropPropertyExtension.FileDrop="True"
                    HorizontalAlignment="Stretch" VerticalAlignment="Center">
            <TextBlock Text="添付プロパティ" Foreground="black" />
        </ListBox>
        <TextBlock Text="{Binding Attached}" Margin="3" Grid.Row="1" Foreground="Black"/>
    </Grid>
</Window>

これで、初回起動時に OnPropertyChanged が呼ばれ、ListBox の AllowDrop と Drop が設定されます。
ファイルをドロップすると OnDrop が発火し、最終的にVMの OnFilesDropped が呼ばれます。

まとめ

  • WPFの添付プロパティを利用することで、コードビハインドを使わずにファイルドロップ機能をMVVMに沿って実装できる。
  • IFileDrop インターフェースを用意し、VMに処理を集約することで責務を分離できる。
  • OnPropertyChanged でイベントの有効化・無効化を制御し、OnDrop でVMに処理を委譲するのがポイント。

コードビハインドを避けたい場合や、再利用可能な設計をしたい場合に便利なパターンです。

コメント