Learn Technologies
Xamarin Forms - ChatApp SignalR - Friends

Xamarin Forms – Chat Application – Friends (part 2)

In this new tutorial, we ‘ll see, how to get the list of Friends. Send a private message to friend and how to save it in database.

If you read this article for the first time, i invite you to read before the previous tutorial available in this link.

Eventually, you can download the code source from my GitHub repository:

Login page

Last time, we created username entry in the main page to enter a public chat room. In this time, let’s change this page to login page. Wee need to change the name entry to email and add new one for password.
Also, don’t forget to change the button text to Sign in and the command binding to SignInCommand.

So, you code will be something like this:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="ChatApp.Mobile.Views.MainPage"
             Title="Login ">
    <Grid RowDefinitions="*,*,*">
        <StackLayout Grid.Row="1"
                     Margin="20,0">
            <Entry Text="{Binding Email}" Placeholder="Email"></Entry>
            <Entry Text="{Binding Pwd}" Placeholder="Password" IsPassword="True"></Entry>

            <Frame Style="{StaticResource ButtonFrameStyle}" >
                <Button Style="{StaticResource ButtonStyle}"
                        Text="Sign In"
                        Command="{Binding SignInCommand}"/>
            </Frame>
        </StackLayout>
            </Grid>
</ContentPage>

In the MainPageViewModel, and 2 properties for Email and Password. Also create new command for SignInCommand.

Let’s begin by explain the sign in business logic. First of all, we gonna call a service called authenticationService in order to send the login request to Web API. The request response will be type of UserModel.

After that, check if the user different form null. If it ‘s, save the user information and the generated token in the application cache.

You an download the code of AuthenticationService and SessionService from this link.

Finally and if all goes well, user can navigate to FriendsPage.

using ChatApp.Mobile.Models;
using ChatApp.Mobile.Services.Interfaces;
using ChatApp.Mobile.Views;
using Prism.Commands;
using Prism.Navigation;
using System;
using System.Windows.Input;

namespace ChatApp.Mobile.ViewModels
{
    public class MainPageViewModel : ViewModelBase
    {
        private string email;
        public string Email
        {
            get => email;
            set => SetProperty(ref email, value);
        }

        private string pwd;
        private readonly IAuthenticationService authenticationService;

        public string Pwd
        {
            get => pwd;
            set => SetProperty(ref pwd, value);
        }

        public ICommand SignInCommand { get; private set; }

        public ICommand NavigateToChatPageCommand { get; private set; }

        public MainPageViewModel(INavigationService navigationService,
             IAuthenticationService authenticationService,
             ISessionService sessionService)
            : base(navigationService, sessionService)
        {
            NavigateToChatPageCommand = new DelegateCommand(NavigateToChatPage);
            SignInCommand = new DelegateCommand(SignIn);
            this.authenticationService = authenticationService;
        }

        private async void SignIn()
        {
            var user = await authenticationService.Login(new LoginModel { Email = Email, Password = Pwd });
            if (user != null)
            {
                await SessionService.SetConnectedUser(user);
                await SessionService.SetToken(new TokenModel
                {
                    Token = user.Token,
                    RefreshToken = user.RefreshToken,
                    TokenExpireTime = user.TokenExpireTimes
                });

                await NavigationService.NavigateAsync("../FriendsPage");
            }
            else
            {
                // TODO Show login error.
            }
        }

        private void NavigateToChatPage()
        {
            var param = new NavigationParameters { { "UserNameId", Email } };
            NavigationService.NavigateAsync($"NavigationPage/{nameof(ChatRoomPage)}", param);
        }
    }
}

Friends Page

Now, we gonna add a new page called FriendsPage. In this page, we retrieve the friends list for the current connected user. To do that, create a new service called UsersService. Inside it, and new method called GetUserFriendsAsync.

using ChatApp.Mobile.Models;
using ChatApp.Mobile.Services.Interfaces;
using Prism.Navigation;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace ChatApp.Mobile.Services.Core
{
    public class UsersService : BaseService, IUsersService
    {
        public UsersService(ISessionService sessionService,
            INavigationService navigationService)
            : base(sessionService, navigationService)
        {
        }
      
        public async Task<IEnumerable<UserModel>> GetUserFriendsAsync(long userId)
        {
            return await Get<IEnumerable<UserModel>>($"Users/getMyFriends/{userId}");
        }
    }
}

