WinUI3で超簡易電卓を作る練習(データバインド編)

:::note info
WinUI3を学びたく、練習として簡単な電卓を作ってみることにしました。
今回はデータバインドを行います。

C#とxamlに関しても初心者ですのでお手柔らかにお願いします。
:::

## 前回の記録

https://qiita.com/nogino_sanebou/items/940a50df5067fa076310

## 今回やりたいこと
数字と四則演算子、カッコのボタンを押下した際、Inputエリアに入力した文字を表示させます。

ただし、今回は数式として成り立たない`2 + + * 1`のような式の解析はしません。

あくまで入力した文字をInputエリアに反映させるだけです。

## データバインドさせるViewModelを作成する
プロジェクトにViewModelフォルダを作成し、その中にInputViewModel.csを作成しました。

![WinUI3-2025-01-19-1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3976537/21add9a3-0745-b2e5-a908-171a8cfaecc9.jpeg)

InputViewModelの中身はシンプルに`string`型の`Formula`プロパティ、つまり式を格納するプロパティを用意します。

```cs:InputViewModel.cs
namespace Calculator.ViewModel;

public class InputViewModel()
{
    public string Formula { get; set; } = string.Empty;
}
```

MainWindow.xaml.csにこのViewModelを初期化する処理を追加します。
```diff_c_sharp:C#
namespace Calculator;

public sealed partial class MainWindow : Window
{
+   public InputViewModel InputViewModel { get; set; }

    public MainWindow()
    {
        this.InitializeComponent();

        // ウィンドウのサイズを変更します。
        // SizeInt32(Width, Height)
        SizeInt32 size = new (350, 550);
        AppWindow.Resize(size);

        // ウィンドウをリサイズしないようにします。
        OverlappedPresenter? presenter = AppWindow.Presenter as OverlappedPresenter;
        if (presenter != null)
        {
            presenter.IsResizable = false;
        }

        // ViewModelを初期化します。
+       InputViewModel = new InputViewModel();
    }
}
```

MainWindow.xamlの入力情報表示エリアにこのモデルをバインドします。
`{x:Bind ...}`の個所がそれです。

```diff_xml:MainWIndow.xmal
<!-- 入力情報表示領域 -->
-   <TextBox Text="" FontSize="32"
+   <TextBox Text="{x:Bind InputViewModel.Formula}" FontSize="32"
             TextAlignment="Right" IsReadOnly="True"
             Grid.Row="1" Grid.ColumnSpan="3" Margin="1" />
```

次にボタンのクリック処理を加えます。
数字と四則演算子、カッコは同じイベントを発生させます。

まずはMainWindow.xaml.csにイベントの処理を追加します。
```diff_c_sharp:MainWindow.xaml.cs
namespace Calculator;

public sealed partial class MainWindow : Window
{
               // 省略
               
+   /// <summary>
+   /// 数字ボタンと四則演算子ボタン、カッコボタンを押下した際の処理です。
+   /// 入力データを入力領域に追記します。
+   /// </summary>
+   /// <param name="sender"></param>
+   /// <param name="e"></param>
+   private void Button_Click(object sender, RoutedEventArgs e)
+   {
+       // ボタンのContentを取得し、InputViewModel.Formulaに追記します。
+       var content = ((Button)sender).Content;
+
+       InputViewModel.Formula += content.ToString() ?? string.Empty;
+   }
}
```

各ボタンにイベントを登録します。
`Click="Button_Click"`を対象ボタンに追記します。

<details><summary>xamlコード</summary>

