Клиент на .NET MAUI

Последнее обновление: 08.08.2022

.NET MAUI позволяет создавать клиентские приложения, которые могут взаимодействовать с хабом SignalR на стороне сервера. .NET MAUI применяет тот же клиент, что применяется вцелом в .NET для взаимодействия с хабом SignalR. Рассмотрим, как сделать подобное приложение.

Создание сервера

Сначала определим код сервера, с которым будет взаимодействовать клиент на .NET MAUI. Для этого создадим проект ASP.NET Core по типу Empty.

Определим в проекте следующий простейший класс хаба:

using Microsoft.AspNetCore.SignalR;

namespace SignalRApp
{
    public class ChatHub : Hub
    {
        public async Task Send(string username, string message)
        {
            await this.Clients.All.SendAsync("Receive", username, message);
        }
    }
}

В методе Send хаб будет принимать имя пользователя и его сообщение и транслировать его на функцию Receive всех подключенных клиентов.

В файле Program.cs определим следующий код:

using SignalRApp;   // пространство имен класса ChatHub

var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseUrls("http://0.0.0.0:8080");

builder.Services.AddSignalR();

var app = builder.Build();


app.UseDefaultFiles();
app.UseStaticFiles();

app.MapHub<ChatHub>("/chat");
app.Run();

Здесь следует отметить, что для того, чтобы наше приложение было доступно извне в локальной сети, применяется вызов builder.WebHost.UseUrls("http://0.0.0.0:8080");, который устанавливает для приложения адрес "http://0.0.0.0:8080". Это значит, что мы можем обратиться к приложения, используя адрес локального компьютера в сети и порт 8080. Обратите внимание, что порт должен быть не занят, иначе следует выбрать другой порт.

И также для теста определим в папке wwwroot простейшую веб-страницу index.html:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>Metanit.com</title>
</head>
<body>
    <div>
        Введите логин:<br />
        <input id="userName" type="text" /><br /><br />
        Введите сообщение:<br />
        <input type="text" id="message" /><br /><br />
        <input type="button" id="sendBtn" value="Отправить" disabled="disabled" />
    </div>
    <div id="chatroom"></div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/6.0.1/signalr.js"></script>
    <script>
        const hubConnection = new signalR.HubConnectionBuilder()
            .withUrl("/chat")
            .build();

        document.getElementById("sendBtn").addEventListener("click", function () {
            const userName = document.getElementById("userName").value;   // получаем введенное имя
            const message = document.getElementById("message").value;

            hubConnection.invoke("Send", userName, message) // отправка данных серверу
                .catch(function (err) {
                    return console.error(err.toString());
                });
        });
        // получение данных с сервера
        hubConnection.on("Receive", function (userName, message) {

            // создаем элемент <b> для имени пользователя
            const userNameElem = document.createElement("b");
            userNameElem.textContent = `${userName}: `;

            // создает элемент <p> для сообщения пользователя
            const elem = document.createElement("p");
            elem.appendChild(userNameElem);
            elem.appendChild(document.createTextNode(message));

            // добавляем новый элемент в самое начало
            // для этого сначала получаем первый элемент
            const firstElem = document.getElementById("chatroom").firstChild;
            document.getElementById("chatroom").insertBefore(elem, firstElem);
        });

        hubConnection.start()
            .then(function () {
                document.getElementById("sendBtn").disabled = false;
            })
            .catch(function (err) {
                return console.error(err.toString());
            });
    </script>
</body>
</html>

Общий проект сервера:

SignalR Hub для работы с .NET MAUI и C#

Создание клиента .NET MAUI

Теперь создадим клиентское приложение на .NET MAUI для взаимодействия с вышеопределенным хабом SignalR. Для этого создадим новый проект по типу .NET MAUI App, который путь будет называться SignalrMauiApp.

Создание проекта .NET MAUI для SignalR в C#

После создания проекта в первую очередь добавим в него Nuget-пакет Microsoft.AspNetCore.SignalR.Client.

Microsoft.AspNetCore.SignalR.Client в .NET MAUI

Сначала добавим в проект MAUI класс ChatViewModel, который будет выполнять роль модели представления и через который будет идти взаимодействие с сервером:

using Microsoft.AspNetCore.SignalR.Client;
using System.Collections.ObjectModel;
using System.ComponentModel;

namespace SignalrMauiApp
{
    public class ChatViewModel : INotifyPropertyChanged
    {
        HubConnection hubConnection;

        public string UserName { get; set; }
        public string Message { get; set; }
        // список всех полученных сообщений
        public ObservableCollection<MessageData> Messages { get; } = new();

