WinUI3で超簡易電卓を作る練習(入力データの妥当性チェック-メッセージ出力編)

:::note info
WinUI3を学びたく、練習として簡単な電卓を作ってみることにしました。
今回は入力データの妥当性チェック処理のうち、ダイアログにメッセージを表示する実装を行います。

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

## 前回の記録

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

## やりたいこと
入力したデータが計算式として成り立つかどうかをチェックします。
その結果、成り立たない場合はエラーメッセージをダイアログで出力します。

今回はデータのチェックまではせず、メッセージをダイアログに出力するコードまでを実装します。

## エラーとする内容の洗い出し

チェック処理自体は次回にしますが、メッセージをダイアログに出力するためのエラー一覧は先に決めておきます。

エラーとなるパターンは以下のようになりそうです。
（考慮不足がでてきた場合は随時追加していくと思います）

* 式の先頭に「×  ÷ . )」が出現した
* 「× ÷ .」が連続して出現した
* 「.」の後に数字以外が出現した
* 「+ - × ÷ . (」が式の末尾に出現した
* 式の先頭に「+ - (」が出現した場合、次の文字が数字、「(」だった
* 式の途中に出現した連続する「+ -」が３個以上だった
　(1+-1などは問題ないが、1+--1」などは成り立たない)
* カッコが対になっていない
* 数字、「. )」の次の文字が「(」だった
* 「(」の後に「× ÷ . )」が出現した
* 記号のみで構成されている
* 先頭が「(」の場合、次に文字が「+ - (」でなかった

これによりエラーコードとエラー内容を以下のようにします。

<table>
  <caption>エラーコード一覧</caption>
  <thead>
    <tr><th>エラーコード</th> <th>エラーメッセージ</th></tr>
  </thead>
  <tr><td>S1</td> <td>正常な式、メッセージなし</td></tr>
  <tr><td>E1</td> <td>式の先頭に「× ÷ . )」が出現しました。</td></tr>
  <tr><td>E2</td> <td>「×、÷、.」が連続して出現しました。</td></tr>
  <tr><td>E3</td> <td>「.」の後に数字以外が出現しました。</td></tr>
  <tr><td>E4</td> <td>末尾に数字、「)」以外が出現しました。</td></tr>
  <tr><td>E5</td> <td>先頭が「+ - (」の場合、<br>次に出現できるのは数字、「(」のみです。</td></tr>
  <tr><td>E6</td> <td>式の途中に連続する「+ -」が３個以上出現しました。</td></tr>
  <tr><td>E7</td> <td>カッコが対になっていません。</td></tr>
  <tr><td>E8</td> <td>数字、「. )」の次に「(」が出現しました。</td></tr>
  <tr><td>E9</td> <td>「(」の後に「× ÷ . )」が出現しました。</td></tr>
  <tr><td>E10</td> <td>記号のみで構成されています。</td></tr>
  <tr><td>E11</td> <td>先頭が「(」の場合、次に出現できるのは数字、「+ - (」のみです。</td></tr>
</table>

#### ViewModelの作成

最初にメッセージを管理する`ErrorViewModel`を`ViewModel`フォルダに作成します。
基本内容は`InputViewModel`や`ResultViewModel`と同様です。

<details><summary>ErrorViewModelの初期コード</summary>

```cs:ErrorViewModel.cs
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace Calculator.ViewModel;

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

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

```
</details>

ここにまずエラーコードを表現する列挙型を定義します。
```diff_c_sharp:ErrorViewModel.cs
namespace Calculator.ViewModel;

public partial class ErrorViewModel : INotifyPropertyChanged
{
+    public enum ERROR_CODE
+    {
+        S1, E1, E2, E3, E4, E5, E6, E7, E8, E9, E10, E11,
+    };

     public event PropertyChangedEventHandler? PropertyChanged = delegate { };

       // 省略
}
```

次にこのコードをキーにしたメッセージマップを定義します。
```diff_c_sharp:ErrorViewModel.cs
namespace Calculator.ViewModel;

public partial class ErrorViewModel : INotifyPropertyChanged
{
    public enum ERROR_CODE
    {
        S1, E1, E2, E3, E4, E5, E6, E7, E8, E9, E10, E11,
    };

+   private static Dictionary<ERROR_CODE, string> CodeMap = new()
+   {
+      { ERROR_CODE.S1, string.Empty },
+      { ERROR_CODE.E1, "式の先頭に「× ÷ . )」が出現しました。" },
+      { ERROR_CODE.E2, "「×、÷、.」が連続して出現しました。" },
+      { ERROR_CODE.E3, "「.」の後に数字以外が出現しました。" },
+      { ERROR_CODE.E4, "末尾に数字、「)」以外が出現しました。"},
+      { ERROR_CODE.E5, "先頭が「+、-、(」の場合、次に出現できるのは数字、「(」のみです。" },
+      { ERROR_CODE.E6, "式の途中に連続する「+、-」が３個以上出現しました。" },
+      { ERROR_CODE.E7, "カッコが対になっていません。" },
+      { ERROR_CODE.E8, "「(」の前に「.、)」、数字が出現しました。" },
+      { ERROR_CODE.E9, "「(」の後に「×、÷、.、)」が出現しました。" },
+      { ERROR_CODE.E10, "記号のみで構成されています。" },
+      { ERROR_CODE.E11, "先頭が「(」の場合、次に出現できるのは数字、「+ - (」のみです。" },
+   };

    public event PropertyChangedEventHandler? PropertyChanged = delegate { };

        // 省略
}

```

次にエラーコードを格納するプロパティを定義します。
```diff_c_sharp:ErrorViewModel.cs
namespace Calculator.ViewModel;

public partial class ErrorViewModel : INotifyPropertyChanged
{
        // 省略
   
    public event PropertyChangedEventHandler? PropertyChanged = delegate { };

+   private ERROR_CODE _code = ERROR_CODE.S1;
+   public ERROR_CODE Code
+   {
+       get { return _code; }
+       set
+       {
+           _code = value;
+           OnPropertyChanged();
+       }
+   }

      // 省略
}

```

次にエラーメッセージを取得するプロパティを定義します。
このプロパティは取得とあるようにセッターはありません。
```diff_c_sharp:ErrorViewModel.cs
namespace Calculator.ViewModel;

public partial class ErrorViewModel : INotifyPropertyChanged
{
        // 省略

    private ERROR_CODE _code = ERROR_CODE.S1;
    public ERROR_CODE Code
    {
        get { return _code; }
        set
        {
            _code = value;
            OnPropertyChanged();
        }
    }

+   public string ErrorMessage
+   {
+       get { return CodeMap[Code]; }
+   }

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

## チェック処理の定義
まだ処理は実装しませんが、`MainWindow.xaml.cs`に`ValidFormula`関数を定義します。
```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 Equal_Button_Click(object sender, RoutedEventArgs e)
    {
        // 現在の入力データを取得します。
        string formula = InputViewModel.Formula;

        // 入力データが空の場合は処理をしません。
        if (formula == string.Empty)
        {
            return;
        }
        // 入力データを元に計算処理を行います。
        ResultViewModel.Calculation(formula);
    }

+   /// <summary>
+   /// 入力データが式として成り立つかを検証します。
+   /// 式として成り立たない場合、対応するエラーコードを返します。
+   /// </summary>
+   /// <returns></returns>
+   private ErrorViewModel.ERROR_CODE ValidFormula()
+   {
+       return ErrorViewModel.ERROR_CODE.S1;
+   }

       // 省略
}

```

## ダイアログの設定
今回、ダイアログは`ContentDialog`を使用します。

最初に表示用のxamlを作成します。
プロジェクトに「View」フォルダを作成し、その中に`ErrorMessageDialogContent.xaml`を作成します。
この際、テンプレートはWinUI3の空白のページを選択します。

![2025.01.21-1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3976537/077a4b85-167d-272f-bf13-1244727ed84c.jpeg)

![2025.01.21-2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3976537/cfd048f2-9ad7-b615-719e-df3b2642ebab.jpeg)

最初にコードビハインド側を修正します。
`ErrorViewModel`を紐づけます。
```cs:ErrorMessageDialogContent.cs
namespace Calculator.View;

public partial class ErrorMessageDialogContent : Page
{
    public ErrorViewModel ErrorViewModel { get; set; }

    public ErrorMessageDialogContent()
    {
        this.InitializeComponent();

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

次にxamlを以下コードのように修正します。
```xml:ErrorMessageDialogContent.xaml
<?xml version="1.0" encoding="utf-8"?>
<Page
    x:Class="Calculator.View.ErrorMessageDialogContent"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:Calculator.View"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <StackPanel VerticalAlignment="Stretch" HorizontalAlignment="Stretch">
        <TextBlock Text="{x:Bind ErrorViewModel.ErrorMessage, Mode=OneWay}"
                   TextWrapping="Wrap" />
    </StackPanel>
</Page>
```

`TextBlock`にエラーメッセージをバインドする形で実装します。

## メッセージの表示処理
`MainWindow.xaml.cs`にメッセージを`ContentDialog`で出力する関数、`ShowErrorMessage`を作成します。
ダイアログの出力は結構迷走したので、後々別途解決記事を書きたいと思います。

取り合えず、実装は以下のようにしました。
```diff_c_sharp:MainWindow.xaml.cs
namespace Calculator;

public sealed partial class MainWindow : Window
{
         // 省略
    
    private ErrorViewModel.ERROR_CODE ValidFormula()
    {
        return ErrorViewModel.ERROR_CODE.S1;
    }

+   /// <summary>
+   /// エラーコードより対応するメッセージをダイアログに出力します。
+   /// エラーコードが正常であった場合、ダイアログを出力しません。
+   /// </summary>
+   /// <param name="errorCode"></param>
+   private async void ShowErrorMessage(ErrorViewModel.ERROR_CODE errorCode)
+   {
+       // エラーコードが正常であった場合、処理を終了します。
+       if (errorCode == ErrorViewModel.ERROR_CODE.S1)
+       {
+           return;
+       }
+
+       // 設定したメッセージをコンテントダイアログに出力します。
+       ErrorMessageDialogContent content = new();
+       content.ErrorViewModel.Code = errorCode;
+       ContentDialog dialog = new()
+       {
+           XamlRoot = this.Content.XamlRoot,
+           Style = Application.Current.Resources["DefaultContentDialogStyle"] as Style,
+           Title = "式の解析に失敗しました",
+           CloseButtonText = "閉じる",
+           Content = content,
+       };
+
+       await dialog.ShowAsync();
+   }

    /// <summary>
    /// Cボタン押下時の処理です。
    /// 入力領域と結果領域のデータを空にします。
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void Clear_Button_Click(object sender, RoutedEventArgs e)
    {
        InputViewModel.Formula = string.Empty;
        ResultViewModel.Result = string.Empty;
    }
}

```

`MainWindow.xaml.cs`の`Equal_Button_Click`の先頭にエラーであった場合はメッセージを出力する処理を追加します。

```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 Equal_Button_Click(object sender, RoutedEventArgs e)
    {
+       // 式が成り立っているかを検証し、エラーであればメッセージを出力します。
+       ErrorViewModel.ERROR_CODE validCode = ValidFormula();
+       if (validCode != ErrorViewModel.ERROR_CODE.S1)
+       {
+           ShowErrorMessage(validCode);
+       }

        // 現在の入力データを取得します。
        string formula = InputViewModel.Formula;

        // 入力データが空の場合は処理をしません。
        if (formula == string.Empty)
        {
            return;
        }
        // 入力データを元に計算処理を行います。
        ResultViewModel.Calculation(formula);
    }

       // 省略
}

```

## 完成
ここまででエラーメッセージをダイアログで表示することができるようになりました。
`ShowErrorMessage`に適当にエラーコードを渡す事でメッセージが表示されることを確認できました。

![2025.01.21-3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3976537/f82cf1ba-8787-471b-d0dd-e2825038f44a.jpeg)

## おわりに
今回はダイアログを表示するだけでしたが、思った以上に手間取りました。
一応できはしましたが、よくわからないコードも多々あるのでスキルアップしたら読み解いていきたいです。

次回は式のチェックを行います。
ここでいよいよ本格的にC#でロジックを書きますね。

これまた一筋縄ではいかなさそうです。

## 次の記事

https://qiita.com/nogino_sanebou/items/4e285778805d7c6f3631