Following on from the first post in the MVX+1 series, in this post we’ll create a basic Tip Calculator (mirroring the original post from the N+1 series). However, in this case the first section is a more detailed set of instructions on how to get the basics of all three platforms setup with MvvmCross. Going forward we’ll use this as a starting point so that we don’t need to cover over this in each subsequent post.
Source code: https://github.com/nickrandolph/MvxPlus1DaysOfMvvmCross/tree/master/Mvx-01-TipCalc
Getting Started Instructions (Native Apps)
Note: In this case the name of the application is TipCalc but these instructions can be followed to get any new project started by simply replacing TipCalc with the name of your project.
- Create a the new solution by creating a new Class Library (.NET Standard) with the project name set to TipCalc.Core and the solution name to just TipCalc.
- Add a new project based on the Blank App (Universal Windows) project template called TipCalc.Uwp (make sure that the Minimum and Target versions are set to at least the Fall Creators Update)
- Add a new project based on the Android App (Xamarin) project template called TipCalc.Droid (use the Blank app template)
- Add a new project based on the iOS App (Xamarin) project template called TipCalc.iOS (use the Blank app template)
- Add a reference to MvvmCross NuGet package (v6.0.0 at time of writing to all four projects)
- TipCalc.Uwp: Update NuGet package Microsoft.NETCore.UniversalWindowsPlatform to the latest stable version (6.0.8 at time of writing)
- TipCalc.Droid: Update the Android version to Use Latest Platform (Project Properties –> Application –> Target Framework)
- TipCalc.Droid: Update Xamarin.Android.Support.Design to latest stable version (27.0.2 at time of writing)
- TipCalc.Droid: Add a reference to MvvmCross.Droid.Support.V7.AppCompat package
- TipCalc.iOS: Unload project; delete packages.config; edit TipCalc.iOS.csproj and add the following ItemGroup
<ItemGroup>
<PackageReference Include=”MvvmCross” Version=”6.0.0″ />
</ItemGroup> - Add a reference to TipCalc.Core to each of the three head projects (ie TipCalc.Uwp, TipCalc.Droid and TipCalc.iOS)
- TipCalc.Core: Rename the default Class1.cs to App.cs, and allow Visual Studio to rename class to App
- TipCalc.Core: Change the App class to inherit from MvxApplication
- TipCalc.Core: Add a folder, ViewModels, and add a class called FirstViewModel.
- TipCalc.Core: Change FirstViewModel to inherit from MvxViewModel
- TipCalc.Core: Override Initialize method in App to register services and set startup view model to FirstViewModel
public override void Initialize()
{
CreatableTypes()
.EndingWith(“Service”)
.AsInterfaces()
.RegisterAsLazySingleton();
RegisterAppStart<FirstViewModel>();
} - TipCalc.Uwp: Add a help class ProxyMvxApplication
public abstract class ProxyMvxApplication: MvxApplication<MvxWindowsSetup<Core.App>, Core.App> { } - TipCalc.Uwp: Change App.xaml and App.xaml.cs to inherit from ProxyMvxApplication
- TipCalc.Uwp: Remove all code in App.xaml.cs other than the constructor which should contain a single call to InitializeComponent
- TipCalc.Uwp: Delete MainPage.xaml and MainPage.xaml.cs
- TipCalc.Uwp: Add a Views folder, and add a FirstView based on the Blank Page template
- TipCalc.Uwp: Change FirstView.xaml and FirstView.xaml.cs to inherit from MvxWindowsPage
- TipCalc.Droid: Add a new class, MainApplication, that inherits from MvxAppCompatApplication
- TipCalc.Droid: Rename MainActivity.cs to SplashScreen.cs and let Visual Studio rename the class
- TipCalc.Droid: Rename activity_main.axml to SplashScreen.axml, and adjust layout to indicate application loading
- TipCalc.Droid: Adjust SplashScreen class to inherit from MvxSplashScreenAppCompatActivity and set NoHistory to true (since we don’t want the user to be able to press the back button and go back to the splash screen)
[Activity(Label = “@string/app_name”, Theme = “@style/AppTheme”, MainLauncher = true, NoHistory = true)]
public class SplashScreen : MvxSplashScreenAppCompatActivity - TipCalc.Droid: Add a folder, Views, and add a new Activity, FirstView.c
- TipCalc.Droid: Add a new Android Layout to the Resources/Layout folder, FirstView.axml
- TipCalc.Droid: You may run into an error: “error XA4210: You need to add a reference to Mono.Android.Export.dll when you use ExportAttribute or ExportFieldAttribute.” If you do, you just need to Add Reference to Mono.Android.Export (search in the Add Reference dialog).
- TipCalc.iOS: Update AppDelegate class to inherit from MvxApplicationDelegate, and remove the default code.
[Register(“AppDelegate”)]
public class AppDelegate : MvxApplicationDelegate<MvxIosSetup<App>, App>
{
} - TipCalc.iOS: Add an Empty Storyboard, called Main.storyboard, to the root of the project
- TipCalc.iOS: Add a ViewController to the Main.storyboard using the designer and set the Class and Storyboard ID to FirstView (also make sure the “User Storyboard ID” checkbox is set to true)
- TipCalc.iOS: Move the generated FirstView.cs and FirstView.designer.cs files (from the previous step) into a new folder called Views, and adjust the namespace of the FirstView class to FirstView.iOS.Views
- TipCalc.iOS: Update the FirstView class to inherit from MvxViewController
[MvxFromStoryboard(“Main”)]
public partial class FirstView : MvxViewController<FirstViewModel>
[Application]
public class MainApplication : MvxAppCompatApplication<MvxAppCompatSetup<App>, App>
{
public MainApplication(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer)
{
}
}
[Activity(Label = “FirstView”)]
public class FirstView : MvxAppCompatActivity<FirstViewModel>
{
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
SetContentView(Resource.Layout.FirstView);
}
}
Now to actually build out the Tip Calculator. Rather than embed the calculation logic into our view model, we’re going to abstract it out into a service. To do this we’ll make use of the IoC container made available by MvvmCross. We’ll register a CalculationService which will be injected into our view model constructor.
Let’s continue our development of the Tip Calculator in TipCalc.Core:
- Add a folder called Services
- Add an interface ICalculationService into the Services folder
public interface ICalculationService
{
double Tip(double subTotal, double generosity);
} - Add a class, CalculationService, which implements ICalculationService, again into the Services folder
public class CalculationService : ICalculationService
{
public double Tip(double subTotal, double generosity)
{
return subTotal * generosity / 100.0;
}
} - Add a constructor to the FirstViewModel which accepts an ICalculationService parameter
- Add properties for SubTotal, Generosity, Tip and Total. Each property should take the following form where SetProperty is called within the setter
private double _subTotal;
public double SubTotal
{
get { return _subTotal; }
set { SetProperty(ref _subTotal, value); }
} - Add a Recalc method which will invoke the Tip method on the ICalculationService
private void Recalc()
{
Tip = _calculationService.Tip(SubTotal, Generosity);
Total = SubTotal + Tip;
} - Add a call to Recalc into the setter for both SubTotal and Generosity
- Set some initial values for the SubTotal and Generosity (if you use the property setters, rather than setting the fields, the Recalc method will be invoked)
private readonly ICalculationService _calculationService;
public FirstViewModel(ICalculationService calculationService)
{
_calculationService = calculationService;
}
That’s it for the core logic for the application. Now we just need to wire up the UI for each platform.
One thing that’s worth noting is that in step 16 of the original setup where the Initialize method is overridden, there is logic in the Initialize method to interrogate the current assembly looking for all classes that end in Service and register them, based on their interface, with the MvvmCross IoC container – this is how MvvmCross knows about the implementation of the ICalculationService which is used when instantiating the FirstViewModel.
Let’s built out the UWP interface in TipCalc.Uwp:
- Add the following XAML inside the existing Grid element:
<StackPanel>
<TextBlock Text=”SubTotal”/>
<TextBox Text=”{Binding SubTotal, Mode=TwoWay}”/>
<TextBlock Text=”How generous?”/>
<Slider Value=”{Binding Generosity, Mode=TwoWay}”
Minimum=”0″
Maximum=”100″/>
<TextBlock Text=”Tip:”/>
<TextBlock Text=”{Binding Tip}”/>
<TextBlock Text=”SubTotal:”/>
<TextBlock Text=”{Binding Total}”/>
</StackPanel>
That’s the UWP interface done – four elements (with TextBlock headings): TextBox and Slider for inputting SubTotal and Generosity using two-way data binding, and two TextBlock for outputting the Tip and Total amounts.
Now let’s do Android:
- Add the following namespace declaration to FirstView.axml
- Add the following xml to FirstView.axml
<TextView
android_text=”SubTotal”
android_textAppearance=”?android:attr/textAppearanceMedium”
android_layout_width=”fill_parent”
android_layout_height=”wrap_content”
android_id=”@+id/textView1″ />
<EditText
android_layout_width=”fill_parent”
android_layout_height=”wrap_content”
android_id=”@+id/editText1″
local:MvxBind=”Text SubTotal” />
<TextView
android_text=”Generosity”
android_textAppearance=”?android:attr/textAppearanceMedium”
android_layout_width=”fill_parent”
android_layout_height=”wrap_content”
android_id=”@+id/textView2″ />
<SeekBar
android_layout_width=”fill_parent”
android_layout_height=”wrap_content”
local:MvxBind=”Progress Generosity”
android_id=”@+id/seekBar1″ />
<TextView
android_text=”Tip”
android_textAppearance=”?android:attr/textAppearanceMedium”
android_layout_width=”fill_parent”
android_layout_height=”wrap_content”
android_id=”@+id/textView3″ />
<TextView
android_textAppearance=”?android:attr/textAppearanceMedium”
android_layout_width=”fill_parent”
android_layout_height=”wrap_content”
local:MvxBind=”Text Tip”
android_id=”@+id/textView4″ />
<TextView
android_text=”Total”
android_textAppearance=”?android:attr/textAppearanceMedium”
android_layout_width=”fill_parent”
android_layout_height=”wrap_content”
android_id=”@+id/textView5″ />
<TextView
android_textAppearance=”?android:attr/textAppearanceMedium”
android_layout_width=”fill_parent”
android_layout_height=”wrap_content”
local:MvxBind=”Text Total”
android_id=”@+id/textView6″ />
In this case we’re leveraging the xml extensions offered by MvvmCross in order to do the data binding using the MvxBind syntax.
Finally, let’s do iOS:
- Add the following code to the FirstView.cs
public override void ViewDidLoad()
{
base.ViewDidLoad();
// Perform any additional setup after loading the view
var label = new UILabel(new RectangleF(10, 0, 300, 40));
label.Text = “SubTotal”;
Add(label);
var subTotalTextField = new UITextField(new RectangleF(10, 40, 300, 40));
Add(subTotalTextField);
var label2 = new UILabel(new RectangleF(10, 80, 300, 40));
label2.Text = “Generosity?”;
Add(label2);
var slider = new UISlider(new RectangleF(10, 120, 300, 40));
slider.MinValue = 0;
slider.MaxValue = 100;
Add(slider);
var label3 = new UILabel(new RectangleF(10, 160, 300, 40));
label3.Text = “Tip”;
Add(label3);
var tipLabel = new UILabel(new RectangleF(10, 200, 300, 40));
Add(tipLabel);
var label4 = new UILabel(new RectangleF(10, 240, 300, 40));
label4.Text = “Total”;
Add(label4);
var totalLabel = new UILabel(new RectangleF(10, 280, 300, 40));
Add(totalLabel);
var set = this.CreateBindingSet<FirstView, FirstViewModel>();
set.Bind(subTotalTextField).To(vm => vm.SubTotal);
set.Bind(slider).To(vm => vm.Generosity);
set.Bind(tipLabel).To(vm => vm.Tip);
set.Bind(totalLabel).To(vm => vm.Total);
set.Apply();
}
In this case for iOS we again make use of the data binding support provided by MvvmCross by using the CreateBindingSet method, followed by a call to Bind for each element property we want to bind, and then finally a call to Apply to complete the setup of data binding.
And that’s it – a Tip Calculator for all three platforms.