In this section, we will begin by setting up the basic structure for our TrackMyWalks
solution to include the folder that will be used to represent our Services
. Let's take a look at how we can achieve this, by performing following the steps:
TrackMyWalks
solution is loaded within the Xamarin Studio IDE.TrackMyWalks
Portable Class Library project, called Services
, as shown in the following screenshot:
Now that we have created the folder structure that will be used to store our navigation services, we can begin to start building the Navigation Service Interface
class that will be used by our Navigation Service
class and in turn used by our ViewModels.
In this section, we will begin by creating a navigation service interface class that will extend from the Xamarin.Forms
navigation abstraction layer. This is so that we can perform ViewModel to ViewModel navigation within our MVVM design pattern, and in turn bind with our content pages to allow navigation between these Views to happen.
We will first need to define the interface for our navigation service, as this will contain and define its methods, and will make it a lot easier if we ever wanted to add new method implementations for our service, without the need to change each of our ViewModels.
Let's take a look at how we can achieve this, by performing the following steps:
Services
folder, as shown in the following screenshot:
IWalkNavService
for the name of the new interface file to create, as shown in the following screenshot:
Up until this point, all we have done is create our IWalkNavService
class file. This Interface class will be used and will act as the base NavigationService
class that each of our ViewModels will inherit from. As we start to build the Navigation Service Interface
class, you will see that it contains a couple of class members that will be used by our content pages and ViewModels, as we will be using this as our base class within our ViewModels used by the TrackMyWalks
application.
To proceed with creating the base IWalkNavService
interface, ensure that the IWalkNavService.cs
file is displayed within the code editor, and enter in the following code snippet:
// // IWalkNavService.cs // TrackMyWalks Navigation Service Interface // // Created by Steven F. Daniel on 03/09/2016. // Copyright © 2016 GENIESOFT STUDIOS. All rights reserved. // using System.Threading.Tasks; using TrackMyWalks.ViewModels; namespace TrackMyWalks.Services { public interface IWalkNavService { // Navigate back to the Previous page in the NavigationStack Task PreviousPage(); // Navigate to the first page within the NavigationStack Task BackToMainPage(); // Navigate to a particular ViewModel within our MVVM Model, // and pass a parameter Task NavigateToViewModel<ViewModel, TParameter>(TParameter parameter) where ViewModel : WalkBaseViewModel; } }
In the preceding code snippet, we started by creating a new Interface class for our IWalkNavService
that allows the ability to navigate to each of our ViewModels, as well as navigating back to the PreviousPage
method, and back to the first page within our hierarchical model, as determined by the BackToMainPage
method.
The NavigateToViewModel
method declares a generic type which is used to restrict the ViewModel to its use to objects of the WalkBaseViewModel
base class, and a strongly-typed TParameter
parameter to be passed along with the navigation.
The term strongly-typed means that, if a variable has been declared of a specific type (string, integer, or defined as a user-defined type), it cannot be assigned a value of a different type later on, as this will result in the compiler notifying you of an error. An example would be: int i = 10; i = "Ten"
The Task
class is essentially used to handle asynchronous operations. This is done by ensuring that the asynchronous method you initiated will eventually finish, thus completing the task. The Task
object is used to return back information once it has finished by returning back a Task
object almost instantaneously, although the underlying work within the method would likely finish later.
To handle this, you can use the await
keyword to wait for the task to complete which will block the current thread and wait until the asynchronous method has completed.
In the previous section, we created our base interface class for our navigation service and defined a number of different methods, which will be used to navigate within our MVVM ViewModel.
These will be used by each of our ViewModels, and the Views (pages) will implement these ViewModels and use them as their BindingContext
.
Let's take a look at how we can achieve this, by performing the following the steps:
Services
folder, as shown in the next screenshot.WalkNavService
for the name of the new class file to create, as shown in the following screenshot:
WalkNavService
class file. This class will be used and will act as the base NavigationService
class that will contain the functionality required that each of our ViewModels will inherit from, in order to navigate between each of the ViewModels within our MVVM model.Navigation
class, you will see that it contains a number of method members that will be used to enable navigation between each of our ViewModels and it will implement the IWalkNavService
Interface. To proceed with creating the base WalkNavService
class, perform the following steps:WalkNavService.cs
file is displayed within the code editor, and enter in the following code snippet: //
// WalkNavService.cs
// TrackMyWalks Navigation Service Class
//
// Created by Steven F. Daniel on 03/09/2016.
// Copyright © 2016 GENIESOFT STUDIOS. All rights reserved.
//
using System;
using Xamarin.Forms;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Reflection;
using System.Linq;
using TrackMyWalks.ViewModels;
using TrackMyWalks.Services;
First, we need to initialize our navigation class to be marked as a dependency, by adding the Dependency
metadata attribute so that it can be resolved by the Xamarin.FormsDependencyService
class. This will enable so that it to find and use our method implementation as defined by our interface.
WalkNavService
class inherits from the IWalkNavService
navigation interface class, so that it can access the method getters and setters. Proceed and enter in the following code snippet as shown here:[assembly: Dependency(typeof(WalkNavService))] namespace TrackMyWalks.Services { public class WalkNavService : IWalkNavService {
INavigation
property named navigation
. The navigation
property will provide our class with a reference to the current Xamarin.Forms.INavigation
instance, and this will need to be set when the navigation service is first initialized. We will see how this is done as we progress through updating our TrackMyWalks
app. Proceed and enter in the following code snippet:public INavigation navigation { get; set; }
_viewMapping
property variable that inherits from the IDictionary
interface. Proceed and enter in the following code snippet:readonly IDictionary<Type, Type> _viewMapping = new Dictionary<Type, Type>();
RegisterViewMapping
which will be used to populate our ViewModel and ContentPage
(View) within the _viewMapping
dictionary property object. Proceed and enter in the following code sections:// Register our ViewModel and View within our Dictionary public void RegisterViewMapping(Type viewModel, Type view) { _viewMapping.Add(viewModel, view); }
PreviousPage
instance method for our WalkNavService
class. This will be used to navigate back to the previous page contained within our NavigationStack
, by first checking the NavigationStack
property of the navigation
property INavigation
interface to ensure that it is not null and that we have more than one ViewModel contained within our NavigationStack
to navigate back to. If we don't perform this check, it could result in our application crashing. Finally, we use the PopAsync
method to remove the last view added to our NavigationStack
, thus returning back to the previous ViewModel. Proceed and enter in the following code sections:// Instance method that allows us to move back to the // previous page. public async Task PreviousPage() { // Check to see if we can move back to the previous page if (navigation.NavigationStack != null && navigation.NavigationStack.Count > 0) { await navigation.PopAsync(true); } }
BackToMainPage
instance method for our WalkNavService
class. This will be used to take us back to the first ContentPage
contained within our NavigationStack
. We use the PopToRootAsync
method of our navigation
property and use the await
operator to wait until the task completes, before removing all ViewModels contained within our NavigationStack
and returning back a Task
object. Proceed and enter the following code:// Instance method that takes us back to the main // Root WalksPage public async Task BackToMainPage() { await navigation.PopToRootAsync(true); }
NavigateToViewModel
instance method for our WalkNavService
class. This will be used to navigate to a specific ViewModel that is contained within our _viewMapping
dictionary object. Next, we use the TryGetValue
method of the _viewMapping
dictionary object to check to see if our ViewModel does indeed exist within our dictionary, and return the name of the ViewModel.viewType
object, and we then use the PushAsync
method to navigate to that view. Finally, we set the BindingContext
for the last pushed view that is contained within our NavigationStack
, and then navigate to the view, passing in any parameters required. Proceed and enter in the following code sections:// Instance method that navigates to a specific ViewModel // within our dictionary viewMapping public async Task NavigateToViewModel<ViewModel, WalkParam>(WalkParam parameter) where ViewModel : WalkBaseViewModel { Type viewType; if (_viewMapping.TryGetValue(typeof(ViewModel), out viewType)) { var constructor = viewType.GetTypeInfo() .DeclaredConstructors .FirstOrDefault(dc => dc.GetParameters() .Count() <= 0); var view = constructor.Invoke(null) as Page; await navigation.PushAsync(view, true); } if (navigation.NavigationStack.Last().BindingContext is WalkBaseViewModel<WalkParam>) await ((WalkBaseViewModel<WalkParam>)( navigation.NavigationStack.Last().BindingContext)). Init(parameter); } } }
In the preceding code snippet, we began by ensuring that our WalkNavService
class inherits from our IWalkNavService
class, and then moved on to create a navigation
property that inherits from our INavigation
class. We then created its associated getter and setter qualifiers.
Finally, we created the instance methods required for our WalkNavService
class.
In this section we will proceed to update our WalkBaseViewModel
class to include references to our IWalkNavService
. Since our WalkBaseViewModel
inherits and is used by each of our ViewModels
, it makes sense to place it within this class. That way, if we need to add additional methods, we can just add them within this class. To proceed, perform the following steps:
WalkBaseViewModel.cs
file is displayed within the code editor, and enter in the following code snippet:// // WalkBaseViewModel.cs // TrackMyWalks Base ViewModel // // Created by Steven F. Daniel on 22/08/2016. // Copyright © 2016 GENIESOFT STUDIOS. All rights reserved. // using System.ComponentModel; using System.Runtime.CompilerServices; using System.Threading.Tasks; using TrackMyWalks.Services; namespace TrackMyWalks.ViewModels { public abstract class WalkBaseViewModel : INotifyPropertyChanged {
IWalkNavService
property named NavService
. The NavService
property will provide our class with a reference to the current navigation
instance that is contained within our IWalkNavService
interface class. Proceed and enter in the following code section:protected IWalkNavService NavService { get; private set; }
navService
parameter that inherits from our IWalkNavService
interface class. Next, we set the NavService
property for our WalkBaseViewModel
base class, to an instance of the navService
parameter. Proceed and enter in the following highlighted code sections:protected WalkBaseViewModel(IWalkNavService navService) { NavService = navService; }
Init
abstract method for our WalkBaseViewModel
class that returns back an asynchronous Task
object. This will be used to initialize our WalkBaseViewModel
. Proceed and enter in the following highlighted code sections: public abstract Task Init();
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(
[CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
WalkBaseViewModel
that inherits from the WalkBaseViewModel
and defines a generic-typed TParameter
object. We then proceed to overload the WalkBaseViewModel
class constructor, and set this class to inherit from our navService
base class. Proceed and enter in the following highlighted code sections:public abstract class WalkBaseViewModel<WalkParam> : WalkBaseViewModel { protected WalkBaseViewModel(IWalkNavService navService) : base(navService) { }
Init
method for our WalkBaseViewModel<WalkParam>
that accepts a default WalkParam
value for our walkDetails
model. Proceed and enter in the following highlighted code sections:public override async Task Init() { await Init(default(WalkParam)); } public abstract Task Init(WalkParam walkDetails); } }
In the preceding code snippet, we began by creating a NavService
property that inherits from our IWalkNavService
class, and then created its associated getter and setter qualifiers.
Next, we update the WalkBaseViewModel
class constructor to set the NavService
property to an instance of our navService
, before creating our Init
abstraction method that will be used to initialize our class.
In the next step, we create a new abstract class for our WalkBaseViewModel
that implements from the WalkBaseViewModel
class, and then overloads our class constructor so that it inherits from our navService
class.
Next, we'll override the Init
method for our WalkBaseViewModel<WalkParam>
that accepts a default WalkParam
value for our walkDetails
model.
We have created our IWalkNavService
Interface class and updated the NavService
class to include all of the necessary class instance methods. We also made some changes to our WalkBaseViewModel
class to inherit from our IWalkNavService
navigation service. We have also included an additional abstraction class that will be used to initialize our WalkBaseViewModel
when navigating between ViewModels
within our MVVM model.
Our next step is to modify the walks main page. In this section, we will be taking a look at how to update our WalksPageViewModel
so that it can take advantage of our navigation service.
Let's take a look at how we can achieve this, by performing the following steps:
WalksPageViewModel.cs
file is displayed within the code editor, and enter in the following highlighted code sections:// // WalksPageViewModel.cs // TrackMyWalks ViewModels // // Created by Steven F. Daniel on 22/08/2016. // Copyright © 2016 GENIESOFT STUDIOS. All rights reserved. // using System; using System.Collections.ObjectModel; using System.Threading.Tasks; using TrackMyWalks.Models; using TrackMyWalks.Services; using Xamarin.Forms; namespace TrackMyWalks.ViewModels { public class WalksPageViewModel : WalkBaseViewModel { ObservableCollection<WalkEntries> _walkEntries; public ObservableCollection<WalkEntries> walkEntries { get { return _walkEntries; } set { _walkEntries = value; OnPropertyChanged(); } }
WalksPageViewModel
class constructor, which will need to include a parameter navService
that is included within our IWalkNavService
interface class. Then we need to set the ViewModel's class constructor to access all instance class members contained within the navService
within the WalksPageViewModel
by using the base
keyword. We then set up an ObservableCollection
called walkEntries
. This accepts a list parameter containing our WalkEntries
model, which will be used to determine whenever the collection has changed within the WalkEntries
model.Init
method within our WalksPageView
model, and a method called LoadWalkDetails
to populate the WalkDetails
. Proceed and enter in the following highlighted code sections:public WalksPageViewModel(IWalkNavService navService) : base(navService) { walkEntries = new ObservableCollection<WalkEntries>(); } public override async Task Init() { await LoadWalkDetails(); }
async
method called LoadWalkDetails
that will be used to add each of the walk entries within our model. We use the Task.Factory.StartNew
to start and execute our task, and then proceed to populate each of our lists of WalkEntries
asynchronously. We then use and specify the await
keyword to wait until our Task
completes. Proceed and enter in the following highlighted code sections:public async Task LoadWalkDetails() { await Task.Factory.StartNew(() => { walkEntries = new ObservableCollection<WalkEntries>() { new WalkEntries { Title = "10 Mile Brook Trail, Margaret River", Notes = "The 10 Mile Brook Trail starts in the Rotary Park near Old Kate, a preserved steam " + "engine at the northern edge of Margaret River. ", Latitude = -33.9727604, Longitude = 115.0861599, Kilometers = 7.5, Distance = 0, Difficulty = "Medium", ImageUrl = "http://trailswa.com.au/media/cache/media/images/ trails/_mid/" + "FullSizeRender1_600_480_c1.jpg" }, new WalkEntries { Title = "Ancient Empire Walk, Valley of the Giants", Notes = "The Ancient Empire is a 450 metre walk trail that takes you around and through some of " + "the giant tingle trees including the most popular of the gnarled veterans, known as " + "Grandma Tingle.", Latitude = -34.9749188, Longitude = 117.3560796, Kilometers = 450, Distance = 0, Difficulty = "Hard", ImageUrl ="http://trailswa.com.au/media/cache /media/images/trails/_mid/" + "Ancient_Empire_534_480_c1.jpg" }, }; }); }
Command
property for our class. This will be used within our WalksPage
and will be used to bind to the Add WalkToolBarItem
. The Command
property will run an action upon being pressed, and then execute a class instance method, to determine whether the command can be executed. Proceed and enter in the following highlighted code sections:Command _createNewWalk; public Command CreateNewWalk { get { return _createNewWalk ?? (_createNewWalk = new Command(async () => await NavService.NavigateToViewModel<WalkEntryViewMod el, WalkEntries>(null))); } }
Command
property to our class. This will be used within the WalksPage
and will be used to handle clicks on a walk item within the ListView
. The Command
property will run an action upon being pressed, and then execute a class instance method to determine whether the command can be executed or not, prior to navigating to the WalksTrailViewModel
and passing in the trailDetails
for the chosen walk within the ListView
. Proceed and enter in the following highlighted code sections:Command<WalkEntries> _trailDetails; public Command<WalkEntries> WalkTrailDetails { get { return _trailDetails ?? (_trailDetails = new Command<WalkEntries>(async (trailDetails) => await NavService.NavigateToViewModel <WalksTrailVi ewModel, WalkEntries>(trailDetails))); } } } }
Now that we have modified our WalksPageViewModel
to include the navigation service class, which will be used by our main WalksPage
, our next step is to modify our walks main page so that it points to a reference of our WalksPageViewModel
, and ensures that all of the necessary Command
bindings and BindingContexts
have been set up correctly.
Now that we have modified our MVVM ViewModel to take advantage of the navigation service, we need to modify our walks main page to bind the WalksPageBindingContext
to the WalksPageViewModel
so that the walk entry details can be displayed and all of the navigational aspects are working as expected.
Let's take a look at how we can achieve this, by performing the following steps:
WalksPage.cs
file is displayed within the code editor, and enter in the following highlighted code sections: //
// WalksPage.cs
// TrackMyWalks
//
// Created by Steven F. Daniel on 04/08/2016.
// Copyright © 2016 GENIESOFT STUDIOS. All rights reserved.
//
using System.Collections.Generic;
using Xamarin.Forms;
using TrackMyWalks.Models;
using TrackMyWalks.ViewModels;
using TrackMyWalks.Services;
namespace TrackMyWalks
{
public class WalksPage : ContentPage
{
_viewModel
within our WalksPage
class. This is of the WalksPageViewModel
type, and will essentially provide us with access to the ContentPage's BindingContext
object. Proceed and enter in the following highlighted code sections:WalksPageViewModel _viewModel { get { return BindingContext as WalksPageViewModel; } } public WalksPage() { var newWalkItem = new ToolbarItem { Text = "Add Walk" };
Binding
to the Command
property that we defined within our WalksPageViewModel
class. This will be called when the user chooses the Add Walk button. Proceed and enter in the following highlighted code sections:// Set up our Binding click event handler newWalkItem.SetBinding(ToolbarItem.CommandProperty, "CreateNewWalk"); // Add the ToolBar item to our ToolBar ToolbarItems.Add(newWalkItem);
WalksPageViewModelBindingContext
to include our IWalkNavService
constructor, which is used by the WalkBaseViewModel
class, and is retrieved from the Xamarin.Forms DependencyService
class. Proceed and enter in the following highlighted code sections:// Declare and initialize our Model Binding Context BindingContext = new WalksPageViewModel(DependencyService.Get <IWalkNavS ervice>()); // Define our Item Template var itemTemplate = new DataTemplate(typeof(ImageCell)); itemTemplate.SetBinding(TextCell.TextProperty, "Title"); itemTemplate.SetBinding(TextCell.DetailProperty, "Notes"); itemTemplate.SetBinding(ImageCell.ImageSourceProperty, "ImageUrl"); var walksList = new ListView { HasUnevenRows = true, ItemTemplate = itemTemplate, SeparatorColor = Color.FromHex("#ddd"), }; // Set the Binding property for our walks Entries walksList.SetBinding(ItemsView<Cell>.ItemsSourceProperty, "walkEntries");
ListView
. We need to make a call to the WalksTrailDetails
command that is included within our WalksPageViewModel
class, so that it can navigate to the WalksTrailViewModel
, whilst passing in the chosen item from within the ListView
. Proceed and enter the following highlighted code sections: // Initialize our event Handler to use when
the item is tapped
walksList.ItemTapped += (object sender,
ItemTappedEventArgs e) =>
{
var item = (WalkEntries)e.Item;
if (item == null) return;
_viewModel.WalkTrailDetails.Execute(item);
item = null;
};
Content = walksList;
}
OnAppearing
instance method of the navigation hierarchy that will be used to display our WalksEntries
prior to the ViewModel appearing on screen.null
, prior to calling the Init
method of our WalksPageViewModel
. Proceed and enter the following highlighted code sections:protected override async void OnAppearing() { base.OnAppearing(); // Initialize our WalksPageViewModel if (_viewModel != null) await _viewModel.Init(); } } }
In this section, we looked at the steps involved in modifying the WalksPage
so that it can take advantage of our updated WalksPageViewModel
. We looked at how to set the content page to an instance of the WalksPageViewModel
so that it knows where to get the list of walk entries. The list will be used and displayed within the ListView
control, and will then update the BindingContext
property for the WalksPage
to point to an instance of the IWalkNavService
interface. As you can see, by using a navigation service within your ViewModels, it makes navigating between each of the ViewModels quite easy.
Now that we have modified the MVVM ViewModel that will be used for the main WalksPage
, our next step is to begin modifying the WalkEntryViewModel
to take advantage of the navigation service, which will be used to create new walk entries, and save this information back to the WalkBaseViewModel
. This will be covered in a later chapter as we progress throughout this book.
Let's take a look at how we can achieve this, by performing the following steps:
WalkEntryViewModel.cs
file is displayed within the code editor, and enter in the following highlighted code sections:// // WalkEntryViewModel.cs // TrackMyWalks ViewModels // // Created by Steven F. Daniel on 22/08/2016. // Copyright © 2016 GENIESOFT STUDIOS. All rights reserved. // using System; using System.Diagnostics.Contracts; using System.Threading.Tasks; using TrackMyWalks.Models; using TrackMyWalks.Services; using TrackMyWalks.ViewModels; using Xamarin.Forms; namespace TrackMyWalks.ViewModels { public class WalkEntryViewModel : WalkBaseViewModel { string _title; public string Title { get { return _title; } set { _title = value; OnPropertyChanged(); SaveCommand.ChangeCanExecute(); } } string _notes; public string Notes { get { return _notes; } set { _notes = value; OnPropertyChanged(); } } double _latitude; public double Latitude { get { return _latitude; } set { _latitude = value; OnPropertyChanged(); } } double _longitude; public double Longitude { get { return _longitude; } set { _longitude = value; OnPropertyChanged(); } } double _kilometers; public double Kilometers { get { return _kilometers; } set { _kilometers = value; OnPropertyChanged(); } } string _difficulty; public string Difficulty { get { return _difficulty; } set { _difficulty = value; OnPropertyChanged(); } } double _distance; public double Distance { get { return _distance; } set { _distance = value; OnPropertyChanged(); } } string _imageUrl; public string ImageUrl { get { return _imageUrl; } set { _imageUrl = value; OnPropertyChanged(); } }
WalksEntryViewModel
class constructor which will now need to include a parameter navService
that is included within the IWalkNavService
interface class. Then we'll set the ViewModel's class constructor to access all instance class members contained within the navService
within the WalksEntryViewModel
, by using the base
keyword. Next, we'll initialize the constructor with default values for our Title
, Difficulty
and Distance
properties.WalkEntryViewModel
class constructor, and enter the following highlighted code sections:public WalkEntryViewModel(IWalkNavService navService) : base(navService) { Title = "New Walk"; Difficulty = "Easy"; Distance = 1.0; }
SaveCommand
command property to include the async
and await
keywords. This command property will be used to bind to the Save
ToolBarItem
and will run an action upon being pressed. It will then execute a class instance method to determine whether the command can be executed. Proceed and enter in the following highlighted code sections:Command _saveCommand; public Command SaveCommand { get { return _saveCommand ?? (_saveCommand = new Command(async () => await ExecuteSaveCommand(), ValidateFormDetails)); } }
ExecuteSaveCommand
instance method to include the async Task
keywords to the method definition, and then include a reference to our PreviousPage
method that is defined within our IWalkNavService
interface to allow our WalkEntryPage
to be dismissed upon the user clicking on the Save button. Proceed and enter in the following highlighted code sections: async Task ExecuteSaveCommand()
{
var newWalkItem = new WalkEntries
{
Title = this.Title,
Notes = this.Notes,
Latitude = this.Latitude,
Longitude = this.Longitude,
Kilometers = this.Kilometers,
Difficulty = this.Difficulty,
Distance = this.Distance,
ImageUrl = this.ImageUrl
};
// Here, we will save the details entered in a later chapter.
await NavService.PreviousPage();
}
// method to check for any form errors
bool ValidateFormDetails()
{
return !string.IsNullOrWhiteSpace(Title);
}
Init
method within the WalkEntryViewModel
. This will be used to initialize the WalkEntryPage
when it is called. We use the Task.Factory.StartNew
method to give the ViewModel enough time to display the page on screen, prior to initializing the ContentPage
contents. Proceed and enter in the following highlighted code sections: public override async Task Init()
{
await Task.Factory.StartNew(() =>
{
Title = "New Walk";
Difficulty = "Easy";
Distance = 1.0;
});
}
}
}
In this section, we began by ensuring that our ViewModel inherits from the WalkBaseViewModel
class and then modifies the WalksEntryViewModel
class constructor to include the parameter navService
which is included within the IWalkNavService
interface class. In our next step, we'll initialize the class constructor with default values for the Title
, Difficulty
, and Distance
properties and then modify the SaveCommand
command method to include a reference to the NavService.PreviousPage
method. This is declared within the IWalkNavService
interface class to allow our WalkEntryPage
to navigate back to the previous calling page when the Save button is clicked.
In this section, we need to bind our model binding context, BindingContext
, to the WalkEntryViewModel
so that the new walk information, which will be entered within this page, can be stored within the WalkEntries
model. Let's take a look at how we can achieve this, by performing the following steps:
WalkEntryPage.cs
file is displayed within the code editor, and enter in the following highlighted code sections:// // WalkEntryPage.cs // TrackMyWalks // // Created by Steven F. Daniel on 04/08/2016. // Copyright © 2016 GENIESOFT STUDIOS. All rights reserved. // using Xamarin.Forms; using TrackMyWalks.Models; using System.Collections.Generic; namespace TrackMyWalks { public class WalkEntryPage : ContentPage {
_viewModel
within the WalkEntryPage
class that is of the WalksEntryViewModel
type, and which will essentially provide us with access to the ContentPage's BindingContext
object. Proceed and enter in the following highlighted code sections:WalkEntryViewModel _viewModel { get { return BindingContext as WalkEntryViewModel; } } public WalkEntryPage() { // Set the Content Page Title Title = "New Walk Entry";
WalkEntryViewModel
BindingContext
to include the IWalkNavService
constructor, which is used by the WalkBaseViewModel
class, and is retrieved from the Xamarin.Forms
DependencyService
class. Proceed and enter in the following highlighted code sections:// Declare and initialize our Model Binding Context BindingContext = new WalkEntryViewModel( DependencyService. Get<IWalkNavService>()); // Define our New Walk Entry fields var walkTitle = new EntryCell { Label = "Title:", Placeholder = "Trail Title" }; walkTitle.SetBinding(EntryCell.TextProperty, "Title", BindingMode.TwoWay); var walkNotes = new EntryCell { Label = "Notes:", Placeholder = "Description" }; walkNotes.SetBinding(EntryCell.TextProperty, "Notes", BindingMode.TwoWay); var walkLatitude = new EntryCell { Label = "Latitude:", Placeholder = "Latitude", Keyboard = Keyboard.Numeric }; walkLatitude.SetBinding(EntryCell.TextProperty, "Latitude", BindingMode.TwoWay); var walkLongitude = new EntryCell { Label = "Longitude:", Placeholder = "Longitude", Keyboard = Keyboard.Numeric }; walkLongitude.SetBinding(EntryCell.TextProperty, "Longitude", BindingMode.TwoWay); var walkKilometers = new EntryCell { Label = "Kilometers:", Placeholder = "Kilometers", Keyboard = Keyboard.Numeric }; walkKilometers.SetBinding(EntryCell.TextProperty, "Kilometers", BindingMode.TwoWay); var walkDifficulty = new EntryCell { Label = "Difficulty Level:", Placeholder = "Walk Difficulty" }; walkDifficulty.SetBinding(EntryCell.TextProperty, "Difficulty", BindingMode.TwoWay); var walkImageUrl = new EntryCell { Label = "ImageUrl:", Placeholder = "Image URL" }; walkImageUrl.SetBinding(EntryCell.TextProperty, "ImageUrl", BindingMode.TwoWay); // Define our TableView Content = new TableView { Intent = TableIntent.Form, Root = new TableRoot { new TableSection() { walkTitle, walkNotes, walkLatitude, walkLongitude, walkKilometers, walkDifficulty, walkImageUrl } } }; var saveWalkItem = new ToolbarItem { Text = "Save" }; saveWalkItem.SetBinding(MenuItem.CommandProperty, "SaveCommand"); ToolbarItems.Add(saveWalkItem); } } }
In this section, we looked at the steps involved in modifying the WalkEntryPage
so that it can take advantage of our updated WalkEntryViewModel
. We looked at how to set the content page to an instance of the WalkEntryViewModel
so that the BindingContext
property for the WalkEntryPage
will now point to an instance of the IWalkNavService
interface.
Now that we have modified the MVVM ViewModel that will be used for our WalkEntry
page, our next step is to begin modifying the WalksTrailViewModel
to take advantage of the navigation service, so that it will be used to display the walk entry information that has been associated with the chosen walk.
Let's take a look at how we can achieve this, by performing the following the steps:
WalksTrailViewModel.cs
file is displayed within the code editor, and enter in the following highlighted code sections: //
// WalksTrailViewModel.cs
// TrackMyWalks ViewModels
//
// Created by Steven F. Daniel on 22/08/2016.
// Copyright © 2016 GENIESOFT STUDIOS. All rights reserved.
//
using System.Threading.Tasks;
using TrackMyWalks.Models;
using TrackMyWalks.Services;
using Xamarin.Forms;
namespace TrackMyWalks.ViewModels
{
public class WalksTrailViewModel :
WalkBaseViewModel<WalkEntries>
{
WalkEntries _walkEntry;
public WalkEntries WalkEntry
{
get { return _walkEntry; }
set
{
_walkEntry = value;
OnPropertyChanged();
}
}
Command
property for our class. This will be used within our WalkTrailPage
and will be used to handle when the user clicks on the Begin This Trial button. The Command
property will run an action upon being pressed, and then execute a class instance method to determine whether the command can be executed or not, prior to navigating to the DistTravelledViewModel
, and passing in the trailDetails
for the chosen walk from the WalksPage
. Proceed and enter in the following highlighted code sections:Command<WalkEntries> _command; public Command<WalkEntries> DistanceTravelled { get { return _command ?? (_command = new Command<WalkEntries>(async (trailDetails) => await NavService.NavigateToViewModel <DistTravelledViewModel, WalkEntries>(trailDetails))); } }
WalksTrailViewModel
BindingContext
to include the IWalkNavService
constructor, which is used by the WalkBaseViewModel
class, and is retrieved from the Xamarin.Forms
DependencyService
class. Proceed and enter in the following highlighted code sections:public WalksTrailViewModel(IWalkNavService navService) : base(navService) { }
Init
method within the WalksTrailViewModel
. This will be used to initialize the WalkTrailPage
when it is called. We use the Task.Factory.StartNew
method to give the ViewModel enough time to display the page on screen, prior to initializing the ContentPage
contents, using the passed in walkDetails
for our model. Proceed and enter in the following highlighted code sections:public override async Task Init(WalkEntries walkDetails) { await Task.Factory.StartNew(() => { WalkEntry = walkDetails; }); } } }
In this section, we begin by ensuring that our ViewModel inherits from the WalkBaseViewModel
class, and that it accepts the WalkEntries
dictionary as its parameter. In our next step, we'll create a DistanceTravelledCommand
method that will navigate to the DistanceTravelledPage
content page within our NavigationStack
that passes the WalkEntry
dictionary to the DistTravelledViewModel
ViewModel and pass a parameter containing the trailDetails
of the chosen walk.
In this section, we need to bind our model binding context, BindingContext
, to the WalksTrailViewModel
so that the walk information details will be displayed from the WalkEntries
model when a walk has been clicked on within the main WalksPage
. Let's take a look at how we can achieve this, by performing the following steps:
WalkTrailPage.cs
file is displayed within the code editor, and enter in the following highlighted code sections:// // WalkTrailPage.cs // TrackMyWalks // // Created by Steven F. Daniel on 04/08/2016. // Copyright © 2016 GENIESOFT STUDIOS. All rights reserved. // using Xamarin.Forms; using TrackMyWalks.Models; using TrackMyWalks.ViewModels; using TrackMyWalks.Services; namespace TrackMyWalks { public class WalkTrailPage : ContentPage { public WalkTrailPage(WalkEntries walkItem) { Title = "Walks Trail";
WalkEntryViewModelBindingContext
to include the IWalkNavService
constructor, which is used by the WalkBaseViewModel
class, and is retrieved from the Xamarin.Forms
DependencyService
class. Proceed and enter in the following highlighted code sections:// Declare and initialize our Model Binding Context BindingContext = new WalksTrailViewModel(DependencyService. Get<IWalkNavService>()); var beginTrailWalk = new Button { BackgroundColor = Color.FromHex("#008080"), TextColor = Color.White, Text = "Begin this Trail" };
beginTrailWalk.Clicked
handler for our button, so that upon being clicked, it will navigate to the DistTravelledViewModel
and pass in the WalkEntry
dictionary for the chosen walk from the WalksPage
. Proceed and enter in the following highlighted code sections:// Declare and initialize our Event Handler beginTrailWalk.Clicked += (sender, e) => { if (_viewModel.WalkEntry == null) return; _viewModel.DistanceTravelled.Execute(_viewModel.WalkEntry); }; var walkTrailImage = new Image() { Aspect = Aspect.AspectFill }; walkTrailImage.SetBinding(Image.SourceProperty, "WalkEntry.ImageUrl"); var trailNameLabel = new Label() { FontSize = 28, FontAttributes = FontAttributes.Bold, TextColor = Color.Black }; trailNameLabel.SetBinding(Label.TextProperty, "WalkEntry.Title"); var trailKilometersLabel = new Label() { FontAttributes = FontAttributes.Bold, FontSize = 12, TextColor = Color.Black, }; trailKilometersLabel.SetBinding(Label.TextProperty, "WalkEntry.Kilometers", stringFormat: "Length: {0} km"); var trailDifficultyLabel = new Label() { FontAttributes = FontAttributes.Bold, FontSize = 12, TextColor = Color.Black }; trailDifficultyLabel.SetBinding(Label.TextProperty, "WalkEntry.Difficulty", stringFormat: "Difficulty: {0}"); var trailFullDescription = new Label() { FontSize = 11, TextColor = Color.Black, HorizontalOptions = LayoutOptions.FillAndExpand }; trailFullDescription.SetBinding(Label.TextProperty, "WalkEntry.Notes"); this.Content = new ScrollView { Padding = 10, Content = new StackLayout { Orientation = StackOrientation.Vertical, HorizontalOptions = LayoutOptions.FillAndExpand, Children = { walkTrailImage, trailNameLabel, trailKilometersLabel, trailDifficultyLabel, trailFullDescription, beginTrailWalk } } }; } } }
In this section, we looked at the steps involved in modifying the WalksTrailPage
so that it can take advantage of the WalksTrailViewModel
. We looked at how to set the content page to an instance of the WalksTrailViewModel
so that the BindingContext
property for the WalkTrailPage
will now point to an instance of the IWalkNavService
interface.
We also slightly modified our Clicked
handler for the beginTrailWalk
button so that it will now navigate to the DistanceTravelledPage
content page within the NavigationStack
, and pass in the WalkEntry
dictionary object to the DistTravelledViewModel
ViewModel.
Now that we have modified the MVVM ViewModel that will be used for our WalkTrailPage
, our next step is to update the DistTravelledViewModel
to take advantage of the navigation service, so that it can display the walk entry information that has been associated with the chosen walk.
Let's take a look at how we can achieve this, by performing the following steps:
DistTravelledViewModel.cs
file is displayed within the code editor, and enter in the following highlighted code sections:// // DistTravelledViewModel.cs // TrackMyWalks ViewModels // // Created by Steven F. Daniel on 22/08/2016. // Copyright © 2016 GENIESOFT STUDIOS. All rights reserved. // using System; using System.Threading.Tasks; using TrackMyWalks.Models; using TrackMyWalks.Services; using TrackMyWalks.ViewModels; using Xamarin.Forms; namespace TrackMyWalks.ViewModels { public class DistTravelledViewModel : WalkBaseViewModel<WalkEntries> { WalkEntries _walkEntry; public WalkEntries WalkEntry { get { return _walkEntry; } set { _walkEntry = value; OnPropertyChanged(); } } double _travelled; public double Travelled { get { return _travelled; } set { _travelled = value; OnPropertyChanged(); } } double _hours; public double Hours { get { return _hours; } set { _hours = value; OnPropertyChanged(); } } double _minutes; public double Minutes { get { return _minutes; } set { _minutes = value; OnPropertyChanged(); } } double _seconds; public double Seconds { get { return _seconds; } set { _seconds = value; OnPropertyChanged(); } } public string TimeTaken { get { return string.Format("{0:00}:{1:00}:{2:00}", this.Hours, this.Minutes, this.Seconds); } }
DistTravelledViewModel
class constructor, which will now need to include a navService
parameter that is included within the IWalkNavService
interface class. We then set the ViewModel's class constructor to access all instance class members contained within the navService
by using the base
keyword and initialize the constructor with default values for the Hours
, Minutes
, Seconds
, and Travelled
properties.DistTravelledViewModel
class constructor, and enter the following highlighted code:public DistTravelledViewModel(IWalkNavService navService) : base(navService) { this.Hours = 0; this.Minutes = 0; this.Seconds = 0; this.Travelled = 100; }
Init
method within the DistTravelledViewModel
, which will be used to initialize the DistanceTravelledPage
content page when it is called. We need to specify and use the Task.Factory.StartNew
method to give the ViewModel enough time to display the page on screen, prior to initializing the ContentPage
contents, using the passed in walkDetails
for our model. Proceed and enter in the following highlighted code sections:public override async Task Init(WalkEntries walkDetails) { await Task.Factory.StartNew(() => { WalkEntry = walkDetails; }); }
BackToMainPage
command property that will be used to bind to the End This Trail button that will run an action upon being pressed. This action will execute a class instance method, to determine whether the Command
can be executed.Command
can be executed, a call will be made to the BackToMainPage
method on the NavService
navigation service class to take the user back to the TrackMyWalks
main page, by removing all existing ViewModels within the NavigationStack
, except the first page. Proceed and enter in the following highlighted code sections:Command _mainPage; public Command BackToMainPage { get { return _mainPage ?? (_mainPage = new Command(async () => await NavService.BackToMainPage())); } } } }
In this section, we updated the DistanceTravelledViewModel
to inherit from our WalkBaseViewModel
Interface class and then modify the DistTravelledViewModel
class constructor to point to an instance of the IWalkNavService
interface class.
We then created the Init
method that will initialize the DistanceTravelledViewModel
when it is called and use the Task.Factory.StartNew
method to give the ViewModel enough time to display the DistanceTravelledPage
content page on screen, prior to initializing the ContentPage
contents, using the passed in walkDetails
for our model.
We also created the BackToMainPage
command property that will be used to bind to the End This Trail
button that will run an action to execute a class instance method, to determine whether the Command
can be executed, and then a call will be made to BackToMainPage
method on the NavService
navigation service class to take the user back to the first page within the NavigationStack
.
Now that we have modified the MVVM ViewModel that will be used by our DistanceTravelledPage
content page, our next step is to begin modifying the DistanceTravelledPage
page to take advantage of our navigation service, and display walk information details. The calculations and distance travelled will be displayed from the WalkEntries
model.
Let's take a look at how we can achieve this, by performing the following steps:
DistanceTravelledPage.cs
file is displayed within the code editor, and enter in the following highlighted code sections: //
// DistanceTravelledPage.cs
// TrackMyWalks
//
// Created by Steven F. Daniel on 04/08/2016.
// Copyright © 2016 GENIESOFT STUDIOS. All rights reserved.
//
using Xamarin.Forms;
using Xamarin.Forms.Maps;
using TrackMyWalks.Models;
using TrackMyWalks.Services;
namespace TrackMyWalks
{
public class DistanceTravelledPage : ContentPage
{
_viewModel
within the DistanceTravelledPage
class, which is of our DistTravelledViewModel
type, and will essentially provide us with access to the ContentPage's BindingContext
object. Proceed and enter in the following highlighted code sections:DistTravelledViewModel _viewModel { get { return BindingContext as DistTravelledViewModel; } } public DistanceTravelledPage() { Title = "Distance Travelled";
DistTravelledViewModelBindingContext
to include our IWalkNavService
constructor, which is used by the WalkBaseViewModel
class, and is retrieved from the Xamarin.Forms DependencyService
class. Proceed and enter in the following highlighted code sections:// Declare and initialize our Model Binding Context BindingContext = new DistTravelledViewModel (DependencyService. Get<IWalkNavService>());
LoadDetails
which will be used to grab the name of the chosen walk and the Latitude
and Longitude
values from the DistTravelledViewModel
as well as zoom into the user entry location, using the MoveToRegion
method. Proceed and enter in the following highlighted code sections:public void LoadDetails() { // Instantiate our map object var trailMap = new Map(); // Place a pin on the map for the chosen // walk type trailMap.Pins.Add(new Pin { Type = PinType.Place, Label = _viewModel.WalkEntry.Title, Position = new Position(_viewModel.WalkEntry.Latitude, _viewModel.WalkEntry.Longitude) }); // Center the map around the list of // walks entry's location trailMap.MoveToRegion(MapSpan.FromCenterAndRadius( new Position(_viewModel.WalkEntry.Latitude, _viewModel.WalkEntry.Longitude), Distance.FromKilometers(1.0))); var trailNameLabel = new Label() { FontSize = 18, FontAttributes = FontAttributes.Bold, TextColor = Color.Black, HorizontalTextAlignment = TextAlignment.Center }; trailNameLabel.SetBinding(Label.TextProperty, "WalkEntry.Title"); var trailDistanceTravelledLabel = new Label() { FontAttributes = FontAttributes.Bold, FontSize = 20, TextColor = Color.Black, HorizontalTextAlignment = TextAlignment.Center }; trailDistanceTravelledLabel.SetBinding(Label.TextProperty, "Travelled", stringFormat: "Distance Travelled: {0} km"); var totalTimeTakenLabel = new Label() { FontAttributes = FontAttributes.Bold, FontSize = 20, TextColor = Color.Black, HorizontalTextAlignment = TextAlignment.Center }; totalTimeTakenLabel.SetBinding(Label.TextProperty, "TimeTaken", stringFormat: "Time Taken: {0}"); var walksHomeButton = new Button { BackgroundColor = Color.FromHex("#008080"), TextColor = Color.White, Text = "End this Trail" };
walksHomeButton.Clicked
handler for our button so that, upon being clicked, it will allow the DistanceTravelledPage
to navigate back to the first page within the NavigationStack
. Proceed and enter in the following highlighted code sections:// Set up our event handler walksHomeButton.Clicked += (sender, e) => { if (_viewModel.WalkEntry == null) return; _viewModel.BackToMainPage.Execute(0); }; this.Content = new ScrollView { Padding = 10, Content = new StackLayout { Orientation = StackOrientation.Vertical, HorizontalOptions = LayoutOptions.FillAndExpand, Children = { trailMap, trailNameLabel, trailDistanceTravelledLabel, totalTimeTakenLabel, walksHomeButton } } }; }
OnAppearing
instance method of the navigation hierarchy that will be used to correctly plot the walk's Longitude
and Latitude
coordinates within the map, along with the walk information, prior to the ViewModel appearing on screen. We need to ensure that the ViewModel has properly been initialized by checking to see that it isn't null
, prior to calling the Init
method of the DistTravelledViewModel
. Proceed and enter in the following highlighted code sections:protected override async void OnAppearing() { base.OnAppearing(); // Initialize our DistanceTravelledViewModel if (_viewModel != null) { await _viewModel.Init(); LoadDetails(); } } } }
In this section, we looked at the steps involved in modifying the DistanceTraveledPage
so that it can take advantage of the DistTravelledViewModel
. We looked at how to set the content page to an instance of the DistTravelledViewModel
so that the BindingContext
property for the DistanceTravelledPage
will now point to an instance of the IWalkNavService
interface.
We also slightly modified our Clicked
handler for the WalksHomeButton
button, so that it will now navigate to the NavService.BackToMainPage
method, which is declared within the IWalkNavService
interface class to allow the DistanceTravelledPage
to navigate back to the first page within the NavigationStack
.
In this section, we need to update our Xamarin.Forms.App
class, by modifying the constructor in the main App
class to create a new instance of the navigation service and register the application's ContentPage
to ViewModel mappings.
Let's take a look at how we can achieve this, by performing the following steps:
TrackMyWalks.cs
file and ensure that it is displayed within the code editor.App
method and enter in the following highlighted code sections:// // TrackMyWalks.cs // TrackMyWalks // // Created by Steven F. Daniel on 04/08/2016. // Copyright © 2016 GENIESOFT STUDIOS. All rights reserved. // using TrackMyWalks.Services; using TrackMyWalks.ViewModels; using Xamarin.Forms; namespace TrackMyWalks { public class App : Application { public App() { // Check the Target OS Platform if (Device.OS == TargetPlatform.Android) { MainPage = new SplashPage(); } else { // The root page of your application var walksPage = new NavigationPage(new WalksPage() { Title = "Track My Walks" }); var navService = DependencyService. Get<IWalkNavService>() as WalkNavService; navService.navigation = walksPage.Navigation; navService.RegisterViewMapping(typeof (WalksPageViewModel ), typeof(WalksPage)); navService.RegisterViewMapping( typeof(WalkEntryViewModel ), typeof(WalkEntryPage)); navService.RegisterViewMapping( typeof(WalksTrailViewModel ), typeof(WalkTrailPage)); navService.RegisterViewMapping( typeof(DistTravelledViewMo del), typeof(DistanceTravelledPage)); MainPage = walksPage; } } protected override void OnStart() { // Handle when your app starts } protected override void OnSleep() { // Handle when your app sleeps } protected override void OnResume() { // Handle when your app resumes } } }
In the preceding code snippet, we begin by declaring a navService
variable that points to an instance of the navigation service as defined by our assembly attribute for the Xamarin.FormsDependencyService
, as declared in the WalkNavService
class.
In our next step, we set the navService.navigation
property to point to an instance of the NavigationPage
class that the walksPage.navigation
property currently points to, and will be used as the main root page.
Finally, we call the RegisterViewMapping
instance method for each of the ViewModels and specify the associated ContentPage
for each.
52.14.134.130