ChatGPT頼りでBlazorでの問い合わせフォームを作ってみた。

## 背景
問い合わせフォームをBlazorで作成しようと思い、ChatGPT君にほぼ頼りながら進めたらちょくちょく騙されていました。
※本来ChatGPTのみに頼らず、他に調べながら行うものです。
## ざっくり要件
* バリデーションチェックを行い、エラーの場合は該当箇所にメッセージが表示すること
* ユーザーと管理者に問い合わせ内容をメールで自動送信されること

## ざっくり動作環境
・net Framework 8.0
・Blazor Web App


## htmlからrazorへ

デザインから以下のHTMLを頂きこれをChatGPT君でメール送信できるように変換しました。
※内容はformのみ抜粋し、一部変更しています。
``` html
<section class="mane">
  <form action="">
    <ul>
      <li>
        <div class="left">
          <p class="name text">お名前</p>
        </div>
        <div class="right">
          <input type="text" placeholder="例）山田 太郎">
          <p class="error text">※エラー文言エラー文言エラー文言エラー文言</p>
        </div>
      </li>
      <li>
        <div class="left">
          <p class="name text">メールアドレス</p>
        </div>
        <div class="right">
          <input type="email" placeholder="例）XXXXXX@△△△.com">
          <p class="error text">※エラー文言エラー文言エラー文言エラー文言</p>
        </div>
      </li>
      <li>
        <div class="left">
          <p class="name text">ご用件</p>
        </div>
        <div class="right">
          <textarea name="" id="" placeholder="入力してください"></textarea>
          <p class="error text">※エラー文言エラー文言エラー文言エラー文言</p>
        </div>
      </li>
    </ul>
    <div class="submit">
      <input type="submit" value="送信する">
    </div>
  </form>
</section>
```
以下、ChatGPT君が提案してくれた内容です。
```  js
<EditForm Model="@contactForm" OnValidSubmit="@HandleSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <ul>
        <li>
            <div class="left">
                <p class="name text">お名前</p>
            </div>
            <div class="right">
                <InputText id="name" @bind-Value="contactForm.Name" placeholder="例）山田 太郎" />
                <ValidationMessage For="@(() => contactForm.Name)" />
            </div>
        </li>

        <li>
            <div class="left">
                <p class="name text">メールアドレス</p>
            </div>
            <div class="right">
                <InputText id="email" @bind-Value="contactForm.Email" placeholder="例）XXXXXX@△△△.com" />
                <ValidationMessage For="@(() => contactForm.Email)" />
            </div>
        </li>

        <li>
            <div class="left">
                <p class="name text">ご用件</p>
            </div>
            <div class="right">
                <InputTextArea id="message" @bind-Value="contactForm.Message" placeholder="入力してください" />
                <ValidationMessage For="@(() => contactForm.Message)" />
            </div>
        </li>
    </ul>

    <div class="submit">
        <button type="submit">送信する</button>
    </div>
</EditForm>

@code {
    private ContactFormModel contactForm = new ContactFormModel();

    private async Task HandleSubmit()
    {
        var response = await Http.PostAsJsonAsync("/api/contact", contactForm);

        if (response.IsSuccessStatusCode)
        {
            Navigation.NavigateTo("/ContactComplete");
        }
        else
        {
            Console.WriteLine("メール送信に失敗しました");
        }
    }

    public class ContactFormModel
    {
        [Required(ErrorMessage = "お名前を入力してください")]
        public string Name { get; set; }

        [Required(ErrorMessage = "メールアドレスを入力してください")]
        [EmailAddress(ErrorMessage = "正しいメールアドレスを入力してください")]
        public string Email { get; set; }

        [Required(ErrorMessage = "メッセージを入力してください")]
        public string Message { get; set; }
    }
}
```
さすがですね。。。
これで８割方変更できました。

ここから、不要な物また元のhtmlに合わせるものを修正しますが、
変換してくれたものをいくつか説明します。

## タグなどの説明
ニュアンスで覚えてしまったので、曖昧です。。。

