3

Navigation

The overarching goal of this book is to show how you can build a solid architecture based on design patterns and best practices; the objective of this chapter is to take our TripLog app one step closer to achieving that goal. By introducing MVVM into our TripLog app in Chapter 2, MVVM and Data Binding, we set up the app with a very clear pattern to separate the user interface from the rest of the logic in the app. Each subsequent chapter, starting with this one, further advances this concept of separation.

In Chapter 2, MVVM and Data Binding, we moved a large portion of the app logic into ViewModels; however, navigation is still being initiated from the Pages (Views). In this chapter, we will create a navigation service that we can use to refactor any navigation logic out of the Page-level code and into the ViewModels. While doing this will not result in any noticeable differences when running the app, it will allow us to make navigation fit more naturally into the rest of the app's logic. Furthermore, as we will see in Chapter 8, Testing, having an abstracted navigation service means we can include assertions about navigation when testing the logic in our ViewModels.

Here's a quick look at what we'll cover in this chapter:

  • Understanding the basics of the Xamarin.Forms navigation API
  • Thinking about navigation in MVVM
  • Creating a navigation service
  • Updating the TripLog app to use the navigation service

We'll start by reviewing the navigation API that comes with Xamarin.Forms.

The Xamarin.Forms navigation API

Along with abstracting common user interface elements into a multi-platform API, Xamarin.Forms also abstracts navigation for iOS, Android, and Windows into a single easy-to-use navigation service. Each mobile platform does navigation in a slightly different way and has a slightly different navigation API; however, at their core, they all accomplish similar tasks, and, in most cases, use a stack structure – last in, first out.

The Xamarin.Forms navigation API uses stack-like terminology, closely resembling the navigation APIs of iOS. The Xamarin.Forms navigation API is exposed through the Xamarin.Forms.INavigation interface, which is implemented via the Navigation property that can be called from any Xamarin.Forms.VisualElement object. Typically, Xamarin.Forms.Page is the object used. Xamarin.Forms.NavigationPage also implements the Xamarin.Forms.INavigation interface and exposes public methods to perform common navigation tasks.

The Xamarin.Forms navigation API supports two types of navigation: standard and modal. Standard navigation is the typical navigation pattern where the user clicks or taps through a series of pages and is able to use either device/operating system-provided functionality (back buttons on Android and Windows), or app-provided elements (navigation bar on iOS and action bar on Android), to navigate back through the stack. Modal navigation is similar to the modal dialog concept in web apps where a new page is layered on top of the calling page, preventing interaction with the calling page until the user performs a specific action to close the modal page. On smaller form factor devices, modal pages typically take up the entire screen, whereas on larger form factors, such as tablets, modal pages may only take up a subset of the screen, more like a dialog. The Xamarin.Forms.INavigation interface exposes two separate read-only properties to view the standard and modal navigation stacks: NavigationStack and ModalStack.

The Xamarin.Forms.INavigation interface provides several methods to asynchronously push and pop pages onto the navigation and modal stacks, as follows:

  • PushAsync(Page page) and PushAsync(Page page, bool animated) to navigate to a new page
  • PopAsync() and PopAsync(bool animated) to navigate back to the previous page, if there is one
  • PushModalAsync(Page page) and PushModalAsync(Page page, bool animated) to modally display a page
  • PopModalAsync() and PopModalAsync(bool animated) to dismiss the current modally displayed page

Notice how each method has an optional animated parameter that allows you to specify if the page should animate when the navigation transition is happening.

In addition to these methods, there are a few methods that help you manipulate the navigation stack, since it is exposed as a read-only property:

  • InsertPageBefore(Page page, Page before) to insert a page before a specific page that is already in the navigation stack
  • RemovePage(Page page) to remove a specific page in the navigation stack
  • PopToRootAsync() and PopToRootAsync(bool animated) to navigate back to the first page and remove all others in the navigation stack

