メモの日々


2022年01月16日(日) [長年日記]

[c#] WPFのコントロールの継承構造

WPFのコントロールがどのようなクラスを継承しているかをメモ。ObjectからControlに到るまでは次のクラスを継承している。

  1. System.Object
  2. System.Windows.Threading.DispatcherObject
  3. System.Windows.DependencyObject
  4. System.Windows.Media.Visual
  5. System.Windows.UIElement
  6. System.Windows.FrameworkElement
  7. System.Windows.Controls.Control

それぞれのクラスの役割については次のページで説明されていて参考になる。


2022年01月17日(月) [長年日記]

[c#] WPFの依存関係プロパティ

WPFで独自のコントロールを作ろうとすると依存関係プロパティを扱うことになる。依存関係プロパティはドキュメントを読んでも説明が回りくどくてなかなか頭に入ってこない。

依存関係プロパティは次のようなものだと考えればいいだろうか。

  • DependencyObjectクラスのサブクラスに定義できる特殊なプロパティ
  • 依存関係プロパティに対してはXAMLから色々な操作(データバインディングとか)を行える

依存関係プロパティの作成方法

依存関係プロパティの作成方法は次のドキュメントで簡潔に説明されていた。

ここにあるコードをそのまま引用する。

public class Aquarium : DependencyObject
{
    public static readonly DependencyProperty HasFishProperty =
        DependencyProperty.Register(
            name: "HasFish",
            propertyType: typeof(bool),
            ownerType: typeof(Aquarium),
            typeMetadata: new FrameworkPropertyMetadata(defaultValue: false));

    public bool HasFish
    {
        get => (bool)GetValue(HasFishProperty);
        set => SetValue(HasFishProperty, value);
    }
}
  • DependencyProperty.Register()の戻り値を、publicなstatic readonlyフィールドとして公開する。
  • フィールド名は「<property name>Property」の形式である必要がある。
  • 上述の<property name>を通常のプロパティとして公開し、これを介して依存関係プロパティへアクセスできるようにする。
  • 必要に応じてメタデータ(上のコードではFrameworkPropertyMetadataオブジェクトにより指定している)を指定する。メタデータについては後述。

依存関係プロパティのメタデータ

メタデータは自分で定義することもできるが、標準で次の3つのクラスが用意されていて、それぞれ次のようなメタデータを扱える。

  • PropertyMetadata
    • プロパティのデフォルト値
    • 値を変更する前に呼び出される、値補正用のコールバック関数
    • 値が変更されたときに呼び出されるコールバック関数
  • UIPropertyMetadata
    • アニメーションを禁止するかどうかのフラグ。「プロパティのアニメーション」というのは値が設定値へ少しずつ近づいていくような振る舞いのようだ。
  • FrameworkPropertyMetadata
    • データバインディングに対応しているかどうかのフラグ
    • 双方向バインディングが可能かどうかのフラグ
    • 親要素の値を引き継ぐかどうかのフラグ
    • ほかにも色々

2022年01月24日(月) [長年日記]

[c#] XAMLの添付プロパティ

依存関係プロパティと似たものに添付プロパティがある。

ここに

添付プロパティは XAML の概念であり、依存関係プロパティは WPF の概念です。

とある。なるほど? XAMLのドキュメントで添付プロパティに言及しているのはこの辺りだろうか。

添付プロパティの例

添付プロパティとは、次のようなXAML片の「local:Hello.Prop1」のことだ。

<Button x:Name="Button1" local:Hello.Prop1="こんにちは" />

Buttonオブジェクトに、元々は無いProp1というプロパティを「添付」しているということだろう。

このような添付プロパティは次のようなクラスを作れば使えるようになる。

    public class Hello
    {
        public static string GetProp1(DependencyObject target) => "hello";
        public static void SetProp1(DependencyObject target, string value) {}
    }

これで任意のDependencyObjectに対してXAML上でHello.Prop1をプロパティとして添付できるようになる。

ただし、上のHelloクラスは常に"hello"を返すだけなので、プロパティとしては不完全だ。

添付プロパティのDependencyPropertyを使った実装

WPFのDependencyPropertyは添付プロパティを実装するようにも設計されている。DependencyPropertyを使うことでバインディングなどにも対応できるようになる。

Helloクラスを次のように変更する。

    public static class Hello
    {
        public static readonly DependencyProperty Prop1Property =
            DependencyProperty.RegisterAttached(
                "Prop1",
                typeof(string),
                typeof(Hello),
                new PropertyMetadata(defaultValue: "hello"));

        public static string GetProp1(DependencyObject target) =>
            (string)target.GetValue(Prop1Property);

        public static void SetProp1(DependencyObject target, string value) =>
            target.SetValue(Prop1Property, value);
    }

こうすると、最初のXAML片にあるボタンを参照する変数button1に対し

button1.GetValue(Hello.Prop1Property)

が "こんにちは" を返すようになる。

  • Helloクラスのインスタンスは不要なのでstaticクラスにした。
  • 依存関係プロパティを実装する際にはDependencyProperty.Register()を使うが、添付プロパティを実装する際にはDependencyProperty.RegisterAttached()を使う。両者の違いはメタデータが引数のownerType以外の型に対しても作用するかどうか。DependencyProperty.Register() or .RegisterAttached()にある回答が参考になる。
  • RegisterAttached()に指定するownerTypeは何でもいいような気がするが、nameとownerTypeが同じプロパティを複数登録することはできない(コンパイルはできるが実行時にエラーになる)ので、衝突を防ぐために自クラスの型を指定すべきなのだと思う。

2022年01月28日(金) [長年日記]

[c#] WPFのListViewに対するクリック処理を添付ビヘイビアで実装

WPFのListViewに対し、その項目をクリックしたときに何かをする処理はどう書くのか。

にListViewItemへイベントハンドラを設定する例が書かれている。これだとViewのコードビハインドに処理を書くことになるが、コードビハインドを使いたくない場合もあるだろう。

ビヘイビア

コードビハインドを使わずにイベント処理などを行う方法として添付プロパティの仕組みを利用した「添付ビヘイビア」という手法がある。

また、添付ビヘイビアをライブラリ化して使いやすくしたものがBlendでSystem.Windows.Interactivityという名前空間で提供されていたが、現在ではそれがXamlBehaviors for WPFとしてMicrosoft.Xaml.Behaviors.Wpfという名前空間で提供されているようだ。このライブラリはNuGetから導入することができる。ドキュメントもあるがすべてが説明されているわけではなさそう。

ListViewのクリック処理を添付ビヘイビアで実装

XamlBehaviors for WPFを使うと少し短く書けるが、添付ビヘイビアの形で実装してもそれほど違いはなさそうなのでここでは添付ビヘイビアを使う例をメモする。

作るのは次のウィンドウだ。左側にあるListViewの項目をクリックすると、クリックした項目の名前が右側に表示される

ListViewを使った画面

XAMLは次のように書く。

<Window x:Class="WpfStudy.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfStudy"
        mc:Ignorable="d"
        Title="MainWindow" Height="200" Width="400">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="1*"/>
            <ColumnDefinition Width="1*"/>
        </Grid.ColumnDefinitions>

        <ListView Grid.Column="0" ItemsSource="{Binding Items}"
                  local:ListViewMouseBehavior.LeftDownCommand="{Binding SetMessageCommand}">
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="番号" DisplayMemberBinding="{Binding Number}"/>
                    <GridViewColumn Header="名前" DisplayMemberBinding="{Binding Name}"/>
                </GridView>
            </ListView.View>
        </ListView>

        <TextBlock Grid.Column="1" Name="textBlock1" Text="{Binding Message}" FontSize="48"/>
    </Grid>
</Window>
  • ListViewに「local:ListViewMouseBehavior.LeftDownCommand="{Binding SetMessageCommand}"」と添付プロパティを設定している。これがListViewがクリックされたときにコマンドを呼び出す添付ビヘイビアだ。

添付ビヘイビアは次のようになる。

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;

namespace WpfStudy
{
    public static class ListViewMouseBehavior
    {
        // 本クラスが公開する添付プロパティ。
        // マウス左クリック時に実行するコマンドを保持する。
        // コマンドのパラメータにはクリックされたListViewItemのContextを設定している。
        public static readonly DependencyProperty LeftDownCommandProperty =
        DependencyProperty.RegisterAttached(
            "LeftDownCommand",
            typeof(ICommand),
            typeof(ListViewMouseBehavior),
            new PropertyMetadata(OnLeftDownCommandPropertyChanged));

        public static ICommand GetLeftDownCommand(ListView target) =>
            (ICommand)target.GetValue(LeftDownCommandProperty);

        public static void SetLeftDownCommand(ListView target, ICommand value) =>
            target.SetValue(LeftDownCommandProperty, value);

        // LeftDownCommandPropertyに対するPropertyChangedCallback。
        // 対象のListViewに対しマウス左クリック時のイベントハンドラを設定する。
        private static void OnLeftDownCommandPropertyChanged(
            DependencyObject d,
            DependencyPropertyChangedEventArgs e)
        {
            if (d is not UIElement ue) return;

            if (e.OldValue != null) ue.PreviewMouseLeftButtonDown -= ExecuteCommand;
            if (e.NewValue != null) ue.PreviewMouseLeftButtonDown += ExecuteCommand;
        }

        // ListViewに対するマウス左クリック時のイベントハンドラ。
        // プロパティに設定されているコマンドを実行する。
        private static void ExecuteCommand(object sender, MouseButtonEventArgs e)
        {
            if (sender is not ListView listView) return;
            if (e.OriginalSource is not DependencyObject source) return;

            var item = FindAncestor<ListViewItem>(source);
            if (item == null) return;

            var command = e.ChangedButton switch
            {
                MouseButton.Left => GetLeftDownCommand(listView),
                _ => null
            };
            if (command == null) return;

            var param = item.Content;
            if (command.CanExecute(param))
            {
                command.Execute(param);
                e.Handled = true;
            }
        }

        // Visual Treeをoから根の方に辿り、見つかったT型のオブジェクトを返す。
        private static T? FindAncestor<T>(DependencyObject o) where T : DependencyObject
        {
            if (o is T result) return result;

            var parent = VisualTreeHelper.GetParent(o);
            return parent == null ? null : FindAncestor<T>(parent);
        }
    }
}
  • 以前書いたようにして添付プロパティを実装している。
  • 添付プロパティのメタデータに指定するPropertyChangedCallbackにて、ListViewのPreviewMouseLeftButtonDownイベントにハンドラを設定している。このようにしてコードビハインド以外でイベント処理を実装する手法が添付ビヘイビアと呼ばれていると理解している。
  • MouseLeftButtonDownではなくPreviewMouseLeftButtonDownイベントを使っているのは、MouseLeftButtonDownのハンドラが呼ばれなかったから。そういうこともあるとリファレンスのImportantの所に書かれている。
  • クリックされた項目のデータを得るにはイベントハンドラにてListViewItemオブジェクトを取得する必要がある。MouseButtonEventArgsオブジェクトのOriginalSourceからビジュアルツリーを根の方に辿ってListViewItemオブジェクトを見つけるということをしている。

上のXAMLで定義したウィンドウのDataContextには次のクラスのインスタンスをセットした。Windows Community Toolkitに含まれるMVVM Toolkitを使っている。

using Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.Toolkit.Mvvm.Input;
using System.Collections.Generic;
using System.Windows.Input;

namespace WpfStudy
{
    class MainWindowModel : ObservableObject
    {
        public MainWindowModel()
        {
            Items = new List<Item>
            {
                new Item(1, "ねずみ"),
                new Item(2, "うし"),
                new Item(12, "いのしし"),
            };

            SetMessageCommand = new RelayCommand<Item>(x => Message = x?.Name ?? "");
        }

        public IEnumerable<Item> Items { get; }

        public string Message
        {
            get => _message;
            set => SetProperty(ref _message, value);
        }
        private string _message = "";

        public ICommand SetMessageCommand { get; }
    }
}
using System.Windows;

namespace WpfStudy
{
    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : Application
    {
        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);

            var w = new MainWindow
            {
                DataContext = new MainWindowModel(),
            };
            w.Show();
        }
    }
}