        // идет ли отправка сообщений
        bool isBusy;
        public bool IsBusy
        {
            get => isBusy;
            set
            {
                if (isBusy != value)
                {
                    isBusy = value;
                    OnPropertyChanged("IsBusy");
                }
            }
        }
        // осуществлено ли подключение
        bool isConnected;
        public bool IsConnected
        {
            get => isConnected;
            set
            {
                if (isConnected != value)
                {
                    isConnected = value;
                    OnPropertyChanged("IsConnected");
                }
            }
        }
        // команда отправки сообщений
        public Command SendMessageCommand { get; } 

        public ChatViewModel()
        {
            // создание подключения
            hubConnection = new HubConnectionBuilder()
                .WithUrl("http://192.168.0.116:8080/chat")
                .Build();

            IsConnected = false;    // по умолчанию не подключены
            IsBusy = false;         // отправка сообщения не идет

            SendMessageCommand = new Command(async () => await SendMessage(), () => IsConnected);

            hubConnection.Closed += async (error) =>
            {
                SendLocalMessage(string.Empty, "Подключение закрыто...");
                IsConnected = false;
                await Task.Delay(5000);
                await Connect();
            };

            hubConnection.On<string, string>("Receive", (user, message) =>
            {
                SendLocalMessage(user, message);
            });
        }
        // подключение к чату
        public async Task Connect()
        {
            if (IsConnected)
                return;
            try
            {
                await hubConnection.StartAsync();
                SendLocalMessage(string.Empty, "Вы вошли в чат...");

                IsConnected = true;
            }
            catch (Exception ex)
            {
                SendLocalMessage(string.Empty, $"Ошибка подключения: {ex.Message}");
            }
        }

        // Отключение от чата
        public async Task Disconnect()
        {
            if (!IsConnected) return;

            await hubConnection.StopAsync();
            IsConnected = false;
            SendLocalMessage(string.Empty, "Вы покинули чат...");
        }

        // Отправка сообщения
        async Task SendMessage()
        {
            try
            {
                IsBusy = true;
                await hubConnection.InvokeAsync("Send", UserName, Message);
            }
            catch (Exception ex)
            {
                SendLocalMessage(string.Empty, $"Ошибка отправки: {ex.Message}");
            }
            
            IsBusy = false;
        }
        // Добавление сообщения
        private void SendLocalMessage(string user, string message)
        {
            Messages.Insert(0, new MessageData(user, message));
        }
        public event PropertyChangedEventHandler PropertyChanged;
        public void OnPropertyChanged(string prop = "")
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(prop));
        }
    }

    public record MessageData(string User, string Message);
}

Раберем данный код. Прежде всего в конце этого кода определен класс MessageData, который представляет одно сообщение. Он имеет два свойства: Message (для хранения собственно текста сообщения) и User (для хранения отправителя сообщения).

Для взаимодействия с хабом в классе ChatViewModel нам потребует класс HubConnection, который предоставляет нам функционал для подключения к хабу и отправки сообщений.

Свойства UserName и Message представляют соответственно имя пользователя и текст сообщения, которые будут отправляться на сервер. Свойство Messages представляет объект ObservableCollection<MessageData> - полученные с сервера сообщения.

Чтобы извещать пользователя о процессе отправки, определено свойство IsBusy - если оно равно true, то приложение находится в процессе оправки сообщения.

Свойство IsConnected указывает, подключено ли приложение к хабу.

Непосредственно для отправки сообщений определена команда SendMessageCommand.

В конструкторе ChatViewModel с помощью класса HubConnectionBuilder создается объект HubConnection. Для его инициализации через метод WithUrl() передается адрес хаба:

hubConnection = new HubConnectionBuilder()
	.WithUrl("http://192.168.0.116:8080/chat")
	.Build();

В каждом конкретном случае адрес будет отличаться. В моем случае адрес компьютера, на котором запущено приложение asp.net - http://192.168.0.116. Поскольку это приложение использует порт 8080, а хаб ChatHub доступен по адресу "/chat", то полный адрес выглядит так: "http://192.168.0.116:8080/chat".

(Чтобы узнать адрес компьютера в локальной сети на Windows в командной строке можно выполнить команду ipconfig)

Затем определяется команда отправки сообщений:

SendMessageCommand = new Command(async () => await SendMessage(), () => IsConnected);

При выполнении команды будет вызываться метод SendMessage. Кроме того, команда будет доступна, если свойство IsConnected равно true, то есть если мы подключены к хабу.

Получив объект HubConnection, мы можем выполнить его настройку. Так, далее устанавливается обрабатчик события завершения подключения:

hubConnection.Closed += async (error) =>
{
	SendLocalMessage(String.Empty, "Подключение закрыто...");
	IsConnected = false;
	await Task.Delay(5000);
	await Connect();
};

При закрытии подключения, которое может происходить по самым разным причинам, коллекцию Messages добавляется диагностическое сообщение для пользователя (поэтому вместо имени пользователя используется пустая строка string.Empty) и затем через 5 секунд мы повторно пытаемся подключиться к хабу.