```xml:MainWindow.xaml
<?xml version="1.0" encoding="utf-8"?>
<Window
    x:Class="Calculator.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:Calculator"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    Title="超簡易電卓">

    <Grid Width="300" Height="500">
        <!-- 4x7のグリッド -->
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <!-- 結果表示領域 -->
        <TextBox Text="" FontSize="32"
                 TextAlignment="End" IsReadOnly="True"
                 Grid.ColumnSpan="4" Margin="1" />
        <TextBlock Text="Result" Foreground="Gray"
                   VerticalAlignment="Bottom"
                   Grid.Row="0" Margin="10" />

        <!-- 入力情報表示領域 -->
        <TextBox Text="{x:Bind InputViewModel.Formula}" FontSize="32"
                 TextAlignment="Right" IsReadOnly="True"
                 Grid.Row="1" Grid.ColumnSpan="3" Margin="1" />
        <TextBlock Text="Input" Foreground="Gray"
                   VerticalAlignment="Bottom"
                   Grid.Row="1" Margin="10" />

        <Button Content="C" FontSize="32"
                Width="70" Height="70"
                Grid.Row="1" Grid.Column="3" Margin="1" />

        <!-- 一段目 -->
        <Button Content="Bs" FontSize="32"
                Width="70" Height="70"
                Grid.Row="2" Grid.Column="0" Margin="1" />
        <Button Content="（" FontSize="32"
                Click="Button_Click"
                Width="70" Height="70"
                Grid.Row="2" Grid.Column="1" Margin="1" />
        <Button Content="）" FontSize="32"
                Click="Button_Click"
                Width="70" Height="70"
                Grid.Row="2" Grid.Column="2" Margin="1" />
        <Button Content="÷" FontSize="32"
                Click="Button_Click"
                Width="70" Height="70"
                Grid.Row="2" Grid.Column="3" Margin="1" />

        <!-- 二段目 -->
        <Button Content="７" FontSize="32"
                Click="Button_Click"
                Width="70" Height="70"
                Grid.Row="3" Grid.Column="0" Margin="1" />
        <Button Content="８" FontSize="32"
                Click="Button_Click"
                Width="70" Height="70"
                Grid.Row="3" Grid.Column="1" Margin="1" />
        <Button Content="９" FontSize="32"
                Click="Button_Click"
                Width="70" Height="70"
                Grid.Row="3" Grid.Column="2" Margin="1" />
        <Button Content="×" FontSize="32"
                Click="Button_Click"
                Width="70" Height="70"
                Grid.Row="3" Grid.Column="3" Margin="1" />

        <!-- 三段目 -->
        <Button Content="４" FontSize="32"
                Click="Button_Click"
                Width="70" Height="70"
                Grid.Row="4" Grid.Column="0" Margin="1" />
        <Button Content="５" FontSize="32"
                Click="Button_Click"
                Width="70" Height="70"
                Grid.Row="4" Grid.Column="1" Margin="1" />
        <Button Content="６" FontSize="32"
                Click="Button_Click"
                Width="70" Height="70"
                Grid.Row="4" Grid.Column="2" Margin="1" />
        <Button Content="ー" FontSize="32"
                Click="Button_Click"
                Width="70" Height="70"
                Grid.Row="4" Grid.Column="3" Margin="1" />
        
        <!-- 四段目 -->
        <Button Content="１" FontSize="32"
                Click="Button_Click"
                Width="70" Height="70"
                Grid.Row="5" Grid.Column="0" Margin="1" />
        <Button Content="２" FontSize="32"
                Click="Button_Click"
                Width="70" Height="70"
                Grid.Row="5" Grid.Column="1" Margin="1" />
        <Button Content="３" FontSize="32"
                Click="Button_Click"
                Width="70" Height="70"
                Grid.Row="5" Grid.Column="2" Margin="1" />
        <Button Content="＋" FontSize="32"
                Click="Button_Click"
                Width="70" Height="70"
                Grid.Row="5" Grid.Column="3" Margin="1" />

        <!-- 五段目 -->
        <Button Content="０" FontSize="32"
                Click="Button_Click"
                Width="145" Height="70"
                Grid.Row="6" Grid.Column="0" Grid.ColumnSpan="2" Margin="1" />
        <Button Content="．" FontSize="32"
                Click="Button_Click"
                Width="70" Height="70"
                Grid.Row="6" Grid.Column="2" Margin="1" />
        <Button Content="＝" FontSize="32"
                Width="70" Height="70"
                Grid.Row="6" Grid.Column="3" Margin="1" />
    </Grid>
</Window>

```
</details>