In FriendsPage, add a CollectionView that will represent the list user’s frirends. After that, define the data template by adding the image and friend name label. In our case, we gonna use a static image for all friend. Nevertheless, you can store the image in database(we gonna do this in the next video).

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:prism="http://prismlibrary.com"
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Class="ChatApp.Mobile.Views.FriendsPage"
             Title="My Friends"
             x:Name="FriendsPageName">
    <Grid>
        <CollectionView ItemsSource="{Binding FriendsList}">
            <CollectionView.ItemTemplate>
                <DataTemplate>
                    <Grid ColumnDefinitions="80, 80,*" RowDefinitions="80">
                        <Frame Padding="0"
                               Margin="5"
                               HasShadow="True"
                               BorderColor="Black"
                               CornerRadius="50"
                               HorizontalOptions="Center">
                            <Image Aspect="AspectFill"
                                   Source="https://i.pinimg.com/236x/b2/7c/b9/b27cb9fc7b499bcd62c6033f1e16624f.jpg"
                                   VerticalOptions="Center"
                                   HorizontalOptions="Center"/>
                        </Frame>
                        <Label Grid.Column="1"
                               VerticalOptions="Center"
                               Text="{Binding Name}" 
                               HorizontalTextAlignment="Center"
                               FontSize="20"
                               TextColor="Black"/>
                        <BoxView Grid.ColumnSpan="3"
                                 HeightRequest="1"
                                 VerticalOptions="End"
                                 BackgroundColor="LightGray"/>
                        <Grid.GestureRecognizers>
                            <TapGestureRecognizer Command="{Binding Source={x:Reference FriendsPageName}, Path=BindingContext.GoToPrivateDiscussionCommand}"
                                                  NumberOfTapsRequired="1"
                                                  CommandParameter="{Binding .}" />
                        </Grid.GestureRecognizers>
                    </Grid>
                </DataTemplate>
            </CollectionView.ItemTemplate>
        </CollectionView>
    </Grid>
  
</ContentPage>

When the user tap on a friend item, we navigate to a PrivateChatPage.

using ChatApp.Mobile.Models;
using ChatApp.Mobile.Services.Interfaces;
using Newtonsoft.Json;
using Prism.Commands;
using Prism.Mvvm;
using Prism.Navigation;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Input;

namespace ChatApp.Mobile.ViewModels
{
    public class FriendsPageViewModel : ViewModelBase
    {
        private IEnumerable<UserModel> friendsList;
        private readonly IUsersService usersService;

        public IEnumerable<UserModel> FriendsList
        {
            get => friendsList;
            set =>SetProperty(ref friendsList, value);
        }

        public ICommand GoToPrivateDiscussionCommand { get; private set; }
        public FriendsPageViewModel(INavigationService navigationService,
            ISessionService sessionService,
            IUsersService usersService)
            : base(navigationService, sessionService)
        {
            this.usersService = usersService;

            GoToPrivateDiscussionCommand = new DelegateCommand<UserModel>(GoToPrivateDiscussion);
        }

        public override async void Initialize(INavigationParameters parameters)
        {
            base.Initialize(parameters);
            var currentUser = await SessionService.GetConnectedUser();
            FriendsList = await usersService.GetUserFriendsAsync(currentUser.ID);
        }

        private async void GoToPrivateDiscussion(UserModel friend)
        {
            var param = new NavigationParameters { { "friend", JsonConvert.SerializeObject(friend) } };
            await NavigationService.NavigateAsync("PrivateChatPage", param);
        }
    }
}

Xamarin Forms - Chat Application

Add PrivateChatPage

