Scope
This Xamarin Workshop Guide was created for the The Portuguese National Meeting of IT Students (ENEI) by Sara Silva and the original content is available here. To extend it to the global community, it was published in a new project called Xam Community Workshop and the main goal is for any developer, or user group to customize it for their events.
Before reading this article you must read:
Guide 9. Use MVVM Pattern
In this step you will learn how refactor your code to use the MVVM pattern.
“The Model-View-ViewModel (MVVM) pattern helps you to cleanly separate the business and presentation logic of your application from its User Interface (UI). Maintaining a clean separation between application logic and UI helps to address numerous development and design issues and can make your application much easier to test, maintain and evolve. It can also greatly improve code re-use opportunities and allows developers and UI designers to more easily collaborate when developing their respective parts of the application.”
- From the MSDN.
To help understand the MVVM pattern, here is a diagram that explains how it works:
Figure 1: MVVM Pattern diagram
In the Sessions App, you can create two view models, one for each page. To help matching views and view models, in general you should have:
- SessionsView will be connected with SessionViewModel
- SessionDetailsView will be connected with SessionDetailsViewModel
Usually all view models are defined in “ViewModels” folders. Then any developer can recognize that the application uses the MVVM pattern and it is easy to match Views and ViewModels. (Any developer however is free to organize the application based on application requirements!).
Let's create the view models!
In the ENEI.SessionsApp project, create a folder called “ViewModels” and then create the class “SessionViewModel”. The result should be something as in the following:
Figure 2: The view model folder
Now you need to refactor the code in SessionView.xaml.cs. For it let's define the SessionViewModel class as in the following:
- public class SessionViewModel
- {
- public SessionViewModel()
- {
- Sessions = new ObservableCollection<Session>();
- }
-
- public ObservableCollection<Session> Sessions { get; set; }
-
- public async Task LoadDataAsync()
- {
- await Task.Run(() =>
- {
- if (Sessions.Count == 0)
- {
- var sessions = SessionsDataSource.GetSessions();
- foreach (var session in sessions)
- {
- Sessions.Add(session);
- }
- }
- });
- }
- }
This way, you defined the Sessions list and the LoadDataAsync in the ViewModel, now you need to create the command for each option in the menu.
In the SessionsView.xaml.cs you have the event's handler from the Tap event (for Like, Favorite, Share and SessionDetails) that are defined in XAML as in the following:
- <Image.GestureRecognizers>
- <TapGestureRecognizer x:Name="DetailsGesture" CommandParameter="{Binding}" Tapped="DetailsGesture_OnTapped" />
- </Image.GestureRecognizers>
These event's handles are not friendly for implementing the MVVM pattern, to solve it create the “ICommand” that allows the calling of the associated action. This way we need to define the ICommand for each option as in the following:
- public ICommand LikeCommand { get; private set; }
- public ICommand FavoriteCommand { get; private set; }
- public ICommand ShareCommand { get; private set; }
- public ICommand SessionDetailsCommand { get; private set; }
And in the constructor we must initialize each one, as in the following:
- public SessionViewModel()
- {
- Sessions = new ObservableCollection<Session>();
- LikeCommand = new Command(ApplyLike);
- FavoriteCommand = new Command(ApplyFavorite);
- ShareCommand = new Command(Share);
- SessionDetailsCommand = new Command(SeeSessionDetails);
- }
Where each method above is defined by:
- ApplyLike
- private void ApplyLike(object param)
- {
- var session = param as Session;
- if (session != null)
- {
- session.NumLikes++;
- }
- }
- ApplyFavorite
- private void ApplyFavorite(object param)
- {
- var session = param as Session;
- if (session != null)
- {
- session.IsFavorite = !session.IsFavorite;
- }
- }
- Share
- private void Share(object param)
- {
- var session = param as Session;
- if (session != null)
- {
- var shareService = DependencyService.Get<IShareService>();
- if (shareService != null)
- {
- var status = string.Format("Não percas a sessão {0} de {1}.", session.Name, session.Speaker.Name);
- shareService.ShareLink("ENEI 2015", status, "https://enei.pt/");
- }
- }
- }
- SeeSessionDetails
- private void SeeSessionDetails(object param)
- {
- var session = param as Session;
- if (session != null)
- {
- MessagingCenter.Send(session, "SeeSessionDetails");
- }
- }
The MessagingCenter is a class that can send and receive messages. In this case, when a user wants to see the session details the view model will send a message with the session to the view and then the view will navigate to the SessionDetailsView.
See more about “Publish and Subscribe with MessagingCenter”.
At the end your SessionViewModel class should be defined as in the following:
- public class SessionViewModel
- {
- public SessionViewModel()
- {
- Sessions = new ObservableCollection < Session > ();
- LikeCommand = new Command(ApplyLike);
- FavoriteCommand = new Command(ApplyFavorite);
- ShareCommand = new Command(Share);
- SessionDetailsCommand = new Command(SeeSessionDetails);
- }
-
- public ObservableCollection < Session > Sessions {get;set;}
- public ICommand LikeCommand {get;private set;}
- public ICommand FavoriteCommand {get;private set;}
- public ICommand ShareCommand {get;private set;}
- public ICommand SessionDetailsCommand { get;private set;}
-
- private void ApplyLike(object param)
- {
- var session = param as Session;
- if (session != null)
- {
- session.NumLikes++;
- }
- }
-
- private void ApplyFavorite(object param)
- {
- var session = param as Session;
- if (session != null)
- {
- session.IsFavorite = !session.IsFavorite;
- }
- }
-
- private void Share(object param)
- {
- var session = param as Session;
- if (session != null)
- {
- var shareService = DependencyService.Get < IShareService > ();
- if (shareService != null)
- {
- var status = string.Format("Não percas a sessão {0} de {1}.", session.Name, session.Speaker.Name);
- shareService.ShareLink("ENEI 2015", status, "https://enei.pt/");
- }
- }
- }
-
- private void SeeSessionDetails(object param)
- {
- var session = param as Session;
- if (session != null)
- {
- MessagingCenter.Send(session, "SeeSessionDetails");
- }
- }
-
- public async Task LoadDataAsync()
- {
- await Task.Run(() = >
- {
- if (Sessions.Count == 0)
- {
- var sessions = SessionsDataSource.GetSessions();
- foreach(var session in sessions)
- {
- Sessions.Add(session);
- }
- }
- }
- }
- }
And the SessionsView.xaml.cs should be changed to:
- public partial class SessionsView: ContentPage
- {
- public SessionsView()
- {
- InitializeComponent();
- MessagingCenter.Subscribe < Session > (this, "SeeSessionDetails", session = > {
- Navigation.PushAsync(new SessionDetailsView(session), true);
- });
- }
-
- protected override async void OnAppearing()
- {
- base.OnAppearing();
- var viewmodel = BindingContext as SessionViewModel;
- if (viewmodel != null)
- {
- await viewmodel.LoadDataAsync();
- }
- }
-
- private void SessionsList_OnItemSelected(object sender, SelectedItemChangedEventArgs e)
- {
-
- if (SessionsList.SelectedItem == null)
- {
- return;
- }
- SessionsList.SelectedItem = null;
- }
- }
The event's handler SessionsList_OnItemSelected will not be changed, because it is a workaround to clean the selected item.
In the SessionsView.xaml we must make a few changes as in the following.
- Define the SessionViewModel as a resource from the page
- <ContentPage.Resources>
- <ResourceDictionary>
- <viewModels:SessionViewModel x:Key="SessionViewModel"/>
- Binding the SessionViewModel to the BindingContext from the view
- <ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
- xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
- xmlns:converters="clr-namespace:ENEI.SessionsApp.Converters;assembly=ENEI.SessionsApp"
- xmlns:viewModels="clr-namespace:ENEI.SessionsApp.ViewModels;assembly=ENEI.SessionsApp"
- x:Class="ENEI.SessionsApp.Views.SessionsView"
- Title="1010 ENEI || Sessões"
- BackgroundColor="White"
- x:Name="ContentPage"
- BindingContext="{StaticResource SessionViewModel}"
- Icon="ic_action_users.png">
- For each option change the TapGestureRecognizer
- <TapGestureRecognizer CommandParameter="{Binding}" Command="{Binding SessionDetailsCommand, Source={StaticResource SessionViewModel}}"/>
The Command is bound to the respective command from the view model, but each developer should be aware that when the view is loaded, the binding context from the Image is defined with a Session from the respective listview's item. For this reason we must define the binding's Source that uses the view model as a static resource. (At this moment it is not possible to apply relative binding and it is not a good practice to define the commands in the model object because it belongs to the view model!)
If you run the application it must behave as before.
It is possible to define the view model to the SessionDetailsView, but because it only shows a Session and does not have any other capability it is not important to change it.
To learn more about this subject it is recommend to read the following articles.
XAML Basics Contents