We've already used PushAsync() a few times in the TripLog app to allow the user to move from page to page. In the next couple of sections of this chapter, we'll create a custom navigation service that extends the Xamarin.Forms navigation API, use it to move those instances of PushAsync() from the Views into the ViewModels, and expose them through commands that will be data bound to the page.

Navigation and MVVM

One of the key purposes of the MVVM pattern is to isolate an app's presentation layer from its other layers. In doing so, an app's business logic is also isolated. One of the thoughts behind this isolation is to have a user interface that is only concerned with displaying data, and that is completely independent of how that data is stored, acquired, manipulated, or shared with the rest of the app. As explained in Chapter 2, MVVM and Data Binding, this is typically accomplished through data binding.

In MVVM, the actions that a user performs on a page are bound to commands on that page's backing ViewModel. It is very common for these actions to result in a transition to another page—either by directly linking to it or by automatically navigating to a previous page after performing a task, such as saving data. Therefore, it makes sense to rethink how we implement navigation in an app that leverages the MVVM pattern so that it can be controlled by the ViewModels and not by the pages.

Most of the common third-party MVVM frameworks and toolkits subscribe to this theory and often even provide a navigation service that is designed for ViewModel consumption.

There are two main approaches to consider when performing navigation within ViewModels—one is the page-centric approach and the other is the ViewModel-centric approach. A page-centric approach involves navigating to another page by a direct reference to that page. A ViewModel-centric approach involves navigating to another page by reference to that page's ViewModel.

The page-centric approach can be accomplished in Xamarin.Forms by simply passing the current Xamarin.Forms.INavigation instance into a ViewModel's constructor. From there, the ViewModel can use the default Xamarin.Forms navigation mechanism to navigate to other pages. The benefits of this approach are that it separates the navigation functionality from the page layer and is fairly quick to implement. However, the downside is that it puts a strong dependency on direct page references into ViewModels. I typically prefer to use the ViewModel-centric approach and keep ViewModels loosely coupled and unaware of the actual page implementations.

ViewModel-centric navigation

As previously discussed, the ViewModel-centric approach alleviates a ViewModel from having any dependencies on the specific implementation of individual pages. In a default Xamarin.Forms solution, this might not appear to be such a big deal, but consider a situation where pages were self-contained in their own library—the library containing ViewModels probably wouldn't have a reference to that library. This is typical of a traditional Xamarin-based multi-platform solution architecture and also a good practice to follow.