Finally, add new page PrivateChatPage, in wich user can send and receive private message from the selected friend.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:prism="http://prismlibrary.com"
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Class="ChatApp.Mobile.Views.PrivateChatPage"
             Title="{Binding Title}">
    <Grid RowDefinitions="*, auto"
          Margin="10">
        <ListView ItemsSource="{Binding MessagesList}"
                  SeparatorVisibility="None"
                  HasUnevenRows="True">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <ViewCell>
                        <Grid RowDefinitions="auto, auto">
                            <Grid ColumnDefinitions="40, *" IsVisible="{Binding IsOwnerMessage}">
                                <!--<Label Text="{Binding UserName}"/>-->
                                <Frame CornerRadius="30"
                                       Grid.Column="1"
                                       HorizontalOptions="End"
                                       HasShadow="True"
                                       Margin="0"
                                       BackgroundColor="{StaticResource PrimaryColor}"
                                       Padding="5"
                                       MinimumWidthRequest="20">
                                    <Label Text="{Binding Message}"
                                           Margin="10"
                                           TextColor="{StaticResource WhiteColor}"
                                           LineBreakMode="WordWrap"/>
                                </Frame>
                            </Grid>
                            <Grid ColumnDefinitions="*, 40"
                                  Grid.Row="1"
                                  IsVisible="{Binding IsOwnerMessage, Converter={StaticResource  BooleanToVisibility}, ConverterParameter=Inverse}">
                                <!--<Label Text="{Binding UserName}"/>-->
                                <Frame CornerRadius="30"
                                       HasShadow="True"
                                       Margin="0"
                                       HorizontalOptions="Start"
                                       BackgroundColor="{StaticResource ElegantDarkColor}"
                                       Padding="5">
                                    <Label Text="{Binding Message}"
                                           Margin="10"
                                           TextColor="{StaticResource DarkColor}"
                                           LineBreakMode="WordWrap"/>
                                </Frame>
                            </Grid>
                            <!--<Grid RowDefinitions="auto, auto"  >
                                <Label Text="{Binding UserName}"/>
                                <Label Text="{Binding Message}"/>
                            </Grid>-->
                        </Grid>
                    </ViewCell>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
        <StackLayout Grid.Row="1"
                     Orientation="Horizontal">
            <Entry Text="{Binding Message}"
                   
                   HorizontalOptions="FillAndExpand"></Entry>
            <Button Text="Send" Command="{Binding SendMsgCommand}"/>
        </StackLayout>
    </Grid>

</ContentPage>

And the ViewModel will look like this:

using ChatApp.Mobile.Models;
using ChatApp.Mobile.Services.Interfaces;
using Newtonsoft.Json;
using Prism.Commands;
using Prism.Mvvm;
using Prism.Navigation;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Input;

namespace ChatApp.Mobile.ViewModels
{
    public class PrivateChatPageViewModel : ViewModelBase
    {
        private UserModel friend;
        public UserModel Friend
        {
            get => friend;
            set => SetProperty(ref friend, value);
        }

        private UserModel currentUser;
        public UserModel CurrentUser
        {
            get => currentUser;
            set => SetProperty(ref currentUser, value);
        }

        private string message;
        public string Message
        {
            get => message;
            set => SetProperty(ref message, value);
        }


        private IEnumerable<MessageModel> messageList;
        private readonly IChatService chatService;

        public IEnumerable<MessageModel> MessagesList
        {
            get => messageList;
            set => SetProperty(ref messageList, value);
        }
        public ICommand SendMsgCommand { get; private set; }

        public PrivateChatPageViewModel(INavigationService navigationService,
            ISessionService sessionService,
            IChatService chatService)
            : base(navigationService, sessionService)
        {
            SendMsgCommand = new DelegateCommand(SendMsg);
            this.chatService = chatService;
        }

        public override async void Initialize(INavigationParameters parameters)
        {
            base.Initialize(parameters);
            var friendString = parameters.GetValue<string>("friend");
            Friend = JsonConvert.DeserializeObject<UserModel>(friendString);
            Title = friend.Name;
            CurrentUser = await SessionService.GetConnectedUser();
            MessagesList = new List<MessageModel>();
            try
            {
                chatService.ReceiveMessage(GetMessage, true);
                await chatService.Connect(CurrentUser.Email);
            }
            catch (System.Exception exp)
            {
                throw;
            }
        }

        public override async void OnNavigatedFrom(INavigationParameters parameters)
        {
            base.OnNavigatedFrom(parameters);
            await chatService.Disconnect(CurrentUser.Email);
        }

        private void GetMessage(string userName, string message)
        {
            AddMessage(userName, message, false);
        }

        private void AddMessage(string userName, string message, bool isOwner)
        {
            var tempList = MessagesList.ToList();
            tempList.Add(new MessageModel { IsOwnerMessage = isOwner, Message = message, UseName = userName });
            MessagesList = new List<MessageModel>(tempList);
            Message = string.Empty;
        }

        private void SendMsg()
        {
            chatService.SendMessage(friend.Email, Message, true);
            AddMessage(friend.Name, Message, true);
        }
    }
}

In ChatService, don’t forget to add isPrivate parameter in SendMessage and receiveMessage methods.

using ChatApp.Mobile.Services.Interfaces;
using Microsoft.AspNetCore.SignalR.Client;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;