``` js
<EditForm Model="@contactForm" OnValidSubmit="@HandleSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />
```
* **OnValidSubmit**
**OnValidSubmit:フォームのバリデーションが成功したとき指定したメソッド（この場合は HandleSubmit）を呼び出します。**
OnValidSubmit以外にも
**OnInvalidSubmit: バリデーションに失敗した場合に発生するイベント。**
**OnSubmit: バリデーションに関わらず、フォームが送信されたときに常に発生するイベント。**
などがあります。
OnValidSubmitのみでバリデーションに失敗した場合デフォルトで再表示してバリデーションエラーメッセージを表示するようになっています。


* **DataAnnotationsValidator**
**DataAnnotationsValidator**はフォームのバリデーションを実行するために必要です。

* **ValidationSummary**
**ValidationSummary**はフォームのバリデーションエラーをまとめて表示します。
なので、入力箇所にエラー文言を出す場合不要な子です。

``` js
<div class="right">
    <InputText id="name" @bind-Value="contactForm.Name" placeholder="例）山田 太郎" />
    <ValidationMessage For="@(() => contactForm.Name)" />
</div>
```
* **bind-Value**
C#コードで定義されたプロパティの値を双方向に結びつけるための物です。
とても便利な子です。

* **ValidationMessage**
バリデーションエラーが起きた際、バリデーションエラーメッセージを表示してくれます。
ここでは、ContactFormModelでエラー文言を決めています。
この子もとても便利なえらい子です。


メール送信周りは、ChatGPT君が提案してくれた物は無視してほかの方法で組み込みましたので、後ほど説明します。


## ChatGPTに仕掛けられた罠①
ChatGPT君は**ContactFormModelクラス**にプロパティの**Name,Email,Message**に入れてメール送信するように提案してくれましたが、
bind-Valueだとクラス内のプロパティにバインドができないことが解りました。
そのため、**ContactFormModelクラス**の外にプロパティを持つことにします。
これに気付くのにかなりの時間を要した。。。
``` C#
@code {
    [SupplyParameterFromForm]
    [Required(ErrorMessage = "お名前を入力してください")]
    public string? Name { get; set; }

    [SupplyParameterFromForm]
    [Required(ErrorMessage = "メールアドレスを入力してください")]
    [EmailAddress(ErrorMessage = "正しいメールアドレスを入力してください")]
    public string? Email { get; set; }

    [SupplyParameterFromForm]
    [Required(ErrorMessage = "メッセージを入力してください")]
    public string? Message { get; set; }

    
  private async Task HandleSubmit()
  {
        //contactFormが使用されているが、この方法でのメール送信をしない為 一旦残している
        var response = await Http.PostAsJsonAsync("/api/contact", contactForm);

        if (response.IsSuccessStatusCode)
        {
            Navigation.NavigateTo("/ContactComplete");
        }
        else
        {
            Console.WriteLine("メール送信に失敗しました");
        }
    }
  ```

  これで**入力データをメールで送る手前まで**、**バリデーションエラーが発生した際にエラー箇所にメッセージが表示する**ところまで完了しました。

