4
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

WPFのユーザーコントロールは依存関係プロパティを作らなくても独自型をバインドできるという話

Last updated at Posted at 2021-09-21

複数のデータをバインドしたいから独自型をつくりたい!

WPFのユーザーコントロールを作るとき、そのユーザーコントロールへのデータバインディングは、ユーザーコントロールの利用者となる親画面の

  1. ViewModelにReactivePropertyを生やして
  2. Viewに配置したユーザーコントロールのプロパティにバインド

というような実装ができるといいですよね。ユーザーコントロールにバインドするデータ本体は親画面のModelで作るわけなのでViewModelから画面に表示する形に加工してバインドするのが自然です。
しかし、ユーザーコントロールへのバインドは既存のTextBlockTextプロパティにセットするのとはワケが違います。ユーザーコントロールは複数の操作を簡略化したり複数のコントロールをまとめるために作るからです。そのため、ユーザーコントロールにバインドしたい型はintstringのようなちゃちなものではなく、適切な名前をつけたプロパティ名に対して自分で定義したclass, recordをバインドしたいと思うのが普通でしょう。
そこでやり方を検索すると「依存関係プロパティを生やしてバインド用のプロパティを作ります」という説明を見かけますが、別にそんなものなくても独自型のバインドはできます。本稿では2つのやり方でのバインド方法を説明するとともに、利点と欠点について説明したいと思います。

前提

画面の実装は以下とします。

  • ユーザーコントロールに依存関係プロパティを作る場合、コードビハインドで実装します。
  • 親画面はMVVMです。
  • <Nullable>enable</Nullable>です。ターゲットフレームワークはnet5.0-windowsです。
  • ReactivePropertyを使います。本稿はReactivePropertyくんを知ってる前提で書いています

作業の前に、バインドしたいデータの型を作っておきましょう。ここでは簡単のためユーザーコントロールはデータを表示するだけとします。表示するだけならメンバの変更が発生しないので普通のrecord型で十分です(親のVMでReactiveProperty<T>を持ってバインドすればよいため)。
もしユーザーコントロールで書き込みしたり、親のVMでメンバを書き換えたりする場合はINotifyPropertyChangedを実装したクラスにするのを忘れないでください。MVVMフレームワークのPrismを使っているならBindableBaseを継承していつものやり方をすればいいです。

MyControlData.cs
public record MyControlData(string Id, int Count, double? Point);

また、画面にバインドするプロパティをViewModelに定義しておきます。

ViewModel.cs
public ReactiveProperty<MyControlData> MyData { get; } = new(new("", 0, null));

依存関係プロパティを作る方法

ユーザーコントロールにDependencyProperty型のプロパティを作る必要があります。これはボイラープレートコードなので、そのまま以下の形式で覚えてしまえばよいでしょう。
ここではユーザーコントロールをUserControl1、画面からバインドする依存関係プロパティの名前をValueとします。また、以下のコードはコードビハインド(UserControl1.xaml.cs)に書きます。MVVMアプリであっても、極端に複雑なことをしていなければユーザーコントロールのコードはすべてコードビハインドに書いたほうがいいです。

UserControl1.cs
public partial class UserControl1 : UserControl
{
    public UserControl1()
    {
        InitializeComponent();
    }
    // 依存関係プロパティの定義に必要なDependencyProperty(ボイラープレート)
    // わかりやすくするため名前は プロパティ名+Property にする
    public static readonly DependencyProperty ValueProperty =
        DependencyProperty.Register(
            nameof(Value),         // プロパティ名
            typeof(MyControlData), // バインドするデータの型
            typeof(UserControl1),  // 自分自身の型
            new PropertyMetadata(  // 初期値をPropertyMetadata経由でつっこむ
                new MyControlData("", 0, null)) 
        );
    // 実際にバインドするプロパティ
    // 上で定義したDependencyPropertyにあーだこーだしてアクセスさせる
    public MyControlData Value 
    {
        get => (MyControlData)GetValue(ValueProperty); 
        set => SetValue(ValueProperty, value); 
    }
}

UserControl1上に配置したコントールにはValue経由で各メンバにアクセスしてバインドします。ただし、UpdateSourceTriggerの設定は忘れないでください。これを忘れると、大本のMyDataプロパティの値を変更してもユーザーコントロールに変更が通知されません。

UserControl1.xaml
<TextBlock Text="{Binding Value.Id, UpdateSourceTrigger=PropertyChanged, RelativeSource={RelativeSource AncestorType={x:Type local:UserControl1}}}" />
<TextBlock Text="{Binding Value.Count, UpdateSourceTrigger=PropertyChanged, RelativeSource={RelativeSource AncestorType={x:Type local:UserControl1}}}" />
<TextBlock Text="{Binding Value.Point, UpdateSourceTrigger=PropertyChanged, RelativeSource={RelativeSource AncestorType={x:Type local:UserControl1}}}" />