Since a ViewModel doesn't navigate directly to a page, it will navigate to a page via the page's ViewModel. This means that when implementing this approach, there is a need to build a relationship, or mapping, between pages and their ViewModels. As with most things in software development, this can be done in a couple of ways. One way is to include a dictionary or key-value type property in the navigation service that maintains a one-to-one mapping of pages and ViewModels using their type. This could also be done externally to the navigation service to provide an additional abstraction. Another approach, which is used by the MVVM Light (http://www.mvvmlight.net/) toolkit's navigation service, is to map the type of ViewModel with a string key that represents the actual page it relates to.

In the next section, we'll create a ViewModel-centric navigation service that includes ViewModel and page type mapping.

Creating a navigation service

In a typical multi-platform mobile app architecture, one would have to implement a platform-specific navigation service for each platform the app supports. In our case, Xamarin.Forms has already done this, so we will simply implement a single navigation service that extends the Xamarin.Forms navigation abstraction so that we can perform ViewModel-to-ViewModel navigation.

The first thing we need to do is define an interface for our navigation service that will define its methods. We start with an interface so that the service can be added to ViewModels via constructor injection, which we'll dive into in Chapter 4, Platform-Specific Services and Dependency Injection, and we can easily provide alternative implementations of the service without changing ViewModels that depend on it. A common scenario for this is creating a mock of the service that gets used when unit testing ViewModels.

In order to create the navigation service, perform the following steps:

  1. Create a new Services folder in the core library.
  1. Create a new interface named INavService with the following members:
    using System;
    using System.ComponentModel;
    using System.Threading.Tasks;
    using TripLog.ViewModels;
    public interface INavService
    {
        bool CanGoBack { get; }
        Task GoBack();
        Task NavigateTo<TVM>()
            where TVM : BaseViewModel;
        Task NavigateTo<TVM, TParameter>(TParameter parameter)
            where TVM : BaseViewModel;
        void RemoveLastView(); 
        void ClearBackStack();
        void NavigateToUri(Uri uri);
        event PropertyChangedEventHandler CanGoBackChanged;
    }
    

This interface defines fairly standard navigation behavior—the ability to navigate to ViewModels, navigate back, clear the navigation stack, and navigate to a regular URI. The NavigateTo() method defines a generic type and restricts its use to objects of the BaseViewModel base class, which we created in the previous chapter. There is also an overloaded NavigateTo() method that enables a strongly typed parameter to be passed along with the navigation.

Before we create the actual implementation of the INavService interface, we will need to make a couple of updates to our BaseViewModel:

  1. Update the BaseViewModel to include a virtual method called Init:
    public class BaseViewModel
    {
        // ...
        public virtual void Init()
        {
        }
    }
    
  2. Next, add a second BaseViewModel base class to the BaseViewModel.cs file with a generic type that will be used to pass strongly typed parameters to the Init() method:
    public class BaseViewModel
    {
        // ...
    }
    public class BaseViewModel<TParameter> : BaseViewModel
    {
        protected BaseViewModel()
        {
        }
        public override void Init()
        {
            Init(default(TParameter));
        }
        public virtual void Init(TParameter parameter)
        {
        }
    }
    
  3. Then, update MainViewModel to override the Init() method from BaseViewModel. The Init() method in MainViewModel will be responsible for loading the log entries. We will refactor the ViewModel and move the log entry list population logic out of the constructor and into a new method named LoadEntries, which will then be called from the Init() override:
    public class MainViewModel : BaseViewModel
    {
        // ...
        public MainViewModel()
        {
            LogEntries = new ObservableCollection<TripLogEntry>();
        }
        public override void Init()
        {
            LoadEntries();
        }
        void LoadEntries()
        {
            LogEntries.Clear();
            LogEntries.Add(new TripLogEntry
            {
                Title = "Washington Monument",
                Notes = "Amazing!",
                Rating = 3,
                Date = new DateTime(2019, 2, 5),
                Latitude = 38.8895,
                Longitude = -77.0352
            });
            LogEntries.Add(new TripLogEntry
            {
                Title = "Statue of Liberty",
                Notes = "Inspiring!",
                Rating = 4,
                Date = new DateTime(2019, 4, 13),
                Latitude = 40.6892,
                Longitude = -74.0444
            });
            LogEntries.Add(new TripLogEntry
            {
                Title = "Golden Gate Bridge",
                Notes = "Foggy, but beautiful.",
                Rating = 5,
                Date = new DateTime(2019, 4, 26),
                Latitude = 37.8268,
                Longitude = -122.4798
            });
        }
    }
    
  4. Next, update NewEntryViewModel to override the Init() method from BaseViewModel. For now, the overridden Init() implementation will be blank:
    public class NewEntryViewModel : BaseValidationViewModel
    {
        // ...
        public NewEntryViewModel()
        {
            // ...
        }
        public override void Init()
        {
        }
        // ...
    }
    
  5. Next, update DetailViewModel to inherit from BaseViewModel<TripLogEntry> and override the Init() method, and set the Entry property with the value of its TripLogEntry parameter, removing the need for the constructor TripLogEntry parameter:
    public class DetailViewModel : BaseViewModel<TripLogEntry>
    {
        // ...
        public DetailViewModel() // <- Remove parameter
        {
        }
        public override void Init(TripLogEntry parameter)
        {
            Entry = parameter;
        }
    }
    
  1. We also need to remove the TripLogEntry parameter from the DetailPage constructor as it will now all be handled between the navigation service and the ViewModel's Init() method:
    public partial class DetailPage : ContentPage
    {
        // ...
        public DetailPage() // <- Remove parameter
        {
            InitializeComponent();
            BindingContext = new DetailViewModel();
            // ...
        }
    }
    

Now that BaseViewModel has been updated, we can create our navigation service that implements INavService and update the app to use the navigation service:

  1. Create a new class within the Services folder of the core library. Name the new class XamarinFormsNavService and make it implement INavService as follows:
    public class XamarinFormsNavService : INavService
    {
        // TODO: INavService implementation goes here.
    }
    
  2. Update the XamarinFormsNavService class to include a public INavigation property named XamarinFormsNav. This XamarinFormsNav property provides a reference to the current Xamarin.Forms.INavigation instance, and will need to be set when the navigation service is first initialized, which we'll see later in this chapter when we update the TripLog app:
    using System;
    using Xamarin.Forms;
    public class XamarinFormsNavService : INavService
    {
        public INavigation XamarinFormsNav { get; set; }
        // TODO: INavService implementation goes here.
    }
    

    As discussed in the previous section, we will implement the navigation service with a page-to-ViewModel mapping. We will do this with an IDictionary<Type, Type> property and a method to register the mappings.

  3. Update the XamarinFormsNavService with an IDictionary<Type, Type> read-only property and add a public method named RegisterViewMapping to populate it:
    using System;
    using System.Collections.Generic;
    using Xamarin.Forms;
    public class XamarinFormsNavService : INavService
    {
        readonly IDictionary<Type, Type> _map = 
            new Dictionary<Type, Type>();
        public void RegisterViewMapping(Type viewModel, Type view)
        {
            _map.Add(viewModel, view);
        }
        // ...
        // TODO: INavService implementation goes here.
    }
    
  4. Next, implement the INavService members. Most of the INavService members will leverage the XamarinFormsNav property to make calls to the Xamarin.Forms navigation API in order to perform the navigation and alter the navigation stack:
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Linq;
    using System.Reflection;
    using System.Threading.Tasks;
    using Xamarin.Forms;
    using TripLog.ViewModels;
    public class XamarinFormsNavService : INavService
    {
        // ...
        public event PropertyChangedEventHandler CanGoBackChanged;
        public INavigation XamarinFormsNav { get; set; }
        public bool CanGoBack => 
            XamarinFormsNav.NavigationStack != null
            && XamarinFormsNav.NavigationStack.Count > 0;
        public async Task GoBack()
        {
            if (CanGoBack)
            {
                await XamarinFormsNav.PopAsync(true);
                OnCanGoBackChanged();
            }
        }
        public async Task NavigateTo<TVM>() 
            where TVM : BaseViewModel
        {
            await NavigateToView(typeof(TVM));
            if (XamarinFormsNav.NavigationStack.Last().BindingContext is BaseViewModel)
            {
                ((BaseViewModel)XamarinFormsNav.NavigationStack.Last().BindingContext).Init();
            }
        }
        public async Task NavigateTo<TVM, TParameter>(TParameter parameter) 
            where TVM : BaseViewModel
        {
            await NavigateToView(typeof(TVM));
            if (XamarinFormsNav.NavigationStack.Last().BindingContext is BaseViewModel<TParameter>)
            {
                ((BaseViewModel<TParameter>)XamarinFormsNav.NavigationStack.Last().BindingContext).Init(parameter);
            }
        }
        public void RemoveLastView()
        {
            if (XamarinFormsNav.NavigationStack.Count < 2)
            {
                return;
            }
            var lastView = XamarinFormsNav.NavigationStack[XamarinFormsNav.NavigationStack.Count - 2];
            XamarinFormsNav.RemovePage(lastView);
        }
        public void ClearBackStack()
        {
            if (XamarinFormsNav.NavigationStack.Count < 2)
            {
                return;
            }
            for (var i = 0; i < XamarinFormsNav.NavigationStack.Count - 1; i++)
            {
                XamarinFormsNav.RemovePage(XamarinFormsNav.NavigationStack[i]);
            }
        }
        public void NavigateToUri(Uri uri)
        {
            if (uri == null)
            {
                throw new ArgumentException("Invalid URI");
            }
            Device.OpenUri(uri);
        }
        async Task NavigateToView(Type viewModelType)
        {
            if (!_map.TryGetValue(viewModelType, out Type viewType))
            {
                throw new ArgumentException("No view found in view mapping for " + viewModelType.FullName + ".");
            }
            // Use reflection to get the View's constructor and create an instance of the View
            var constructor = viewType.GetTypeInfo()
                                      .DeclaredConstructors
                                      .FirstOrDefault(dc => !dc.GetParameters().Any());
            var view = constructor.Invoke(null) as Page;
            await XamarinFormsNav.PushAsync(view, true);
        }
        void OnCanGoBackChanged() => CanGoBackChanged?.Invoke(this, new PropertyChangedEventArgs("CanGoBack"));
        // ...
    }
    
  1. Finally, the navigation service class needs to be marked as a dependency so that it can be resolved by the Xamarin.Forms DependencyService. This is accomplished by adding an assembly attribute to the class before the namespace block, as shown in the following code:
    [assembly: Dependency(typeof(XamarinFormsNavService))]
    namespace TripLog.Services
    {
        public class XamarinFormsNavService : INavService
        {
            // ...
        }
    }
    

In Chapter 4, Platform-Specific Services and Dependency Injection, we will remove this as we replace the Xamarin.Forms DependencyService with a third-party dependency injection library.

Updating the TripLog app

With the navigation service completed, we can now update the rest of the TripLog app to leverage it. To start with, we will update the constructor in the main App class in App.xaml.cs to create a new instance of the navigation service and register the app's Page-to-ViewModel mappings:

using System;
using Xamarin.Forms;
using TripLog.Services;
using TripLog.Views;
using TripLog.ViewModels;
public App()
{
    InitializeComponent();
    var mainPage = new NavigationPage(new MainPage());
    var navService = DependencyService.Get<INavService>() as XamarinFormsNavService;
    navService.XamarinFormsNav = mainPage.Navigation; 
    navService.RegisterViewMapping(typeof(MainViewModel), typeof(MainPage));
    navService.RegisterViewMapping(typeof(DetailViewModel), typeof(DetailPage)); 
    navService.RegisterViewMapping(typeof(NewEntryViewModel), typeof(NewEntryPage));
    MainPage = mainPage;
}

Updating BaseViewModel

Since most ViewModels in the TripLog app will need to use the navigation service, it makes sense to include a reference to it in the BaseViewModel class. We will do this by passing an instance of INavService into the BaseViewModel constructor and setting a protected INavService property:

using TripLog.Services;
public class BaseViewModel : INotifyPropertyChanged
{
    // ...
    protected INavService NavService { get; private set; }
    protected BaseViewModel(INavService navService)
    {
        NavService = navService;
    }
    // ...
}
public class BaseViewModel<TParameter> : BaseViewModel
{
    protected BaseViewModel(INavService navService)
        : base(navService)
    {
    }
    // ...
}

Each of the ViewModels that inherit from BaseViewModel need to be updated to include an INavService parameter in their constructors that is then passed to its BaseViewModel base class:

  1. Update the MainViewModel constructor with an INavService parameter that is passed to the base class constructor:
    public MainViewModel(INavService navService)
        : base(navService)
    {
        // ...
    }
    
  2. Update the DetailViewModel constructor with an INavService parameter that is passed to the base class constructor:
    public DetailViewModel(INavService navService)
        : base(navService)
    {
    }
    
  3. Update the NewEntryViewModel constructor with an INavService parameter that is passed to the base class constructor:
    public NewEntryViewModel(INavService navService)
        : base(navService)
    {
        // ...
    }
    

The BaseValidationViewModel base class needs to be updated to include an INavService constructor parameter as well:

public BaseValidationViewModel(INavService navService)
    : base(navService)
{
}

In addition, each ViewModel instantiation needs to be updated to pass in an INavService, which can be retrieved from the Xamarin.Forms DependencyService:

  1. Update the MainViewModel instantiation in the MainPage constructor:
    public MainPage()
    {
        BindingContext = new MainViewModel(DependencyService.Get<INavService>());
    }
    
  2. Update the DetailViewModel instantiation in the DetailPage constructor:
    public DetailPage()
    {
        BindingContext = new DetailViewModel(DependencyService.Get<INavService>());}
    
  1. Update the NewEntryViewModel instantiation in the NewEntryPage constructor:
    public NewEntryPage()
    {
        // ...
        BindingContext = new NewEntryViewModel(DependencyService.Get<INavService>());
    }
    

Updating MainViewModel

In order to move the navigation functionality from MainPage to MainViewModel, we need to add two new Command properties—one for creating a new log entry and another for viewing the details of an existing log entry:

public class MainViewModel : BaseViewModel
{
    // ...
    public Command<TripLogEntry> ViewCommand => 
        new Command<TripLogEntry>(async entry => 
            await NavService.NavigateTo<DetailViewModel, TripLogEntry>(entry));
        
    public Command NewCommand => 
        new Command(async () => 
            await NavService.NavigateTo<NewEntryViewModel>());
    // ...
}

With the Command properties in place on MainViewModel, we can now update MainPage to use these commands instead of using the Xamarin.Forms navigation APIs directly from the page:

Replace the Clicked attribute on the New ToolbarItem element with a Command attribute whose value is a binding to the NewCommand:

<ToolbarItem Text="New" Command="{Binding NewCommand}" />

Because we are binding the New ToolbarItem element to the NewCommand now, we no longer need the New_Clicked() event handler method in the MainPage code-behind, so it can be deleted. We can also delete the Trips_SelectionChanged() event handler method in the MainPage code-behind as we will bind the item selection to the ViewCommand using a TapGestureRecognizer, within the ItemTemplate of the CollectionView as follows:

<ContentPage ...
    x:Class="TripLog.Views.MainPage"
    xmlns:vm="clr-namespace:TripLog.ViewModels"
    Title="TripLog">
    <!-- ... -->
    <Grid Padding="10">
        <Grid.GestureRecognizers>
            <TapGestureRecognizer
                Command="{Binding 
                  Source={RelativeSource 
                      AncestorType={x:Type vm:MainViewModel}}, 
                  Path=ViewCommand}"
                CommandParameter="{Binding}" />
        </Grid.GestureRecognizers>
        <!-- ... -->
    </Grid>
                
    <!-- ... -->
