Building a Real-Time Chat Application

In this chapter, we will build a chat app with real-time communication. In the app, you will be able to send and receive messages and photos to and from other users, which will appear without a manual refresh. We will look at how we can use SignalR to implement a real-time connection with the server.

The following topics will be covered in this chapter:

  • How to use SignalR in a Xamarin.Forms app
  • How to use template selectors for a CollectionView
  • How to use CSS styling in a Xamarin.Forms app

Let's get started.

Technical requirements

Before you can build the app for this project, you need to build the backend that we detailed in Chapter 8, Setting Up a Backend for a Chat App Using Azure Services. You will also need to have Visual Studio for Mac or PC installed, as well as the Xamarin components. See Chapter 1, Introduction to Xamarin, for more details on how to set up your environment. The source code for this chapter is available in this book's GitHub repository: https://github.com/PacktPublishing/Xamarin.Forms-4-Projects.

Project overview

When building a chat app, it is really important to have real-time communication because the user expects messages to arrive more or less immediately. To achieve this, we will use SignalR, which is a library for real-time communication. SignalR will use WebSockets if they are available and, if not, it will have several fallback options it can use instead. In the app, a user will be able to send text, and photos from the photo library on the device.

The build time for this project is about 180 minutes.

Getting started

We can use either Visual Studio 2019 on a PC or Visual Studio for Mac to complete this project. To build an iOS app using Visual Studio for PC, you have to have a Mac connected. If you don't have access to a Mac at all, you can choose to just build the Android part of the app.

Building the chat app

It's time to start building the app. We recommend that you use the same solution we used inChapter 8, Setting Up a Backend for a Chat App Using Azure Services, because this will make code sharing easier. In that solution, we created a Mobile App (Xamarin.Forms) called Chat:

Select the Blank template andiOS and Androidas the platforms.Now that we have created the project, we will update all NuGet packages to the latest versions because the project templates are not updated as often as the packages that are used inside the templates are:

Creating the chat service

The first thing we will do is create a chat service that will be used by both the iOS and Android applications. To make the code more testable and to make it easier to replace the chat service if we want to use another provider in the future, we need to perform the following steps:

  1. In the Chat project, add a reference to the Chat.Messages project.
  2. Create a new folder in the Chat project called Services.
  3. Create a new interface calledIChatService in the Services folder.
  4. Create aboolproperty calledIsConnected.
  5. Create a methodcalledSendMessagethat takesMessageas an argument and returnsTask.
  1. Create a methodcalledCreateConnectionthat returnsTask. This method will create and start a connection to the SignalR service.
  2. Create a method called Dispose that returnsTask. This method will be used when the app goes to sleep to ensure that the connection to the SignalR service has been closed properly:
using Chat.Events;
using Chat.Messages;
using System;
using System.Threading.Tasks;

namespace Chat.Services
{
publicinterfaceIChatService
{
boolIsConnected{get;}

TaskCreateConnection();
TaskSendMessage(Messagemessage);
TaskDispose();
}
}

The interface will also contain an event, but before we add the event to the interface, we will create an EventArgs class that the event will use. Perform the following steps:

  1. In the Chat project, create a new folder calledEvents.
  2. Create a new class calledNewMessageEventArgs in the Events folder.
  3. Add EventArgs as a base class.
  4. Create a property calledMessage of the Message type with a public getter and a private setter.
  5. Create an empty constructor.
  6. Create a constructor with Message as a parameter.
  7. Set the parameter of the constructor to the Message property.

The following code is the result of completing these steps:

using Chat.Messages;
using System;
namespace Chat.Events
{
publicclassNewMessageEventArgs:EventArgs
{
publicMessageMessage{get;privateset;}

publicNewMessageEventArgs(Messagemessage)
{
Message=message;
}
}
}

Now that we have created a new EventArgs class, we can use it and add an event to the interface. We will name the event NewMessage:

publicinterfaceIChatService
{
eventEventHandler<NewMessageEventArgs>NewMessage;

boolIsConnected{get;}

TaskCreateConnection();
TaskSendMessage(Messagemessage);
TaskDispose();
}

The first thing we will do in the service is make a call to the GetSignalRInfo service that we created in Chapter 8, Setting Up a Backend for a Chat App Using Azure Services, to obtain information about how to connect to the SignalR service. To serialize that information, we will create a new class, as follows:

  1. In the Chat project, create a new folder calledModels.
  2. Create a new class calledConnectionInfo.
  3. Add a string property calledUrl for our string.
  4. Add a string property calledAccessToken for our string:
publicclassConnectionInfo
{
publicstringUrl{get;set;}
publicstringAccessToken{get;set;}
}

Now that we have the interface and a model to obtain the connection information, it is time to create an implementation of the IChatService interface. To use SignalR, we need to add a package for NuGet that will give us the necessary classes. Perform the following steps:

  1. In the Chat project, install the Microsoft.AspNetCore.SignalR.Client and Newtonsoft.Json NuGet packages.
  2. In the Services folder, create a new class calledChatService.
  1. Add the IChatService interface to ChatService and implement it.
  2. Add a private field forHttpClient calledhttpClient.
  3. Add a private field forHubConnection calledhub.
  4. Add a private field for SemaphoreSlimcalledsemaphoreSlim and create a new instance with an initial and maximum count of 1 in the constructor:
using Chat.Events;
using Chat.Messages;
using Microsoft.AspNetCore.SignalR.Client;
using Newtonsoft.Json;
using System;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

public class ChatService : IChatService
{
private HttpClient httpClient;
private HubConnection hub;
private readonly SemaphoreSlim semaphoreSlim =
new SemaphoreSlim(1, 1);

public event EventHandler<NewMessageEventArgs> NewMessage;
publicboolIsConnected{get;set;}

publicasyncTaskCreateConnection()
{
}

publicasyncTaskSendMessage(Messagemessage)
{
}

publicasyncTaskDispose()
{
}
}

We will start with CreateConnection, which will call the GetSignalRInfo function. We will then use the information in the response to connect to the SignalR service and start listening for messages. To do this, carry out the following steps:

  1. Add a call to the WaitAsync method of SemaphoreSlim to make sure that only one thread can use the method at any time.
  1. Check whether httpClient is null. If it is, create a new instance. We will reuse the httpClient instance because this is better from a performance perspective.
  2. Make a call to GetSignalRInfo and serialize the result to a ConnectionInfo object:
public async Task CreateConnection()
{
awaitsemaphoreSlim.WaitAsync();

if(httpClient==null)
{
httpClient=newHttpClient();
}

varresult=awaithttpClient.GetStringAsync("https://{theNameOfTheFunctionApp}.azurewebsites.net/api/GetSignalRInfo");

varinfo=JsonConvert.DeserializeObject<Models.ConnectionInfo>
(result);

}

When we have the necessary information about how to connect to the SignalR service, we can use HubConnectionBuilder to create a connection. Then, we can start listening for messages:

  1. Create a new HubConnectionBuilder.
  2. Use the WithUrl method to specify the URL to the SignalR service as the first argument. The second argument is an Action of the HttpConnectionObject type. This means that you will get an object of the HttpConnectionObject type as a parameter.
  3. In the action, set AccessTokenProvider to a Func that returns the value of the AccessToken property on the ConnectionInfo object.
  4. Use the Build method of HubConnectionBuilder to create a connection object.
  5. Add an Action that will run when new messages arrive using the On<object> method on the HubConnection object. The action will be specified as the second argument. For the first argument, we will specify the name of the target (we specified the target in Chapter 8, Setting Up a Backend for a Chat App Using Azure Services, when we sent the message), which is newMessage.
  1. In Action, convert the incoming message into a string using theToStringmethod and deserialize it to aMessageobject so we can read its TypeInfoproperty. To do this, use the JsonConvert class and the DeserializeObject<Message> method.
