This article is step 3 in the TipCalc tutorial for MvvmCross v3 - Hot Tuna!
TipCalc
MvvmCross
We started with the goal of creating an app to help calculate what tip to leave in a restaurant.
We had a plan to produce a UI based on this concept:
To satisfy this, we built a 'Core' Portable Class Library project which contained:
Core
ICalculation
TipViewModel
We then added our first User Interface - for Xamarin.Android:
Xamarin.Android
For our next project, let's shift to Xamarin.iOS.
Xamarin.iOS
To create an iPhone MvvmCross UI, you can use the Visual Studio project template wizards, but here we'll instead build up a new project 'from empty', just as we did for the Core and Android projects.
Also, to work with iPhone, for now we will switch to working on the Mac with Xamarin Studio.
Add a new project to your solution - a 'Xamarin.iOS' iPhone application with name TipCalc.UI.Touch.
TipCalc.UI.Touch
Within this, you'll find the normal iOS application constructs:
info.plist
No-one really needs an MyViewController. " align="top" src="http://www.codeproject.com/script/Forums/Images/smiley_smile.gif" />
MyViewController
Also, delete MyViewController.xib (if there is one).
MyViewController.xib
Add references to the new project for the portable libraries:
DataBinding
Normally these will be found in a folder path like {SolutionRoot}/Libs/Mvx/Portable/.
Add references to the new project for the Xamarin.iOS specific libraries:
Each of these extends the functionality of its PCL counterpart with iOS specific additions.
Normally, these will be found in a folder path like {SolutionRoot}/Libs/Mvx/Touch/.
Also, within that same folder, you need to add:
Add a reference to your TipCalc.Core project - the project we created in the last step which included:
TipCalc.Core
Calculation
Just as we said during the Android construction, Every MvvmCross UI project requires a Setup class.
This class sits in the root namespace (folder) of our UI project and performs the initialisation of the MvvmCross framework and your application, including:
ViewModel
Most of this functionality is provided for you automatically. Within your iOS UI project, all you have to supply are:
For TipCalc, here's all that is needed in Setup.cs:
using System; using Cirrious.MvvmCross.Touch.Platform; using TipCalc.Core; using Cirrious.MvvmCross.Touch.Views.Presenters; namespace TipCalc.UI.Touch { public class Setup : MvxTouchSetup { public Setup (MvxApplicationDelegate appDelegate, IMvxTouchViewPresenter presenter) : base(appDelegate, presenter) { } protected override Cirrious.MvvmCross.ViewModels.IMvxApplication CreateApp () { return new App(); } } }
Your AppDelegate provides a set of callback that iOS uses to inform you about events in your application's lifecycle.
AppDelegate
To use this AppDelegate within MvvmCross, we need to:
modify it so that it inherits from MvxApplicationDelegate instead of UIApplicationDelegate:
MvxApplicationDelegate
UIApplicationDelegate
public partial class AppDelegate : MvxApplicationDelegate
modify it so that the method that is called on startup (FinishedLaunching) does some UI application setup:
FinishedLaunching
create a new presenter - this is the class that will determine how Views are shown - for this sample, we choose a 'standard' one:
View
var presenter = new MvxTouchViewPresenter(this, window);
create and call Initialize on a Setup:
Initialize
Setup
var setup = new Setup(this, presenter); setup.Initialize();
with Setup completed, use the Mvx Inversion of Control container in order to find and Start the IMvxAppStart object:
Start
IMvxAppStart
var startup = Mvx.Resolve<IMvxAppStart>(); startup.Start();
Together, this looks like:
using System; using System.Collections.Generic; using System.Linq; using MonoTouch.Foundation; using MonoTouch.UIKit; using Cirrious.MvvmCross.Touch.Platform; using Cirrious.MvvmCross.Touch.Views.Presenters; using Cirrious.MvvmCross.ViewModels; using Cirrious.CrossCore.IoC; namespace TipCalc.UI.Touch { [Register("AppDelegate")] public partial class AppDelegate : MvxApplicationDelegate { UIWindow window; public override bool FinishedLaunching(UIApplication app, NSDictionary options) { window = new UIWindow(UIScreen.MainScreen.Bounds); var presenter = new MvxTouchViewPresenter(this, window); var setup = new Setup(this, presenter); setup.Initialize(); var startup = Mvx.Resolve<IMvxAppStart>(); startup.Start(); window.MakeKeyAndVisible(); return true; } } }
Create a Views folder:
Within this, add a new 'iPhone UIViewController' and call it TipView.
iPhone UIViewController
TipView
This will generate:
Double click on the XIB file.
This will open the XIB editor within xCode.
Just as we did with Android, I won't go into depth here about how to use the XIB iOS editor - instead I'll just cover the bare basics, and I'll also try to provide some comparisons for those familiar with XAML.
Drag/drop from the 'Object Library' to add:
UILabels
static
TextBlock
UITextField
SubTotal
TextBox
UISlider
Generosity
ProgressBar
UILabel
Tip
Using drag and drop, you should be able to quite quickly generate a design similar to:
Once you have your UI drawn, you can then link those UI displayed fields to ObjectiveC variables called outlets.
ObjectiveC
After you have done this, then the Xamarin.iOS tools within Xamarin Studio will then automatically detect these changes and map those ObjectiveC fields to C# properties back in your iOS app.
To start doing this, you need to open the 'Assistant Editor' from menu option 'View' -> 'Assistant Editor' -> 'Show Assistant Editor' within xCode. This will display a small pane with some ObjectiveC code in it - this is the 'Assistant Editor'.
Once you have done this, then you can ctrl-click (right click) on each of the 3 SubTotal, Generosity and Tip fields in turn.
For each of them:
Following this process, you should be able to create three ObjectiveC outlet variables for the three fields:
SubTotalTextField
GenerositySlider
TipLabel
With this done, save your xCode changes (using the File Menu) and then exit xCode.
Back in Xamarin Studio, you should now see that the Xamarin products have updated the TipView.designer.cs file - it will now contain three [Outlet] properties with those same three names:
Close the TipView.designer.cs file - this file is an auto-generated partial class, and Xamarin Studio can regenerate it at any time - so there is no point in editing it yourself.
Instead open TipView.cs - this contains an editable part of the same partial class.
Because we want our TipView to be not only a UIViewController but also an Mvvm View, then change the inheritance of TipView so that it inherits from MvxViewController.
UIViewController
MvxViewController
public class TipView : MvxViewController
Now, to link TipView to TipViewModel, create a public new TipViewModel ViewModel property - exactly as you did in Xamarin.Android:
public new TipViewModel ViewModel
public new TipViewModel ViewModel { get { return (TipViewModel) base.ViewModel; } set { base.ViewModel = value; } }
To add the data-binding code, go to the ViewDidLoad method in your TipView class. This is a method that will be called after the View is loaded within iOS but before it is displayed on the screen.
ViewDidLoad
This makes ViewDidLoad a perfect place for us to call some data-binding extension methods which will specify how we want the UI data-bound to the ViewModel:
public override void ViewDidLoad () { base.ViewDidLoad (); this.Bind (this.TipLabel, (TipViewModel vm) => vm.Tip ); this.Bind (this.SubTotalTextField, (TipViewModel vm) => vm.SubTotal ); this.Bind (this.GenerositySlider, (TipViewModel vm) => vm.Generosity ); }
What this code does is to generate 'in code' exactly the same type of data-binding information as we generated 'in XML' in Android.
Note that before the calls to this.Bind are made, then we first call base.ViewDidLoad(). This is important because base.ViewDidLoad() is where MvvmCross locates the TipViewModel that this TipView will bind to.
this.Bind
base.ViewDidLoad()
Altogether, this looks like:
using System; using System.Drawing; using MonoTouch.Foundation; using MonoTouch.UIKit; using Cirrious.MvvmCross.Touch.Views; using Cirrious.MvvmCross.Binding.BindingContext; using TipCalc.Core; namespace TipCalc.UI.Touch { public partial class TipView : MvxViewController { public new TipViewModel ViewModel { get { return (TipViewModel)base.ViewModel; } set { base.ViewModel = value; } } public TipView () : base ("TipView", null) { } public override void ViewDidLoad () { base.ViewDidLoad (); this.CreateBinding (this.TipLabel).To( (TipViewModel vm) => vm.Tip ).Apply(); this.CreateBinding (this.SubTotalTextField).To( (TipViewModel vm) => vm.SubTotal ).Apply(); this.CreateBinding (this.GenerositySlider.To( (TipViewModel vm) => vm.Generosity ).Apply(); } } }
You will no doubt have noticed that data-binding in iOS looks very different to the way it looked in Android - and to what you may have expected from XAML.
This is because the XIB format used in iOS is a lot less human manipulable and extensible than the XML formats used in Android AXML and Windows XAML - so it makes more sense to use C# rather than the XIB to register our bindings.
Within this section of the tutorial, all of our iOS bindings look like:
this.CreateBinding (this.TipLabel).To ((TipViewModel vm) => vm.Tip ).Apply();
What this line means is:
Text
As with Android, this will be a TwoWay binding by default - which is different to what XAML developers may expect to see.
TwoWay
If you had wanted to specify the TipLabel property to use Text explicitly instead of relying on the default, then you could have done this with:
this.CreateBinding (this.TipLabel).For(label => label.Text).To( (TipViewModel vm) => vm.Tip ).Apply();
In later topics, we'll cover more on binding in iOS, including more on binding to non-default fields; other code-based binding code mechanisms; custom bindings; using ValueConverters; and creating bound sub-views.
ValueConverters
At this point, you should be able to run your application.
When it starts... you should see:
This seems to work perfectly, although you may notice that if you tap on the SubTotal property and start entering text, then you cannot afterwards close the keyboard.
This is a View concern - it is a UI problem. So we can fix it just in the iOS UI code - in this View. For example, to fix this here, you can add a gesture recognizer to the end of the ViewDidLoad method like:
View.AddGestureRecognizer(new UITapGestureRecognizer(() => { this.SubTotalTextField.ResignFirstResponder(); }));
There's more we could do to make this User Interface nicer and to make the app richer... but for this first application, we will leave it here for now.
Let's move on to Windows!