Кроме того, нам надо настроить прием сообщений. Для этого применяется метод On:

hubConnection.On<string, string>("Receive", (user, message) =>
{
	SendLocalMessage(user, message);
});

В классе хаба мы транслируем всем подключенным клиентам на функцию Receive две строки: Clients.All.SendAsync("Receive", username, message);. Поэтому в данном случае метод On() типизирован двумя объектами string - для получения имени пользователя и его сообщения. Первый парамет метода указавает название функции - Receive, а второй параметр представляет лямбда-выражение, в котором мы получаем от сервера данные.

В методе Connect() осуществляется подключение к хабу. Для этого применяется вызов hubConnection.StartAsync(). После его успешного выполнения мы можем взаимодействовать с сервером.

В методе Disconnect() происходит отключение от сервера. Для этого применяется вызов hubConnection.StopAsync()

Метод SendMessage() предназначен для отправки сообщений хабу. Это осуществляется посредством вызова hubConnection.InvokeAsync("Send", UserName, Message); - на хабе вызывается метод Send, которому передаются значения UserName и Message.

Теперь используем этот класс. Для этого определим к следующий интерфейс на странице MainPage.xaml:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="SignalrMauiApp.MainPage">

    <StackLayout>
        <ActivityIndicator IsRunning="{Binding IsBusy}" IsVisible="{Binding IsBusy}"
                           HorizontalOptions="CenterAndExpand" />
        <StackLayout Padding="5">
            <Label FontSize="18" Text="Логин" />
            <Entry x:Name="userNameBox"  Text="{Binding UserName}" HorizontalOptions="FillAndExpand"/>
            <Label FontSize="Small" Text="Сообщение" />
            <Entry HorizontalOptions="FillAndExpand" Text="{Binding Message}"/>
            <Button Text="Отправить" IsEnabled="{Binding IsConnected}" Command="{Binding SendMessageCommand}"/>
        </StackLayout>
        <ListView ItemsSource="{Binding Messages}">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <ViewCell>
                        <ViewCell.View>
                            <StackLayout Orientation="Horizontal">
                                <Label Text="{Binding User}" FontAttributes="Bold" />
                                <Label Text="{Binding Message}" Margin="10, 0, 0, 0" />
                            </StackLayout>
                        </ViewCell.View>
                    </ViewCell>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </StackLayout>
</ContentPage>

Элемент ActivityIndicator извещает пользователя об процессе отправки сообщений. Для ввода данных определены два текстовых поля. И по нажатию на кнопку вызывается команда SendMessageCommand, которая оправляет введенные данные.

Для отображения сообщений определен элемент ListView.

В файле MainPage.xaml.cs определим привязку ChatViewModel к странице:

namespace SignalrMauiApp;

public partial class MainPage : ContentPage
{
    ChatViewModel viewModel;
    public MainPage()
    {
        InitializeComponent();
        viewModel = new ChatViewModel();
        BindingContext = viewModel;
    }

    protected override async void OnAppearing()
    {
        base.OnAppearing();
        await viewModel.Connect();
    }

    protected override async void OnDisappearing()
    {
        base.OnDisappearing();
        await viewModel.Disconnect();
    }
}

В методе OnAppearing, то есть когда начинается отображение страницы, осуществляется подключение к хабу. В методе OnDisappearing, когда пользователь покидает страницу или приложение переходит в фоновый режим, то выполняется отключение от хаба.

Важный момент, который надо учитывать при тестировании: использование протокола http. В реальной применении приложении asp.net core с хабом SignalR рекомендуется запускать по протоколу https. Однако при тестирование гораздо проще использовать протокол http, что позволяет избежать возни с сертификатами. И в примере выше с хабом SignalR приложение ASP.NET как раз запускается по протоколу http. В приложении для Windows проблем с использованием http не возникнет. А вот приложение для Android по умолчанию использует только протокол https. Чтобы в подключить для Android протокол http, необходимо перейти к проекте MAUI к папке Platforms/Android к файлу AndroidManifwest.xml:

Проект .NET MAUI для работы с SignalR в C#

По умолчанию он выглядит следующим образом:

<?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"
	></application>
	<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
	<uses-permission android:name="android.permission.INTERNET" />
</manifest>

Для подключения поддержки http необходимо в элемент application добавить атрибут:

android:usesCleartextTraffic="true"

То есть в итоге код файла будет выглядеть следующим образом:

<?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>
	<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
	<uses-permission android:name="android.permission.INTERNET" />
</manifest>

Запустим сначала приложение ASP.NET Core, а затем приложение на .NET MAUI. И все пользователи, которые подключены к хабу, вне зависимости через javascript-клиент или maui-клиент, смогут взаимодействовать с хабом.

Отправка сообщений хабу SignalR из приложения на .NET MAUI и C#
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850