## メール送信機能
メール送信は、以下のサイトを参考に作成しました。
メール送信の仕組みをあまり理解できていない自分でも分かり易く説明してくれます。
[C#、Blazor】Webアプリ開発入門編（Ex1）C#で簡単Eメール送信！　～Blazorアプリへも組み込む～【ASP.NET Core】](https://prota-p.com/csharp_web_blazor_ex1_email/)

問い合わせを送信した際、サイト管理者と問い合わせ者の双方に送る必要があるため、メール送信処理は共通化しています。
また、セキュリティを意識しメールサーバ情報は環境変数で管理するようにしています。
※デバックでの環境変数設定についても上記のサイトが教えてくれます。至れり尽くせりですね！

``` C#:完成形
@code {
    [SupplyParameterFromForm]
    [Required(ErrorMessage = "お名前を入力してください")]
    public string? Name { get; set; }

    [SupplyParameterFromForm]
    [Required(ErrorMessage = "メールアドレスを入力してください")]
    [EmailAddress(ErrorMessage = "正しいメールアドレスを入力してください")]
    public string? Email { get; set; }

    [SupplyParameterFromForm]
    [Required(ErrorMessage = "メッセージを入力してください")]
    public string? Message { get; set; }
  
  private async Task HandleSubmit()
  {
    try
    {
        //送信情報
        string fromEmail = Environment.GetEnvironmentVariable("EMAIL_SENDER_EMAIL");
        var fromAddress = new MailAddress(fromEmail, fromUserName);
        string fromPassword = Environment.GetEnvironmentVariable("EMAIL_SENDER_PASSWORD");
        var toAddress = new MailAddress(Email);

        //ユーザー向け
        string fromUserName = "問い合わせ自動返信";
        string subject = "お問い合わせありがとうございます。";
        string emailBody = "本文";
        await SendEmailAsync(fromEmail, fromUserName, fromPassword, Email, subject, emailBody);

        //管理者向け
        string tomEmail = Environment.GetEnvironmentVariable("EMAIL_ADMIN_EMAIL");
        string fromUserNameAdmin = "問い合わせ";
        string subjectAdmin = "お問い合わせがありました。";
        string emailBodyAdmin = "本文";
        await SendEmailAsync(fromEmail, fromUserNameAdmin, fromPassword, tomEmail, subjectAdmin, emailBodyAdmin);

         // 処理が成功したら、確認ページに遷移
        Navigation.NavigateTo("/ContactComplete");
    }
    catch (Exception ex) when (ex is not NavigationException)
    {
      Navigation.NavigateTo("/ErrorPage");
    }
  }

  //送信処理
  private async Task SendEmailAsync(string fromEmail, string fromUserName, string fromPassword, string toEmail, string subject, string body)
  {
    var fromAddress = new MailAddress(fromEmail, fromUserName);
    var toAddress = new MailAddress(toEmail);

    using (var message = new MailMessage(fromAddress, toAddress)
      {
        Subject = subject,
        Body = body
      })
    {
      var smtp = new SmtpClient
        {
          Host = Environment.GetEnvironmentVariable("EMAIL_SMTP_SERVER"),
          Port = 587,
          EnableSsl = true,
          DeliveryMethod = SmtpDeliveryMethod.Network,
          UseDefaultCredentials = false,
          Credentials = new NetworkCredential(fromAddress.Address, fromPassword)
        };
      await smtp.SendMailAsync(message);
    }
  }
}
```

## ChatGPTに仕掛けられた罠②
メール送信機能もChatGPT君に質問しながら進めていましたが、始めTryCatchの提案が以下のやり方でした。
``` C#:ChatGPTの提案
    try
    {
        //送信情報
        string fromEmail = Environment.GetEnvironmentVariable("EMAIL_SENDER_EMAIL");
        //～～～いろいろな処理～～～
        await SendEmailAsync(fromEmail, fromUserNameAdmin, fromPassword, tomEmail, subjectAdmin, emailBodyAdmin);
        
         // 処理が成功したら、確認ページに遷移
        Navigation.NavigateTo("/ContactComplete");
    }
    catch (Exception ex) //完成版は"when (ex is not NavigationException)"が付いている
    {
      Navigation.NavigateTo("/ErrorPage");
    }
```

この方法だと、**Navigation.NavigateTo**の処理で必ずエラーとなりエラー処理に入ってしまします。
これは、NavigateToメソッドが、内部的にNavigationExceptionという特殊な例外をスローするように設計されているため、catchブロックによって捕捉されてしまうのが原因でした。
そのため、NavigationExceptionだけは例外とcatchで指定する必要があります。
この対策は、[Perplexity](https://www.perplexity.ai/)君から教えてもらいました。

## まとめ
ChatGPT君頼りに問い合わせフォームを作ってみましたが、所々.net 8.0であることを忘れたアドバイスをしたり、頑なに非を認めない面を見ることができました。
大枠はChatGPT君に頼り上手くいかない場合切り捨てて自分で調べるのが良い距離感だと感じました。