The reason we have to deserialize the object twice is that we only get the value of properties in the Message class the first time we use it. When we know which subclass of Message we've received, we can use this to deserialize that information for that class. We are casting it to Message so that we can pass it to the NewMessageEventArgs object. In this case, we will not lose the properties of the subclass. To access the properties, we just cast the class back to the subclass.
  1. When we know what type the message is, we can use this to deserialize the object to the actual type. Use the DeserializeObject method of JsonConvert, pass the JSON string and TypeInfo to it, and then cast it to Message.
  2. Invoke the NewMessage event and pass the current instance of ChatService and a new NewMessageEventArgs object to it. Pass the Message object to the constructor of NewMessageEventArgs.
  3. Once we have a connection object and we have configured what will happen when a message arrives, we can start listening to messages with the StartAsync method of HubConnection.
  4. Set the IsConnected property to true.
  5. Use the Release method of SemaphoreSlim to let other threads go to the CreateConnection method:
varconnectionBuilder=newHubConnectionBuilder();
connectionBuilder.WithUrl(info.Url,(Microsoft.AspNetCore.Http.Connections.Client.HttpConnectionOptionsobj)=>
{
obj.AccessTokenProvider=()=>Task.Run(()=>
info.AccessToken);
});

hub=connectionBuilder.Build();
hub.On<object>("newMessage",(message)=>
{
varjson=message.ToString();
varobj=JsonConvert.DeserializeObject<Message>(json);
varmsg=(Message)JsonConvert.DeserializeObject(json,obj.TypeInfo);
NewMessage?.Invoke(this,newNewMessageEventArgs(msg));
});

awaithub.StartAsync();

IsConnected=true;
semaphoreSlim.Release();

The next method we need to implement is SendMessage. This will send a message to an Azure function, which will add the message to the SignalR service:

  1. Use the Serialize method on the JsonConvert class to serialize the Message object to JSON.
  2. Create a StringContent object and pass the JSON string as the first argument, Encoding.UTF8 as the second argument, and the application/json content-type as the last argument to the constructor.
  3. Create a new instance of HttpClient if the httpClient variable is null.
  4. Use the PostAsync method on the HttpClient object with the URL as the first argument and the StringContent object as the second argument in order to post the message to the function:
publicasyncTaskSendMessage(Messagemessage)
{
varjson=JsonConvert.SerializeObject(message);

varcontent=newStringContent(json,Encoding.UTF8,
"application/json");

if (httpClient == null)
{
httpClient = new HttpClient();
}

await
httpClient.PostAsync
("https://{TheNameOfTheFunctionApp}.azurewebsites.net/api/messages"
, content);
}

The last method we need to implement is Dispose. This will close the connection when the app enters its background state, for example, when a user hits the home button or switches apps:

  1. Use the WaitAsync method to ensure that there are no threads trying to create a connection or dispose of a connection when we are running the method.
  2. Add an if statement to ensure that the hub field isn't null.
  3. If it isn't null, call the StopAsync and DisposeAsync methods of HubConnection.
  4. Set the httpClient field to null.
  5. Set IsConnected to false.
  6. Release SemaphoreSlimwith the Release method:
publicasyncTaskDispose()
{
awaitsemaphoreSlim.WaitAsync();

if(hub!=null)
{
awaithub.StopAsync();
awaithub.DisposeAsync();
}

httpClient=null;

IsConnected=false;

semaphoreSlim.Release();
}

Initializing the app

Now, we are ready to write the initialization code for the app. We will set up Inversion-of-Control (IoC) and carry out the necessary configuration.

Creating a resolver

We need to create a helper class that will ease the process of resolving object graphs through Autofac. This will help us create types based on a configured IoC container.

In this project, we will useAutofacas the IoC library:

  1. Install the Autofac NuGet packagein the Chat project.
  2. Create a new class called Resolverin the Chatproject.
  3. Add a private static field called container of theIContainer type (from Autofac).
  4. Add a public static method called InitializewithIContaineras a parameter. Set the value of the parameter to the container field.
  5. Add a generic static public method called Resolve, which will return an instance that is based on the argument type, with the Resolve method of IContainer:
using Autofac;

public class Resolver
{
private static IContainer container;

public static void Initialize(IContainer container)
{
Resolver.container = container;
}

public static T Resolve<T>()
{
return container.Resolve<T>();
}
}

Creating a Bootstrapper

Here, we will create aBootstrapperclass so that we can set up the common configurations that we need in the startup phase of the app. Usually, there is one part of the Bootstrapper class for each target platform and one that is shared for all platforms. In this project, we only need the shared part:

  1. Create a new class called Bootstrapperin theChatproject.
  2. Add a new public static method called Init.
  3. Create a newContainerBuilderand register the types to container.
  1. Create aContainerusing theBuildmethod of ContainerBuilder. Create a variable called containerthat contains the Container instance.
  2. Use theInitializemethod on Resolverand pass thecontainervariable as an argument, as shown in the following code:
using Autofac;
using Chat.Chat;
using System;
using System.Reflection;

public class Bootstrapper
{
public static void Init()
{
var builder = new ContainerBuilder();

builder.RegisterType<ChatService>().As<IChatService>
().SingleInstance();

var currentAssembly = Assembly.GetExecutingAssembly();

builder.RegisterAssemblyTypes(currentAssembly)
.Where(x => x.Name.EndsWith("View",
StringComparison.Ordinal));

builder.RegisterAssemblyTypes(currentAssembly)
.Where(x => x.Name.EndsWith("ViewModel",
StringComparison.Ordinal));

var container = builder.Build();

Resolver.Initialize(container);
}
}

Call theInitmethod ofBootstrapperin the constructor in theApp.xaml.csfile, after the call toInitializeComponents:

public App()
{
InitializeComponent();
Bootstrapper.Init();
MainPage = new MainPage();
}

Creating a base ViewModel

We now have a service that is responsible for handling communication with the backend. Now, it's time to create a ViewModel. First, however, we will create a base view model, where we can put the code that will be shared between all the view models of the app:

  1. Create a new folder calledViewModels.
  2. Create a new class called ViewModel.
  3. Makethe newclass public and abstract.
  4. Add a static field calledNavigation of the INavigation type. This will be used to store a reference to the navigation services provided by Xamarin.Forms.
  5. Add a static field calledUser of the string type. The field will be used when connecting to the chat service so that messages you send will be displayed with your name attached.
  6. Add and implement theINotifiedPropertyChangedinterface. This is necessary because we want to use data bindings.
  7. Add a Set method. This will make it easier for us to raise the PropertyChanged event from the INotifiedPropertyChanged interface. This method will check if the value has changed. If it has, it will raise the event:
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Xamarin.Forms;

