rksoftware

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

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

devblogs.microsoft.com

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

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

■ .NET 7 での WinForms の Command バインドの新機能

WinForms のボタンとツールバーアイテムに ICommand をバインドするためのアップデートがあるそうです。
アップデートされた要素を一つずつ確認して行きたいのですが、その前に気になったところが。ICommand の実装例も掲載されています。この中に気になるコードがありました。

※コメントや改行は私の好みに書き換えています。

public class RelayCommand : ICommand
{
    public event EventHandler? CanExecuteChanged;

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

    public RelayCommand(Action commandAction, Func<bool>? canExecuteCommandAction = null)
    {
        ArgumentNullException.ThrowIfNull(commandAction, nameof(commandAction));

        _commandAction = commandAction;
        _canExecuteCommandAction = canExecuteCommandAction;
    }

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

この中のここです。

_canExecuteCommandAction is null || _canExecuteCommandAction.Invoke();

なるほど! そういうのもあるのか! という感じでした。普段何も考えずに

_canExecuteCommandAction?.Invoke() ?? true;

と書いていました。覚えておこう。

■ ButtonBase クラスの Comand プロパティ

ButtonBase クラスに Comand プロパティが追加されました。
ButtonBase は .NET Framework 1.1 からある由緒正しい老舗クラスですね。 learn.microsoft.com

Comand プロパティは .NET 7 から。 learn.microsoft.com

記事にも書かれていますが、ButtonBase の派生クラスである System.Windows.Forms.Button、System.Windows.Forms.CheckBox、System.Windows.Forms.RadioButton で使えるはずです。試してみましょう。

■ プレビュー機能

Command プロパティを使おうと思ったらエラーで怒られが発生しました、

エラー    CA2252  'Command' を使用するには、プレビュー機能を選択する必要があります。詳細については、「https://aka.ms/dotnet-warnings/preview-features」を参照してください。

対処として .csproj ファイルへ記述を追加します。

変更前

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

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net7.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <UseWindowsForms>true</UseWindowsForms>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

</Project>

追加する内容

<EnablePreviewFeatures>True</EnablePreviewFeatures>

変更後

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

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net7.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <UseWindowsForms>true</UseWindowsForms>
    <ImplicitUsings>enable</ImplicitUsings>
    <EnablePreviewFeatures>True</EnablePreviewFeatures>
  </PropertyGroup>

</Project>

■ ソースコード

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

            var viewModel = new ViewModel();
            var bindingSource = new System.Windows.Forms.BindingSource(components);
            bindingSource.DataSource = viewModel;

            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 checkBox = new System.Windows.Forms.CheckBox
            {
                AutoSize = true,
                Location = new System.Drawing.Point(16, 76),
                Size = new System.Drawing.Size(101, 24),
                Text = "checkBox1",
                UseVisualStyleBackColor = true,
            };
            var radioButton = new System.Windows.Forms.RadioButton
            {
                AutoSize = true,
                Location = new System.Drawing.Point(16, 108),
                Size = new System.Drawing.Size(117, 24),
                TabStop = true,
                Text = "radioButton1",
                UseVisualStyleBackColor = true,
            };
            var enabledChange = new System.Windows.Forms.Button
            {
                AutoSize = true,
                Location = new System.Drawing.Point(16, 140),
                Size = new System.Drawing.Size(94, 29),
                Text = "EnabledChange",
                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));
            checkBox.DataBindings.Add(new System.Windows.Forms.Binding("Command", bindingSource, "CheckBoxCommand", true));
            radioButton.DataBindings.Add(new System.Windows.Forms.Binding("Command", bindingSource, "RadioButtonCommand", true));
            enabledChange.DataBindings.Add(new System.Windows.Forms.Binding("Command", bindingSource, "EnabledChangeCommand", true));
            label.DataBindings.Add(new System.Windows.Forms.Binding("Text", bindingSource, "Text", true));
            this.Controls.Add(button);
            this.Controls.Add(checkBox);
            this.Controls.Add(radioButton);
            this.Controls.Add(enabledChange);
            this.Controls.Add(label);
        }
    }
}
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 _commandAction;
    private readonly Func<bool>? _canExecuteCommandAction;

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

    public bool CanExecute(object? parameter) => _canExecuteCommandAction?.Invoke() ?? true;
    public void Execute(object? parameter) => _commandAction.Invoke();
    public void NotifyCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
public class ViewModel : BindableBase
{
    bool _enabled = true;
    private string _text;
    public string Text { get => _text; private set => SetProperty(ref _text, value); }
    public System.Windows.Input.ICommand ButtonCommand { get; init; }
    public System.Windows.Input.ICommand CheckBoxCommand { get; init; }
    public System.Windows.Input.ICommand RadioButtonCommand { get; init; }
    public System.Windows.Input.ICommand EnabledChangeCommand { get; init; }

    public ViewModel()
    {
        ButtonCommand = new Command((() => Text += $"Button clicked\n"), canExecute);
        CheckBoxCommand = new Command((() => Text += $"CheckBox clicked\n"), canExecute);
        RadioButtonCommand = new Command((() => Text += $"RadioButton clicked\n"), canExecute);
        EnabledChangeCommand = new Command(enabledChange, null);

        bool canExecute() => _enabled;
        void enabledChange()
        {
            _enabled = !_enabled;
            Text += $"EnabledChangeButton clicked\n";
            ((Command)ButtonCommand).NotifyCanExecuteChanged();
            ((Command)CheckBoxCommand).NotifyCanExecuteChanged();
            ((Command)RadioButtonCommand).NotifyCanExecuteChanged();
        };
    }
}

実行結果

上から順にボタンをクリックしていきました。計算通り、バインディングされています。

■ コードの要点

↑ のコードでバインディングしているところは

var viewModel = new ViewModel();
var bindingSource = new System.Windows.Forms.BindingSource(components);
bindingSource.DataSource = viewModel;

button.DataBindings.Add(new System.Windows.Forms.Binding("Command", bindingSource, "ButtonCommand", true));
checkBox.DataBindings.Add(new System.Windows.Forms.Binding("Command", bindingSource, "CheckBoxCommand", true));
radioButton.DataBindings.Add(new System.Windows.Forms.Binding("Command", bindingSource, "RadioButtonCommand", true));
enabledChange.DataBindings.Add(new System.Windows.Forms.Binding("Command", bindingSource, "EnabledChangeCommand", true));
label.DataBindings.Add(new System.Windows.Forms.Binding("Text", bindingSource, "Text", true));

です。

次回

次回は BindableComponent クラスを見ていきましょう。