ここまでできたら、あとは利用者のViewのxamlでいつもどおりバインドするだけです。

View.xaml
<!-- 自分で作ったプロパティ(ここではValue)にReactivePropertyをバインド -->
<local:UserControl1 Value="{Binding MyData.Value}" />

もう1つの方法:DataContext

実装量としてはこちらの方法が少ないです。UserControl2.xaml.csのコードビハインドへの実装は完全に不要なためです。まず、バインドする型MyControlDataを定義後、ユーザーコントロールのxamlで各コントロールに以下のようにバインドを設定します。例によってUpdateSourceTriggerの設定は忘れないようにしてください。
なお、混同を防ぐためこの方法のコントロールはUserControl2とします。

UserControl2.xaml
<!-- バインドするMyControlData型の各メンバをそのまま設定する -->
<TextBlock Grid.Row="0" Text="{Binding Id, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Grid.Row="1" Text="{Binding Count, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Grid.Row="2" Text="{Binding Point, UpdateSourceTrigger=PropertyChanged}" />

その後、利用者の画面側のViewのxamlに以下のように記述します。

View.xaml
<Window x:Class="MyNamespace.MainWindow"
        ~~ 名前空間とかいろいろ設定 ~~
    >
    <!-- 先頭でResourcesブロックにバインド内容を登録する -->
    <Window.Resources>
        <DataTemplate DataType="{x:Type local:MyControlData}"> <!-- バインドする独自型 -->
            <local:UserControl2 /> <!-- ユーザーコントロール名 -->
        </DataTemplate>
    </Window.Resources>
    <!-- 依存関係プロパティでは新しく作ったプロパティにバインドしていた -->
    <!-- この方法ではDataContextにバインドする -->
    <local:UserControl2 DataContext="{Binding MyData.Value}" />
</Window>

これだけで独自型のバインド設定は完了です。簡単ですね。

※ 利用者のViewとViewModelの接続をどのようにしているかは皆さんの利用しているMVVMフレームワークによると思いますが、もし試しにViewModelを使わずコードビハインドでViewを定義する場合、画面のコンストラクタでInitializeComponent()の実行後にDataContext = this;とするのを忘れないようにしてください。

利点と欠点

方法が複数あるということは、使いどころ、利点と欠点というものが存在します。

  • DataContextを使う場合、ユーザーコントロールのxamlのバインドで扱えるのはDataContextに設定する型のみ
    • データを加工した値等を画面表示に使いたい場合、その独自型のメンバとして画面表示用の値を持つプロパティやメソッドを実装し、それをバインドする必要がある。つまり、「データを画面表示用に加工する」責務はその独自型が担うことになる
    • DataContextは1こしか設定できないため、違う型を複数指定できない。それらを持つobject[]にするとか、複数の型をまとめる型を更に作る必要がある
  • 依存関係プロパティは複数作れる
    • 異なる2つの型のデータを別々の依存関係プロパティにすることで、それぞれバインドすることができる
    • データを画面表示内容に加工する責務はユーザーコントロールのView(コードビハインドで完結させない場合はViewModel)になる
      • 独自型は単なるデータとして扱い、コードビハインドでReactivePropertyを作って独自型をINotifyPropertyChangedにすれば、「コンストラクタでそれぞれの依存関係プロパティをObserveProperty().Select(~~).ToReadOnlyReactivePropety()したプロパティをxamlでバインド」という、普段からよくやっている方法を採れる
    • 実装はやや面倒くさい

どうやって使い分けるか

私の場合は、データクラス側をいろんなところで使い回すかどうかで考えるようにしています。いろんなところで共有する型の場合、依存関係プロパティにしてデータの加工はユーザーコントロールの責任にすることで、データの利用者(ユーザーコントロール)によって見せ方を変えるといったことができるためです。
もちろん、あるユーザーコントロール専用のデータクラスにするならDataContextで十分でしょう。そのような場合はユーザーコントロールの内部クラスにしたくなりますがxamlでは入れ子のメンバーをバインドできないため内部クラスにはできないことに注意してください。
ユーザーコントロールだろうとなんだろうと必ず表示用データは親画面のViewModelで加工する(#,##0のような低次元のフォーマットはxaml)、というルールとするのもアリだと思います。この場合はユーザーコントロールと画面表示値(stringenum)の構造体が必ず1:1になり、Modelが管理する生データを親画面のViewModelで構造体に変換してバインドするようなイメージになると思います。

余談

ユーザーコントロールって単語長すぎ!読みにくいしタイプするの疲れる

4
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?