public abstract class ViewModel : INotifyPropertyChanged
{
public static INavigation Navigation { get; set; }
public static string User { get; set; }

public event PropertyChangedEventHandler PropertyChanged;
protected void Set<T>(ref T field, T newValue,
[CallerMemberName] string propertyName =
null)
{
if (!EqualityComparer<T>.Default.Equals(field, newValue))
{
field = newValue;
PropertyChanged?.Invoke(this, new
PropertyChangedEventArgs(propertyName));
}
}
}

Creating the main view

Now that we have our ViewModel base class set up, as well as all of the code for receiving and sending messages, it's time to create the two views. These will act as the user interface of the app.

We are going to start by creating the main view. This is the view that will be displayed when the user starts the app. We will add an entry control (an input text box) so that the user can enter a username and add a command to navigate to the chat view.

The main view will be composed of the following:

  • A ViewModel file called MainViewModel.cs
  • A XAML file called MainView.xaml, which contains the layout
  • A code-behind file called MainView.xaml.cs, which will carry out the data binding process

Let's start by creating the ViewModel for MainView.

Creating MainViewModel

The MainViewModel that we are about to create will hold a username that the user will enter into the UI. It will also contain a Command property called Start. This will be bound to a Button that the user will click after entering their username:

  1. In the ViewModel folder, create a class called MainViewModel.cs.
  2. Inherit the class from ViewModel.
  3. Make the class public.
  4. Add a property called Username of thestringtype.
  5. Add a property called Start of the ICommand type and implement it, as shown in the following code. The Start command will assign Username from the Username property and assign it to the static User property in the base ViewModel. Then, it will create a new instance of ChatView by using Resolver and pushing it onto the navigation stack.

MainViewModel should now look as follows:

using System.Windows.Input;
using Chat.Views;
using Xamarin.Forms;
namespace Chat.ViewModels
{
public class MainViewModel : ViewModel
{
public string Username { get; set; }

public ICommand Start => new Command(() =>
{
User = Username;

var chatView = Resolver.Resolve<ChatView>();
Navigation.PushAsync(chatView);
});
}
}

Now that we have MainViewModel, we need a view to go with it. It's time to create MainView.

Creating MainView

MainView will display a user interface that allows the user to enter a name before starting the chat. This section will be about creating the MainView XAML file and the code behind that view.

We will start by removing the template-generated MainPage and replacing it with an MVVM-friendly MainView.

Replacing MainPage

When we created the app, the template generated a page called MainPage. Since we are using MVVM as a pattern, we need to remove this page and replace it with a view called MainView instead:

  1. In the root of the Chat project, delete the page called MainPage.
  2. Create a new folder called Views.
  3. Add a new XAML page called MainView in the Views folder.

Editing the XAML

Now, it's time to add some content to the newly created MainView.xaml file. The icons mentioned here can be found in the same folder that they have been added to (you can check this by looking at the project on GitHub – the GitHub URL can be found at the beginning of this chapter). There is a lot going on here, so make sure to check what you write against the code:

  1. Add the chat.png icon to the Drawable folder that is inside the Resources folder in the Android project.
  2. Add the [email protected] icon to the Resources folder in the iOS project.
  3. Open the MainView.xaml file.
  4. Add a Title property to the ContentPage node. This is the title that will be displayed in the navigation bar of the app.
  5. Add a Grid and define two rows in it. The first one should have a height of "*", while the second one should have a height of "2*". This will partition the space in two rows, of which the first will take up 1/3 of the space and the second will take up 2/3 of the space.
  6. Add an Image with Source set to "chat.png" and its VerticalOptions and HorizontalOptions set to "Center".
  1. Add StackLayout with Grid.Row set to "1", Padding set to "10", and Spacing set to "20". The Grid.Row property positions StackLayout in the second row. Padding adds 10 units of space aroundStackLayout, while Spacing defines the amount of space between each element added in StackLayout.
  2. In StackLayout, add an Entry node that has its Text property set to "{Binding UserName}" and the Placeholder property set to "Enter a username". Binding the Text node will make sure that when the user enters a value in the Entry control, it's updated in ViewModel.
  3. In StackLayout, add a Button control that will have the Text property set to "Start" and its Command property set to "{Binding Start}". The Command property binding will execute when the user taps the button. It will run the code that we defined in the MainViewModel class.

When finished, the code should look as follows (shown in bold):

 <?xml version="1.0" encoding="UTF-8"?>
<ContentPage
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Chat.Views.MainView" Title="Welcome">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="2*" />
</Grid.RowDefinitions>
<Image Source="chat.png" VerticalOptions="Center"
HorizontalOptions="Center" />

<StackLayout Grid.Row="1" Padding="10" Spacing="20">
<Entry Text="{Binding Username}"
Placeholder="Enter a username" />

<Button Text="Start" Command="{Binding Start}" />
</StackLayout>
</Grid>
</ContentPage>

The layout is complete. Now, we need to turn our attention to the code behind this view so that we can wire up some loose ends.

Fixing the code behind the view

As with all views, when using MVVM, we need to pass our view of ViewModel. Since we are using dependency injection in this project, we will pass it through the constructor and then assign it to the view's BindingContext. We will also make sure that we enable safe areas to avoid controls being partially hidden behind the iPhone X notch at the top:

  1. Open the MainView.xaml.cs file.
  2. Add a parameter called viewModel of the MainViewModel type in the constructor of the MainView class. The argument for this parameter will be injected by Autofac at runtime.
  3. Add a platform-specific statement that instructs the application to use safe areas on iOS. A safe area makes sure that the app does not use the space on the side of the notch at the top of the screen on an iPhone X.
  4. Assign the viewModel argument to the BindingContext property of the view.

The changes that need to be made have been marked in bold in the following code:

using Chat.ViewModels;
using Xamarin.Forms;
using Xamarin.Forms.PlatformConfiguration.iOSSpecific;

using
Xamarin.Forms.Xaml;

public partial class MainView : ContentPage
{
public MainView(MainViewModel viewModel)
{
InitializeComponent();

On<Xamarin.Forms.PlatformConfiguration.iOS>
().SetUseSafeArea(true);


BindingContext = viewModel;
}
}

Now, our MainView is complete, but we still need to tell the application to use it as the entry point view.

Setting the main view

The entry point view, also referred to as the application's MainPage, is set during the initialization of a Xamarin.Forms app. Usually, it is set in the constructor of the App class. We will be creating MainView through the resolver we created earlier and wrapping it in NavigationPage to enable platform-specific navigation on the device that the app runs on. We could have used Shell as well, but in this case, there are no reasons to use it:

  1. Open the App.xaml.cs file.
  2. Resolve an instance to a MainView class by using Resolver and storing it in a variable called mainView.
  3. Create a new instance of NavigationPage by passing the mainView variable as a constructor argument and assigning it to a variable called navigationPage.
  4. Assign the navigationPage.Navigation property to the static Navigation property, which is of the ViewModel type. This property will be used when navigating between pages later on.
  5. Assign the navigationPage variable to the MainPage property of the App class. This sets the starting view of our application:
public App()
{
InitializeComponent();
Boostrapper.Init();

var mainView = Resolver.Resolve<MainView>();
var navigationPage = new NavigationPage(mainView);
ViewModel.Navigation = navigationPage.Navigation;
MainPage = navigationPage;
}

That's it for MainView; nice and easy. Now, let's move on to something more interesting: ChatView. This will be used to send and receive messages.

Creating ChatView

