rksoftware

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

みんな大好き Windows フォームアプリでデータバインディングを改めて試してみた

みんな大好き WinForms で、データバインディングが話題なので改めて試してみました。 試した動作はこんな感じです。

github.com

きちんと動作しています。
TextBox への入力の変化で別の TextBox の値を変え、ボタンにバインドした Command も CanExecute も含めてきちんと動作しています。 すばらしいですね。

これで、いつでも新規 WinForms 案件に安心して挑めますね!

■ コード概要

WPF アプリと WinForms アプリで VM を共有して試してみました。

WinForms のコード

namespace WinForms;

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();

        Mvvm.ViewModel dataSource = new();
        this.textBox1.DataBindings.Add(new System.Windows.Forms.Binding("Text", dataSource, "Num1", true, System.Windows.Forms.DataSourceUpdateMode.OnPropertyChanged));
        this.textBox2.DataBindings.Add(new System.Windows.Forms.Binding("Text", dataSource, "Num2", true));
        this.textBox3.DataBindings.Add(new System.Windows.Forms.Binding("Text", dataSource, "Num3", true));
        this.textBox4.DataBindings.Add(new System.Windows.Forms.Binding("Text", dataSource, "Num3", true));
        this.textBox5.DataBindings.Add(new System.Windows.Forms.Binding("Text", dataSource, "Num3", true));
        this.button1.DataBindings.Add(new System.Windows.Forms.Binding("Command", dataSource, "Command", true));
    }
}

WPF のコード

<Window x:Class="Wpf.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:Wpf"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800"
        xmlns:vm="clr-namespace:Mvvm;assembly=Mvvm">
    <Window.DataContext>
        <vm:ViewModel/>
    </Window.DataContext>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <TextBlock Grid.Column="1" Text="+"/>
        <TextBlock Grid.Column="3" Text="="/>
        <TextBox Grid.Column="0" Text="{Binding Num1, UpdateSourceTrigger=PropertyChanged}"/>
        <TextBox Grid.Column="2" Text="{Binding Num2, UpdateSourceTrigger=LostFocus}"/>
        <TextBox Grid.Row="0" Grid.Column="4" Text="{Binding Num3, Mode=OneWay}"/>
        <TextBox Grid.Row="1" Grid.Column="4" Text="{Binding Num3, Mode=OneWayToSource}"/>
        <TextBox Grid.Row="2" Grid.Column="4" Text="{Binding Num3, Mode=TwoWay}"/>

        <Button Grid.Row="2" Content="ボタン" Command="{Binding Command}"/>
    </Grid>
</Window>

ViewModel のコード

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;

namespace Mvvm
{
    public class ViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler? PropertyChanged;
        bool SetProperty<T>(ref T prop, T value, [CallerMemberName] string? name = null)
        {
            if (object.Equals(prop, value)) return false;
            prop = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
            ((Command)Command).InvokeCanExecuteChanged(this);
            return true;
        }

        public int? Num1 { get => _num1; set { if (SetProperty(ref _num1, value)) Add(); } } int? _num1;
        public int? Num2 { get => _num2; set { if (SetProperty(ref _num2, value)) Add(); } } int? _num2;
        public int? Num3 { get => _num3; set => SetProperty(ref _num3, value); } int? _num3;
        public ICommand Command { get; init; }

        void Add() => Num3 = Num1 + Num2;

        public ViewModel() => (Command) = (new Command { ExecuteAction = () => (Num1, Num2) = (0, 0), CanExecuteFunc = () => (Num1, Num2) != (0, 0) });
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;

namespace Mvvm
{
    internal class Command : ICommand
    {
        public event EventHandler? CanExecuteChanged;
        internal required Action ExecuteAction { get; init; }
        internal required Func<bool> CanExecuteFunc { get; init; }

        public bool CanExecute(object? parameter) => CanExecuteFunc.Invoke();
        public void Execute(object? parameter) => ExecuteAction.Invoke();
        internal void InvokeCanExecuteChanged(object sender) => CanExecuteChanged?.Invoke(sender, new EventArgs());
    }
}

■ デザイナで

WinForms のデザイナでも設定ができるようですが、設定してみても動きませんでした。
取り敢えずコードで書いて動いたからいいかなと。実際デザイナで設定するのは効率悪いし、デザイナから作られるソースを読むのも効率悪いし、コードで書くのが良い気がします。

Form に DataContext を設定



ソリューションエクスプローラーでプロパティに ViewModel のクラスが生えましたが、Form の DataContext は空です。なぜ。

TextBox の DataContext



プロパティまで指定することしかできません。

詳細 から設定すると、Text プロパティに ViewModel のプロパティを設定できますね。


これが正しそう。値変更のタイミングも選択できます。

Button の Command


どうもデザイナで Command 設定できないです。なぜ。

こうデザイナで設定自体はあるのですが、扱い方をまだ把握できていません。。。

■ WPF での Mode は?

TwoWay とか OneWayToSource とかのアレですが、

public Binding(string propertyName, object dataSource, string dataMember, bool formattingEnabled, DataSourceUpdateMode dataSourceUpdateMode, object nullValue, string formatString, IFormatProvider formatInfo)

引数を見てもそれらしいプロパティないですね。

■ とにかく

私が扱い方を把握しきれていませんがとにかく、WinForms でも MVVM できたきがします。

■ 追記

DataContext プロパティ、WPF 等と同じように、親コントロールのものを子コントロールも持つようですね。こんなことができました。これはいいですね。

namespace WinForms;

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();

        this.DataContext = new Mvvm.ViewModel();
        AddBinding(textBox1, "Text", "Num1");
        AddBinding(textBox2, "Text", "Num2");
        AddBinding(textBox3, "Text", "Num3");
        AddBinding(textBox4, "Text", "Num3");
        AddBinding(textBox5, "Text", "Num3");
        AddBinding(button1, "Command", "Command");

        static void AddBinding(Control control, string property, string dataMember) => control.DataBindings.Add(property, control.DataContext, dataMember, true);
    }
}