Now that we have basic knowledge of the MvvmCross framework, let's put that knowledge to work and convert the NationalParks
app to leverage the capabilities we just learned.
We will start by creating the core project. This project will contain all the code that will be shared between the iOS and Android app primarily in the form of ViewModels. The core project will be built as a Portable Class Library.
To create NationalParks.Core
, perform the following steps:
NationalParks.Core
for the project Name field, enter NationalParks.MvvmCross
for the Solution field, and click on OK.NationalParks.Core
project and navigate to Project | Add Packages from the main menu. Enter MvvmCross starter
in the search field.NationalParks.Core
as a result of adding the package, and they are as follows:packages.config
file, which contains a list of libraries (dlls
) associated with the MvvmCross starter kit package. These entries are links to actual libraries in the Packages
folder of the overall solution.ViewModels
folder with a sample ViewModel named FirstViewModel
.App
class in App.cs
, which contains an Initialize()
method that starts the MvvmCross app by calling RegisterAppStart()
to start FirstViewModel
. We will eventually be changing this to start the MasterViewModel
class, which will be associated with a View that lists national parks.The next step is to create an Android app project in the same solution.
To create NationalParks.Droid
, complete the following steps:
NationalParks.MvvmCross
solution, right-click on it, and navigate to Add | New Project.NationalParks.Droid
for the Name field, and click on OK.NationalParks.Droid
and navigating to Project | Add Packages from the main menu.NationalParks.Droid
as a result of adding the package, which are as follows:packages.config
: This file contains a list of libraries (dlls
) associated with the MvvmCross starter kit package. These entries are links to an actual library in the Packages
folder of the overall solution, which contains the actual downloaded libraries.FirstView
: This class is present in the Views
folder, which corresponds to FirstViewModel
, which was created in NationalParks.Core
.FirstView
: This layout is present in Resourceslayout
, which is used by the FirstView
activity. This is a traditional Android layout file with the exception that it contains binding declarations in the EditView
and TextView
elements.Setup
: This file inherits from MvxAndroidSetup
. This class is responsible for creating an instance of the App
class from the core project, which in turn displays the first ViewModel via a call to RegisterAppStart()
.SplashScreen
: This class inherits from MvxSplashScreenActivity
. The SplashScreen
class is marked as the main launcher activity and thus initializes the MvvmCross
app with a call to Setup.Initialize()
.NationalParks.Core
by selecting the References
folder, right-click on it, select Edit References, select the Projects tab, check NationalParks.Core
, and click on OK.MainActivity.cs
as it is no longer needed and will create a build error. This is because it is marked as the main launch and so is the new SplashScreen
class. Also, remove the corresponding Resourceslayoutmain.axml
layout file.FirstViewModel
, which is linked to the corresponding FirstView
instance with an EditView
class, and TextView
presents the same Hello MvvmCross text. As you edit the text in the EditView
class, the TextView
class is automatically updated by means of data binding. The following screenshot depicts what you should see:Before we start creating the Views and ViewModels for our app, we first need to bring in some code from our previous efforts that can be used to maintain parks. For this, we will simply reuse the NationalParksData
singleton and the FileHandler
classes that were created previously.
To reuse the NationalParksData
singleton and FileHandler
classes, complete the following steps:
NationalParks.PortableData
and NationalParks.IO
from the solution created in Chapter 6, The Sharing Game, to the NationalParks.MvvmCross
solution folder.NationalParks.PortableData
in the NationalParks.Droid
project.NationalParks.IO
in the NationalParks.Droid
project and add a link to FileHandler.cs
from the NationalParks.IO
project. Recall that the FileHandler
class cannot be contained in the Portable Class Library because it uses file IO APIs that cannot be references from a Portable Class Library.We will be using data binding to bind UI controls to the NationalPark
object and thus, we need to implement the INotifyPropertyChanged
interface. This ensures that changes made to properties of a park are reported to the appropriate UI controls.
To implement INotifyPropertyChanged
, complete the following steps:
NationalPark.cs
in the NationalParks.PortableData
project.NationalPark
class implements INotifyPropertyChanged
interface.INotifyPropertyChanged
interface, right-click on it, navigate to Refactor | Implement interface, and press Enter. Enter the following code snippet:public class NationalPark : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; . . . }
OnPropertyChanged()
method that can be called from each property setter method:void OnPropertyChanged( [CallerMemberName] string propertyName = null) { var handler = PropertyChanged; if (handler != null) { handler(this, new PropertyChangedEventArgs(propertyName)); } }
Name
property:string _name; public string Name { get { return _name; } set { if (value.Equals (_name, StringComparison.Ordinal)) { // Nothing to do - the value hasn't changed; return; } _name = value; OnPropertyChanged(); } }
NationalParksData
singleton in our new project, and it supports data binding.Now, we are ready to create the Views and ViewModels required for our app. The app we are creating will follow the same flow that was used in previous chapters:
The process for creating views and ViewModels in an Android app generally consists of three different steps:
In our case, this process will be slightly different because we will reuse some of our previous work, specifically, the layout files and the menu definitions.
To reuse layout files and menu definitions, perform the following steps:
Master.axml
, Detail.axml
, and Edit.axml
from the Resourceslayout
folder of the solution created in Chapter 5, Developing Your First Android App with Xamarin.Android, to the Resourceslayout
folder in the NationalParks.Droid
project, and add them to the project by selecting the layout folder and navigating to Add | Add Files.MasterMenu.xml
, DetailMenu.xml
, and EditMenu.xml
from the Resourcesmenu
folder of the solution created in Chapter 5, Developing Your First Android App with Xamarin.Android, to the Resourcesmenu
folder in the NationalParks.Droid
project, and add them to the project by selecting the menu
folder and navigating to Add | Add Files.We are now ready to implement the first of our View/ViewModel combinations, which is the master list view.
The first step is to create a ViewModel and add a property that will provide data to the list view that displays national parks along with some initialization code.
To create MasterViewModel
, complete the following steps:
ViewModels
folder in NationalParks.Core
, right-click on it, and navigate to Add | New File.MasterViewModel
for the Name field, and click on New.MasterViewModel
inherits from MvxViewModel
; you will also need to add a few using
directives:. . . using Cirrious.CrossCore.Platform; using Cirrious.MvvmCross.ViewModels; . . . namespace NationalParks.Core.ViewModels { public class MasterViewModel : MvxViewModel { . . . } }
NationalPark
elements to MasterViewModel
. This property will later be data-bound to a list view:private List<NationalPark> _parks; public List<NationalPark> Parks { get { return _parks; } set { _parks = value; RaisePropertyChanged(() => Parks); } }
Start()
method on MasterViewModel
to load the _parks
collection with data from the NationalParksData
singleton. You will need to add a using
directive for the NationalParks.PortableData
namespace again:. . . using NationalParks.PortableData; . . . public async override void Start () { base.Start (); await NationalParksData.Instance.Load (); Parks = new List<NationalPark> ( NationalParksData.Instance.Parks); }
MasterViewModel
is the first ViewModel that's started. Open App.cs
in NationalParks.Core
and change the call to RegisterAppStart()
to reference MasterViewModel
:RegisterAppStart<ViewModels.MasterViewModel>();
Update Master.axml
so that it can leverage the data binding capabilities provided by MvvmCross.
To update Master.axml
, complete the following steps:
Master.axml
and add a namespace definition to the top of the XML to include the NationalParks.Droid
namespace:xmlns:local="http://schemas.android.com/apk/res/NationalParks.Droid"
This namespace definition is required in order to allow Android to resolve the MvvmCross-specific elements that will be specified.
ListView
element to a Mvx.MvxListView
element:<Mvx.MvxListView android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/parksListView" />
MvxListView
element, binding the ItemsSource
property of the list view to the Parks
property of MasterViewModel
, as follows:. . . android:id="@+id/parksListView" local:MvxBind="ItemsSource Parks" />
local:MvxItemTemplate="@layout/nationalparkitem"
NationalParkItem
layout and provide TextView
elements to display both the name and description of a park, as follows:<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:local="http://schemas.android.com/apk/res/NationalParks.Droid" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="wrap_content"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="40sp"/> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="20sp"/> </LinearLayout>
. . . local:MvxBind="Text Name" /> . . . local:MvxBind="Text Description" /> . . .
Next, create MasterView
, which is an MvxActivity
instance that corresponds with MasterViewModel
.
To create MasterView
, complete the following steps:
ViewModels
folder in NationalParks.Core
, right-click on it, navigate to Add | New File.MasterView
in the Name field, and select New.MvxActivity
; you will also need to add a few using
directives as follows:using Cirrious.MvvmCross.Droid.Views; using NationalParks.Core.ViewModels; . . . namespace NationalParks.Droid.Views { [Activity(Label = "Parks")] public class MasterView : MvxActivity { . . . } }
Setup.cs
and add code to initialize the file handler and path for the NationalParksData
singleton to the CreateApp()
method, as follows:protected override IMvxApplication CreateApp() { NationalParksData.Instance.FileHandler = new FileHandler (); NationalParksData.Instance.DataDir = System.Environment.GetFolderPath( System.Environment.SpecialFolder.MyDocuments); return new Core.App(); }
NationalParks.json
file to the device or emulator using the Android Device Monitor. All the parks in NationalParks.json
should be displayed.Now that we have the master list view displaying national parks, we can focus on creating the detail view. We will follow the same steps for the detail view as the ones we just completed for the master view.
We start creating DetailViewModel
by using the following steps:
MasterViewModel
, create a new ViewModel named DetailViewModel
in the ViewModel
folder of NationalParks.Core
.NationalPark
property to support data binding for the view controls, as follows:protected NationalPark _park; public NationalPark Park { get { return _park; } set { _park = value; RaisePropertyChanged(() => Park); } }
Parameters
class that can be used to pass a park ID for the park that should be displayed. It's convenient to create this class within the class definition of the ViewModel that the parameters are for:public class DetailViewModel : MvxViewModel { public class Parameters { public string ParkId { get; set; } } . . .
Init()
method that will accept an instance of the Parameters
class and get the corresponding national park from NationalParkData
:public void Init(Parameters parameters) { Park = NationalParksData.Instance.Parks. FirstOrDefault(x => x.Id == parameters.ParkId); }
Next, we will update the layout file. The main changes that need to be made are to add data binding specifications to the layout file.
To update the Detail.axml
layout, perform the following steps:
Detail.axml
and add the project namespace to the XML file:xmlns:local="http://schemas.android.com/apk/res/NationalParks.Droid"
TextView
elements that correspond to a national park property, as demonstrated for the park name:<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/nameTextView" local:MvxBind="Text Park.Name" />
Now, create the MvxActivity
instance that will work with DetailViewModel
.
To create DetailView
, perform the following steps:
MasterView
, create a new view named DetailView
in the Views
folder of NationalParks.Droid
.OnCreateOptionsMenu()
and OnOptionsItemSelected()
methods so that our menus will be accessible. Copy the implementation of these methods from the solution created in Chapter 6, The Sharing Game. Comment out the section in OnOptionsItemSelect()
related to the Edit
action for now; we will fill that in once the edit view is completed.The last step is to add navigation so that when an item is clicked on in MvxListView
on MasterView
, the park is displayed in the detail view. We will accomplish this using a command
property and data binding.
To add navigation, perform the following steps:
MasterViewModel
and add an IMvxCommand
property; this will be used to handle a park that is being selected:protected IMvxCommand ParkSelected { get; protected set; }
Action
delegate that will be called when the ParkSelected
command is executed, as follows:protected void ParkSelectedExec(NationalPark park) { ShowViewModel<DetailViewModel> ( new DetailViewModel.Parameters () { ParkId = park.Id }); }
command
property in the constructor of MasterViewModel
:ParkClicked = new MvxCommand<NationalPark> (ParkSelectedExec);
MvvListView
in Master.axml
to bind the ItemClick
event to the ParkClicked
command on MasterViewModel
, which we just created:local:MvxBind="ItemsSource Parks; ItemClick ParkClicked"
We are now almost experts at implementing new Views and ViewModels. One last View to go is the edit view.
Like we did previously, we start with the ViewModel.
To create EditViewModel
, complete the following steps:
EditViewModel
, add a data binding property and create a Parameters
class for navigation.Init()
method that will accept an instance of the Parameters
class and get the corresponding national park from NationalParkData
in the case of editing an existing park or create a new instance if the user has chosen the New
action. Inspect the parameters passed in to determine what the intent is:public void Init(Parameters parameters) { if (string.IsNullOrEmpty (parameters.ParkId)) Park = new NationalPark (); else Park = NationalParksData.Instance. Parks.FirstOrDefault( x => x.Id == parameters.ParkId); }
Update Edit.axml
to provide data binding specifications.
To update the Edit.axml
layout, you first need to open Edit.axml
and add the project namespace to the XML file. Then, add the data binding specifications to each of the EditView
elements that correspond to a national park property.
Create a new MvxActivity
instance named EditView
to will work with EditViewModel
.
To create EditView
, perform the following steps:
DetailView
, create a new View named EditView
in the Views
folder of NationalParks.Droid
.OnCreateOptionsMenu()
and OnOptionsItemSelected()
methods so that the Done
action will accessible from the ActionBar. You can copy the implementation of these methods from the solution created in Chapter 6, The Sharing Game. Change the implementation of Done
to call the Done
command on EditViewModel
.Add navigation to two places: when New (+) is clicked from MasterView
and when Edit is clicked in DetailView
. Let's start with MasterView.
To add navigation from MasterViewModel
, complete the following steps:
MasterViewModel.cs
and add a NewParkClicked
command property along with the handler for the command. Be sure to initialize the command in the constructor, as follows:protected IMvxCommand NewParkClicked { get; set; } protected void NewParkClickedExec() { ShowViewModel<EditViewModel> (); }
Note that we do not pass in a parameter class into ShowViewModel()
. This will cause a default instance to be created and passed in, which means that ParkId
will be null. We will use this as a way to determine whether a new park should be created.
NewParkClicked
command up to the actionNew
menu item. We do not have a way to accomplish this using data binding, so we will resort to a more traditional approach—we will use the OnOptionsItemSelected()
method. Add logic to invoke the Execute()
method on NewParkClicked
, as follows:case Resource.Id.actionNew: ((MasterViewModel)ViewModel). NewParkClicked.Execute (); return true;
To add navigation from DetailViewModel
, complete the following steps:
DetailViewModel.cs
and add a EditParkClicked
command property along with the handler for the command. Be sure to initialize the command in the constructor, as shown in the following code snippet:protected IMvxCommand EditPark { get; protected set;} protected void EditParkHandler() { ShowViewModel<EditViewModel> ( new EditViewModel.Parameters () { ParkId = _park.Id }); }
command
property in the constructor for MasterViewModel
, as follows:EditPark = new MvxCommand<NationalPark> (EditParkHandler);
OnOptionsItemSelect()
method in DetailView
to invoke the DetailView.EditPark
command when the Edit
action is selected:case Resource.Id.actionEdit: ((DetailViewModel)ViewModel).EditPark.Execute (); return true;
NationalParks.Droid
. You should now have a fully functional app that has the ability to create new parks and edit the existing parks. Changes made to EditView
should automatically be reflected in MasterView
and DetailView
.The process of creating the Android app with MvvmCross provides a solid understanding of how the overall architecture works. Creating the iOS solution should be much easier for two reasons: first, we understand how to interact with MvvmCross and second, all the logic we have placed in NationalParks.Core
is reusable, so that we just need to create the View portion of the app and the startup code.
To create NationalParks.iOS
, complete the following steps:
NationalParks.MvvmCross
solution, right-click on it, and navigate to Add | New Project.NationalParks.iOS
in the Name field, and click on OK.NationalParks.iOS
and navigating to Project | Add Packages from the main menu.NationalParks.iOS
as a result of adding the package. They are as follows:packages.config
: This file contains a list of libraries associated with the MvvmCross starter kit package. These entries are links to an actual library in the Packages
folder of the overall solution, which contains the actual downloaded libraries.FirstView
: This class is placed in the Views
folder, which corresponds to the FirstViewModel
instance created in NationalParks.Core
.Setup
: This class inherits from MvxTouchSetup
. This class is responsible for creating an instance of the App
class from the core project, which in turn displays the first ViewModel via a call to RegisterAppStart()
.AppDelegate.cs.txt
: This class contains the sample startup code, which should be placed in the actual AppDelete.cs
file.We are now ready to create the user interface for the iOS app. The good news is that we already have all the ViewModels implemented, so we can simply reuse them. The bad news is that we cannot easily reuse the storyboards from our previous work; MvvmCross apps generally use XIB files. One of the reasons for this is that storyboards are intended to provide navigation capabilities and an MvvmCross app delegates that responsibility to ViewModel and presenter. It is possible to use storyboards in combination with a custom presenter, but the remainder of this chapter will focus on using XIB files, as this is the more common use. The screen layouts as used in Chapter 4, Developing Your First iOS App with Xamarin.iOS, can be used as depicted in the following screenshot:
The first view we will work on is the master view.
To implement the master view, complete the following steps:
ViewController
class named MasterView
by right-clicking on the Views
folder of NationalParks.iOS
and navigating to Add | New File | iOS | iPhone View Controller.MasterView.xib
and arrange controls as seen in the screen layouts. Add outlets for each of the edit controls.MasterView.cs
and add the following boilerplate logic to deal with constraints on iOS 7, as follows:// ios7 layout if (RespondsToSelector(new Selector("edgesForExtendedLayout"))) EdgesForExtendedLayout = UIRectEdge.None;
ViewDidLoad()
method, add logic to create MvxStandardTableViewSource
for parksTableView
:MvxStandardTableViewSource _source; . . . _source = new MvxStandardTableViewSource( parksTableView, UITableViewCellStyle.Subtitle, new NSString("cell"), "TitleText Name; DetailText Description", 0); parksTableView.Source = _source;
Note that the example uses the Subtitle
cell style and binds the national park name and description to the title and subtitle.
ViewDidShow()
method. In the previous step, we provided specifications for properties of UITableViewCell
to properties in the binding context. In this step, we need to set the binding context for the Parks
property on MasterModelView
:var set = this.CreateBindingSet<MasterView, MasterViewModel>(); set.Bind (_source).To (vm => vm.Parks); set.Apply();
NationalParks.json
should be displayed.Now, implement the detail view using the following steps:
ViewController
instance named DetailView
.DetailView.xib
and arrange controls as shown in the following code. Add outlets for each of the edit controls.DetailView.cs
and add the binding logic to the ViewDidShow()
method:this.CreateBinding (this.nameLabel). To ((DetailViewModel vm) => vm.Park.Name).Apply (); this.CreateBinding (this.descriptionLabel). To ((DetailViewModel vm) => vm.Park.Description). Apply (); this.CreateBinding (this.stateLabel). To ((DetailViewModel vm) => vm.Park.State).Apply (); this.CreateBinding (this.countryLabel). To ((DetailViewModel vm) => vm.Park.Country). Apply (); this.CreateBinding (this.latLabel). To ((DetailViewModel vm) => vm.Park.Latitude). Apply (); this.CreateBinding (this.lonLabel). To ((DetailViewModel vm) => vm.Park.Longitude). Apply ();
Add navigation from the master view so that when a park is selected, the detail view is displayed, showing the park.
To add navigation, complete the following steps:
MasterView.cs
, create an event handler named ParkSelected
, and assign it to the SelectedItemChanged
event on MvxStandardTableViewSource
, which was created in the ViewDidLoad()
method:. . . _source.SelectedItemChanged += ParkSelected; . . . protected void ParkSelected(object sender, EventArgs e) { . . . }
ParkSelected
command on MasterViewModel
, passing in the selected park:((MasterViewModel)ViewModel).ParkSelected.Execute ( (NationalPark)_source.SelectedItem);
NationalParks.iOS
. Selecting a park in the list view should now navigate you to the detail view, displaying the selected park.We now need to implement the last of the Views for the iOS app, which is the edit view.
To implement the edit view, complete the following steps:
ViewController
instance named EditView
.EditView.xib
and arrange controls as in the layout screenshots. Add outlets for each of the edit controls.EditView.cs
and add the data binding logic to the ViewDidShow()
method. You should use the same approach to data binding as the approach used for the details view.DoneClicked
, and within the event handler, invoke the Done
command on EditViewModel
:protected void DoneClicked (object sender, EventArgs e) { ((EditViewModel)ViewModel).Done.Execute(); }
ViewDidLoad()
, add UIBarButtonItem
to NavigationItem
for EditView
, and assign the DoneClicked
event handler to it, as follows:NavigationItem.SetRightBarButtonItem( new UIBarButtonItem(UIBarButtonSystemItem.Done, DoneClicked), true);
Add navigation to two places: when New (+) is clicked from the master view and when Edit is clicked on in the detail view. Let's start with the master view.
To add navigation to the master view, perform the following steps:
MasterView.cs
and add an event handler named NewParkClicked
. In the event handler, invoke the NewParkClicked
command on MasterViewModel
:protected void NewParkClicked(object sender, EventArgs e) { ((MasterViewModel)ViewModel). NewParkClicked.Execute (); }
ViewDidLoad()
, add UIBarButtonItem
to NavigationItem
for MasterView
and assign the NewParkClicked
event handler to it:NavigationItem.SetRightBarButtonItem( new UIBarButtonItem(UIBarButtonSystemItem.Add, NewParkClicked), true);
To add navigation to the details view, perform the following steps:
DetailView.cs
and add an event handler named EditParkClicked
. In the event handler, invoke the EditParkClicked
command on DetailViewModel
:protected void EditParkClicked (object sender, EventArgs e) { ((DetailViewModel)ViewModel).EditPark.Execute (); }
ViewDidLoad()
, add UIBarButtonItem
to NavigationItem
for MasterView
, and assign the EditParkClicked
event handler to it:NavigationItem.SetRightBarButtonItem( new UIBarButtonItem(UIBarButtonSystemItem.Edit, EditParkClicked), true);
One last detail that needs to be taken care of is to refresh the UITableView
control on MasterView
when items have been changed on EditView
.
To refresh the master view list, perform the following steps:
MasterView.cs
and call ReloadData()
on parksTableView
within the ViewDidAppear()
method of MasterView
:public override void ViewDidAppear (bool animated) { base.ViewDidAppear (animated); parksTableView.ReloadData(); }
NationalParks.iOS
. You should now have a fully functional app that has the ability to create new parks and edit existing parks. Changes made to EditView
should automatically be reflected in MasterView
and DetailVIew
.