ChatView is a standard chat client. It will have an area for displaying incoming and outgoing messages, as well as a text field at the bottom that the user can type a message into. It will also have a button for taking a photo and a button for sending messages if the user doesn't hit return on the on-screen keyboard.

We will start by creating ChatViewModel, which contains all of the necessary logic by acting as the glue between the view and the model. Our model, in this case, is represented by our ChatService.

After that, we will create ChatView, which handles how the Graphical User Interface (GUI) is rendered.

Creating ChatViewModel

As we mentioned previously, ChatViewModel is the glue between the visual representation (View) and the model (which is basically our ChatService). ChatViewModel will store messages and any communication that's made with ChatService by hooking up the necessary functionality for sending and receiving messages.

Creating the class

ChatViewModel is a simple class that inherits from the ViewModel base class we created earlier. In the first code exercise, we will create the class, adding relevant using statements and a property called Messages, which we will use to store the messages that we have received. The view will use the Message collection to display the messages in a ListView.

Since this is a large block of code, we recommend that you write it first and then go over the numbered list to get to grips with what has been added to the class:

  1. Create a new class called ChatViewModel in the ViewModels folder of the Chat project.
  2. Make the class public and inherit it from the ViewModel base class to gain the common base functionality from the base class.
  3. Add a readonly property called chatService of the IChatService type. This will store a reference to an object that implements IChatService and make the concrete implementation of ChatService replaceable. It's good practice to expose any service as an interface.
  4. Add a public property called Messages of the public ObservableCollection<Message>type with a private setter. This collection will hold all messages. The private setter makes the property inaccessible from outside this class. This maintains the integrity of the collection by ensuring messages are not inserted anywhere except inside the class.
  5. Add a constructor parameter called chatService of the IChatService type. When we use dependency injection, this is where Autofac will inject an object that implements IChatService.
  6. In the constructor, assign the chatService parameter to the chatService property. This will store the reference to ChatService so that we can use it during the lifetime of ChatViewModel.
  7. In the constructor, instantiate the Messages property to a new ObservableCollection<Message>.
  8. In the constructor, create a Task.Run statement that will call the chatService.CreateConnection() method if the chatService.IsConnected property is false. The reason we're using Task.Run is because we want the code to run asynchronously, even if this is done from the constructor. End the Task.Run statement by sending a new UserConnected message:
 using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using Acr.UserDialogs;
using Chat.Messages;
using Chat.Services;
using Plugin.Media;
using Plugin.Media.Abstractions;
using Xamarin.Forms;

namespace Chat.ViewModels
{
public class ChatViewModel : ViewModel
{
private readonly IChatService chatService;
public ObservableCollection<Message> Messages { get;
private set; }

public ChatViewModel(IChatService chatService)
{
this.chatService = chatService;

Messages = new ObservableCollection<Message>();

Task.Run(async() =>
{
if(!chatService.IsConnected)
{
await chatService.CreateConnection();
}

await chatService.SendMessage(new
UserConnectedMessage(User));
});
}
}
}

Now that we have instantiated our ChatViewModel, it's time to add a property that will hold whatever the user is typing at that moment.

Adding the text property

At the bottom of the GUI, there will be a text field (an entry control) that will allow the user to enter a message. This entry will be data-bound to a property that we will call Text in ChatViewModel. Whenever the user changes the text, this property will be set. This is a classic case of data-binding:

  1. Add a new private field called text of the stringtype.
  2. Add a public property called Text that returns the private text field in the getter and makes a call to the Set() method of the base class in the setter. The Set method is defined in the ViewModel base class and will raise an event back to the view if the property changes in ChatViewModel, effectively keeping them in sync:
private string text;
public string Text
{
get => text;
set => Set(ref text, value);
}

Now, we have a property that we can use for data binding. Let's look at the code we will use to receive messages from ChatService.

Receiving messages

When a message is sent from the server, over SignalR, ChatService will parse this message and transform it into a Message object. Then, it will raise an event called NewMessage, which is defined in ChatService.

In this section, we will implement an event handler to handle these events and add them to the Messages collection, unless a message with the same ID already exists.

Again, perform the following steps and look at the code to get to grips with it:

  1. In ChatViewModel, create a method called ChatService_NewMessage, which will be a standard event handler. This has two parameters: sender, which is of the object type, and e, which is of the Events.NewMessageEventArgs type.
  2. Wrap the code in this method in Device.BeginInvokeOnMainThread(). We're doing this since we are going to add messages to the Message collection. Items that are added to this collection will modify the view, and any code that modifies the view must be run on the UI thread.
  3. In Device.BeginInvokeOnMainThread, add the incoming message from e.Message to the Messages collection if a message with that specific Message.Id isn't already present. This is to avoid message duplication.

The method should look as follows:

private void ChatService_NewMessage(object sender, Events.NewMessageEventArgs e)
{
Device.BeginInvokeOnMainThread(() =>
{
if (!Messages.Any(x => x.Id == e.Message.Id))
{
Messages.Add(e.Message);
}
});
}

Now that the event handler has been defined, we need to hook it up in the constructor:

  1. Locate the constructor of the ChatViewModel class.
  2. Wire up a chatService.NewMessage event to the ChatService_NewMessage handler we just created. A good place to do this is under the instantiation of the Messages collection.

The code marked in bold is what we should add to the ChatViewModel class:

public ChatViewModel(IChatService chatService)
{
this.chatService = chatService;

Messages = new ObservableCollection<Message>();

chatService.NewMessage += ChatService_NewMessage;

Task.Run(async() =>
{
if(!chatService.IsConnected)
{
await chatService.CreateConnection();
}

await chatService.SendMessage(new UserConnectedMessage(User));
});
}

The app will now be able to receive messages. How about sending them? Well, stay tuned!

Creating the LocalSimpleTextMessage class

To make it easier to recognize whether a message is coming from the server or whether it is being sent by the user of the device that the code is executing on, we will create a LocalSimpleTextMessage:

  1. Create a new class called LocalSimpleTextMessage in the Chat.Messages project.

  2. Add SimpleTextMessage as the base class.

  3. Create a constructor with SimpleTextMessage as the parameter.

  4. Set the value to all of the base properties with the value from the parameter, as shown in the following code:

publicclass LocalSimpleTextMessage : SimpleTextMessage
{
public LocalSimpleTextMessage(SimpleTextMessage message)
{
Id = message.Id;
Text = message.Text;
Timestamp = message.Timestamp;
Username = message.Username;
TypeInfo = message.TypeInfo;
}
}

Sending text messages

Sending text messages is also very straightforward. We need to create a command that we can bind for the GUI. The command will be executed eitherwhen the user hits return or when the user clicks the send button. When a user does either of these two things, the command will create a new SimpleTextMessage and pass in the current user to identify the message for other users. We will use the text from the ChatViewModel text property, which, in turn, is bound to the Entry control.

We will then add the message to the Messages collection, triggering ListView, which will be handling messages that need to be updated. After that, we will pass the message to ChatService and clear the ChatViewModel text property. By doing this, we notify the GUI that it has changed and let the data binding magic clear the field.

