.NET MAUIとSignalRでリアルタイム通信

# はじめに
今回は.NET MAUIでASP.NETのSignalRというリアルタイム接続をサポートする機能を活用したチャットアプリを作っていきます。

# ASP.NETのテンプレートからSignalRのサーバーアプリを作成する。
ASP.NETのMVCテンプレートからSignalR用のサーバーアプリを作ります。

![テンプレート.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3007605/3d470394-7fc5-ed27-5772-90ac27cdec0a.png)
使用するフレームワークは.NET6を選択します。（.NET7では私の環境だとエラーが出ました）

program.csには以下のようにSignalRをserviceに追加し、SignalRのハブを追加します。
```cs
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();

//↓SignalRの追加
builder.Services.AddSignalR();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
    
    //↓デバッグ（開発段階）ではこのメソッドを実行しない
    app.UseHttpsRedirection();
}
//↓今回デバッグではHTTPS接続を使用しないため、↑へ移動した
//app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

//↓MapHuでChatHubを追加、()の中はエンドポイント
app.MapHub<ChatHub>("/chat");

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();
```
次にMapHubで追加させるChatHubクラスを作成する。
```cs
using Microsoft.AspNetCore.SignalR;

namespace SignalRdemo
{
    //Hubクラスを継承させる
    public class ChatHub:Hub
    {
        //↓SendMessageメソッドの引数はユーザー名とメッセージの内容とする
        public async void SendMessage(string user,string message)
        {
            //↓すべての接続されているクライアントに対して"ReceiveMessage"APIを通して送付する
            await Clients.All.SendAsync("ReceiveMessage",user,message);
        }
    }
}
```
そしてサーバーアプリをローカルホストではなく、実行したPCのIPアドレスで実行させるためにPropetiesの中に入っているlaunchsetting.jsonを変更する
```json
{
　～
  "profiles": {
    "SignalRdemo": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      //↓localhostを0.0.0.0に変更すると立ち上げたときPCのIPでサーバーを構築できる
      "applicationUrl": "https://0.0.0.0:7281;http://0.0.0.0:5103",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
　　～
  }
}
```

# .NET MAUI側でチャット機能の実装
次に.NET MAUI側でチャット機能を実装させる
MainPage.xamlではサンプルのScrollViewを削除し、以下のグリッドを追加する
```xml
    <Grid RowDefinitions="Auto,7*,Auto,Auto"//←グリッドを4つに分割する。2番目の7*前の領域の7倍という意味
          Margin="8">
        <!--ユーザー名を記入するエントリー-->
        <Entry x:Name="userEntry"
               Placeholder="Your Name"/>
        <!--チャットを表示する部分ー-->
        <ScrollView Grid.Row="1">
            <Label x:Name="lblChat"
                   FontSize="14"
                   HorizontalOptions="StartAndExpand"/>
        </ScrollView>
        <!--メッセージを入力するエントリー-->
        <Entry x:Name="yourMessage"
               Grid.Row="2"
               Placeholder="Entry your message"/>
        <!--送信ボタン-->
        <Button x:Name="SendMessage"
                Grid.Row="3"
                Text="SendMessage"
                HorizontalOptions="FillAndExpand"
                Clicked="SendMessage_Clicked"/>
    </Grid>
```
コードビハインドでチャット機能を実装させる
まず、SignalRを使用するため以下のパッケージをNuGetする
Microsoft.AspNetCore.SignalR.Client（←プログラムが.NET6なので.NET6で一番最新のものを追加した）
```cs
using Microsoft.AspNetCore.SignalR.Client;//←名前空間を記入

namespace SignalRdemoClient
{
    public partial class MainPage : ContentPage
    {
        //↓SignalRのHubConnectionを読み込む
        private readonly HubConnection hubConnection;
        public MainPage()
        {
            InitializeComponent();
            //↓自分のPCのIPアドレスとlaunchsetting.jsonで書かれていたhttp://のポート番号を書く
            var baseUrl = "http://(自分のPCのIPアドレス(ex:123.45.67.89)):5103";

            //SignalRと接続するための準備
            hubConnection=new HubConnectionBuilder()
                .WithUrl($"{baseUrl}/chat")//←先ほどのURLの後にエンドポイント("/chat")を追加する
                .Build();

            //↓受け取ったAPIからユーザー名とメッセージを反映する↓ChatHubで作成したAPIと一緒の名前
            hubConnection.On<string, string>("ReceiveMessage", (user, message) =>
            {
                //受け取ったメッセージに改行文字を加えたものをどんどんラベルに追加していく
                lblChat.Text += $"{user}:{message}{Environment.NewLine}";
            });

            Task.Run(() =>
            {
                //↓SignalRと接続している
                Dispatcher.Dispatch(async () =>
                {
                    await hubConnection.StartAsync();
                });
            });
        }

        private async void SendMessage_Clicked(object sender, EventArgs e)
        {
            //↓入力したエントリーの文字を配列にしてサーバー側のSendMessageメソッドに渡す。
            await hubConnection.InvokeCoreAsync("SendMessage", args: new[]
            {
                userEntry.Text,
                yourMessage.Text,
            });
            //↓メッセージを送付したら入力していたメッセージ欄の文字を削除する
            yourMessage.Text= String.Empty;
        }
    }
}
```
各プラットフォームにはAPIと通信するための権限が必要となります。
Androidでは以下のようにAndroidManifest.xmlに追加します。
```xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
	<application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true"
                 android:usesCleartextTraffic="true">//←applicationにandroid:usesCleartextTrafficを追加してtrueとする
    </application>
	<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
	<uses-permission android:name="android.permission.INTERNET" />
</manifest>
```
# デバッグについて
このアプリは最初にサーバーが機能していないと使えないので、サーバーアプリの方から先に立ち上げる必要があります。
ソリューションエクスプローラーからサーバープロジェクトを右クリック、デバッグ→デバッグなしで開始とするとサーバーアプリが立ち上がります。
デバッグモードにならないため、いつも通り、.NET MAUIアプリをデバッグすることができます。
![デバッグ方法.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3007605/895bf5f7-6014-7834-c734-ada63650e658.png)

# さいごに
この方法でPC内のWindowsアプリ同士でSignalR接続はできたのですが、ローカルAndroidで送信ボタンを押すと以下のようなエラーが出ました。
>failed to connect to /{PC側のIPアドレス} (port 5103) from /{Android側のIPアドレス} (port 46710) after 86400000ms: isConnected failed: ETIMEDOUT (Connection timed out)

PC側のファイアーウォールで設定しないといけないかもしれません。
だれかわかる人いたらコメント欄で教えてください。