The Core Project
EditMvvmCross applications normally consist on:
- A ‘Core’ project in the form of a .NET Core library, which will contain all the shared code (so you want to maximize the amount of code placed in this project). The Core will contain Models, ViewModels, Services, Converters, …
- One ‘Platform’ project per targeted platform. These projects will contain some framework initialization code, Views and SDK dependant code.
Normally, you start development from the Core project - and that’s exactly what we’ll do here.
Although it is recommended that you install any of the community made solution templates, we’ll use a blank solution on this tutorial.
Create the new .NET Core library
Using Visual Studio, create your new .NET Class Library project using the File |
New Project wizard. Choose .Net 7 as project framework. |
Call it something like TipCalc.Core
and name the solution TipCalc
.
Delete any auto-generated class
No-one really needs something like Class1.cs
:)
Install MvvmCross. Yey!
Open the Nuget Package Manager and search for the package MvvmCross
.
If you don’t really enjoy the NuGet UI experience, then you can alternatively open the Package Manager Console, and type:
Install-Package MvvmCross
Add the Tip Calculation Service
Create a folder called Services
.
Within this folder create a new interface, which will be used for calculating tips:
namespace TipCalc.Core.Services;
public interface ICalculationService
{
double TipAmount(double subTotal, int generosity);
}
Within the Services
folder now create an implementation for the interface:
namespace TipCalc.Core.Services;
public class CalculationService : ICalculationService
{
public double TipAmount(double subTotal, int generosity)
{
return subTotal * ((double)generosity)/100.0;
}
}
This provides us with some simple business logic for our app.
Add the ViewModel
At a sketch level, we want a user interface that:
- Uses our calculation service to calculate the tip
- Has inputs of:
- The current bill (the SubTotal)
- A feeling for how much tip we’d like to leave (the generosity)
- Has an output for the calculated tip to leave
To represent this user interface we need to build a ‘model’ for it. In other words, we need a ViewModel
.
Within MvvmCross, all ViewModels should inherit from MvxViewModel
.
So now let’s create a folder called ViewModels
, and inside of it a new class named TipViewModel
. This is what it should look like:
using MvvmCross.ViewModels;
using TipCalc.Core.Services;
namespace TipCalc.Core.ViewModels;
public class TipViewModel : MvxViewModel
{
readonly ICalculationService _calculationService;
public TipViewModel(ICalculationService calculationService)
{
_calculationService = calculationService;
}
public override async Task Initialize()
{
await base.Initialize();
_subTotal = 100;
_generosity = 10;
Recalculate();
}
private double _subTotal;
public double SubTotal
{
get => _subTotal;
set
{
_subTotal = value;
RaisePropertyChanged(() => SubTotal);
Recalculate();
}
}
private int _generosity;
public int Generosity
{
get => _generosity;
set
{
_generosity = value;
RaisePropertyChanged(() => Generosity);
Recalculate();
}
}
private double _tip;
public double Tip
{
get => _tip;
set
{
_tip = value;
RaisePropertyChanged(() => Tip);
}
}
private void Recalculate()
{
Tip = _calculationService.TipAmount(SubTotal, Generosity);
}
}
It is possible that this TipViewModel
will already make sense to you. If it does, then skip ahead to ‘Add the App(lication)’. If not, then here are some further explanations:
- the
TipViewModel
is constructed with anICalculationService
service, which is injected using the MvvmCross Dependency Injection engine.
readonly ICalculationService _calculationService;
public TipViewModel(ICalculationService calculationService)
{
_calculationService = calculationService;
}
- After construction, the
TipViewModel
runs theInitialize
method, which is part of the ViewModel lifecycle - during this it sets some initial values.
public override async Task Initialize()
{
await base.Initialize();
_subTotal = 100;
_generosity = 10;
Recalculate();
}
- The view data held within the
TipViewModel
is exposed through properties, where:- Each of these properties is backed by a private member variable
- Each of these properties has a getter and a setter
- All of the set accessors call
RaisePropertyChanged
to tell the baseMvxViewModel
that the data has changed - The
SubTotal
andGenerosity
set accessors also callRecalculate()
private double _subTotal;
public double SubTotal
{
get => _subTotal;
set
{
_subTotal = value;
RaisePropertyChanged(() => SubTotal);
Recalculate();
}
}
private int _generosity;
public int Generosity
{
get => _generosity;
set
{
_generosity = value;
RaisePropertyChanged(() => Generosity);
Recalculate();
}
}
private double _tip;
public double Tip
{
get => _tip;
set
{
_tip = value;
RaisePropertyChanged(() => Tip);
}
}
- The private
Recalculate
method uses the_calculationService
to updateTip
from the current values ofSubTotal
andGenerosity
.
private void Recalculate()
{
Tip = _calculationService.TipAmount(SubTotal, Generosity);
}
Add the App(lication) class
With our CalculationService
and our TipViewModel
defined, we now just need to add the main App
code. This code:
- Will sit in a single class within the root folder of our .NET Standard project.
- Will inherit from the
MvxApplication
class - Is normally just called
App
- Is responsible for providing:
- Registration of which interfaces and implementations the app uses
- Registration of which
ViewModel
theApp
will show when it starts
‘Registration’ here means creating an entry on the ‘Inversion of Control’ Container - IoC -. This record tells the IoC Container what to do when anything asks for an instance of the registered interface.
Our “Tip Calculation” App class will register the ICalculationService
as a dynamic service:
Mvx.IoCProvider.RegisterType<ICalculationService, CalculationService>();
The previous line tells the IoC Container that whenever any code requests an ICalculationService
reference, an object of type CalculationService
should be created and returned.
Also note that the single static class Mvx
acts as a single place for both registering and resolving interfaces and their implementations.
Within the App class we also decide that we want the app to start with the TipViewModel
:
RegisterAppStart<TipViewModel>();
The previous line tells the MvvmCross framework that TipViewModel
should be the first ViewModel / View pair that should appear on foreground when the app starts.
In summary, this is what App.cs should look like:
using MvvmCross.ViewModels;
using MvvmCross;
using TipCalc.Core.Services;
using TipCalc.Core.ViewModels;
namespace TipCalc.Core;
public class App : MvxApplication
{
public override void Initialize()
{
Mvx.IoCProvider.RegisterType<ICalculationService, CalculationService>();
RegisterAppStart<TipViewModel>();
}
}
Our Core project is complete :)
Just to recap the steps we’ve followed:
- We created a new .NET Standard project
- We added the MvvmCross libraries via NuGet
- We added a
ICalculationService
interface and implementation pair - We added a
TipViewModel
which:- Inherits from
MvxViewModel
- Declares a dependency on
ICalculationService
on its constructor - Presents a number of public properties each of which called
RaisePropertyChanged
internally
- Inherits from
- We added an
App
which:- Inherits from
MvxApplication
- Registers the
ICalculationService
/CalculationService
pair - Registers a ViewModel to use for when the app starts
- Inherits from
These are the same steps that you need to go through for every new MvvmCross application.
The next step is about building a first UI for our MvvmCross application.
Side note: What is ‘Inversion of Control’?
We won’t go into depth here about what IoC - Inversion of Control - is.
Instead, we will just say that:
- Within each MvvmCross application, there is a very special object, which is a
singleton
. - This
singleton
lives within theMvx.IoCProvider
singleton instance. - The application startup code can use the
Mvx.Register...
methods to letMvx.IoCProvider
know how to resolve certain requests during the lifetime of the app. - After the registration has been done, then when any code asks for an
interface
implementation, it can do that by using theMvx.IoCProvider.Resolve
methods.
One common pattern the app is also using is ‘constructor injection’:
- Our
TipViewModel
uses this pattern. - It presents a constructor like:
public TipViewModel(ICalculationService calculationService)
. - When the app is running a part of the MvvmCross framework called the
ViewModelLocator
is used to find and create ViewModels. - When a
TipViewModel
is needed, theViewModelLocator
uses a call toMvx.IocConstruct
to create one. - This
Mvx.IocConstruct
call creates theTipViewModel
using theICalculationService
implementation that it finds usingMvx.IoCProvider.Resolve
This is obviously only a very brief introduction.