Refer to the following steps and look at the code to get to grips with it:

  1. Create a new property called Send of the ICommand type.
  2. Assign it a new Command instance.
  3. Create a new instance of the SimpleTextMessage class by passing the User property of the base class as an argument. Assign the instance to a variable called message.
  4. Set the Text property of the message variable to the Text property of the ChatViewModel class. This copies the current text in the chat entry defined by the GUI later on.
  5. Create a LocalSimpleTextMessage object and pass in the message variable as a constructor argument. LocalSimpleTextMessage is a SimpleTextMessage and makes it possible for the view to recognize it as a message that the user of the app sent, effectively rendering it on the right-hand side of the chat area. Add the LocalSimpleTextMessage instance to the Messages collection. This will display the message in the view.
  6. Make a call to the chatService.SendMessage() method and pass the message variable as an argument.
  1. Empty the Text property of ChatViewModel to clear the entry control in the GUI:
public ICommand Send => new Command(async()=> 
{
var message = new SimpleTextMessage(User)
{
Text = this.Text
};

Messages.Add(new LocalSimpleTextMessage(message));

await chatService.SendMessage(message);

Text = string.Empty;
});

What good is a chat app if we can't send photos? We'll implement this in the next section.

Installing the Acr.UserDialogs plugin

Acr.UserDialogs is a plugin that makes it possible to use several standard user dialogs from code that are shared between platforms. To install and configure it, there are a few steps we need to follow:

  1. Install the Acr.UserDialogsNuGet package for the Chat-, Chat.iOS, andChat.Android projects.
  2. In the MainActivity.cs file, add UserDialogs.Init(this) to the OnCreate method:
protectedoverridevoidOnCreate(BundlesavedInstanceState)
{
TabLayoutResource=Resource.Layout.Tabbar;
ToolbarResource=Resource.Layout.Toolbar;

base.OnCreate(savedInstanceState);

UserDialogs.Init(this);

global::Xamarin.Forms.Forms.Init(this,savedInstanceState);
LoadApplication(newApp());
}

Installing the Media plugin

We will use theXam.Plugin.MediaNuGet package to access the photo library of the device. We need to install this package for the Chat-, Chat.iOS, and Chat.Android projects in the solution. Before we can use the package, however, we need to do some configuration for each platform. We will start with Android:

  1. The plugin needs the WRITE_EXTERNAL_STORAGEand READ_EXTERNAL_STORAGE permissions. The plugin will add these for us, but we need to override OnRequestPermissionResult in MainActivity.cs first.
  2. Call theOnRequestPermissionsResultmethod.
  3. Add CrossCurrentActivity.Current.Init(this, savedInstanceState)after initializing Xamarin.Forms in the OnCreate method in the MainActivity.cs file, as shown in the following code:
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, Android.Content.PM.Permission[] grantResults)
{
Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);
}

We also need to add some configuration for the file paths that the users will be able to pick photos from:

  1. Add a folder called xmlto the Resources folder in the Android project.
  2. Create a new XML file called file_paths.xmlin the new folder.
  3. Add the following code tofile_paths.xml:
<?xml version="1.0" encoding="utf-8"?>
<pathsxmlns:android="http://schemas.android.com/apk/res/android">
    <external-files-pathname="my_images"path="Pictures" />
    <external-files-pathname="my_movies"path="Movies" />
</paths>

The last thing we need to do to set up the plugin for the Android project is add the following code to theAndroidManifest.xmlfield, inside the application element:

<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="xfb.Chat">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="27" />
<application android:label="Chat.Android">
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false" android:grantUriPermissions="true">

<meta-data android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"></meta-data>

</provider>
</application>
</manifest>

For the iOS project, the only thing we need to do is add the following four usage descriptions to info.plist:

<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs access to photos.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>This app needs access to the photo gallery.</string>

Sending photos

To be able to send photos, we will have to use a source of photos. In our case, we will be using the phone's camera as the source. The camera will return the photo as a stream after it has been taken. We need to convert that stream into a byte array and then Base64 encode it into a string that is easy to send over SignalR.

The method that we are about to create, called ReadFully(), takes a stream and turns it into a byte array, which is a step toward achieving the Base64-encoded string. This is a standard piece of code that creates a buffer that will be used when we are reading the Stream parameter and writing it to MemoryStream in chunks until we have read the full stream, hence the name of the method.

Perform the following steps and check out the code to get to grips with it:

  1. Create a method called ReadFully that takes a stream called input as a parameter and returns a byte array.
  2. Inside a using statement, create a new MemoryStream called ms.
  3. Copy the input Stream to the memory stream.
  4. Return MemoryStream as an array using the ToArray method:
private byte[] ReadFully(Stream input)
{
using (MemoryStream ms = new MemoryStream())
{
ms.CopyTo(input);
return ms.ToArray();
}
}

After doing this, we're left with a large chunk of code. This code exposes a command that will be executed when the user clicks the photo button in the app. It starts by configuring CrossMedia (a media plugin), which indicates the quality the photo should be, and then it starts the photo picker. When the photo picker returns from the async call to PickPhotoAsync(), we start uploading the photo. To notify the user, we use UserDialogs.Instance.ShowLoading to create a loading overlay with a message to indicate that we are uploading the photo.

Then, we will get the stream of the photo, convert it into a byte array using the ReadFully() method, and Base64 encode it into a string. The string will be wrapped in a PhotoMessage instance, added to the local Message collection of the ChatViewModel, and then sent to the server.

Perform the following steps and study the code to get to grips with it:

  1. Create a new property called Photo of the ICommand type. Assign it a new Command instance.
  2. Create an anonymous async method (a lambda expression) and add the code defined in the upcoming steps to it. You can see the full code for this method in the code snippet that follows.
  3. Create a new instance of the PickMediaOptions class and set the CompressionQuality property to 50.
  4. Call CrossMedia.Current.PickPhotoAsync with an async method call and save the result to a local variable called photo.
  5. Install the NuGet package.
  6. Show a message dialog by calling UserDialogs.Instance.ShowLoading() with the text "Uploading photo".
  7. Get the photostream by calling the GetStream() method of the photo variable and save it as a variable called stream.
  1. Convert the stream into a byte array by calling the ReadFully() method.
  2. Convert the byte array into a Base64-encoded string using the Convert.ToBase64String() method. Save the string as a variable called base64photo.
  1. Create a new PhotoMessage instance and pass User as the constructor argument. Set the Base64Photo property to the base64photo variable and the FileEnding property to the file ending of the photo.Path string using the Split function of the string object. Store the new PhotoMessage instance in a variable called message.
  2. Add the message object to the Messages collection.
  3. Send the message to the server by calling the async chatService.SendMessage() method.
  4. Hide the loading dialog by calling UserDialogs.Instance.HideLoading().

The code that follows shows how this can be implemented:

public ICommand Photo => new Command(async() =>
{
var options = new PickMediaOptions();
options.CompressionQuality = 50;

var photo = await CrossMedia.Current.PickPhotoAsync();

UserDialogs.Instance.ShowLoading("Uploading photo");

var stream = photo.GetStream();
var bytes = ReadFully(stream);

var base64photo = Convert.ToBase64String(bytes);

var message = new PhotoMessage(User)
{
Base64Photo = base64photo,
FileEnding = photo.Path.Split('.').Last()
};

Messages.Add(message);
await chatService.SendMessage(message);

UserDialogs.Instance.HideLoading();
});

The ChatViewModel is complete. Now, it's time to create our GUI.

Creating the ChatView

