rksoftware

Visual Studio とか C# とかが好きです

まだまだ現役! Windows フォームアプリの新機能を確認する (6)

devblogs.microsoft.com

最近あまり面白い話題が少ない感じでしたがついに来ました。みんな大好き Windows フォームアプリのお話です。
↑ のブログ記事。見ていきましょう!

※ 現在 WPF でなく WinForms を愛用している日本の IT 技術者が WinForms アプリを MVVM で作りたいと思っているかは別として。

■ DataContext プロパティ

Control クラスに DataContext プロパティと DataContextChange イベントが追加されたとのことです。
確かに、バージョン 7 からとなっています。
learn.microsoft.com learn.microsoft.com

このプロパティ、バインディングには影響がないそうですが、コントロールの親から子へと伝播して親子で同じ値になってくれるとのことです。その際、イベントが親子すべてで発生すると。

やってみましょう。

■ 検証コード

namespace WinFormsApp1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            this.components = new System.ComponentModel.Container();
            InitializeComponent();

            var button = new System.Windows.Forms.Button
            {
                AutoSize = true,
                Location = new System.Drawing.Point(16, 32),
                Size = new System.Drawing.Size(94, 29),
                Text = "button1",
                UseVisualStyleBackColor = true,
            };
            var label = new System.Windows.Forms.Label
            {
                AutoSize = true,
                Location = new System.Drawing.Point(20, 172),
                Size = new System.Drawing.Size(50, 20),
                Text = "label1",
            };
            this.Controls.Add(button);
            this.Controls.Add(label);

            button.Click += (object? sender, EventArgs e) => this.DataContext = new object();

            this.DataContextChanged += (object? sender, EventArgs e) =>label.Text += "Form::DataContextChanged\n";
            button.DataContextChanged += (object? sender, EventArgs e) => label.Text += "Button::DataContextChanged\n";
            label.DataContextChanged += (object? sender, EventArgs e) => label.Text += "Label::DataContextChanged\n";
        }
    }
}

実行結果

計算通り! ボタンを押すたびに、各コントロールの DataContextChanged が呼ばれています。

■ DataContext の変更に応じてバインディングする ViewModel を変える

こんな感じでしょうか?

public class BindableBase : System.ComponentModel.INotifyPropertyChanged
{
    public event System.ComponentModel.PropertyChangedEventHandler? PropertyChanged;

    protected bool SetProperty<T>(ref T property, T value, [System.Runtime.CompilerServices.CallerMemberName] string propertyName = "")
    {
        if (Object.Equals(property, value)) return false;
        property = value;
        PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
        return true;
    }
}

class Command : System.Windows.Input.ICommand
{
    public event EventHandler? CanExecuteChanged;

    private readonly Action<object?> _commandAction;
    private readonly Func<bool>? _canExecuteCommandAction;

    public Command(Action<object?> commandAction, Func<bool>? canExecuteCommandAction = null)
    {
        _commandAction = commandAction;
        _canExecuteCommandAction = canExecuteCommandAction;
    }

    public bool CanExecute(object? parameter) => _canExecuteCommandAction?.Invoke() ?? true;
    public void Execute(object? parameter) => _commandAction.Invoke(parameter);
    public void NotifyCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}

public class ViewModel1 : BindableBase
{
    private string _text;
    public string Text { get => _text; private set => SetProperty(ref _text, value); }
    public System.Windows.Input.ICommand ButtonCommand { get; init; }

    public ViewModel1()
    {
        ButtonCommand = new Command(((parameter) => Text += $"1:Button clicked: {parameter?.ToString()}\n"));
    }
}

public class ViewModel2 : BindableBase
{
    private string _text;
    public string Text { get => _text; private set => SetProperty(ref _text, value); }
    public System.Windows.Input.ICommand ButtonCommand { get; init; }

    public ViewModel2()
    {
        ButtonCommand = new Command(((parameter) => Text += $"2:Button clicked: {parameter?.ToString()}\n"));
    }
}
namespace WinFormsApp1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            this.components = new System.ComponentModel.Container();
            InitializeComponent();

            var viewModel1 = new ViewModel1();
            var viewModel2 = new ViewModel2();
            var bindingSource = new System.Windows.Forms.BindingSource(components);
            bindingSource.DataSource = viewModel1;

            var button = new Button
            {
                AutoSize = true,
                Location = new System.Drawing.Point(16, 32),
                Size = new System.Drawing.Size(94, 29),
                Text = "button1",
                UseVisualStyleBackColor = true,
            };
            var button2 = new Button
            {
                AutoSize = true,
                Location = new System.Drawing.Point(16, 64),
                Size = new System.Drawing.Size(94, 29),
                Text = "button2",
                UseVisualStyleBackColor = true,
            };
            var label = new System.Windows.Forms.Label
            {
                AutoSize = true,
                Location = new System.Drawing.Point(20, 172),
                Size = new System.Drawing.Size(50, 20),
                Text = "label1",
            };
            button.DataBindings.Add(new System.Windows.Forms.Binding("Command", bindingSource, "ButtonCommand", true));
            label.DataBindings.Add(new System.Windows.Forms.Binding("Text", bindingSource, "Text", true));
            this.Controls.Add(button);
            this.Controls.Add(button2);
            this.Controls.Add(label);

            button2.Click += (object? sender, EventArgs e) => this.DataContext = this.DataContext == viewModel2 ? viewModel1 : viewModel2;
            this.DataContextChanged += (object? sender, EventArgs e) => bindingSource.DataSource = ((System.Windows.Forms.Control?)sender)?.DataContext;
        }
    }
}

実行結果

button2 をクリックするたびに ViewModel が切り替わります。

■ コードの要点

この部分ですね。

button2.Click += (object? sender, EventArgs e) => this.DataContext = this.DataContext == viewModel2 ? viewModel1 : viewModel2;
this.DataContextChanged += (object? sender, EventArgs e) => bindingSource.DataSource = ((System.Windows.Forms.Control?)sender)?.DataContext;

しかしこのコード、DataContext プロパティの伝播を使っていません。BindingSource の DataSource プロパティが実質 DataContext ですし。
子コントロールにごっつい自作クラスにして、BindingSource を独自に持つ形なら活かせますかね? 今後に期待、なのでしょうか。

次回

次回は皆さんお待ちかね! UI デザイナを見ていきましょう。WinForms と言えば UI デザイナ! UI デザイナあってこその WinForms! UI デザイナなしに WinForms のコードを書く人はおそらく正直、変態と言われても仕方がないレベルだと思います。
UI デザイナ。期待しましょう!