namespace ChatApp.Mobile.Services.Core
{
    public class ChatService: IChatService
    {
        private readonly HubConnection hubConnection;
        public ChatService()
        {
            hubConnection = new HubConnectionBuilder().WithUrl("http://192.168.1.36:45459/chathub").Build();
        }

        public async Task Connect(string userEmail)
        {
            await hubConnection.StartAsync();
            await hubConnection.InvokeAsync("OnConnect", userEmail);
        }

        public async Task Disconnect(string userEmail)
        {
            await hubConnection.InvokeAsync("OnDisconnect", userEmail);
            await hubConnection.StopAsync();

        }

        public async Task SendMessage(string userId, string message, bool isPrivate= false)
        {
            if(isPrivate)
            {
                await hubConnection.InvokeAsync("SendPrivateMessage", userId, message);
            }
            else
            {
                await hubConnection.InvokeAsync("SendMessage", userId, message);
            }
        }

        public void ReceiveMessage(Action<string, string> GetMessageAndUser, bool isPrivate = false)
        {
            if(isPrivate)
            {
                hubConnection.On("ReceivePrivateMessage", GetMessageAndUser);
            }
            else
            {
                hubConnection.On("ReceiveMessage", GetMessageAndUser);
            }
        }
    }
}

Web API SignalR

In the other side, update you signalR hub by adding 3 new methods.

The first one is SendPrivateMessage. Obviously, in this method, we gonne send the message to the different active connections you the user’s friend. Which means, the friend can be connected on mobile and desktop applications. So, we send the message to this 2 devices like Facebook for exemple.
The last step in this method, is to save the conversation in data base.

Also, add OnConnect and Ondisconnect methods to track the user active state.

using ChatApp.Managers.Interfaces;
using ChatApp.Models;
using Microsoft.AspNetCore.SignalR;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ChatApp.WebAPI.Hubs
{
    public class ChatHub : Hub
    {
        private readonly IUsersManager usersManager;
        private readonly IConversationsManager conversationsManager;

        public ChatHub(IUsersManager usersManager, IConversationsManager conversationsManager)
        {
            this.usersManager = usersManager;
            this.conversationsManager = conversationsManager;
        }

        public async Task SendMessage(string userId, string message)
        {
            await Clients.Others.SendAsync("ReceiveMessage", userId, message);
        }

        public async Task SendPrivateMessage(string userEmail, string message)
        {

            var senderUser = usersManager.GetUserByConnectionId(Context.ConnectionId);
            var friend = usersManager.GetUserByEmail(userEmail);
            var friendConnections = friend.Connections.Where(x => x.IsConnected);
            foreach (var connection in friendConnections)
            {
                await Clients.Client(connection.ConnectionID).SendAsync("ReceivePrivateMessage", userEmail, message);
            }
            // Inser in to database..
            var conversationModel = conversationsManager.GetConversationByUsersId(senderUser.ID, friend.ID);

            if (conversationModel == null)
            {
                var conversationId = conversationsManager.AddOrUpdateConversation(senderUser.ID, friend.ID);
                conversationsManager.AddReply(message, conversationId, senderUser.ID);
            }
            else
            {
                conversationsManager.AddReply(message, conversationModel.ID, senderUser.ID);
            }
        }

        public async Task OnConnect(string userEmail)
        {
            var user = usersManager.GetUserByEmail(userEmail);
            usersManager.AddUserConnections(new ConnectionModel
            {
                ConnectionID = Context.ConnectionId,
                IsConnected = true,
                UserAgent = Context.GetHttpContext().Request.Headers["User-Agent"],
                UserID = user.ID
            });

            await base.OnConnectedAsync();
        }

        public async Task OnDisconnect(string userEmail)
        {
            var user = usersManager.GetUserByEmail(userEmail);
            usersManager.UpdateUserConnectionsStatus(user.ID, false, Context.ConnectionId);
            await base.OnDisconnectedAsync(null);
        }
    }
}


When you download the Web API code source, don’t forget to create the data base and populate it by different users, friends relationship . you can use swagger to add users for exemple.

Run the application on 2 devices, and enjoy sending messages.

Follow Me For Updates

Subscribe to my YouTube channel or follow me on Twitter or GitHub to be notified when I post new content.

5 1 vote
Article Rating
Subscribe
Notify of
guest
1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Amanuel

why  var senderUser = usersManager.GetUserByConnectionId(Context.ConnectionId) throw an null exeption

1
0
Would love your thoughts, please comment.x
()
x