.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>
Общий проект сервера:
Теперь создадим клиентское приложение на .NET MAUI для взаимодействия с вышеопределенным хабом SignalR. Для этого создадим новый проект по типу .NET MAUI App, который путь будет называться SignalrMauiApp.
После создания проекта в первую очередь добавим в него Nuget-пакет Microsoft.AspNetCore.SignalR.Client.
Сначала добавим в проект 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:
По умолчанию он выглядит следующим образом:
<?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-клиент, смогут взаимодействовать с хабом.