</ContentPage>

While it is possible to use the SelectionChangedCommand on the CollectionView, it is actually recommended to use a TapGestureRecognizer to handle CollectionView item tap events. SelectionChangedCommand is best used for handling and taking action on a selection of multiple items, for example deleting multiple items at once.

The BindingContext for the individual items within a CollectionView, as well as other controls that contain a templated list of items, is the item itself – not the BindingContext of the parent ContentPage (the page's ViewModel). Therefore, the TapGestureRecognizer we added uses a RelativeSource within its Command Binding to access the page's ViewModel and use it as the source for the Binding. In order to reference the MainViewModel type within the XAML binding we created a xmlns for the ViewModels namespace in the ContentPage element. The ViewCommand we are binding to takes a TripLogEntry parameter, so we used the CommandParameter to pass the item itself by simply calling {Binding}.

Initializing MainViewModel

The XamarinFormsNavService custom navigation service we created handles initializing ViewModels automatically when they are navigated to by calling the Init() method in BaseViewModel. However, because the main page is launched by default and not via navigation, we will need to manually call the Init() method on the page's ViewModel when the page first appears.

Update MainPage by overriding its OnAppearing() method to call its ViewModel's Init() method:

public partial class MainPage : ContentPage
{
    MainViewModel ViewModel => BindingContext as MainViewModel;
    // ...
    protected override void OnAppearing()
    {
        base.OnAppearing();
        // Initialize MainViewModel
        ViewModel?.Init();
    }
}

Now, when the app is launched and the main page is loaded, the ViewModel will be initialized and load all of the trip log entries.

Updating NewEntryViewModel

In Chapter 2, MVVM and Data Binding, we added SaveCommand to NewEntryViewModel, but once the SaveCommand executed, nothing occurred.

Once SaveCommand performs its logic to save the new log entry, it should navigate the user back to the previous page. We can accomplish this by updating the execute Action of SaveCommand to call the GoBack() method in the navigation service that we created in the last section:

public class NewEntryViewModel : BaseValidationViewModel
{
    // ...
    Command _saveCommand; 
    public Command SaveCommand => 
        _saveCommand ?? (_saveCommand = new Command(async () => await Save(), CanSave));
    // ...
    async Task Save()
    {
        var newItem = new TripLogEntry
        {
            Title = Title, 
            Latitude = Latitude, 
            Longitude = Longitude, 
            Date = Date,
            Rating = Rating, 
            Notes = Notes
        };
        // TODO: Persist Entry in a later chapter.
        await NavService.GoBack();
    }
    // ..
}

Notice that because the Save() method now calls an asynchronous method, it needs to use async and await, and its return type needs to be updated from void to Task.

Updating DetailPage

Finally, we need to update how the map on DetailPage is being bound to the data in the DetailViewModel. Since the ViewModel is being initialized via the navigation service now, it happens after the page is constructed, and therefore the map doesn't have the data it needs. Normally, this would not be a problem thanks to data binding; however, since the map control does not allow for data binding, we will need to handle its data differently. The best way for the page to check when its ViewModel has data for its map control is to handle the ViewModel's PropertyChanged event. If the ViewModel's Entry property changes, the map control should be updated accordingly, as shown in the following steps:

  1. First, move the two statements that plot and center the coordinates on the map control out of the constructor, and into a separate private method named UpdateMap in the DetailPage class:
    public partial class DetailPage : ContentPage
    {
        // ...
        public DetailPage()
        {
            InitializeComponent();
            BindingContext = new DetailViewModel(DependencyService.Get<INavService>());
        }
        void UpdateMap()
        {
            if (ViewModel.Entry == null)
            {
                return;
            }
            // Center the map around the log entry's location 
            map.MoveToRegion(MapSpan.FromCenterAndRadius(new Position(ViewModel.Entry.Latitude, ViewModel.Entry.Longitude), Distance.FromMiles(.5)));
            // Place a pin on the map for the log entry's location
            map.Pins.Add(new Pin
            {
                Type = PinType.Place,
                Label = ViewModel.Entry.Title,
                Position = new Position(ViewModel.Entry.Latitude, ViewModel.Entry.Longitude)
            });
        }
    }
    
  1. Next, handle the ViewModel's PropertyChanged event to update the map when the ViewModel's Entry property is changed:
    public partial class DetailPage : ContentPage
    {
        // ...
        protected override void OnAppearing()
        {
            base.OnAppearing();
            if (ViewModel != null)
            {
                ViewModel.PropertyChanged += OnViewModelPropertyChanged;
            }
        }
        protected override void OnDisappearing()
        {
            base.OnDisappearing();
            if (ViewModel != null)
            {
                ViewModel.PropertyChanged -= OnViewModelPropertyChanged;
            }
        }
        void OnViewModelPropertyChanged(object sender, PropertyChangedEventArgs args)
        {
            if (args.PropertyName == nameof(DetailViewModel.Entry))
            {
                UpdateMap();
            }
        }
        void UpdateMap()
        {
            // ...
        }
    }
    

Now that we have refactored the Views and ViewModels throughout the app's code base to use our navigation service, all page navigation can now be initiated from the ViewModels. When we run the app now, everything should look and behave just as it did at the end of the last chapter.

Summary

In this chapter, we created a service that extends the default Xamarin.Forms navigation API to enable a ViewModel-centric navigation. Even though this change does not result in a visible change to the app's appearance or functionality, it helps enforce a better separation between the presentation layer and the business logic in ViewModels. In Chapter 4, Platform-Specific Services and Dependency Injection, we will create some additional services that abstract platform-specific APIs and replace the Xamarin.Forms DependencyService with a more flexible IoC and dependency injection alternative.

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

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