Creating a navigation service

In a typical cross-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're simply going to implement a single navigation service that extends the Xamarin.Forms navigation abstraction so 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 are starting with an interface so the service can be added to ViewModels via constructor injection, which we'll dive into in Chapter 4, Platform Specific Services and Dependency Injection. We are also starting with an interface so 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 and within it create a new interface named INavService:
    public interface INavService
    {
        bool CanGoBack { get; }
    
        Task GoBack ();
    
        Task NavigateTo<TVM> ()
            where TVM : BaseViewModel;
    
        Task NavigateTo<TVM, TParameter> (TParameter parameter)
            where TVM : BaseViewModel;
    
        Task RemoveLastView();
    
        Task ClearBackStack();
    
        Task NavigateToUri(Uri uri);
    
        event PropertyChangedEventHandler CanGoBackChanged;
    }

This interface defines fairly standard navigation behavior – the ability to navigate to ViewModels, the ability to navigate back, the ability to clear the navigation stack, as well as the ability to 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 need to make a couple updates to our BaseViewModel:

  1. Update the BaseViewModel to include an abstract Init method:
    public abstract Task Init();

    This Init method provides ViewModels with a way of initializing without using the constructor, which is useful when a ViewModel needs to be refreshed.

  2. Next, add a second BaseViewModel abstract base class with a generic-type that will be used to pass strongly typed parameters to the Init method:
    public abstract class BaseViewModel<TParameter>
        : BaseViewModel
    {
        protected BaseViewModel () : base ()
        { }
    
        public override async Task Init()
        {
            await Init (default(TParameter));
        }
    
        public abstract Task Init (TParameter parameter);
    }
  3. Next, update MainViewModel and NewEntryViewModel to override the Init method; for now, the NewEntryViewModel Init will just be a blank implementation.

    The Init method in MainViewModel will be responsible for loading the log entries. We will move the log entry list population logic out of the constructor and into a new async method called LoadEntries, which will be called from the Init override:

    public MainViewModel () : base ()
    {
        LogEntries = new ObservableCollection<TripLogEntry> ();
    }
    
    public override async Task Init ()
    {
        await LoadEntries ();
    }
    
    async Task LoadEntries()
    {
        LogEntries.Clear ();
    
        await Task.Factory.StartNew (() => {
            LogEntries.Add (new TripLogEntry {
                Title = "Washington Monument",
                Notes = "Amazing!",
                Rating = 3,
                Date = new DateTime(2015, 2, 5),
                Latitude = 38.8895,
                Longitude = -77.0352
            });
            LogEntries.Add (new TripLogEntry {
                Title = "Statue of Liberty",
                Notes = "Inspiring!",
                Rating = 4,
                Date = new DateTime(2015, 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(2015, 4, 26),
                Latitude = 37.8268,
                Longitude = -122.4798
            });
        });
    }
    
  4. 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:
    public class DetailViewModel : BaseViewModel<TripLogEntry>
    {
        // ...
    
        public DetailViewModel ()
        {
    
        }
    
        public override async Task Init (TripLogEntry logEntry)
        {
           Entry = logEntry;
        }
    }

    Notice that because we are now setting the Entry property within the Init method, we can remove the TripLogEntry parameter from the constructor.

  5. We also need to remove the TripLogEntry parameter from the DetailPage constructor as it will now be all handled between the navigation service and the ViewModel's Init method:
    public class DetailPage : ContentPage
    {
        public DetailPage () // <- Remove parameter
        {
            // ...
        }
    
        // ...
    }

    Now that BaseViewModel has been updated, we can create our navigation service that implements INavService.

  6. Create a new class within the Services folder of the core library. Name the new class XamarinFormsNavService and make it implement INavService:
    public class XamarinFormsNavService : INavService
    {
    }
  7. Update the XamarinFormsNavService 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 section when we update the TripLog app:
    public class XamarinFormsNavService : INavService
    {
        public INavigation XamarinFormsNav { get; set; }
    
        // 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.

  8. Update the XamarinFormsNavService with an IDictionary<Type, Type> readonly property and add a public method to populate it named RegisterViewMapping:
    readonly IDictionary<Type, Type> _map = new Dictionary<Type, Type>();
    
    public void RegisterViewMapping (Type viewModel, Type view)
    {
        _map.Add (viewModel, view);
    }
  9. Next, implement the INavService methods. Most of the INavService methods will leverage the XamarinFormNav property to make calls to the Xamarin.Forms navigation API in order to perform navigation and alter the navigation stack:
    public bool CanGoBack {
        get {
            return 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)
            await ((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>)
            await ((BaseViewModel<TParameter>)(XamarinFormsNav
             .NavigationStack.Last ().BindingContext))
             .Init (parameter);
    }
    
    async Task NavigateToView(Type viewModelType)
    {
        Type viewType;
    
        if (!_map.TryGetValue (viewModelType, out viewType))
            throw new ArgumentException ("No view found in View Mapping for " + viewModelType.FullName + ".");
    
        var constructor = viewType.GetTypeInfo ()
    .DeclaredConstructors
    .FirstOrDefault(dc => dc.GetParameters ().Count() <= 0);
        var view = constructor.Invoke (null) as Page;
    
        await XamarinFormsNav.PushAsync (view, true);
    }
    
    public async Task RemoveLastView ()
    {
        if (XamarinFormsNav.NavigationStack.Any())
        {
                    var lastView = XamarinFormsNav.NavigationStack [XamarinFormsNav.NavigationStack.Count - 2];
            XamarinFormsNav.RemovePage(lastView);
        }
    }
    
    public async Task ClearBackStack ()
    {
        if (XamarinFormsNav.NavigationStack.Count <= 1)
            return;
    
    for (var i = 0; i < XamarinFormsNav.NavigationStack.Count - 1; i++)
         XamarinFormsNav.RemovePage
           (XamarinFormsNav.NavigationStack [i]);
    }
    
    public async Task NavigateToUri (Uri uri)
    {
        if (uri == null)
            throw new ArgumentException("Invalid URI");
    
        Device.OpenUri(uri);
    }
    
    public event PropertyChangedEventHandler CanGoBackChanged;
    
    void OnCanGoBackChanged()
    {
        var handler = CanGoBackChanged;
        if (handler != null)
            handler(this, new
                 PropertyChangedEventArgs("CanGoBack"));
    }
  10. 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 block:
    [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 IoC and dependency injection library.

Updating the TripLog app

With the navigation service complete, 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 to create a new instance of the navigation service and register the app's page-to-ViewModel mappings:

public App ()
{
    // The root page of your application
    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 it in the BaseViewModel class:

public abstract class BaseViewModel : INotifyPropertyChanged
{
     protected INavService NavService { get; private set; }

     protected BaseViewModel (INavService navService)
     {
         NavService = navService;
     }

    // ...
}

Each of the ViewModels that inherit from BaseViewModel will need to be updated to include an INavService parameter in their constructors that is passed to the BaseViewModel class. The BaseViewModel<TParameter> base class needs to be updated to include an INavService constructor parameter as well.

In addition, each ViewModel initialization 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 (TripLogEntry entry)
    {
         BindingContext = new DetailViewModel (
            DependencyService.Get<INavService> ());
    
        // ...
    }
  3. Update the NewEntryViewModel instantiation in the NewEntryPage constructor:
    public NewEntryPage ()
    {
         BindingContext = new NewEntryViewModel (
            DependencyService.Get<INavService> ());
    
        // ...
    }

Updating MainViewModel

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

Command<TripLogEntry> _viewCommand;
public Command<TripLogEntry> ViewCommand {
    get { return _viewCommand
?? (_viewCommand = new Command<TripLogEntry> (async (entry) => await ExecuteViewCommand(entry))); }
}

Command _newCommand;
public Command NewCommand {
    get { return _newCommand
?? (_newCommand = new Command (async () => await ExecuteNewCommand ())); }
}

async Task ExecuteViewCommand(TripLogEntry entry)
{
    await NavService.NavigateTo<DetailViewModel, TripLogEntry> (entry);
}
async Task ExecuteNewCommand()
{
    await NavService.NavigateTo<NewEntryViewModel> ();
}

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

  1. Create a private MainViewModel property named _vm in the MainPage class that simply provides access to the page's BindingContext, but casted as a MainViewModel type:
    public class MainPage : ContentPage
    {
        MainViewModel _vm {
            get { return BindingContext as MainViewModel; }
        }
    
        // ...
    }
  2. Update the ItemTapped event on the entries ListView to call the ViewCommand:
    entries.ItemTapped += async (sender, e) => {
         var item = (TripLogEntry)e.Item;
        _vm.ViewCommand.Execute(item);
    };
  3. Replace the Clicked event on the newButton ToolbarItem with a Binding to the NewCommand:
    newButton.SetBinding (ToolbarItem.CommandProperty, "NewCommand");

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 need to manually call the Init method on the page's ViewModel when the page first appears.

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

protected override async void OnAppearing ()
{
    base.OnAppearing ();

    // Initialize MainViewModel
    if (_vm != null)
        await _vm.Init();
}

Updating NewEntryViewModel

In Chapter 2, MVVM and DataBinding, we already added SaveCommand to NewEntryViewModel; however, 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 SaveCommand's execute Action to call the GoBack method in the navigation service that we created in the last section:

async Task ExecuteSaveCommand()
{
    var newItem = new TripLogEntry {
            Title = this.Title,
            Latitude = this.Latitude,
            Longitude = this.Longitude,
            Date = this.Date,
            Rating = this.Rating,
            Notes = this.Notes
    };

    // TODO: Implement logic to persist Entry in a later chapter.

    await NavService.GoBack ();
}

Notice that because the ExecuteSaveCommand 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. Because 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, but since the map control does not allow for data binding we 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, update the map to a private class property named _map:
    public class DetailPage : ContentPage
    {
        // ...
    
        readonly Map _map;
    
        public DetailPage ()
        {
            // ...
    
            // var map = new Map ();
            _map = new Map ();
    
            // ...
    
            mainLayout.Children.Add (_map);
            mainLayout.Children.Add (detailsBg, 0, 1);
            mainLayout.Children.Add (details, 0, 1);
    
            Grid.SetRowSpan (_map, 3);
    
            Content = mainLayout;
        }
    }
  2. Next, move the two statements that center and the plot points on the map control out of the constructor and into a separate private method in the DetailPage class:
    void UpdateMap ()
     {
         if (_vm.Entry == null)
             return;
    
         // Center the map around the log entry's location
         _map.MoveToRegion (MapSpan.FromCenterAndRadius (
            new Position (_vm.Entry.Latitude,
                _vm.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 = _vm.Entry.Title,
             Position = new Position (_vm.Entry.Latitude,
                _vm.Entry.Longitude)
         });
     }
  3. Finally, handle the ViewModel's PropertyChanged event to update the map when the ViewModel's Entry property is changed:
    public DetailPage ()
    {
         BindingContextChanged += (sender, args) =>
         {
             if (_vm == null) return;
    
             _vm.PropertyChanged += (s, e) => {
                 if (e.PropertyName == "Entry")
                     UpdateMap ();
             };
         };
    
        // ...
    }
..................Content has been hidden....................

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