In this section, you will learn how to use the Model-View-ViewModel (MVVM) architectural pattern.
Begin by creating a model class named DataPoint2d
, which will represent XY points on a plot. Follow these steps to create the model:
Arm64.MobileApp.XamarinForms
project and choose Add -> New Folder from the context menuNext, modify the newly created DataPoint2d.cs
file with the following code:
namespace Arm64.MobileApp.XamarinForms.Models
{
public class DataPoint2d
{
public double X { get; set; }
public double Y { get; set; }
}
}
In this section you will implement view models for the application. Begin by implementing the BaseViewModel, which will contain the re-usable common functionality for other view models. To implement the BaseViewModel you proceed as follows:
Arm64.MobileApp.XamarinForms
project and choose Add -> New Folder from the context menu.Next, modify the newly created file as follows:
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace Arm64.MobileApp.XamarinForms.ViewModels
{
public class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(propertyName));
}
protected void SetProperty<T>(ref T property,
T value, [CallerMemberName] string propertyName = "")
{
property = value;
OnPropertyChanged(propertyName);
}
}
}
The BaseViewModel
class uses the INotifyPropertyChanged interface for data binding, enabling associated views to be notified of changes in underlying properties. This ensures the views can update the content displayed to the user.
To implement the INotifyPropertyChanged
interface, the class must define the PropertyChanged event. Views associated with view models automatically subscribe to this event to update their controls accordingly.
Additionally, you have implemented two protected methods accessible in derived classes:
The next step involves implementing commands which act as event handlers for controls. Unlike event handlers that are framework-dependent, commands are independent and can be implemented directly in the view model. This allows the same command to be used in WPF, Xamarin, or .NET MAUI apps. To implement a command, create a class that implements the System.Windows.Input.ICommand interface, which comprises:
Below is a simple implementation of the ICommand interface in a new file, SimpleCommand.cs
, saved under the ViewModels folder:
using System;
using System.Windows.Input;
namespace Arm64.MobileApp.XamarinForms.ViewModels
{
public class SimpleCommand : ICommand
{
public event EventHandler CanExecuteChanged;
private readonly Action<object> action;
public SimpleCommand(Action<object> action)
{
this.action = action;
}
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
if (CanExecute(parameter))
{
action(parameter);
}
}
}
}
The SimpleCommand class includes the CanExecuteChanged event and implements the CanExecute and Execute methods from the ICommand interface. The Execute method first calls CanExecute. If it returns true, Execute invokes a method encapsulated in the action field of the SimpleCommand class. The action field, which implements the actual command logic, is set in the class constructor.
To illustrate this in practice, we implement the actual view model. Under the ViewModels folder, create a new file, MainViewModel.cs
, and implement it as follows:
using Arm64.MobileApp.XamarinForms.Helpers;
using Arm64.MobileApp.XamarinForms.Models;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Text;
using Xamarin.Forms;
namespace Arm64.MobileApp.XamarinForms.ViewModels
{
public class MainViewModel : BaseViewModel
{
private string computationTime = "";
public string ComputationTime
{
get => computationTime;
set => SetProperty(ref computationTime, value);
}
private readonly List<DataPoint2d> computationTimeHistory = new List<DataPoint2d>();
public ObservableCollection<DataPoint2d> DataPoints { get; set; } =
new ObservableCollection<DataPoint2d>();
private SimpleCommand runCalculationsCommand;
public SimpleCommand RunCalculationsCommand
{
get
{
if (runCalculationsCommand == null)
{
runCalculationsCommand = new SimpleCommand((object parameter) =>
{
try
{
var computationTime = PerformanceHelper.MeasurePerformance(
MatrixHelper.SquareMatrixMultiplication, executionCount: 10);
ComputationTime = $"Platform: {Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE")}" +
$"\nComputation time: {computationTime:f2} ms";
computationTimeHistory.Add(new DataPoint2d
{
X = computationTimeHistory.Count + 1,
Y = computationTime
});
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
});
}
return runCalculationsCommand;
}
}
private SimpleCommand plotResultsCommand;
public SimpleCommand PlotResultsCommand
{
get
{
if (plotResultsCommand == null)
{
plotResultsCommand = new SimpleCommand((object parameter) =>
{
// Clear DataPoints and update with new data on the UI thread
Device.BeginInvokeOnMainThread(() =>
{
DataPoints.Clear();
foreach (var point in computationTimeHistory)
{
DataPoints.Add(point);
}
});
});
}
return plotResultsCommand;
}
}
}
}
In MainViewModel, which derives from BaseViewModel, you have created a private field computationTime and its associated property ComputationTime. Notice how SetProperty from BaseViewModel is used to raise the PropertyChanged event, notifying the view of changes in the ComputationTime property.
This approach eliminates the need to manually rewrite values from the ComputationTime property to a specific property of a visual control (e.g., Label.Text). Instead, data binding is used, allowing the view to automatically receive notifications about source property changes. The ComputationTime in the view model is set using a command (RunCalculationsCommand), thereby decoupling the logic from the view without any explicit view references.
RunCalculationsCommand
is created using the constructor of the SimpleCommand class
, defining the action inline. The action involves calling PerformanceHelper.MeasurePerformance
to measure the execution time of ten invocations of the MatrixHelper.SquareMatrixMultiplication
method. The computation time is stored in a local variable and formatted into a string displaying the processor architecture (obtained using the PROCESSOR_ARCHITECTURE environment variable) and the computation time with a “ms” suffix. This string is then assigned to the ComputationTime property of the MainViewModel.
Additionally, RunCalculationsCommand
stores the computation time in the computationTimeHistory field, a list of DataPoint2d instances. Each instance represents an XY point, with X being the sequential number of the computation and Y being the computation time.
To plot the computation times, PlotResultsCommand
is used. This command does not involve any direct chart-related logic; instead, it transfers each element from computationTimeHistory
to the DataPoints property, an ObservableCollection
. As ObservableCollection
implements INotifyPropertyChanged
, it automatically notifies when items are added, updated, or removed, making it unnecessary to use BaseViewModel.SetProperty in this context.
In the final step, you need to connect the MainViewModel to the MainPage view. To do this, open MainPage.xaml
and add the following declarations:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Arm64.MobileApp.XamarinForms.MainPage"
xmlns:syncfusion="clr-namespace:Syncfusion.SfChart.XForms;assembly=Syncfusion.SfChart.XForms"
xmlns:viewModels="clr-namespace:Arm64.MobileApp.XamarinForms.ViewModels">
<ContentPage.BindingContext>
<viewModels:BaseViewModel/>
</ContentPage.BindingContext>
<!--Other declarations do not change-->
</ContentPage>
The first declaration:
xmlns:viewModels="clr-namespace:Arm64.MobileApp.XamarinForms.ViewModels"
adds the C# namespace to the XAML, enabling the use of C# types as XAML tags. Consequently, the second block:
<ContentPage.BindingContext>
<viewModels:MainViewModel />
</ContentPage.BindingContext>
creates an instance of the MainViewModel class and assigns it to the ContentPage.BindingContext property, which is utilized by the view for data binding.
Next, modify the Buttons section by adding bindings to RunCalculationsCommand
and PlotResultsCommand
:
<!--Buttons-->
<StackLayout Grid.Row="0"
Grid.Column="0"
Orientation="Vertical">
<Button Text="Run calculations"
Command="{Binding RunCalculationsCommand}"/>
<Button Text="Plot results"
Command="{Binding PlotResultsCommand}" />
</StackLayout>
Then, update the Label declaration to bind to the ComputationTime
property of the MainViewModel:
<!--Label-->
<Label Grid.Row="0"
Grid.Column="1"
Text="{Binding ComputationTime, Mode=OneWay}" />
Finally, modify the chart declarations so that the values displayed in the chart are sourced from the DataPoints property of the MainViewModel:
<syncfusion:LineSeries EnableAnimation="True"
Label="Computation time"
ItemsSource="{Binding DataPoints}"
XBindingPath="X"
YBindingPath="Y" />
With these modifications, the MainPage view will be fully connected to MainViewModel, enabling dynamic updates through data binding.
You can now run the application to demonstrate the performance differences between Arm64 and x64 architectures. First, you need to configure the application for both Arm64 and x64 platforms in Visual Studio. To do this, follow these steps:
After setting up both platforms, start by running the application using the x64 platform:
Now, launch the application using x64 platform. Click the Run calculations button several times, and then click the Plot results button. You should get the following:
Next, switch to the ARM64 platform and repeat the process:
You should notice shorter computation times on the ARM64 platform, demonstrating its performance advantage:
In this learning path, you developed an application using Xamarin.Forms, allowing it to run on multiple platforms while sharing the same codebase. The application included functionality to measure and compare computation times, specifically focusing on matrix multiplication performance. By running the application on both x64 and Arm64 platforms, you were able to observe and compare the performance. The results showed a noticeable performance advantage on the Arm64 architecture, highlighting its efficiency in handling computation-intensive tasks.