As mobile app developer, we constantly have the need to exchange information between the app and the backend. In most cases, a RESTful-API is the solution. But what if a constant flow of data exchange in both directions is required? In this post we will take a look at MQTT and how to create your own simple chat app in .NET MAUI.
MQTT stands for Message Queue Telemetry Transport and is a machine-to-machine (M2M) connectivity protocol. It is used to exchange messages between sensors, mobile phones, cars or anything else you can think of. To do that you need a subscriber and a publisher which are linked to a server - also named broker.
MQTT uses a publish and subscribe model to send messages to 1 or more clients. Clients do not have addresses like in email systems, and messages are not directly sent to them. Instead in MQTT, a publisher publishes messages on a topic and a subscriber must subscribe to that channel to receive the message. It is like TV or radio.
The job of a MQTT broker is to filter messages based on topics and then distribute them to subscribers. A client can receive these messages by subscribing to that topic on the same broker. There is no direct connection between a publisher and a subscriber. All clients can publish ("broadcast") and subscribe ("receive").
The broker is responsible for receiving all messages, filtering the messages, determining who is subscribed to each topic, and sending the message to those subscribed clients. The broker is the heart of any publish/subscribe protocol. Depending on the implementation, a broker can handle millions of concurrently connected MQTT clients.
An MQTT client is any device (from a microcontroller up to a full-fledged server) that runs an MQTT library and connects to an MQTT broker over a network.
All clients must have a client name or ID to be part of the message exchange. The client name is used by the MQTT broker to track subscriptions therefor it has to be unique. If you try to connect to a broker with the same name as an existing client, the existing client connection will be dropped. Since most MQTT client implementations try to reconnect after a disconnect, this can result in a loop of disconnect and connect.
By default, MQTT clients establish a clean session with a broker. A clean session means the broker isn't expected to remember anything about the client when it disconnects.
In an unclean session, the broker remembers client subscriptions and may store undelivered messages for the client. However, this depends on the quality of service used when subscribing and or publishing to those topics.
For our .NET MAUI chat app we need a broker. For this, we create a new console project and install the MQTTnet packages. With the following code we create the simplest MQTT server with a TCP endpoint listening by default on port 1883 and stop the server by pressing any key.
var mqttServer = new MqttFactory().CreateMqttServer();
mqttServer.StartAsync(new MqttServerOptions());
Console.WriteLine("Press any key to exit.");
Console.ReadLine();
mqttServer.StopAsync();
Configure your broker to your needs like changing the port to 1884 or setting up the server client id can be done with the MqttServerOtionsBuilder.
But you can also store all messages received in a log file. For example int the MessagesLog.txt file.
const string LogFilename = "/Users/{user}/Desktop/MessagesLog.txt";
var option = new MqttServerOptionsBuilder().WithDefaultEndpoint().WithDefaultEndpointPort(1884);
var mqttServer = new MqttFactory().CreateMqttServer(option.Build());
mqttServer.InterceptingPublishAsync += context =>
{
var message = Encoding.UTF8.GetString(context.ApplicationMessage.Payload);
if (File.Exists(LogFilename) == false)
File.CreateText(LogFilename);
File.AppendAllText(LogFilename, $"Client: {context.ClientId}, sent time: {DateTime.Now}, message: {JsonConvert.SerializeObject(message)} \r\n");
return CompletedTask.Instance;
};
If a client is disconnected a broker can store received messages and send to clients when they connect and subscribe to the server. These messages are called retain messages. If you want to read more about the retained flag, I recommend hivemq MQTT Essentials series.
MQTTnet gives us events to store messages and load them. Add the RetainedMessage events to your mqttServer. The server handles the sending of messages automatically. You can remove the WithApplicationMessageInterceptor if you don't need it.
mqttServer.LoadingRetainedMessageAsync += async eventArgs =>
{
try
{
var json = await File.ReadAllTextAsync(RetainedMessagesFilename);
eventArgs.LoadedRetainedMessages = JsonConvert.DeserializeObject<List<MqttApplicationMessage>>(json);
}
catch (FileNotFoundException)
{
Console.WriteLine("No retained messages stored yet.");
}
catch (Exception exception)
{
Console.WriteLine(exception);
}
};
mqttServer.RetainedMessageChangedAsync += async eventArgs =>
{
try
{
if (File.Exists(RetainedMessagesFilename) == false)
File.CreateText(RetainedMessagesFilename);
File.WriteAllText(RetainedMessagesFilename, JsonConvert.SerializeObject(eventArgs.StoredRetainedMessages));
}
catch (Exception exception)
{
Console.WriteLine(exception);
}
};
If you can't find your logs, search for your file in the Finder on MacOS or Windows Explorer. Your file should be there.
That is all we need to do for the broker.
If you have a project in Xamarin or .NET MAUI, we can help you save time.
Before proceeding on your own, take a look at our services. Our team of experienced developers is enthusiastic about discussing your project and helping you finish with an excellent standard.
Have a look at app development services
With the broker working, it is time to create a .NET MAUI application that uses MQTTnet to exchange messages between users in nearly real time.
First, we create a new project in which we will set up the MQTT client in a view model class. For the WithTcpServer method, use your IP address and the client id is the current user.
IMqttClient _mqttClient;
string _currentUser = "user1";
string _chatPartner = "user2";
public async Task CreateClient()
{
// Create the client object
_mqttClient = new MqttFactory().CreateMqttClient();
var options = new MqttClientOptionsBuilder()
.WithClientId(_currentUser)
.WithTcpServer("YOUR IP", 1884)
.Build();
}
We also set events in the CreateClient method to be notified when the client will be connected, disconnected, or is receiving messages.
// Set up events
_mqttClient.ConnectedAsync += MqttClient_ConnectedAsync;
_mqttClient.DisconnectedAsync += MqttClient_DisconnectedAsync;
_mqttClient.ApplicationMessageReceivedAsync += MqttClient_ApplicationMessageReceivedAsync;
For Connected and Disconnected I just write the current state. For received messages, we need to update the chat history with the incoming message and the sender.
private Task MqttClient_ApplicationMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs arg)
{
ChatHistory += $"{arg.ClientId}: \n{Encoding.UTF8.GetString(arg.ApplicationMessage.Payload)}\n";
return Task.CompletedTask;
}
private Task MqttClient_DisconnectedAsync(MqttClientDisconnectedEventArgs arg)
{
Console.WriteLine("DISCONNECTED");
return Task.CompletedTask;
}
private Task MqttClient_ConnectedAsync(MqttClientConnectedEventArgs arg)
{
Console.WriteLine("CONNECTED");
return Task.CompletedTask;
}
Finally, we need to connect to the broker and subscribe to our chat channel to be notified when someone sends a message to the channel.
// Connect to broker
await _mqttClient.ConnectAsync(options);
await _mqttClient.SubscribeAsync(newMqttClientSubscribeOptionsBuilder().WithTopicFilter($"chatChannel/{_currentUser}").Build());
Now prepare the page to see your received messages and send your personal message. To send a message, we need to create an applicationMessage. It contains the receiver's channel and the message you want to send. Using the PublishAsync it will be sent to the specified channel.
I'm using the MVVM source generator to implement this view model.
public partial class MainViewModel : ObservableObject
{
[ObservableProperty]
string _chatHistory;
[ObservableProperty]
string _message;
[RelayCommand]
async void Send()
{
var applicationMessage = new MqttApplicationMessageBuilder()
.WithTopic($"chatChannel/{_chatPartner}")
.WithPayload(Message)
.Build();
await _mqttClient.PublishAsync(applicationMessage);
ChatHistory += $"You: \n{Message}\n";
Message = string.Empty;
}
}
For the view we need an editor to show all messages, an entry to enter the message and a button to send the message.
<Grid RowDefinitions="9*,1*" ColumnDefinitions="8*,2*">
<Editor Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" IsReadOnly="True" Text="{Binding ChatHistory}"/>
<Entry Grid.Row="1" Grid.Column="0" Placeholder="Enter message" Text="{Binding Message}"/>
<Button Grid.Row="1" Grid.Column="1" Text="Send" Command="{Binding SendCommand}"/>
</Grid>
Don't forget to set the binding between your view model and your page. Also, create the client using the CreateClient method in OnAppearing().
MainViewModel _viewModel;
public MainPage(MainViewModel viewModel)
{
InitializeComponent();
BindingContext = _viewModel = viewModel;
}
protected override void OnAppearing()
{
base.OnAppearing();
_viewModel.CreateClient().ContinueWith(t => { });
}
You can find the code in our Broker repository and Client repository.
The last step is to start the broker and two simulators. But start the second simulator with the variable _currentUser and _chatPartner the other way round.
string _currentUser = "user2";
string _chatPartner = "user1";
Simply type your message and send it. Both simulators should see the message you've sent. You can also start as many simulators as you like and subscribe to the same channel. When a client publishes a message to that channel, all subscribers will receive that message. But remember to change the client id/name for each client.
You like how we approach things?
You have made it this far, as a developer you have gained an insight into our work. Migrations are just one part of our Xamarin and .NET MAUI work. We support you in all areas of app development.
Let's talk
If you send the app to the background, you'll still get messages as long as your app is alive, so you won't be disconnected from the server. You can use local notifications to let the user know that there is a new message.
But what happens if your app where shut down by the OS?
In this case, you have no way of knowing if you have received a message because you are disconnected. You can solve this problem by using push notifications. Because push notifications don't depend on your connection to the server. So when the user sees the notification, he could start the app, reconnect and get the latest messages.
Let me know if this type of article was helpful for you. If you have any questions please get in contact.
With over 7 years of experience in the development of cross-platform apps with Xamarin and .NET MAUI, Martin is one of the old hands at Cayas Software. Customers from the agricultural, logistics and healthcare industries appreciate his calm manner and analytical approach to the implementation of their Xamarin and .NET MAUI projects. He shares some of his daily work with Xamarin and .NET MAUI in his articles.
This post is a continuation of the Hackathon topic post, where the technical implementation of voice commands in .NET MAUI is revealed, as well as the challenges the development team faced and how they successfully solved them.
With the end of support for Xamarin approaching in May 2024, developers are busy migrating existing Xamarin.Forms projects to .NET MAUI as its successor. So are we, of course. In this article, I'll show 7 steps we've always had to take during the transition to make your upgrade .NET MAUI easier.
.NET MAUI enable us to write platform and device-factor independent apps, which makes it neccessary to adapt dynamically to the users screen size and form. In this blog post you learn how to make your XAML layouts adapt to different device orientations, using a similar syntax to OnIdiom and OnPlatform that you might already be familiar with.