ChatView is responsible for creating the user interface that the user will interact with. It will display local and remote messages, both text and photos, and also notify a user when another user has joined the chat. We'll start by creating a converter that will convert photos represented as Base64-encoded strings into an ImageSource that can be used as the source of the image control process in XAML.

Creating Base64ToImageConverter

When we take a picture using the phone's camera, it will be handed to us as a byte array. To send this to the server, we will convert it into a Base64-encoded string. To display that message locally, we will need to convert it back into a byte array and then pass that byte array to a helper method of the ImageSource class to create an instance of the ImageSource object. This object will make sense to the Image control and an image will be displayed.

Since there is a lot of code here, we suggest that you perform the following steps and look at each line of code in detail as you follow them:

  1. Create a folder called Converters in the Chat project.
  2. Create a new class called Base64ImageConverter in the Converters folder; ensure the class implements the IValueConverter interface.
  3. In the Convert() method of the class, cast the value object parameter to a string called base64String.
  4. Convert base64String into a byte array using the System.Convert.FromBase64String() method. Save the result in a variable called bytes.
  5. Create a new MemoryStream by passing the byte array into its constructor. Save the stream in a variable called stream.
  6. Call the ImageSource.FromStream() method and pass the stream as a lambda expression that returns the stream variable. Return the ImageSource object that's created by doing this.
  7. The ConvertBack() method doesn't need to be implemented since we will never convert an image back into a Base64-encoded string via data binding. We will just let it throw a NotImplementedException:
using System;
using System.Globalization;
using Xamarin.Forms;
using System.IO;

namespace Chat.Converters
{
public class Base64ToImageConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, CultureInfo
culture)
{
var base64string = (string)value;
var bytes =
System.Convert.FromBase64String(base64string);
var stream = new MemoryStream(bytes);
return ImageSource.FromStream(() => stream);
}

public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo
culture)
{
throw new NotImplementedException();
}
}
}

Now, it's time to start adding some actual XAML code to the view. We will start by creating the main layout skeleton, which we will then gradually build on until we have the finished view.

Creating the skeleton ChatView

This XAML file will contain the view that lists the messages we have sent and the messages we have received. It's quite a large file to create, so for this part, I suggest that you copy the XAML and study every step carefully:

  1. Create a new XAML ContentPage in the Views folder called ChatView.
  2. Add XML namespaces for Chat.Selectors and Chat.Converters and call them selectors and converters.
  3. Add a ContentPage.Resources node. This will contain the resources for this view.
  4. Add ScrollView as the page content. ScrollView should only be able to scroll on iOS to handle when the onscreen keyboard is visible. On Android, this isn't necessary and causes some other problems. Use OnPlatform to set Orientation to Neither for Android and Vertical for iOS.
  5. Add Grid as the only child of ScrollView and name it MainGrid by setting the x:Name property to MainGrid.
  1. Create a RowDefinitions element that contains three rows. The first should have a height of *, the second a height of 1, and the third a platform-specific height based on the platform by using an OnPlatform element.
  2. Save some space for the CollectionView that will be inserted later on.
  3. Add a BoxView. This will act as a visual divider by setting the HeightRequest property to 1, the BackgroundColor property to #33000000, and the Grid.Row property to 1. This will position BoxView in the one-unit-high row of the grid, effectively drawing a single line across the screen.
  4. Add another Grid. This will use the space of the third row by setting the Grid.Row property to 2. Also, add some padding by setting the Padding property to 10. Define three rows in the grid with heights of 30, *, and 30:
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:selectors="clr-namespace:Chat.Selectors"
xmlns:converters="clr-namespace:Chat.Converters"
x:Class="Chat.Views.ChatView">
<ContentPage.Resources>
<!-- TODO Add resources -->
</ContentPage.Resources>
<ScrollView>
<ScrollView.Orientation>
<OnPlatform x:TypeArguments="ScrollOrientation">
<On Platform="iOS" Value="Vertical" />
<On Platform="Android" Value="Neither" />
</OnPlatform>
</ScrollView.Orientation>
<Grid x:Name="MainGrid">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="1" />
<RowDefinition>
<RowDefinition.Height>
<OnPlatform x:TypeArguments="GridLength">
<On Platform="iOS" Value="50" />
<On Platform="Android" Value="100" />
</OnPlatform>
</RowDefinition.Height>
</RowDefinition>
</Grid.RowDefinitions>

<!-- TODO Add CollectionView -->

<BoxView Grid.Row="1" HeightRequest="1"
BackgroundColor="#33000000" />
<Grid Grid.Row="2" Padding="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="30" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="30" />
</Grid.ColumnDefinitions>
<!-- TODO Add buttons and entry controls -->

</Grid>
</Grid>
</ScrollView>
</ContentPage>

Now that we have completedthe main skeleton of our page, we need to start adding some specific content. First, we will addResourceDictionaryto create aDataTemplateselector. This will select the correct layouts for different chat messages. Then, we need to putBase64ToImageConverterto use. To do that, we need to define it in the view.

Adding ResourceDictionary

Now, it's time to add some resources to the view. In this case, we will be adding a template selector, which we will create later on, and Base64ToImageConverter, which we created earlier. The template selector will look at each row that we will bind to ListView. This will be presenting messages and selecting the best layout template that suits that message:

  1. Locate the <!-- TODO Add resources --> comment inside the ContentPage.Resources element.
  2. Add the XAML to the sample as follows, right underneath the comment mentioned in step 1:
        <ResourceDictionary>
<selectors:ChatMessageSelector
x:Key="SelectMessageTemplate" />
<converters:Base64ToImageConverter x:Key="ToImage" />
</ResourceDictionary>

This will create one instance of each resource that we've defined and make them accessible to the rest of the view.

Adding CollectionView

We will be using a CollectionView to display the messages in the chat app. Again, perform the following steps and take a look at the code to make sure you understand each step:

  1. Locate the <!-- TODO Add CollectionView --> comment in the ChatView.xaml file.
  2. Add a CollectionView and set the x:Name property to MessageList.
  1. Data-bind CollectionView by setting the ItemsSource property to {Binding Messages}. This will make CollectionView aware of any changes that are made in ObservableCollection<Message>, which is exposed through the Messages property. Any time a message is added or removed, ListView will update to reflect this change.
  1. Add the SelectMessageTemplate resource we defined in the previous section to the ItemTemplate property. This will run some code each time an item is added to make sure that we programmatically select the correct visual template for a specific message. Don't worry about this right now – we will write the code for this soon.
  2. Now, need to define a placeholder where we will add resources. The resources we will be adding are the different DataTemplates that will be used to render different types of messages.

The XAML should look as follows:

<CollectionView x:Name="MessageList" ItemsSource="{Binding Messages}" 
ItemTemplate="{StaticResource SelectMessageTemplate}">
<CollectionView.Resources>
<ResourceDictionary>
<!-- Resources go here later on -->
</ResourceDictionary>
</CollectionView.Resources>
</CollectionView>

Adding templates

Now, we will add five different templates, each corresponding to a specific message type that the app either sends or receives. Each of these templates goes under the <!-- Resources go here later on --> comment from the code snippet in the previous section.

We won't be explaining each of these templates step by step since the XAML that they contain should be starting to feel familiar to you at this point.

Each template starts the same way: the root element is a DataTemplate with a name. The name is important because we will be referencing it in our code very soon. The content that follows after this element is the actual content that the row will be constructed from.

