Une application .NET MAUI MQTT vous permet de livrer une seule base de code C# qui supervise et pilote des appareils depuis Android, iOS, Windows et macOS. Dans ce tutoriel, vous allez créer une petite application MAUI avec MQTTnet qui affiche une température en direct, indique la présence de l'appareil et commute un relais, et vous traiterez la partie que la plupart des exemples ignorent : ce qui se passe lorsque l'application passe en arrière-plan. Les exemples utilisent le serveur MQTT public gratuit broker.connectmq.com, puis passent à des identifiants ConnectMQ privés.
Ce que vous allez créer
- Une seule page avec un indicateur de connexion, la dernière température et un interrupteur de relais
- Un view model qui détient un client MQTTnet et expose des propriétés bindables
- Des commandes publiées sur
.../relay/setet un état lu sur.../relay/state - Une reconnexion à la reprise, avec réabonnement
Les topics, tous sous un préfixe unique pour éviter les collisions avec les autres utilisateurs du broker partagé :
| Topic | Sens | Payload |
|---|---|---|
connectmq/demo/m4ui7x/device1/temperature |
appareil vers application | {"value": 22.4} |
connectmq/demo/m4ui7x/device1/status |
appareil vers application (retained) | online / offline |
connectmq/demo/m4ui7x/device1/relay/state |
appareil vers application (retained) | on / off |
connectmq/demo/m4ui7x/device1/relay/set |
application vers appareil | on / off |
Séparer set de state est un choix délibéré : l'application demande un changement, l'appareil confirme l'état réel. L'interrupteur à l'écran reflète ce que l'appareil signale, pas ce que l'utilisateur a touché.
Créer le projet
Il vous faut le SDK .NET 8 ou 9 avec le workload MAUI (dotnet workload install maui).
dotnet new maui -n DeviceControl
cd DeviceControl
dotnet add package MQTTnet
dotnet add package CommunityToolkit.Mvvm
CommunityToolkit.Mvvm élimine l'essentiel du code répétitif lié aux notifications de changement de propriété. Il est facultatif, mais raccourcit nettement le view model.
Le view model
Créez MainViewModel.cs. La règle essentielle de toute application .NET MAUI MQTT : MQTTnet déclenche ses événements sur des threads d'arrière-plan, et les propriétés liées à l'interface doivent être mises à jour sur le thread principal. L'oublier provoque sur Android et iOS des plantages déroutants, car ils ne se produisent pas systématiquement sous Windows.
using System.Text.Json;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using MQTTnet;
using MQTTnet.Formatter;
using MQTTnet.Protocol;
namespace DeviceControl;
public partial class MainViewModel : ObservableObject
{
private const string Prefix = "connectmq/demo/m4ui7x/device1";
private readonly IMqttClient _client = new MqttClientFactory().CreateMqttClient();
private readonly MqttClientOptions _options;
private bool _suppressToggle;
[ObservableProperty] private string connectionText = "Disconnected";
[ObservableProperty] private string temperatureText = "--";
[ObservableProperty] private string deviceStatus = "unknown";
[ObservableProperty] private bool relayOn;
public MainViewModel()
{
_options = new MqttClientOptionsBuilder()
.WithTcpServer("broker.connectmq.com", 8883)
.WithTlsOptions(o => o.UseTls())
.WithClientId(GetOrCreateClientId())
.WithProtocolVersion(MqttProtocolVersion.V500)
.WithKeepAlivePeriod(TimeSpan.FromSeconds(30))
.Build();
_client.ApplicationMessageReceivedAsync += e =>
{
var topic = e.ApplicationMessage.Topic;
var payload = e.ApplicationMessage.ConvertPayloadToString();
MainThread.BeginInvokeOnMainThread(() => Apply(topic, payload));
return Task.CompletedTask;
};
_client.DisconnectedAsync += _ =>
{
MainThread.BeginInvokeOnMainThread(() => ConnectionText = "Disconnected");
return Task.CompletedTask;
};
}
private static string GetOrCreateClientId()
{
var id = Preferences.Get("mqtt_client_id", null as string);
if (id is null)
{
id = $"maui-{Guid.NewGuid():N}"[..20];
Preferences.Set("mqtt_client_id", id);
}
return id;
}
private void Apply(string topic, string payload)
{
if (topic.EndsWith("/temperature"))
{
try
{
using var doc = JsonDocument.Parse(payload);
TemperatureText = $"{doc.RootElement.GetProperty("value").GetDouble():0.0} °C";
}
catch (Exception) { TemperatureText = "invalid"; }
}
else if (topic.EndsWith("/status"))
{
DeviceStatus = payload;
}
else if (topic.EndsWith("/relay/state"))
{
_suppressToggle = true;
RelayOn = payload == "on";
_suppressToggle = false;
}
}
[RelayCommand]
public async Task ConnectAsync()
{
if (_client.IsConnected) return;
try
{
ConnectionText = "Connecting...";
await _client.ConnectAsync(_options);
await _client.SubscribeAsync(new MqttClientSubscribeOptionsBuilder()
.WithTopicFilter(f => f.WithTopic($"{Prefix}/temperature"))
.WithTopicFilter(f => f.WithTopic($"{Prefix}/status"))
.WithTopicFilter(f => f.WithTopic($"{Prefix}/relay/state")
.WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce))
.Build());
ConnectionText = "Connected";
}
catch (Exception ex)
{
ConnectionText = $"Error: {ex.Message}";
}
}
public async Task DisconnectAsync()
{
if (_client.IsConnected) await _client.DisconnectAsync();
}
partial void OnRelayOnChanged(bool value)
{
if (_suppressToggle || !_client.IsConnected) return;
_ = _client.PublishAsync(new MqttApplicationMessageBuilder()
.WithTopic($"{Prefix}/relay/set")
.WithPayload(value ? "on" : "off")
.WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce)
.Build());
}
}
Le flag _suppressToggle empêche une boucle d'écho : lorsque l'appareil signale un nouvel état, modifier RelayOn publierait sinon à nouveau une commande. Le client ID est généré une seule fois et stocké dans Preferences : il reste ainsi stable d'un lancement à l'autre, ce qui compte si vous activez plus tard les sessions persistantes.
La page
Remplacez le contenu de 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"
xmlns:local="clr-namespace:DeviceControl"
x:Class="DeviceControl.MainPage"
x:DataType="local:MainViewModel"
Title="Device Control">
<VerticalStackLayout Padding="24" Spacing="16">
<Label Text="{Binding ConnectionText}" FontSize="14" />
<Label Text="Temperature" FontSize="16" />
<Label Text="{Binding TemperatureText}" FontSize="48" FontAttributes="Bold" />
<Label Text="{Binding DeviceStatus, StringFormat='Device: {0}'}" />
<HorizontalStackLayout Spacing="12">
<Label Text="Relay" VerticalOptions="Center" />
<Switch IsToggled="{Binding RelayOn}" />
</HorizontalStackLayout>
<Button Text="Reconnect" Command="{Binding ConnectCommand}" />
</VerticalStackLayout>
</ContentPage>
Et MainPage.xaml.cs :
namespace DeviceControl;
public partial class MainPage : ContentPage
{
private readonly MainViewModel _vm;
public MainPage(MainViewModel vm)
{
InitializeComponent();
BindingContext = _vm = vm;
}
protected override async void OnAppearing()
{
base.OnAppearing();
await _vm.ConnectAsync();
}
}
Enregistrez les deux dans MauiProgram.cs avec builder.Services.AddSingleton<MainViewModel>(); et builder.Services.AddSingleton<MainPage>();, et assurez-vous que AppShell résout la page via l'injection de dépendances (le ShellContent avec ContentTemplate du modèle par défaut fonctionne une fois la page enregistrée).
Gérer le cycle de vie de l'application
Les systèmes d'exploitation mobiles suspendent les applications en arrière-plan et ferment leurs sockets. Android peut maintenir le processus en vie un moment tout en coupant le réseau ; iOS suspend presque immédiatement. Essayer de garder une connexion MQTT ouverte en arrière-plan sans foreground service (Android) ni stratégie de notifications push (iOS) est un combat perdu d'avance. L'approche pragmatique pour une application de supervision :
- Se déconnecter proprement lorsque l'application passe en arrière-plan.
- Se reconnecter et se réabonner à la reprise.
- S'appuyer sur les retained messages pour l'état, afin que l'écran soit correct dès la reconnexion.
Câblez cela dans App.xaml.cs en surchargeant CreateWindow et en vous branchant sur les événements de la fenêtre :
protected override Window CreateWindow(IActivationState? activationState)
{
var window = new Window(new AppShell());
var vm = activationState!.Context.Services.GetRequiredService<MainViewModel>();
window.Stopped += async (_, _) => await vm.DisconnectAsync();
window.Resumed += async (_, _) => await vm.ConnectAsync();
return window;
}
Comme l'appareil publie status et relay/state en retained messages, le broker les livre dès l'abonnement. L'application n'affiche jamais un interrupteur périmé au réveil. Pour les alertes lorsque l'application est fermée, la bonne architecture est un service backend abonné côté serveur qui envoie des notifications push ; consultez le service d'arrière-plan MQTT avec ASP.NET Core pour cette partie.
Simuler l'appareil
Nul besoin de matériel pour tester. Depuis un terminal, jouez le rôle de l'appareil :
# Retained presence and relay state
mosquitto_pub -h broker.connectmq.com -p 1883 -i dev1-sim-a -r -t connectmq/demo/m4ui7x/device1/status -m online
mosquitto_pub -h broker.connectmq.com -p 1883 -i dev1-sim-b -r -t connectmq/demo/m4ui7x/device1/relay/state -m off
# Temperature reading
mosquitto_pub -h broker.connectmq.com -p 1883 -i dev1-sim-c -t connectmq/demo/m4ui7x/device1/temperature -m '{"value": 22.4}'
# Watch the commands the app sends
mosquitto_sub -h broker.connectmq.com -p 1883 -i dev1-sim-d -t connectmq/demo/m4ui7x/device1/relay/set -v
Touchez l'interrupteur dans l'application et la commande apparaît dans le dernier terminal. Publiez ensuite on sur relay/state comme le ferait l'appareil, et l'interrupteur confirme. Un vrai firmware suit le même contrat ; le tutoriel MQTT ESP32 présente un côté appareil que vous pouvez associer à cette application.
Afficher un retour pendant qu'une commande est en attente
Comme l'interrupteur reflète l'état confirmé par l'appareil, il existe un court délai entre le toucher et la confirmation. Les utilisateurs interprètent ce délai comme « l'application est cassée », à moins que vous ne le rendiez visible. Une approche simple consiste en une propriété Pending qui s'active à la publication de la commande et se désactive à l'arrivée de relay/state, liée à un ActivityIndicator. Ajoutez un délai d'expiration de quelques secondes : si aucune confirmation n'arrive, affichez « L'appareil n'a pas répondu » et laissez l'interrupteur sur le dernier état confirmé. Ce petit détail rend une application .NET MAUI MQTT digne de confiance, et il fait remonter les vrais problèmes, comme un appareil hors ligne ou un bug de firmware, au lieu de les masquer.
Si vous voulez aussi savoir si l'appareil est joignable avant que l'utilisateur ne touche l'interrupteur, appuyez-vous sur le topic retained status alimenté par le Last Will de l'appareil. Désactivez l'interrupteur tant que le statut est offline.
Pièges propres à chaque plateforme
| Plateforme | Piège | Solution |
|---|---|---|
| Android | SocketException ou connexion refusée sur l'émulateur |
Vérifiez que l'émulateur a accès à Internet ; la permission INTERNET figure par défaut dans le modèle MAUI |
| Android | Plantage lors de la mise à jour des labels depuis le handler de messages | Repassez toujours sur MainThread |
| iOS | La connexion tombe quelques secondes après le passage en arrière-plan | Comportement attendu ; déconnectez sur Stopped, reconnectez sur Resumed |
| Windows | Fonctionne sous Windows, plante sur mobile | Généralement un bug de threading masqué par le dispatcher Windows ; testez tôt sur un téléphone |
| Toutes | TCP en clair sur 1883 bloqué sur les réseaux mobiles ou le Wi-Fi invité | Préférez TLS sur 8883, ou WebSocket sur 8084 avec WithWebSocketServer |
| Toutes | Deux instances de l'application avec le même ID s'expulsent mutuellement | Stockez un client ID par installation, comme montré ci-dessus |
Identifiants de production et sécurité
Distribuer une application mobile signifie que le binaire peut être décompilé : n'y intégrez donc jamais un mot de passe administrateur partagé. Les options, de la plus simple à la plus robuste :
- Émettre un identifiant par utilisateur ou par installation, provisionné par votre backend après la connexion.
- Garder les commandes derrière votre propre API : l'application appelle votre backend, et le backend publie sur le broker.
- Stocker les identifiants que l'application détient malgré tout dans
SecureStorage, pas dansPreferences.
Avec ConnectMQ, chaque broker est fourni avec un nom d'utilisateur, un mot de passe et un client ID générés, et les client ID sont liés à leurs identifiants. Branchez-les dans l'options builder avec .WithCredentials("YOUR_USERNAME", "YOUR_PASSWORD") et .WithClientId("YOUR_CLIENT_ID"), et pointez WithTcpServer vers l'hôte de votre broker. L'article sur les bonnes pratiques de sécurité MQTT approfondit la stratégie d'identifiants, et le tutoriel MQTTnet explique les options du client utilisées ici.
Tester sur le serveur MQTT public gratuit convient pendant que vous construisez l'interface. Les applications de production, où de vrais utilisateurs pilotent de vrais équipements, nécessitent un broker MQTT managé privé avec authentification et TLS. Pour un panorama plus large des options mobiles et desktop au-delà de .NET, consultez le comparatif des bibliothèques client MQTT.
Démarrer avec ConnectMQ
Créez votre broker privé sur app.connectmq.com avec le plan Serverless gratuit, sans carte bancaire. Il prend en charge jusqu'à 25 connexions, de quoi accueillir une équipe de développement et quelques appareils de test, et le client MQTT intégré au navigateur du portail vous permet de jouer le rôle de l'appareil sans terminal.
Questions fréquentes
Une application .NET MAUI peut-elle recevoir des messages MQTT en arrière-plan ?
Pas de manière fiable par elle-même. Les systèmes d'exploitation mobiles suspendent le réseau en arrière-plan : utilisez donc les retained messages pour retrouver l'état à la reprise, et un subscriber côté serveur avec notifications push pour les alertes lorsque l'application est fermée.
MQTTnet fonctionne-t-il sur iOS et Android avec MAUI ?
Oui. MQTTnet est une bibliothèque purement .NET qui s'exécute sur toutes les cibles MAUI. Utilisez TLS sur le port 8883 et veillez à ce que les mises à jour de l'interface se fassent sur le thread principal.
L'application mobile doit-elle se connecter directement au broker ou passer par une API ?
Pour la supervision, une connexion directe avec des identifiants en lecture seule est efficace. Pour piloter de vrais équipements, de nombreuses équipes font transiter les commandes par leur propre backend afin d'appliquer autorisations et journalisation d'audit avant la publication.
Comment migrer une application MQTT Xamarin.Forms vers MAUI ?
Le code MQTT se transpose généralement tel quel si vous utilisiez MQTTnet. Passez à l'API MQTTnet actuelle (MqttClientFactory en version 5), remplacez les appels Xamarin.Essentials par leurs équivalents MAUI et retestez le threading sur chaque plateforme.



