XAMLを使わないWPF入門02「ビューモデルのプロパティとバインド」

コンピュータ

XAMLを使わない状態でデータバインディングはどのようにするか調べてみました。

サンプルコード

プロジェクトの作成

dotnet new wpf -f net8.0 -n NoXAML02
cd NoXAML02
rm *.xaml
rm MainWindow.xaml.cs

ファイル名:NoXAML02.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net8.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <UseWPF>true</UseWPF>
    <!-- Main メソッドを持つ型を指定 -->
    <StartupObject>NoXAML02.App</StartupObject>
  </PropertyGroup>

</Project>

ファイル名:MainWindowViewModel.cs

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace NoXAML02;
public class MainWindowViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;
    protected void OnPropertyChanged([CallerMemberName] string? name = null) =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    
    private int _count;
    public int Count
    {
        get => _count;
        set
        {
            if (_count == value) return;
            _count = value;
            OnPropertyChanged();
        }
    }
}

INotifyPropertyChangedを実装した普通のViewModel

ファイル名:CounterStackPanel.cs

using System.Windows;
using System.Windows.Controls;

namespace NoXAML02;
public class CounterStackPanel : StackPanel
{
    // ① DependencyProperty の登録
    public static readonly DependencyProperty CountProperty =
        DependencyProperty.Register(
            nameof(Count),
            typeof(int),
            typeof(CounterStackPanel),
            new PropertyMetadata(0, OnCountChanged)
        );

    public int Count
    {
        get => (int)GetValue(CountProperty);
        set => SetValue(CountProperty, value);
    }

    private readonly TextBlock _text;
    private readonly Button _button;

    public CounterStackPanel()
    {
        Orientation = Orientation.Horizontal;
        Margin = new Thickness(10);

        _text = new TextBlock { VerticalAlignment = VerticalAlignment.Center };
        _button = new Button
        {
            Content = "+",
            Width = 30,
            Height = 30,
            Margin = new Thickness(5, 0, 0, 0)
        };
        _button.Click += (_, __) => Count++;

        Children.Add(_text);
        Children.Add(_button);

        UpdateText();
    }

    // ② DependencyProperty の変更コールバック
    private static void OnCountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ((CounterStackPanel)d).UpdateText();
    }

    private void UpdateText()
    {
        _text.Text = $"Count: {Count}";
    }
}

StackPanelを継承して、カウントアップされる値を表示するTextBlockとカウントアップするbuttonを配置したユーザーコントロール

ファル名:App.xaml.cs

using System.Windows;
using System.Windows.Data;

namespace NoXAML02;

partial class App : Application
{
    // エントリポイント
    [STAThread]
    public static void Main()
    {
        var app = new App();
        app.Startup += (sender, e) =>
        {
            // ① ViewModel を作成
            var vm = new MainWindowViewModel { Count = 5 };

            // ② カスタムコントロールを作成
            var counter = new CounterStackPanel();

            // ③ コントロールの CountProperty を ViewModel.Count に TwoWay バインド
            var binding = new Binding(nameof(vm.Count))
            {
                Source = vm,
                Mode = BindingMode.TwoWay
            };
            BindingOperations.SetBinding(counter, CounterStackPanel.CountProperty, binding);

            // ④ Window を生成し、DataContext と Content に設定
            var window = new Window
            {
                Title = "継承型カスタムコントロールのバインド例",
                Width = 300,
                Height = 150,
                DataContext = vm,
                Content = counter
            };
            window.Show();

        };
        app.Run();
    }
}

実行イメージ

1.「+」ボタンを押します。

2.数値がカウントアップします。

解説

BindingOperations.SetBinding()メソッドは、バインディングオブジェクト(BindingBase)をターゲットオブジェクト(DependencyObject)とプロパティ(DependencyProperty)と関連付けします。

public static void BindingOperations.SetBinding(DependencyObject target, DependencyProperty dp, BindingBase binding);

バインディングオブジェクト

            var binding = new Binding(nameof(vm.Count))
            {
                Source = vm,
                Mode = BindingMode.TwoWay
            };

ターゲットオブジェクト

            var counter = new CounterStackPanel();

StackPanelの継承内容は
Object→DependencyObject→UIElement→FrameworkElement→Panel→StackPanel
DependencyObjectの派生クラスであることが確認できる。

プロパティ

public class CounterStackPanel : StackPanel
{
    // ① DependencyProperty の登録
    public static readonly DependencyProperty CountProperty =
        DependencyProperty.Register(
            nameof(Count),
            typeof(int),
            typeof(CounterStackPanel),
            new PropertyMetadata(0, OnCountChanged)
        );

PropertyMetadataクラスは、依存関係プロパティの動作を定義するとのこと。

public PropertyMetadata (object defaultValue, System.Windows.PropertyChangedCallback propertyChangedCallback, System.Windows.CoerceValueCallback coerceValueCallback);

サンプルコードのdefaultValueは0になっています。coerceValueCallbackはOnCountChangedを指定しています。
OnCountChanged()→UpdateText()の順番で呼出されて、TextBlockのテキスト内容が変更されます。

MainWindowViewModelのCountプロパティの値が、バインドされることによってCounterStackPanelのTextBlockのテキストに連動するようになります。また、CounterStackPanelのButtonをクリックすることで加算された値がMainWindowViewModelのCountプロパティの値に連動します。

最後に

今回ビューモデルのプロパティとバインドすることが出来ました。次回はコマンドとバインディングする方法の記事にする予定です。

コメント