The bindings inside DataTemplate will also be local to each item or row that CollectionView renders. In this case, it will be an instance of a Message class since we are data binding CollectionView to a collection of Message objects. You will see some StyleClass properties in the code. These will be used when we finalize the styling for the app using Cascading Style Sheets (CSS).

Our task here is to write each of these templates under the <!-- Resources go here later on --> comment.

SimpleText is the DataTemplate that is selected when Message is a remote message. It will be rendered on the left-hand side of the list view, just as you might expect. It displays a username and a text message:

<DataTemplate x:Key="SimpleText">
<Grid Padding="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Frame StyleClass="remoteMessage" HasShadow="false">
<StackLayout>
<Label Text="{Binding Username}"
StyleClass="chatHeader" />
<Label Text="{Binding Text}" StyleClass="chatText" />
</StackLayout>
</Frame>
</Grid>
</DataTemplate>

The LocalSimpleText template is the same as the SimpleText data template, except that it renders on the right-hand side of CollectionView by setting the Grid.Column property to 1, effectively using the right column:

<DataTemplate x:Key="LocalSimpleText">
<Grid Padding="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Frame Grid.Column="1" StyleClass="localMessage"
HasShadow="false">
<StackLayout>
<Label Text="{Binding Username}"
StyleClass="chatHeader" />
<Label Text="{Binding Text}" StyleClass="chatText" />
</StackLayout>
</Frame>
</Grid>
</DataTemplate>

DataTemplate is used when a user connects to the chat:

<DataTemplate x:Key="UserConnected">
<StackLayout Padding="10" BackgroundColor="#33000000"
Orientation="Horizontal">
<Label Text="{Binding Username}" StyleClass="chatHeader"
VerticalOptions="Center" />
<Label Text="connected" StyleClass="chatText"
VerticalOptions="Center" />
</StackLayout>
</DataTemplate>

A photo that is uploaded to the server is accessible via a URL. This DataTemplate displays an image based on a URL and is used for remote images:

<DataTemplate x:Key="Photo">
<Grid Padding="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackLayout>
<Label Text="{Binding Username}"
StyleClass="chatHeader" />
<Image Source="{Binding Url}" Aspect="AspectFill"
HeightRequest="150" HorizontalOptions="Fill" />
</StackLayout>
</Grid>
</DataTemplate>

A message that contains a photo is sent by the user and rendered directly based on the Base64-encoded image that we generate from the camera. Since we don't want to wait for the image to upload, we use this DataTemplate, which utilizes the Base64ImageConverter that we wrote earlier to transform the string into an ImageSource that can be displayed by the Image control:

<DataTemplate x:Key="LocalPhoto">
<Grid Padding="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackLayout Grid.Column="1">
<Label Text="{Binding Username}"
StyleClass="chatHeader" />
<Image Source="{Binding Base64Photo, Converter=
{StaticResource ToImage}}"
Aspect="AspectFill" HeightRequest="150"
HorizontalOptions="Fill" />
</StackLayout>
</Grid>
</DataTemplate>

These are all of the templates we need. Now, it's time to add some code to make sure we select the right template for the message to display.

Creating a template selector

Using a template selector is a powerful way of injecting different layouts based on the items that are being data-bound. In this case, we will look at each message that we want to display and select the best DataTemplate for them. The code is somewhat repetitive, so we will be using the same approach that we used for the XAML – simply adding the code and letting you study it yourself:

  1. Create a folder called Selectors in the Chat project.

  2. Create a new class called ChatMessageSelector in the Selectors folder and inherit it from DataTemplateSelector.

  3. Add the following code, which will look at each object that is data-bound and pull the correct DataTemplate from the resources we just added:

using Chat.Messages;
using Xamarin.Forms;

namespace Chat.Selectors
{
public class ChatMessageSelector : DataTemplateSelector
{
protected override DataTemplate OnSelectTemplate(object
item, BindableObject container)
{
var list = (CollectionView)container;

if(item is LocalSimpleTextMessage)
{
return
(DataTemplate)list.Resources["LocalSimpleText"];
}
else if(item is SimpleTextMessage)
{
return (DataTemplate)list.Resources["SimpleText"];
}
else if(item is UserConnectedMessage)
{
return
(DataTemplate)list.Resources["UserConnected"];
}
else if(item is PhotoUrlMessage)
{
return (DataTemplate)list.Resources["Photo"];
}
else if (item is PhotoMessage)
{
return (DataTemplate)list.Resources["LocalPhoto"];
}

return null;
}
}
}

Adding the buttons and entry control

Now, we will add the buttons and the entry that the user will use to write chat messages. The icons that we will be using can be found in the GitHub repository for this chapter. For Android, the icons will be placed in the Drawable folder inside the Resource folder and for iOS, they will be in the Resource folder. The icons are in the same folder on GitHub:

  1. Locate the <!-- TODO Add buttons and entry controls --> comment in the ChatView.xaml file.
  2. Add an Image. Source should be set to photo.png and VerticalOptions and HorizontalOptions should be set to Center. Source will be used to display an image, while HorizontalOptions and VerticalOptions will be used to center the image in the middle of the control.
  3. Add a TapGestureRecognizer object to the GestureRecognizers property of Image. Command will be executed when a user taps the image. Bind the Command property of Image to the Photo property on ViewModel.
  1. Add an Entry control to allow the user to enter a message to be sent. The Text property should be set to {Binding Text}. Set the Grid.Column property to 1 and ReturnCommand to {Binding Send} in order to execute the send command in ChatViewModel when a user hits Enter.
  2. Add an Image with the Grid.Column property set to 2, Source set to send.png, and Command set to {Binding Send} (the same as the return command). Center it horizontally and vertically.
  3. Add aTapGestureRecognizerto theGestureRecognizersproperty of Image.Command will be executed when a user taps the image. Bind theCommandproperty of Imageto theSendproperty on ViewModel:
<Image Source="photo.png" VerticalOptions="Center" HorizontalOptions="Center">
<Image.GestureRecognizers>
<TapGestureRecognizer Command="{Binding Photo}" />
</Image.GestureRecognizers>
</Image>
<Entry Text="{Binding Text}" Grid.Column="1"
ReturnCommand="{Binding Send}" />
<Image Grid.Column="2" Source="send.png" VerticalOptions="Center" HorizontalOptions="Center">
<Image.GestureRecognizers>
<TapGestureRecognizer Command="{Binding Photo}" />
</Image.GestureRecognizers>
</Image>

Fixing the code behind

Now that the XAML is complete, we have some work to do in the code behind. We'll start by adding some using statements:

  1. Open the ChatView.xaml.cs file.
  2. Add using statements for Chat.ViewModels, Xamarin.Forms, and Xamarin.Forms.PlatformConfiguration.iOSSpecific.
  3. Add a private field called viewModel of the ChatViewModel type, which will hold a local reference to ChatViewModel.

The class should now look as follows. The code in bold indicates what should have changed:

using System.Linq;
using Chat.ViewModels;
using Xamarin.Forms;
using Xamarin.Forms.PlatformConfiguration.iOSSpecific;

namespace Chat.Views
{
public partial class ChatView : ContentPage
{
private ChatViewModel viewModel;

public ChatView()
{
InitializeComponent();
}
}
}