## 実行してみる
実装したところで早速動かしてみましたが、なんと動きませんでした。
ボタンを押下しても何も変わりません。

バインドがうまくいってないのか確認のため、ViewModelに初期値を設定してみました。
`string.Empty`を`"0.0000"`に修正しています。

```cs:InputViewModel.cs
namespace Calculator.ViewModel;

public class InputViewModel()
{
    public string Formula { get; set; } = "0.0000";
}
```
![WinUI3-2025-01-19-2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3976537/84124732-eb0f-eedf-3895-a8f111d85e61.jpeg)

すると修正した内容が表示されました。
バインドは出来ている模様です。

そうなるとボタンの`Content`が`Formula`に設定されていない可能性が考えられます。

以下にブレークポイントを置いてプロパティの中身を見てみました。

![WinUI3-2025-01-19-3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3976537/a5fb2a43-ce79-65af-b8ef-f3ec3e1f3c5b.jpeg)

７のボタンを押下したところ、ちゃんと`Content`の値が`Formula`に設定されていることが確認できました。

![WinUI3-2025-01-19-4.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3976537/9f597177-7d16-2e63-d2f1-32525f716b31.jpeg)

このことからバインド設定したテキストボックスに更新が反映されていない事がわかります。

## 更新を反映させるために
まだ詳しい仕組みを理解しきれていませんが、`InotifyPropertyChanged`を継承することで解決できました。

コードは以下の通りです。
```cs:InputViewModel.cs
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace Calculator.ViewModel;

public class InputViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged = delegate { };

    private string _formula = string.Empty;
    public string Formula
    {
        get { return _formula; }
        set
        {
            _formula = value;
            OnPropertyChanged();
        }
    }

    public void OnPropertyChanged([CallerMemberName] string? propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}
```

`PropertyChangedEventHandler.Invoke`が何かうまいことやっているのでしょうね。
値を設定した後に呼び出すことで反映されました。

ここら辺の仕組みについては直ぐに理解できそうな雰囲気ではなかったので、もっとレベルが上がってからチャレンジしていきたいです。

反映されたといいましたが、もう一点、xaml側でも追記の設定必要でした。
`x:Bind`に`Mode=OneWay`を加える必要がありました。
```diff_xml:MainWindow.xaml
<!-- 入力情報表示領域 -->
-    <TextBox Text="{x:Bind InputViewModel.Formula}" FontSize="32"
+    <TextBox Text="{x:Bind InputViewModel.Formula, Mode=OneWay}" FontSize="32"
              TextAlignment="Right" IsReadOnly="True"
              Grid.Row="1" Grid.ColumnSpan="3" Margin="1" />
```

このオプションを指定することで変更を検知できるようになるようです。
ちなみにこのオプションは`TwoWay`というのもありまして、こちらを指定しても動作します。

`OneWay`はコードビハインド側と一方通行(コードビハインド側から値が渡ってくるだけ)で、`TwoWay`は双方向となるようです。

## 動作結果をみて
取り合えず文字が大きいので調整したいですね。
出来ることならばWindowsのCalcみたいに文字の数に応じてサイズが動的に変わると格好良いです。

次回はクリアボタンやイコールボタンでの結果表示などに取り掛かりたいです。
結果表示といってもまだ実際に計算はしませんが……。

![WinUI3-2025-01-19-5.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3976537/16527a8a-b123-64b7-c635-02e9cb77f8f6.jpeg)

## 次の記事

https://qiita.com/nogino_sanebou/items/842192025b1b3b2dc96b