Skip to content

Latest commit

 

History

History
338 lines (240 loc) · 12.3 KB

9. Use MVVM pattern.md

File metadata and controls

338 lines (240 loc) · 12.3 KB

1010 ENEI || Xamarin Workshop

Previous step Guide 8: Change the App.cs to App.xaml

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 respectiv e parts of the application.”

in MSDN

To help understand the MVVM pattern, here is a diagram which explain how it works:

MVVM Pattern diagram

Figure 53: 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 a ViewModels folders, this way any developers will recognize that the application uses MVVM pattern and it is easy to match Views and ViewModels (but any developer is free to organize the application based in application requirements!).

Let’s create the view models!

In ENEI.SessionsApp project, create a folder called ViewModels, and then create the class SessionViewModel. The result should be something as following:

MVVM Pattern diagram

Figure 54: The view model folder

Now you need to refactor the code in SessionView.xaml.cs. For it let’s define the SessionViewModel class as 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 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) which are defined in Xaml as following:

<Image.GestureRecognizers>
  <TapGestureRecognizer x:Name="DetailsGesture"
                    CommandParameter="{Binding}"
                    Tapped="DetailsGesture_OnTapped" />
</Image.GestureRecognizers>

These event’s handle are not friendly for who want implement MVVM pattern, to solve it was create the “ICommand” that allow to call the associate action. This way we need to define the ICommand for each option as 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 is required to initialize each one, as 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 has the capability to send and receive message. 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 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)
    {
        //workarround to clean the select item
        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 is required to do few changes:

  • 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 binding 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 is required to define the binding’s Source, which uses the view model as a static resource (at this moment is not possible to apply relative binding and is not good practice to define the commands in the model object because it belong 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 show a Session and do not have any other capability is not important that change it.

To learn more about this subject is recommend to read the following articles: XAML Basics Contents

Next step 10. Move ItemTemplate to Resources