When a new message arrives, this will be added to the Messages collection in ChatViewModel. To make sure that CollectionView scrolls appropriately so that the new message is visible, we need to write some additional code:

  1. Create a new method called Messages_CollectionChanged that takes an object as the first parameter and NotifyCollectionChangedEventArgs as the second parameter.
  2. Add a call to the MessageList.ScrollTo() method and pass the last Message in the viewModel.Messages collection by calling viewModel.Messages.Last(). Pass null as the second argument. The third argument should be set to ScrollPosition.End, indicating that we want to make the entire messages' CollectionView row visible. The last argument should be set to true to enable animations.

The method should now look as follows:

private void Messages_CollectionChanged(object sender, 
System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
MessageList.ScrollTo(viewModel.Messages.Last(), null,
ScrollToPosition.End, true);
}

Now, it's time to extend the constructor so that it takes ChatViewModel as a parameter and sets BindingContext in the way that we are used to. The constructor will also make sure that we use the safe area when rendering controls and that we hook up to the events that will handle changes in the Messages collection of ChatViewModel:

  1. Modify the constructor in the ChatView class so that it takes a ChatViewModel as the only parameter. Name it viewModel.
  2. Assign the viewModel parameter from the constructor to the local viewModel field in the class.
  3. Then, call the InitializeComponent() method. Add a platform-specific call to the SetUseSafeArea(true) method to ensure that the app will be visually safe to use on an iPhone X and not partially hidden behind the notch at the top:
 public ChatView(ChatViewModel viewModel)
{
this.viewModel = viewModel;

InitializeComponent();
On<Xamarin.Forms.PlatformConfiguration.iOS>
().SetUseSafeArea(true);


viewModel.Messages.CollectionChanged +=
Messages_CollectionChanged;

BindingContext = viewModel;
}

Every time a view appears, the OnAppearing() method is called. This method is virtual and can be overridden. We will use this feature to make sure that we have the correct height onMainGrid. This is because we have to wrap everything in aScrollViewbecause the view has to be able to scroll when the keyboard appears. If we don't calculate the width ofMainGrid, it could be bigger than the screen becauseScrollViewallows it to expand:

  1. Override the OnAppearing() method.
  2. Calculate the safe area to use by calling the platform-specific On<Xamarin.Forms.PlatformConfiguration.iOS>().SafeAreaInsets() method. This will return a Xamarin.Forms.Thickness object that contains the inset information we need in order to calculate the height of MainGrid. Assign the Thickness object to a variable called safeArea.
  1. Set the MainGrid.HeightRequest property to the height of the view (this.Height) and then subtract the Top and Bottomproperties of safeArea:
protected override void OnAppearing()
{
base.OnAppearing();
var safeArea = On<Xamarin.Forms.PlatformConfiguration.iOS>
().SafeAreaInsets();
MainGrid.HeightRequest = this.Height - safeArea.Top -
safeArea.Bottom;
}

Styling

Styling is an important part of an app. Just like we can with HTML, we can style by setting properties on each control directly, or by setting Style elements in the application's resource dictionary. Recently, however, a new way of styling has emerged in Xamarin.Forms, which is using Cascading Style Sheets, better known as CSS.

Since CSS doesn't cover all cases, we will fall back to standard application resource dictionary styling as well.

Styling with CSS

Xamarin.Forms supports styling via CSS files. For web developers, it can be more intuitive to use compared to XAML styling. It provides a subset of the functionalities you would expect from normal CSS, but support is getting better with each version. We are going to use two different selectors to apply the styling we need.

First, let's create the style sheet. We'll discuss the content of it afterward:

  1. Create a folder called Css in the Chat project.
  2. Create a new text file in the Css folder called Styles.css.
  3. Copy the style sheet into that file, as follows:
button {
background-color: #A4243B;
color: white;
}

.chatHeader {
color: white;
font-style: bold;
font-size: small;
}

.chatText {
color: white;
font-size: small;
}

.remoteMessage {
background-color: #F04D6A;
padding: 10;
}

.localMessage {
background-color: #24A43B;
padding: 10;
}

The first selector, button, applies to every button control in the entire application. It sets the background color to #A4243B and the foreground color to white. You can do this for almost every type of control in Xamarin.Forms.

The second kind of selectors we will be using are class selectors, which are the ones beginning with a period, such as .chatHeader. These selectors are used in XAML with theStyleClassproperty. Take a look at the ChatView.xaml file we created earlier – you'll find these in the template resources.

Each property in the CSS is mapped to a property on the control itself. There are also some Xamarin.Forms-specific properties that can be used, but those are out of the scope of this book. If you search for Xamarin.Forms and CSS on the internet, you'll find all of the information you'll need to dive deeper into this.

Applying the style sheet

A style sheet is no good on its own. We need to apply it to our application. We also need to set some styling on NavigationPage here as well, since we can't gain access to it from the CSS directly.

We will be adding some resources and a reference to the style sheet. Copy the code that follows and refer to these steps to study what each line does:

  1. Open the App.xaml file in the Chat project.
  2. In the Application.Resources node, add a <StyleSheet Source="/Css/Styles.css" /> node to reference the style sheet.
  1. The next node we need to work on is the StyleSheet node. Add a Style node with TargetType set to "NavigationPage". Create a setter for the BarBackgroundColor property with a value of "#273E47" and a setter for the BarTextColor property with a value of "White".

The App.xaml file should now look as follows:

<?xml version="1.0" encoding="utf-8"?>
<Application
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Chat.App">
<Application.Resources>
<StyleSheet Source="/Css/Styles.css" />
<ResourceDictionary>
<Style TargetType="NavigationPage">
<Setter Property="BarBackgroundColor" Value="#273E47" />
<Setter Property="BarTextColor" Value="White" />
</Style>
</ResourceDictionary>
</Application.Resources>
</Application>

Handling life cycle events

Finally, we need to add some life cycle events that will take care of our SignalR connection in case the app goes to sleep or when it wakes up again:

  1. Open the App.xaml.cs file.
  2. Add the following code somewhere inside the App class:
protected override void OnSleep()
{
var chatService = Resolver.Resolve<IChatService>();
chatService.Dispose();
}

protected override void OnResume()
{
Task.Run(async() =>
{
var chatService = Resolver.Resolve<IChatService>();

if (!chatService.IsConnected)
{
await chatService.CreateConnection();
}
});

Page view = null;

if(ViewModel.User != null)
{
view = Resolver.Resolve<ChatView>();
}
else
{
view = Resolver.Resolve<MainView>();
}

var navigationPage = new NavigationPage(view);
MainPage = navigationPage;
}

The OnSleep() method will be called when the user minimizes the app. It will dispose of any active chatService that is running by closing the active connections. The OnResume() method does a little bit more than this. It will recreate the connection if there isn't one already active and, depending on whether the user is set or not, it will resolve to the correct view. If a user isn't present, it will display MainView; otherwise, it will display ChatView. Finally, it sets the selected view, wrapped in a navigation page.

Summary

That's that – good work! In this chapter, we created a chat app that connects to our backend. We have learned how to work with SignalR, how to style an app with CSS, how to use template selectors in a CollectionView, and how to use a value converter to convert a byte[] into a Xamarin.Forms ImageSource.

In the next chapter, we will dive into an augmented world! We will create an AR game for iOS and Android using UrhoSharp, along with ARKit (iOS) and ARCore (Android).

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.66.13