This application is designed to help you monitor your running. During training, it displays the distance covered, the current pulse, and calculates the average time you spend per 1 km (or 1 mile, depending on your settings). You can take a pause during your workout. At the end of your workout, the application shows a summary.
In version 1.0 it is not possible to change the age of the user or the unit of measurement of the distance (km/mi.). This can only be changed in code.
In the current version, the application does not save the collected data. There is no access to previous workouts.
It is planned to add more functionality to the application. If you have any ideas or comments about how the application works, please let us know!
When starting the application for the first time, you are asked to allow access to the device sensors and GPS location.
It is necessary for the proper operation of the application and in case of refusal of access, the following message is displayed and the application is closed as a result.
If permissions are granted, the application starts with Welcome Screen and is almost ready to be used.
The only thing to do before start workout is to make sure that the GPS location on your device is enabled.
In other case starting the workout will be blocked by the app.
Clicking OK button on the Welcome Screen switches to the Home Screen where you can start your workout by tapping anywhere on the screen if the GPS location is enabled.
GPS location is enabled and the workout can be started
GPS location is disabled and starting workout is blocked
The start of the workout is preceded by a countdown and after a few seconds the application displays a screen that will accompany you through the whole workout.
It provides some important workout related information, like:
The ongoing workout can be paused at any time by pressing the device back button. It is also paused when the GPS location is manually turned off during the workout.
The Pause Screen indicates that the application stops measuring the elapsed time and receiving data from the sensors.
GPS location is enabled and the workout can be resumed
GPS location is disabled and resuming workout is blocked
The workout can be resumed by pressing the device back button one more time, but only when the GPS location is enabled.
The Pause Screen also gives the opportunity to finish workout. Clicking the FINISH button displays notification that workout is completed
and switches the app to the workout Details Screen.
This screen displays the recorded workout parameters in a form of a scrolling list
which contains:
Clicking the OK button switches the app to the Home Screen, from where you can start another workout.
Please refer to Getting Started with Creating a .NET App to set up the C# application development environment for Tizen.
The application is written according to the MVVM pattern and the directory structure corresponds to the application layers.
In addition, there are two more directories with assets:
Application needs three specific privileges to be defined in
there privileges are:
~/tizen-manifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest package="org.tizen.example.Workout" version="1.0.0" api-version="4" xmlns="http://tizen.org/ns/packages">
<profile name="wearable" />
<ui-application appid="org.tizen.example.Workout" exec="Workout.dll" multiple="false" nodisplay="false" taskmanage="true" type="dotnet" launch_mode="single">
<label>Workout</label>
<icon>Workout.png</icon>
<metadata key="http://tizen.org/metadata/prefer_dotnet_aot" value="true" />
<background-category value="sensor" />
<background-category value="location" />
<splash-screens />
</ui-application>
<privileges>
<privilege>http://tizen.org/privilege/healthinfo</privilege>
<privilege>http://tizen.org/privilege/location</privilege>
<privilege>http://tizen.org/privilege/appmanager.launch</privilege>
</privileges>
<provides-appdefined-privileges />
<feature name="http://tizen.org/feature/sensor.heart_rate_monitor">true</feature>
<feature name="http://tizen.org/feature/location">true</feature>
<feature name="http://tizen.org/feature/location.gps">true</feature>
</manifest>
Application depends on following packages nuget packages:
ElottieSharp.Forms v0.9.5-preview Tizen.NET v5.0.0.14629 Tizen.Wearable.CircularUI v1.5.1
The PrivilegeManager service is used to ask for the permissions necessary to operate the application.
Method “PrivilegeCheck” is used to retrieve the permission status from the API. If the request has not been accepted or declined yet, the PrivacyPrivilegeManager.RequestPermission will display a prompt.
~/Services/Privilege/PrivilegeManager.cs
/// <summary>
/// Checks a selected privilege. Requests a privilege if not set.
/// </summary>
/// <param name="privilege">The privilege to check.</param>
private void PrivilegeCheck(string privilege)
{
switch (PrivacyPrivilegeManager.CheckPermission(privilege))
{
case CheckResult.Allow:
SetPermission(privilege, true);
break;
case CheckResult.Deny:
SetPermission(privilege, false);
break;
case CheckResult.Ask:
PrivacyPrivilegeManager.GetResponseContext(privilege)
.TryGetTarget(out PrivacyPrivilegeManager.ResponseContext context);
if (context != null)
{
context.ResponseFetched += PPM_RequestResponse;
}
PrivacyPrivilegeManager.RequestPermission(privilege);
break;
}
AllPrivilegesChecked();
}
Response for request is managed by handling the ResponseFetched event of context of the privilege.
~/Services/Privilege/PrivilegeManager.cs
/// <summary>
/// Handles privilege request response.
/// </summary>
/// <param name="sender">The object that raised the event.</param>
/// <param name="requestResponse">Request response data.</param>
private void PPM_RequestResponse(object sender, RequestResponseEventArgs requestResponse)
{
if (requestResponse.cause == CallCause.Answer)
{
switch (requestResponse.result)
{
case RequestResult.AllowForever:
SetPermission(requestResponse.privilege, true);
break;
case RequestResult.DenyForever:
case RequestResult.DenyOnce:
SetPermission(requestResponse.privilege, false);
break;
}
}
AllPrivilegesChecked();
}
The application uses the PrivilegeManager at the very beginning.
~/Workout.cs
/// <summary>
/// Handles creation phase of the forms application.
/// Loads Xamarin application.
/// </summary>
protected override void OnCreate()
{
base.OnCreate();
_privilegeManager.PrivilegesChecked += OnPrivilegesChecked;
_privilegeManager.CheckAllPrivileges();
_homeButtonService.HomeButtonKeyDown += OnHomeButtonKeyDown;
Display.StateChanged += OnDisplayStateChange;
}
Main part of application is started when all privileges have been checked.
~/Workout.cs
/// <summary>
/// Handles "PrivilegesChecked" event.
/// Loads Xamarin application.
/// </summary>
/// <param name="sender">The object that raised the event.</param>
/// <param name="args">Event arguments. Not used.</param>
private void OnPrivilegesChecked(object sender, EventArgs args)
{
_privilegeManager.PrivilegesChecked -= OnPrivilegesChecked;
LoadApplication(new App());
}
If at least one necessary permissions has been rejected, we assume that the application does not make sense and display the appropriate message.
~/App.xaml.cs
namespace Workout
{
/// <summary>
/// Main application class.
/// </summary>
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class App : Application
{
...
/// <summary>
/// Initializes application.
/// </summary>
public App()
{
InitializeComponent();
...
if (PrivilegeService.AllPermissionsGranted())
{
PageNavigationService.Instance.GoToWelcomePage();
}
else
{
PageNavigationService.Instance.GoToPrivilegeDeniedPage();
}
}
}
}
This part of the tutorial describes the way of retrieving workout related data to provide them to the application model. The application defines several service classes which are responsible for it. The role of each of them is described in the following subsections.
LocalTimeService is a static class which emits TimeChanged event to notify the application and allow it to display local time on selected application screens. For this purpose it uses System.Timers.Timer class with an interval set to 1000 milliseconds.
An instance of the Timer class is created when the _timer private field is declared.
~/Services/LocalTimeService.cs
/// <summary>
/// Internal timer instance.
/// </summary>
private static readonly Timer _timer = new Timer(1000);
In the LocalTimeService class constructor a handler of the Elapsed event is attached and the timer is started.
~/Services/LocalTimeService.cs
/// <summary>
/// Sets up event emitter and starts timer.
/// </summary>
static LocalTimeService()
{
_timer.Elapsed += OnElapsed;
_timer.Start();
}
The OnElapsed handler function emits the Updated event which provides an object that is set to the current date and time. It uses private variable storing information about previous value of date and time to emit the event only if the current value of minutes has changed.
~/Services/LocalTimeService.cs
/// <summary>
/// Handles "Elapsed" event of the timer.
/// Invokes "Updated" event if values of minutes have changed since last tick.
/// </summary>
/// <param name="sender">The object that raised the event.</param>
/// <param name="args">Event arguments. Not used.</param>
private static void OnElapsed(object sender, EventArgs args)
{
DateTime _now = DateTime.Now;
if (_previousDateTime.Minute != _now.Minute)
{
Updated?.Invoke(null, _now);
_previousDateTime = _now;
}
}
StopWatchService class inherits from the System.Diagnostic.Stopwatch class and allows the application to receive notifications about the time elapsed from the beginning of the workout.
For this purpose it uses System.Timers.Timer object
~/Services/StopWatchService.cs
/// <summary>
/// Internal timer instance.
/// </summary>
private readonly Timer _timer;
and initializes it with an interval set to 100 milliseconds.
~/Services/StopWatchService.cs
/// <summary>
/// Initializes class instance.
/// </summary>
public StopWatchService()
{
_timer = new Timer(100);
_timer.Elapsed += OnElapsed;
}
The OnElapsed function, which handles the Elapsed event of the created _timer, uses the Elapsed property of the System.Diagnostic.Stopwatch class to access value of the total elapsed time of the stopwatch instance.
~/Services/StopWatchService.cs
/// <summary>
/// Handles "Elapsed" event of the timer.
/// Invokes "Updated" event when value of seconds have changed.
/// </summary>
/// <param name="sender">The object that raised the event.</param>
/// <param name="args">Event arguments. Not used.</param>
private void OnElapsed(object sender, EventArgs args)
{
TimeSpan elapsed = Elapsed;
if (_previousElapsedTime.Seconds != elapsed.Seconds)
{
Updated?.Invoke(this, elapsed);
_previousElapsedTime = elapsed;
}
}
Information about elapsed time is emitted with the Updated event only if the value of elapsed seconds has changed.
To start or stop measuring the elapsed workout time the StopWatchService class defines two public methods.
~/Services/StopWatchService.cs
/// <summary>
/// Starts time measurement.
/// </summary>
public void StartMeasurement()
{
_timer.Start();
Start();
}
/// <summary>
/// Stops time measurement.
/// </summary>
public void StopMeasurement()
{
_timer.Stop();
Stop();
}
They use Start and Stop methods provided by the System.Timers.Timer object to start or stop notification about elapsed time. It also uses Start and Stop methods of the System.Diagnostic.Stopwatch object to start or stop the workout elapsed time measurement.
Heart rate value is one of the most important workout parameters, so the application defines HeartRateMonitorService class to obtain it. This service uses the Tizen.Sensor.HeartRateMonitor API which provides all functionality required to notify the app about heart rate changes.
When the HeartRateMonitorService is initialized an instance of the Tizen.Sensor.HeartRateMonitor class is created.
~/Services/HeartRateMonitorService.cs
/// <summary>
/// Initializes HeartRateMonitorService class.
/// Invokes NotSupported event if heart rate sensor is not supported.
/// </summary>
public void Init()
{
try
{
_hrm = new HRM
{
Interval = 1000,
PausePolicy = SensorPausePolicy.None
};
_hrm.DataUpdated += OnDataUpdated;
}
catch (Exception)
{
NotSupported?.Invoke(this, EventArgs.Empty);
}
}
The application sets the interval for the sensor data event to 1000 milliseconds. It also sets the PausePolicy property so that the sensor data are received even if the device screen is off as well as in power save mode. To handle heart rate changes the application attaches a handler function to the DataUpdated event which is called at the frequency of the set interval.
When OnDataUpdated handler function is executed, the current heart rate value is obtained form HeartRateMonitorDataUpdatedEventArgs parameter and emitted with DataUpdated event.
~/Services/HeartRateMonitorService.cs
/// <summary>
/// Handles "DataUpdated" event of the HeartRateMonitor object provided by the Tizen Sensor API.
/// Invokes "DataUpdated" event.
/// </summary>
/// <param name="sender">The object that raised the event.</param>
/// <param name="heartRateMonitorData">Heart rate monitor event data.</param>
private void OnDataUpdated(object sender, HeartRateMonitorDataUpdatedEventArgs heartRateMonitorData)
{
DataUpdated?.Invoke(this, heartRateMonitorData.HeartRate);
}
To start or stop receiving data from the HeartRateMonitor sensor, the HeartRateMonitorService class defines two public methods.
~/Services/HeartRateMonitorService.cs
/// <summary>
/// Starts measuring the heart rate.
/// </summary>
public void Start()
{
_hrm.Start();
}
/// <summary>
/// Stops measuring the heart rate.
/// </summary>
public void Stop()
{
_hrm.Stop();
}
They use Start and Stop methods provided by the Tizen.Sensor.HeartRateMonitor object.
To calculate average pace or distance traveled the application uses the LocationService class which provides GPS location related data. This is a singleton class and its one and only instance is available through the public static Instance property.
When the LocationService is initialized an instance of the Tizen.Location.Locator class is created.
~/Services/LocationService.cs
/// <summary>
/// Initializes LocationService class instance.
/// </summary>
private LocationService()
{
_locator = new Locator(LocationType.Hybrid)
{
Interval = _gpsCallbackInterval
};
AttachEvents();
}
The Tizen.Location.Locator class constructor takes one parameter which defines connection type used in acquiring location data. LocationType.Hybrid means that the device chooses the best connection method currently available, choosing from GPS, Passive and WiFi. The Interval property specifies the frequency the API will provide information about location changes.
AttachEvents method assigns handler functions of the Tizen.Location.Locator events used by the service.
~/Services/LocationService.cs
/// <summary>
/// Sets service listeners.
/// </summary>
private void AttachEvents()
{
_locator.ServiceStateChanged += (sender, args) => ServiceStateChanged?.Invoke(this, args.ServiceState);
_locator.LocationChanged += (sender, args) => LocationChanged?.Invoke(this, args.Location);
_locator.SettingChanged += (sender, args) => SettingChanged?.Invoke(this, args.IsEnabled);
}
Each of them invokes corresponding event to notify other application modules about occurring changes.
The SettingChanged event is invoked when the GPS location on the device is switched on or off by the user. In that case the application receives a boolean value indicating whether the GPS location is enabled on the device.
The ServiceStateChanged event is invoked when the state of receiving GPS signals changes. In that case the application is notified with a ServiceState enumeration value indicating whether the device receives GPS signals or not.
The LocationChanged event is invoked at intervals defined in the constructor and uses the Tizen.Location object to provide periodically information about updated device location.
To start or stop receiving locaction data, the LocationService class defines two public methods.
~/Services/LocationService.cs
/// <summary>
/// Starts Locator.
/// </summary>
public void Start()
{
_locator.Start();
}
/// <summary>
/// Stops Locator.
/// </summary>
public void Stop()
{
_locator.Stop();
}
They use Start and Stop methods provided by the Tizen.Location.Locator object.
Additionally, the LocationService defines public IsGPSLocationEnabled method, which allows at any time to check whether the GPS location on the device is on or off.
~/Services/LocationService.cs
/// <summary>
/// Returns information about GPS location state.
/// </summary>
public bool IsGPSLocationEnabled()
{
return LocatorHelper.IsEnabledType(LocationType.Hybrid);
}
The SettingsService provides information that is useful for location model class (to perform calculations) as well as for view classes (to display settings related content). In 1.0 version of the application the functionality of the service has been simplified, so that it returns only defined values and does not allow to change them. This is a singleton class and its one and only instance is available through the public static Instance property.
Distance
To provide information about distance settings the SettingsService defines _distanceDictionary private field being a dictionary of distance properties
~/Services/SettingsService.cs
/// <summary>
/// Dictionary of distance properties.
/// </summary>
private readonly Dictionary<DistanceUnit, Distance> _distanceDictionary;
which is initialized in the class constructor.
~/Services/SettingsService.cs
/// <summary>
/// Initializes class instance.
/// </summary>
public SettingsService()
{
_distanceDictionary = new Dictionary<DistanceUnit, Distance>
{
{DistanceUnit.Km, new Distance() { Unit = "km", UnitToKmRatio = 1 }},
{DistanceUnit.Mile, new Distance() { Unit = "mi.", UnitToKmRatio = 1.609344 }}
};
}
It allows access to defined settings depending on value provided by DistanceUnit enumerator.
~/Models/Settings/DistanceUnit.cs
namespace Workout.Models.Settings
{
/// <summary>
/// Enumerator that contains all distance units.
/// </summary>
public enum DistanceUnit
{
/// <summary>
/// Kilometer.
/// </summary>
Km,
/// <summary>
/// Mile.
/// </summary>
Mile
}
}
To access current settings of the distance properties the SettingsService defines Distance property
~/Services/SettingsService.cs
/// <summary>
/// Current distance properties.
/// </summary>
public Distance Distance => _distanceDictionary[DistanceUnit.Km];
which refers to the selected value from the _distanceDictionary, represented by an object of Distance class.
The Distance class defines two properties that describe distance related settings.
~/Models/Settings/Distance.cs
namespace Workout.Models.Settings
{
/// <summary>
/// Provides distance related data.
/// </summary>
public class Distance
{
#region properties
/// <summary>
/// Unit.
/// </summary>
public string Unit
{
get; set;
}
/// <summary>
/// Unit to kilometer ratio.
/// Defines how many times the unit is larger than 1 kilometer.
/// </summary>
public double UnitToKmRatio
{
get; set;
}
#endregion
}
}
The first one provides string value representing currently set distance unit, while the second provides double factor value specifying how many times the one distance unit is larger than one kilometer.
Age
The SettingsService defines also the Age property which is useful for heart rate model class while performing calculations to achieve additional heart rate related data.
~/Services/SettingsService.cs
/// <summary>
/// Current age.
/// </summary>
public int Age { get; } = 40;
For now it is simply a property initialized with a value, but later versions of the application will allow to modify it.
This part of the tutorial describes the stage of data processing so that they are ready to be used in the view model. It is divided into two subsections, one of which describes how data are collected, while the other describes how they are aggregated to the form in which they are made available for the view models.
Data receiving has been logically divided into smaller models, whose role is focused on the specific type of workout data. As a result, the application defines two separate models responsible for location and heart rate data respectively.
The LocationModel class is responsible for creating, maintaining and notifying changes of the location data represented by the LocationData class instance.
~/Models/LocationData.cs
namespace Workout.Models
{
/// <summary>
/// Location data class.
/// </summary>
public class LocationData
{
#region properties
/// <summary>
/// Distance in meters.
/// </summary>
public double Distance
{
get; set;
}
/// <summary>
/// GPS state.
/// </summary>
public bool IsGPSEnabled
{
get; set;
}
#endregion
}
}
This class defines two properties, the first of which is responsible for storing information about the distance traveled during workout, while the second maintains information whether the device is receiving GPS signals.
The LocationModel class defines _locationData private field for an instance of the LocationData class
~/Models/LocationModel.cs
/// <summary>
/// Current location data.
/// </summary>
private LocationData _locationData;
and initializes it in the class constructor when the SetInitialData method is executed.
~/Models/LocationModel.cs
/// <summary>
/// Sets starting data values.
/// </summary>
private void SetInitialData()
{
_locationData = new LocationData
{
Distance = 0,
IsGPSEnabled = false
};
}
/// <summary>
/// Initializes class instance.
/// Sets initial LocationData.
/// </summary>
public LocationModel()
{
_locationService = LocationService.Instance;
SetInitialData();
_locationService.LocationChanged += OnLocationChanged;
_locationService.ServiceStateChanged += OnServiceStateChanged;
}
It also uses LocationService class to handle LocationChanged and ServiceStateChanged events (described in the Retrieving data from API (Services) section), which provide location related data.
When the OnLocationChanged handler function is executed for the first time, the _canCalculateDistance flag is set to true and received location data is assigned to the _lastLocation private field.
~/Models/LocationModel.cs
/// <summary>
/// Handles "LocationChanged" event of LocationService.
/// Adds distance between location provided as event argument and previous location.
/// Triggers update event.
/// </summary>
/// <param name="sender">The object that raised the event.</param>
/// <param name="location">New location.</param>
private void OnLocationChanged(object sender, Location location)
{
if (_canCalculateDistance && !_isDistanceCalculationLocked)
{
_locationData.Distance += location.GetDistanceTo(_lastLocation) / SettingsService.Instance.Distance.UnitToKmRatio;
}
else
{
_canCalculateDistance = true;
}
_lastLocation = location;
EmitUpdate();
}
To correctly calculate the distance, the previous location data is necessary, therefore the calculation is possible only in subsequent calls to the event handler function if distance calculation is not locked by the _isDistanceCalculationLocked flag. The application uses the GetDistanceTo method of the Tizen.Location object provided by the LocationService, which takes the previous location data and returns the distance between the current and the specified location. Returned value, divided by Distance.UnitToKmRatio factor provided by the SettingsService, increases the Distance property of the instance of the LocationData class maintained by the model.
As it is described in the “Retrieving data from API (Services) > Location” subsection, the OnServiceStateChanged handler function is executed when the state of receiving GPS signals changes.
~/Models/LocationModel.cs
/// <summary>
/// Handles "ServiceStateChanged" event of LocationService.
/// Updates stored state.
/// Triggers update event.
/// </summary>
/// <param name="sender">The object that raised the event.</param>
/// <param name="serviceState">New service state.</param>
private void OnServiceStateChanged(object sender, ServiceState serviceState)
{
_locationData.IsGPSEnabled = serviceState == ServiceState.Enabled;
EmitUpdate();
}
It uses the Tizen.Location.ServiceState param provided with the event to determine the value of the IsGPSenabled property of the instance of the LocationData class maintained by the model.
Both handler functions, whenever are called, they execute private EmitUpdate method.
~/Models/LocationModel.cs
/// <summary>
/// Emits event with current location data.
/// </summary>
private void EmitUpdate()
{
Updated?.Invoke(this, new LocationUpdatedEventArgs(_locationData));
}
This method emits the Updated event to notify the application with current location data, provided as Data property of an instance of the LocationUpdatedEventArgs class.
~/Models/LocationUpdatedEventArgs.cs
using System;
namespace Workout.Models
{
/// <summary>
/// Event arguments class for location updated event.
/// Provides location data.
/// </summary>
public class LocationUpdatedEventArgs : EventArgs
{
#region properties
/// <summary>
/// Location data.
/// </summary>
public LocationData Data { get; }
#endregion
#region methods
/// <summary>
/// The class constructor.
/// </summary>
/// <param name="data">Location data.</param>
public LocationUpdatedEventArgs(LocationData data)
{
Data = data;
}
#endregion
}
}
LocationModel class provides several additional public methods to influence the flow of location data.
The Stop method
~/Models/LocationModel.cs
/// <summary>
/// Stops locator.
/// </summary>
public void Stop()
{
_locationService.Stop();
}
simply calls the Stop method of the LocationService class. It is executed at the end of a workout.
Two other methods LockMeasurement and UnlockMeasurement maintains the value of _isDistanceCalculationLocked flag.
~/Models/LocationModel.cs
/// <summary>
/// Unlocks distance measurement.
/// </summary>
public void UnlockMeasurement()
{
_isDistanceCalculationLocked = false;
}
/// <summary>
/// Locks distance measurement.
/// </summary>
public void LockMeasurement()
{
_isDistanceCalculationLocked = true;
}
The status of this flag determines whether Distance property of the LocationData class is updated when the LocationChanged event of the LocationService class is detected.
And the Reset method, which is used to reset the model before starting a new workout.
~/Models/LocationModel.cs
/// <summary>
/// Resets model data.
/// </summary>
public void Reset()
{
_canCalculateDistance = false;
_locationData.Distance = 0;
UnlockMeasurement();
}
It resets the _canCalculateDistance flag to ensure that the application will not try to calculate the distance when the OnLocationChanged event handler id called for the first time. It also sets the Distance property of the _locationData private field to 0 to start new distance measurement from the beginning. Additionally it calls UnlockMeasurement method, to be sure that the new distance measurement will not be locked.
The HeartRateMonitorModel class is responsible for creating, maintaining and notifying changes of the heart rate data represented by the HeartRateMonitorData class instance.
~/Models/HeartRateMonitorData.cs
namespace Workout.Models
{
/// <summary>
/// Heart rate monitor data class.
/// </summary>
public class HeartRateMonitorData
{
#region properties
/// <summary>
/// Bpm value.
/// </summary>
public int Bpm
{
get; set;
}
/// <summary>
/// Current Bpm range.
/// </summary>
public int BpmRange
{
get; set;
}
/// <summary>
/// Array of Bpm range occurrences.
/// </summary>
public int[] BpmRangeOccurrences
{
get; set;
}
/// <summary>
/// Bpm on scale. From 0 to 1.
/// </summary>
public double NormalizedBpm
{
get; set;
}
#endregion
}
}
The class defines four properties, each of which provides values used later by other model class responsible for aggregation data.
Bpm
Current heart rate as an integer value.
NormalizedBpm
Normalized value of Bpm expressed as a double value in the range from 0 to 1. It is calculated from the formula
~/Models/HeartRateMonitorModel.cs
double normalizedBpm = Math.Clamp((bpm - _minBpm) / (double)(_maxBpm - _minBpm), 0, 1);
on the basis of the current Bpm value as well as assumed _minBpm and _maxBpm values of Bpm calculated as follows
BpmRange
Current value of Bpm expressed as an integer value in the range from 0 to 5. It is calculated from the formula
~/Models/HeartRateMonitorModel.cs
int bpmRange = bpm < _minBpm ? 0 : Math.Min((int)((normalizedBpm * (_bpmRanges - 1)) + 1), _bpmRanges - 1);
where the _bpmRanges value is defined by private constant integer field set to 6.
**BpmRangeOccurrences**
A list that allows to count the occurrence of the BpmRange value. The application later uses it to quickly determine the most common BpmRange during training.
When the constructor of the HeartRateMonitorModel class is executed the application initializes mentioned above values of _maxBpm and _minBpm.
~/Models/HeartRateMonitorModel.cs
/// <summary>
/// HeartRateMonitorModel class constructor.
/// </summary>
public HeartRateMonitorModel()
{
_maxBpm = _haskellFoxConstant - SettingsService.Instance.Age;
_minBpm = _maxBpm / _maxToMinRatio;
_service = new HeartRateMonitorService();
_service.DataUpdated += OnServiceDataUpdated;
_service.NotSupported += OnServiceNotSupported;
InitializeBpmRangeOccurrences();
_service.Init();
}
It creates an instance of the HeartRateMonitorService class, assigns handlers to its DataUpdated and NotSupported events and initializes the service by calling its public Init method.
It also initializes value of the _bpmRangeOccurrences private property by calling the InitializeBmpRangeOccurrences private method.
~/Models/HeartRateMonitorModel.cs
/// <summary>
/// Initializes value of _bpmRangeOccurrences field.
/// </summary>
private void InitializeBpmRangeOccurrences()
{
_bpmRangeOccurrences = new int[_bpmRanges];
}
This value must be initialized before each workout. Updated value of this field is assigned later to the BpmRangeOccurrences property of the HeartRateMonitorData class.
When the OnServiceNotSupported handler is executed, the application invokes the NotSupported event to notify that heart rate sensor is not supported on the device.
~/Models/HeartRateMonitorModel.cs
/// <summary>
/// Handles "NotSupported" of the HeartRateMonitorService object.
/// Invokes "NotSupported" to other application's modules.
/// </summary>
/// <param name="sender">The object that raised the event.</param>
/// <param name="args">Event arguments. Not used.</param>
private void OnServiceNotSupported(object sender, EventArgs args)
{
NotSupported?.Invoke(this, EventArgs.Empty);
}
The current version of the application does not handle this event, but later versions will do it to perform certain actions when a lack of heart rate sensor support will be detected.
When the handler of the DataUpdated event is executed, the application uses provided Bpm value to calculate all other properties of the HeartRateMonitorData class.
~/Models/HeartRateMonitorModel.cs
/// <summary>
/// Handles "DataUpdated" of the HeartRateMonitorService object.
/// Invokes "Updated" to other application's modules.
/// </summary>
/// <param name="sender">The object that raised the event.</param>
/// <param name="bpm">Heart rate value.</param>
private void OnServiceDataUpdated(object sender, int bpm)
{
double normalizedBpm = Math.Clamp((bpm - _minBpm) / (double)(_maxBpm - _minBpm), 0, 1);
int bpmRange = bpm < _minBpm ? 0 : Math.Min((int)((normalizedBpm * (_bpmRanges - 1)) + 1), _bpmRanges - 1);
if (!_isMeasurementPaused)
{
_bpmRangeOccurrences[bpmRange]++;
}
Updated?.Invoke(this, new HeartRateMonitorUpdatedEventArgs(new HeartRateMonitorData
{
Bpm = bpm,
BpmRange = bpmRange,
BpmRangeOccurrences = _bpmRangeOccurrences,
NormalizedBpm = normalizedBpm
}));
}
When the heart rate measurement is paused, what is indicated by the _isMeasurementPaused flag, the application does not update the _bpmRangeOccurrences list.
When all possible calculations are done, the Updated event is invoked to notify the application with current heart rate monitor data, provided as Data property of an instance of the HeartRateMonitorUpdatedEventArgs class.
~/Models/HeartRateMonitorUpdatedEventArgs.cs
using System;
namespace Workout.Models
{
/// <summary>
/// Event arguments class for heart rate monitor updated event.
/// Provides heart rate monitor data.
/// </summary>
public class HeartRateMonitorUpdatedEventArgs : EventArgs
{
#region properties
/// <summary>
/// Heart rate monitor data.
/// </summary>
public HeartRateMonitorData Data { get; }
#endregion
#region methods
/// <summary>
/// The class constructor.
/// </summary>
/// <param name="data">Heart rate monitor data.</param>
public HeartRateMonitorUpdatedEventArgs(HeartRateMonitorData data)
{
Data = data;
}
#endregion
}
}
HeartRateMonitorModel class provides several additional public methods to influence the flow of heart rate data.
The Pause method
~/Models/HeartRateMonitorModel.cs
/// <summary>
/// Pauses HRM measurement.
/// </summary>
public void Pause()
{
_isMeasurementPaused = true;
}
which is used to pause the heart rate measurement when the current workout is paused. It sets the _isMeasurementPaused flag to prevent updating the _bpmRangeOccurrences list when the DataUpdated event of the HeartRateMonitorService class is handled.
The Start method
~/Models/HeartRateMonitorModel.cs
/// <summary>
/// Starts notification about changes of heart rate value.
/// </summary>
public void Start()
{
_isMeasurementPaused = false;
_service.Start();
}
which is used to start the heart rate measurement when the workout is started or resumed. It resets the _isMeasurementPaused flag and executes the Start method of the HeartRateMonitorService class.
The Stop method
~/Models/HeartRateMonitorModel.cs
/// <summary>
/// Stops notification about changes of heart rate value.
/// </summary>
public void Stop()
{
_service.Stop();
}
which is used to stop the heart rate measurement when the workout is finished. It executes the Stop method of the HeartRateMonitorService class.
And the Reset method
~/Models/HeartRateMonitorModel.cs
/// <summary>
/// Resets HRM measurement.
/// </summary>
public void Reset()
{
InitializeBpmRangeOccurrences();
}
which is used to reset the heart rate measurement when the workout is cleared. It executes the InitializeBmpRangeOccurrences private method.
To aggregate all collected workout related data to the form in which they are made available for the view models, the application defines the WorkoutModel class. This is a singleton class and its one and only instance is available through the public static Instance property.
It creates, maintains and notifies changes of the workout data property
~/Models/Workout/WorkoutModel.cs
/// <summary>
/// Gets workout data.
/// </summary>
public WorkoutData WorkoutData { get; private set; }
represented by the WorkoutData class instance.
~/Models/Workout/WorkoutData.cs
using System;
namespace Workout.Models.Workout
{
/// <summary>
/// Workout data class.
/// </summary>
public class WorkoutData
{
#region properties
/// <summary>
/// Start time.
/// </summary>
public DateTime StartTime
{
get; set;
}
/// <summary>
/// Local time.
/// </summary>
public DateTime LocalTime
{
get; set;
}
/// <summary>
/// Beats per minute.
/// </summary>
public int Bpm
{
get; set;
}
/// <summary>
/// Current Bpm range.
/// </summary>
public int BpmRange
{
get; set;
}
/// <summary>
/// Array of Bpm range occurrences.
/// </summary>
public int[] BpmRangeOccurrences
{
get; set;
}
/// <summary>
/// Bpm converted to value from 0 to 1 for current Bpm range.
/// </summary>
public double NormalizedBpm
{
get; set;
}
/// <summary>
/// Distance.
/// </summary>
public double Distance
{
get; set;
}
/// <summary>
/// Distance unit.
/// </summary>
public string DistanceUnit
{
get; set;
}
/// <summary>
/// Average time for 1 distance unit.
/// </summary>
public double Pace
{
get; set;
}
/// <summary>
/// GPS state.
/// </summary>
public bool IsGPSEnabled
{
get; set;
}
/// <summary>
/// Stop watch.
/// </summary>
public TimeSpan ElapsedTime
{
get; set;
}
#endregion
}
}
The class contains properties providing all workout related data, which are later used by the main application view model.
When the constructor of the WorkoutModel class is executed
~/Models/Workout/WorkoutModel.cs
/// <summary>
/// Initializes class instance.
/// </summary>
private WorkoutModel()
{
SetInitialData();
_heartRateMonitorModel = new HeartRateMonitorModel();
_locationModel = new LocationModel();
_stopWatchService = new StopWatchService();
AddEventListeners();
}
the application initializes WorkoutData property by calling the SetInitialData private method.
~/Models/Workout/WorkoutModel.cs
/// <summary>
/// Sets starting data values.
/// </summary>
private void SetInitialData()
{
WorkoutData = new WorkoutData
{
Bpm = 0,
BpmRange = 0,
Distance = 0,
DistanceUnit = SettingsService.Instance.Distance.Unit,
ElapsedTime = default(TimeSpan),
IsGPSEnabled = false,
StartTime = DateTime.Now,
LocalTime = DateTime.Now,
Pace = 0,
NormalizedBpm = 0
};
}
It also creates instances of model classes responsible for collecting the workout related data, creates an instance of the StopWatchService class and executes the AddEventListeners method to attach all necessary event handlers.
~/Models/Workout/WorkoutModel.cs
/// <summary>
/// Sets up events listeners.
/// </summary>
private void AddEventListeners()
{
LocalTimeService.Updated += OnLocalTimeServiceUpdated;
_locationModel.Updated += OnLocationModelUpdated;
_heartRateMonitorModel.Updated += OnHeartRateMonitorModelUpdated;
_stopWatchService.Updated += OnStopWatchServiceUpdated;
}
Each assigned handler function uses the data provided with the event to update specific properties of the WorkoutData class instance stored by the WorkoutData property.
When the Updated event of the LocalTimeService is triggered the OnLocalTimeServiceUpdated handler is executed.
~/Models/Workout/WorkoutModel.cs
/// <summary>
/// Handles "Updated" event of the LocalTimeService class.
/// Updates the value of the local time property.
/// </summary>
/// <param name="sender">The object that raised the event.</param>
/// <param name="localTime">Local time.</param>
private void OnLocalTimeServiceUpdated(object sender, DateTime localTime)
{
WorkoutData.LocalTime = localTime;
EmitUpdate();
}
It uses information about the local time provided by the LocalTimeService to update the LocalTime property of the WorkoutData class instance.
When the Updated event of the LocationModel is triggered the OnLocationModelUpdated handler is executed.
~/Models/Workout/WorkoutModel.cs
/// <summary>
/// Handles "Updated" event of the LocationModel object.
/// Updates the value of distance.
/// </summary>
/// <param name="sender">The object that raised the event.</param>
/// <param name="args">Updated values from location model.</param>
private void OnLocationModelUpdated(object sender, LocationUpdatedEventArgs args)
{
WorkoutData.Distance = args.Data.Distance;
WorkoutData.IsGPSEnabled = args.Data.IsGPSEnabled;
EmitUpdate();
}
It uses information provided by the Data property of the LocationUpdatedEventArgs object to update the Distance and IsGPSEnabled properties of the WorkoutData class instance.
When the Updated event of the HeartRateMonitorModel is triggered the OnHeartRateMonitorModelUpdated handler is executed.
~/Models/Workout/WorkoutModel.cs
/// <summary>
/// Handles "Updated" event of the HeartRateMonitorModel object.
/// Updates the value of the bpm property.
/// </summary>
/// <param name="sender">The object that raised the event.</param>
/// <param name="args">Updated values from heart rate monitor model.</param>
private void OnHeartRateMonitorModelUpdated(object sender, HeartRateMonitorUpdatedEventArgs args)
{
WorkoutData.Bpm = args.Data.Bpm;
WorkoutData.BpmRange = args.Data.BpmRange;
WorkoutData.BpmRangeOccurrences = args.Data.BpmRangeOccurrences;
WorkoutData.NormalizedBpm = args.Data.NormalizedBpm;
EmitUpdate();
}
It uses information provided by the Data property of the HeartRateMonitorUpdatedEventArgs to update the Bpm, BpmRange, BpmRangeOccurrences and NormalizedBpm properties of the WorkoutData class instance.
When the Updated event of the StopWatchService is triggered the OnStopWatchServiceUpdated handler is executed.
~/Models/Workout/WorkoutModel.cs
/// <summary>
/// Handles "Updated" event of the StopWatchService object.
/// Calculates the value of average time per one distance unit.
/// </summary>
/// <param name="sender">The object that raised the event.</param>
/// <param name="elapsedTime">Elapsed time.</param>
private void OnStopWatchServiceUpdated(object sender, TimeSpan elapsedTime)
{
WorkoutData.ElapsedTime = elapsedTime;
if (WorkoutData.Distance > 0)
{
WorkoutData.Pace = WorkoutData.ElapsedTime.TotalMinutes / WorkoutData.Distance * 1000;
}
EmitUpdate();
}
It uses information about elapsed wotkout time provided by the StopWatchService to update the ElapsedTime and Pace properties of the WorkoutData class instance.
No matter which of these events is handled, the application calls the EmitUpdate private method.
~/Models/Workout/WorkoutModel.cs
/// <summary>
/// Emits event with current workout data.
/// </summary>
private void EmitUpdate()
{
Updated?.Invoke(this, new WorkoutUpdatedEventArgs(WorkoutData));
}
This method emits the Updated event to notify the application with current workout data, provided as Data property of an instance of the WorkoutUpdatedEventArgs class.
~/Models/Workout/WorkoutUpdatedEventArgs.cs
using System;
namespace Workout.Models.Workout
{
/// <summary>
/// Event arguments class for workout updated event.
/// Provides workout data.
/// </summary>
public class WorkoutUpdatedEventArgs : EventArgs
{
#region properties
/// <summary>
/// Workout data.
/// </summary>
public WorkoutData Data { get; }
#endregion
#region methods
/// <summary>
/// The class constructor.
/// </summary>
/// <param name="data">Workout data.</param>
public WorkoutUpdatedEventArgs(WorkoutData data)
{
Data = data;
}
#endregion
}
}
WorkoutModel class provides several additional public methods to influence the flow of workout data.
The StartWorkout method executed to start the workout.
~/Models/Workout/WorkoutModel.cs
/// <summary>
/// Starts workout.
/// </summary>
public void StartWorkout()
{
_heartRateMonitorModel.Start();
_locationModel.UnlockMeasurement();
_stopWatchService.StartMeasurement();
}
It starts notification about changes of the heart rate value by calling the Start method of the HeartRateMonitorModel class. It also executes the UnlockMeasurement method of the LoactionModel to ensure that the distance traveled during the workout is calculated properly. Finally, it calls the StartMeasurement method of the StopWatchService to start measurement of the elapsed time of the workout.
The PauseWorkout method executed to pause the current workout.
~/Models/Workout/WorkoutModel.cs
/// <summary>
/// Pauses workout.
/// Emits "Paused" event.
/// </summary>
public void PauseWorkout()
{
_heartRateMonitorModel.Pause();
_locationModel.LockMeasurement();
_stopWatchService.StopMeasurement();
Paused?.Invoke(this, EventArgs.Empty);
}
It pauses the heart rate related measurement by calling the Pause method of the HeartRateMonitorModel class. It also executes the LockMeasurement method of the LoactionModel to lock the calculation of the distance traveled. Then, it calls the StopMeasurement method of the StopWatchService to stop measurement of the elapsed time of the workout. Finally, it invokes the Paused event to notify the application that the workout is paused.
The FinishWorkout method executed to finish the current workout.
~/Models/Workout/WorkoutModel.cs
/// <summary>
/// Finishes workout. Stops models providing data.
/// </summary>
public void FinishWorkout()
{
_heartRateMonitorModel.Stop();
_locationModel.Stop();
}
It stops notification about changes of the heart rate value by calling the Stop method of the HeartRateMonitorModel class. It also executes the Stop method of the LoactionModel to stop notification about location changes.
And the ClearWorkout method executed to clear all data related to the previous workout
~/Models/Workout/WorkoutModel.cs
/// <summary>
/// Clears workout.
/// </summary>
public void ClearWorkout()
{
_stopWatchService.Reset();
_locationModel.Reset();
_heartRateMonitorModel.Reset();
SetInitialData();
}
It resets value of the elapsed time of the workout by calling the Reset method of the StopWatchService class. It also resets the measurement data of the LocationModel and HeartRateMonitorModel classes by calling their Reset methods. Finally it executes the SetInitialData private method to reinitialize the WorkoutData property.
This part of the tutorial describes the way of providing data to the view through the binding mechanism so that the view can put data on the screen. This is the primary responsibility of the view model layer, but not the only one. It exposes methods, commands and takes responsibility for navigation logic to help maintain the state of the view. It also manipulates the model as a result of user actions on the view.
The application defines several view model classes and each of them is responsible for operating a dedicated view. For simplicity, this tutorial focuses only on two application’s view model classes. The MainViewModel class, providing data for the main view of the application, displayed during the workout. And the DetailsPageViewModel class providing data for the details page view, displayed when the workout is finished.
The mechanism of data binding is well described in the How to create basic data binding tutorial. In simple words, it allows to connect two properties of objects so that change in one object causes change in the other. The view model classes operating on data prepared by the models provide properties that are binded to the properties of view elements. They must implement the INotifyPropertyChanged interface providing the PropertyChanged event of type PropertyChangedEventHandler, which is triggered in order to notify view properties about changes.
In the case of the Workout app, each view model class, that exposes properties for views, inherits from the BaseViewModel class.
~/ViewModels/BaseViewModel.cs
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace Workout.ViewModels
{
/// <summary>
/// Class that provides basic functionality for view models.
/// </summary>
public class BaseViewModel : INotifyPropertyChanged
{
#region properties
/// <summary>
/// PropertyChanged event handler.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
#endregion
#region methods
/// <summary>
/// Updates value of the "storage" argument with value given by the second argument.
/// Notifies the application about update of the property which has executed this method.
/// </summary>
/// <typeparam name="T">Type of storage field.</typeparam>
/// <param name="storage">Field reference.</param>
/// <param name="value">New value.</param>
/// <param name="propertyName">Name of property triggered change.</param>
/// <returns>Returns true if storage is successfully updated, false otherwise.</returns>
protected bool SetProperty<T>(ref T storage, T value,
[CallerMemberName] string propertyName = null)
{
if (Equals(storage, value))
{
return false;
}
storage = value;
OnPropertyChanged(propertyName);
return true;
}
/// <summary>
/// Notifies the application about update of the property with name given as a parameter.
/// </summary>
/// <param name="propertyName">Property name.</param>
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
}
This class implements the INotifyPropertyChanged interface so it can use the event of type PropertyChangedEventHandler, which is triggered when the SetProperty method is executed. This method takes the reference to the element to be updated and a new value, assigns the new value to the element and invokes the PropertyChanged event passing the name of the property that executed it.
The application view model classes are initialized when the corresponding views are created. While the application is running, the displayed views can change frequently, which means that the same view model classes can be initialized multiple times. While this is not problematic when it comes to accessing the WorkoutModel model class or some services, as these are designed as singletons, it can be problematic when it comes to releasing resources reserved by view models, such as event handler functions.
Each of the application view model class, which may encounter the problem described above, implement the IDisposable interface and use the dispose pattern, which allows to remove event listeners by calling the public Dispose method. The following code shows the implementation of the dispose pattern, based on an example of the MainViewModel class.
~/ViewModels/Workout/MainViewModel.cs
#region methods
...
/// <summary>
/// Releases all resources currently used by this instance.
/// </summary>
/// <param name="disposing">
/// True if managed resources should be disposed, false otherwise.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
RemoveEventListeners();
_disposed = true;
}
/// <summary>
/// Destroys the current object.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Destroys the MainViewModel object.
/// </summary>
~MainViewModel()
{
Dispose(false);
}
#endregion
As mentioned above, the MainViewModel class provides data to the main application view which is displayed during the workout. It displays all the real-time generated and continuously changed data related to the current workout. The role of the MainViewModel class of the view model is to prepare the data so that binded to the view may reflect the current values.
For this purpose, the class defines a set of public properties whose values are modified according to the data provided by the model as an object of the WorkoutData class.
~/ViewModels/Workout/MainViewModel.cs
#region properties
/// <summary>
/// Gets or sets beats per minute.
/// </summary>
public int Bpm
{
get => _bpm;
set => SetProperty(ref _bpm, value);
}
/// <summary>
/// Gets or sets local time.
/// </summary>
public DateTime LocalTime
{
get => _localTime;
set => SetProperty(ref _localTime, value);
}
/// <summary>
/// Total distance.
/// </summary>
public string Distance
{
get => _distance;
set => SetProperty(ref _distance, value);
}
/// <summary>
/// GPS state.
/// </summary>
public bool IsGPSEnabled
{
get => _isGPSEnabled;
set => SetProperty(ref _isGPSEnabled, value);
}
/// <summary>
/// Heart rate indicator image.
/// </summary>
public string HeartRateIndicatorImage
{
get => _heartRateIndicatorImage;
set => SetProperty(ref _heartRateIndicatorImage, value);
}
/// <summary>
/// Heart rate indicator rotation.
/// </summary>
public string HeartRateIndicatorRotation
{
get => _heartRateIndicatorRotation;
set => SetProperty(ref _heartRateIndicatorRotation, value);
}
/// <summary>
/// Distance unit.
/// </summary>
public string DistanceUnit
{
get => _distanceUnit;
set => SetProperty(ref _distanceUnit, value);
}
/// <summary>
/// Average time for 1 distance unit.
/// </summary>
public string Pace
{
get => _pace;
set => SetProperty(ref _pace, value);
}
/// <summary>
/// Gets or sets elapsed time.
/// </summary>
public TimeSpan ElapsedTime
{
get => _elapsedTime;
set => SetProperty(ref _elapsedTime, value);
}
#endregion
Each of them, when it changes its value, uses inherited SetProperty method, which allows you to notify view classes about occurring changes.
Most of them correspond exactly to the data provided in the WorkoutData object, such as:
There are also two:
the values of which are determined by other values.
When the constructor of the ManViewModel class is executed, it access the instances of the WorkoutModel and LocationService classes
~/ViewModels/Workout/MainViewModel.cs
/// <summary>
/// Initializes class instance.
/// </summary>
public MainViewModel()
{
_workoutModel = WorkoutModel.Instance;
_locationService = LocationService.Instance;
UpdateWorkoutProperties(_workoutModel.WorkoutData);
AddEventListeners();
}
and initializes all properties with initial workout data by calling the UpdateWorkoutProperties private method.
~/ViewModels/Workout/MainViewModel.cs
/// <summary>
/// Updates values of workout properties.
/// </summary>
/// <param name="data">Workout data.</param>
private void UpdateWorkoutProperties(WorkoutData data)
{
LocalTime = data.LocalTime;
Distance = (data.Distance / 1000).ToString("F2");
DistanceUnit = data.DistanceUnit;
Pace = data.Pace.ToString("F2");
Bpm = data.Bpm;
ElapsedTime = data.ElapsedTime;
IsGPSEnabled = data.IsGPSEnabled;
HeartRateIndicatorImage = "images/hrm_indicator/" + data.BpmRange.ToString() + ".png";
HeartRateIndicatorRotation = ((1 - data.NormalizedBpm) * 180).ToString();
}
It also executes the AddEventListener private method to access data
~/ViewModels/Workout/MainViewModel.cs
/// <summary>
/// Sets up events listeners.
/// </summary>
private void AddEventListeners()
{
_workoutModel.Updated += OnWorkoutModelUpdated;
_workoutModel.Paused += OnWorkoutModelPaused;
_locationService.SettingChanged += OnLocationServiceSettingChanged;
}
provided with the Updated, Paused and SettingsChanged events.
When the OnWorkoutModelUpdated event handler is executed
~/ViewModels/Workout/MainViewModel.cs
/// <summary>
/// Handles "Updated" event of the WorkoutModel object.
/// Updates values of workout properties.
/// </summary>
/// <param name="sender">The object that raised the event.</param>
/// <param name="args">Event arguments.</param>
private void OnWorkoutModelUpdated(object sender, WorkoutUpdatedEventArgs args)
{
UpdateWorkoutProperties(args.Data);
}
the application takes the WorkoutData object provided as Data property of the WorkoutUpdatedEcentArgs parameter and uses it to update workout properties by calling the UpdateWorkoutProperties.
When the OnLocationServiceSettingChanged event handler is executed
~/ViewModels/Workout/MainViewModel.cs
/// <summary>
/// Handles "SettingChanged" event of LocationService.
/// Navigates to pause page if GPS location is disabled.
/// </summary>
/// <param name="sender">The object that raised the event.</param>
/// <param name="locationState">Location state.</param>
private void OnLocationServiceSettingChanged(object sender, bool locationState)
{
if (!locationState)
{
PauseWorkout();
}
}
the application checks the value of the locationState parameter indicating whether the GPS location is enabled on the device and if it is not, it executes the PauseWorkout public method.
~/ViewModels/Workout/MainViewModel.cs
/// <summary>
/// Pauses workout.
/// </summary>
public void PauseWorkout()
{
_workoutModel.PauseWorkout();
}
This method uses the reference to the WorkoutModel instance to pause the current workout by calling its PauseWorkout method.
When the workout becomes paused the Paused event of the WorkoutModel class is triggered and the OnWorkoutModelPaused method is executed.
~/ViewModels/Workout/MainViewModel.cs
/// <summary>
/// Handles "Paused" event of the WorkoutModel object.
/// Navigates to pause page.
/// </summary>
/// <param name="sender">The object that raised the event.</param>
/// <param name="args">Event arguments. Not used.</param>
private void OnWorkoutModelPaused(object sender, EventArgs args)
{
PageNavigationService.Instance.GoToWorkoutPausePage();
}
This method uses the PageNavigationService to navigate the app to the WorkoutPausePageView. This is because the application allows to continue the workout only if the GPS location on the device is enabled.
One more worth mentioning is the public StartWorkout method.
~/ViewModels/Workout/MainViewModel.cs
/// <summary>
/// Starts workout.
/// </summary>
public void StartWorkout()
{
_workoutModel.StartWorkout();
}
This method uses the reference to the WorkoutModel instance to start the workout by calling its StartWorkout method.
The DetailsPageViewModel class provides data to the details page view which is displayed when the workout is finished. This page shows a list containing the summary data related to the already finished workout. Additionally, the header of this list displays the current time as well as the date of the workout. The role of the DetailsPageViewModel class of the view model is to prepare the data sources for both the header and the content of the list displayed on the target view.
For this purpose, the class defines several public properties.
~/ViewModels/Workout/DetailsPageViewModel.cs
/// <summary>
/// Workout details header data.
/// </summary>
public DetailsHeaderData HeaderData
{
get;
set;
}
/// <summary>
/// Workout elapsed time.
/// </summary>
public string ElapsedTime
{
get;
set;
}
/// <summary>
/// Workout distance.
/// </summary>
public string Distance
{
get;
set;
}
/// <summary>
/// Workout average pace.
/// </summary>
public string AveragePace
{
get;
set;
}
/// <summary>
/// Workout intensity.
/// </summary>
public string Intensity
{
get;
set;
}
The HeaderData property provides an instance of the DetailsHeaderData object, which contains data for the header displayed on the details page view.
~/Models/Workout/DetailsHeaderData.cs
using System;
using Xamarin.Forms;
namespace Workout.Models.Workout
{
/// <summary>
/// Details header data class.
/// </summary>
public class DetailsHeaderData : BindableObject
{
#region properties
/// <summary>
/// Bindable property definition for local time.
/// </summary>
public static readonly BindableProperty LocalTimeProperty =
BindableProperty.Create("LocalTime", typeof(DateTime), typeof(DetailsHeaderData), default(DateTime));
/// <summary>
///Header title.
/// </summary>
public string Title
{
get;
set;
}
/// <summary>
///Start time.
/// </summary>
public DateTime StartTime
{
get;
set;
}
/// <summary>
/// Local time.
/// </summary>
public DateTime LocalTime
{
get => (DateTime)GetValue(LocalTimeProperty);
set => SetValue(LocalTimeProperty, value);
}
#endregion
}
}
It inherits from the BindableObject class so that it can notify about changes of selected properties, which can be updated while the details view is displayed. In this case, such a property is the LocalTime, which provides data for displaying the current time. The Title property provides a string used as the list header title, while the StartTime property allows the application do display the start workout date.
Other properties provide details of the completed workout, such as elapsed time, distance traveled, average pace and workout intensity.
When the constructor of the DetailsPageViewModel class is executed, the application access an instance of the WorkoutModel, to finish the workout by calling the FinishWorkout method as well as to get the data related to the already finished workout.
~/ViewModels/Workout/DetailsPageViewModel.cs
/// <summary>
/// Initializes class instance.
/// </summary>
public DetailsPageViewModel()
{
_workoutModel = WorkoutModel.Instance;
_workoutModel.FinishWorkout();
WorkoutData workoutData = _workoutModel.WorkoutData;
HeaderData = new DetailsHeaderData
{
StartTime = workoutData.StartTime,
LocalTime = workoutData.LocalTime,
Title = "Workout\ndetails"
};
int[] bpmRangeOccurrences = workoutData.BpmRangeOccurrences;
ElapsedTime = string.Format("{0:hh\\.mm\\\'ss}", workoutData.ElapsedTime);
Distance = (workoutData.Distance / 1000).ToString("F2") + " " + SettingsService.Instance.Distance.Unit;
AveragePace = workoutData.Pace.ToString("F2") + " min/" + SettingsService.Instance.Distance.Unit;
Intensity = Array.LastIndexOf(bpmRangeOccurrences, bpmRangeOccurrences.Max()).ToString();
InitCommands();
AddEventListeners();
}
Using obtained data it initializes values of HeaderData and other properties being a data sources for the target view.
It also executes two additional methods, InitCommands and AddEventListeners.
The first one initializes the FinishCommand, which execution is handled to call the public Finish method.
~/ViewModels/Workout/DetailsPageViewModel.cs
#region methods
/// <summary>
/// Initializes commands.
/// </summary>
private void InitCommands()
{
FinishCommand = new Command(ExecuteFinish);
}
/// <summary>
/// Handles execution of "FinishCommand".
/// Finishes workout.
/// </summary>
private void ExecuteFinish()
{
Finish();
}
...
/// <summary>
/// Finishes workout.
/// Clears workout data and navigates to home page.
/// </summary>
public void Finish()
{
_workoutModel.ClearWorkout();
PageNavigationService.Instance.GoToHomePage();
}
The Finish method clears all the workout related information by calling the ClearWorkout method of the WorkoutModel and uses the PageNavigationService class to switch the app view to the home page.
The second one assigns a handler of the Updated event of the WorkoutModel class.
~/ViewModels/Workout/DetailsPageViewModel.cs
/// <summary>
/// Sets up events listeners.
/// </summary>
private void AddEventListeners()
{
_workoutModel.Updated += OnWorkoutModelUpdated;
}
When the OnWorkoutModelUpdated handler is executed the application updates the LocalTime property of the header data source, what allows the app to notify the view about the current time.
~/ViewModels/Workout/DetailsPageViewModel.cs
/// <summary>
/// Handles "Updated" event of the WorkoutModel object.
/// Updates value of local time.
/// </summary>
/// <param name="sender">The object that raised the event.</param>
/// <param name="workoutData">Workout data.</param>
private void OnWorkoutModelUpdated(object sender, WorkoutUpdatedEventArgs workoutData)
{
HeaderData.LocalTime = workoutData.Data.LocalTime;
}
This section explains the way of displaying on the screen the data prepared by application logic. As it is described in the “How to use application?” the application contains several screens that are displayed before, during and after the workout. Some of them are quite simple, others are more complicated. For simplicity, this tutorial focuses only on two the most important application’s screens, the one displayed during the workout and the other displayed at the end of the workout and showing its summary.
The application uses the Tizen.Wearable.CircularUI extension of the Xamarin.Forms framework, which provides set of components customized for the wearable profile that makes development easier and efficient.
The font sizes used in the view layer are defined as resources in the main application class.
~/App.xaml.cs
#region methods
/// <summary>
/// Initializes application.
/// </summary>
public App()
{
...
if (!Information.TryGetValue("http://tizen.org/feature/screen.dpi", out int dpi))
{
dpi = 301;
}
Resources["FontSizeXXXXS"] = 19 * _fontSizeMultiplier / dpi;
Resources["FontSizeXXXS"] = 22 * _fontSizeMultiplier / dpi;
Resources["FontSizeXXS"] = 24 * _fontSizeMultiplier / dpi;
Resources["FontSizeXS"] = 27 * _fontSizeMultiplier / dpi;
Resources["FontSizeS"] = 30 * _fontSizeMultiplier / dpi;
Resources["FontSizeM"] = 36 * _fontSizeMultiplier / dpi;
Resources["FontSizeL"] = 38 * _fontSizeMultiplier / dpi;
Resources["FontSizeXL"] = 42 * _fontSizeMultiplier / dpi;
Resources["FontSizeXXL"] = 46 * _fontSizeMultiplier / dpi;
Resources["FontSizeXXXL"] = 62 * _fontSizeMultiplier / dpi;
...
}
#endregion
The calculation is based on the DPI value of the device screen provided by Tizen.System.Information API.
This view reflects all changes of all workout related data that take place during the workout.
For this purpose it creates an instance of the MainViewModel class and assigns it as a BindingContext every time it is created.
~/Views/Workout/MainView.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewModels="clr-namespace:Workout.ViewModels.Workout;assembly=Workout"
x:Class="Workout.Views.Workout.MainView"
NavigationPage.HasNavigationBar="False">
<ContentPage.BindingContext>
<viewModels:MainViewModel />
</ContentPage.BindingContext>
...
</ContentPage>
From now on, the view class can use all the properties, commands and methods exposed by this view model.
The main view is divided into a smaller parts responsible for showing the selected workout property, each of which is positioned absolutely in the view content.
~/Views/Workout/MainView.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewModels="clr-namespace:Workout.ViewModels.Workout;assembly=Workout"
x:Class="Workout.Views.Workout.MainView"
NavigationPage.HasNavigationBar="False">
...
<ContentPage.Content>
<AbsoluteLayout BackgroundColor="#7B7BB4">
...
</AbsoluteLayout>
</ContentPage.Content>
</ContentPage>
Label with workout time
To access the workout time data the view of the application uses binding where the source is the ElapsedTime of the MainViewModel and the target is the Text property of the dedicated Label element.
~/Views/Workout/MainView.xaml
<AbsoluteLayout BackgroundColor="#7B7BB4">
...
<Label Text="{Binding ElapsedTime, StringFormat='{0:hh\\:mm\\\'ss}'}"
TextColor="#FFF"
FontSize="{StaticResource FontSizeS}"
AbsoluteLayout.LayoutFlags="XProportional"
AbsoluteLayout.LayoutBounds=".5, 270, AutoSize, AutoSize" />
</AbsoluteLayout>
It uses StringFormat property of Binding to format provided TimeSpan data to the string expected by the app.
Label with distance traveled
To display information about the workout distance traveled the application creates binding to the Distance and the DistanceUnit properties of the MainViewModel class.
~/Views/Workout/MainView.xaml
<AbsoluteLayout BackgroundColor="#7B7BB4">
...
<FlexLayout AbsoluteLayout.LayoutFlags="WidthProportional"
AbsoluteLayout.LayoutBounds="0, 180, 1, 75">
<FlexLayout FlexLayout.Grow="1" />
<Label Text="{Binding Distance}"
TextColor="#AAFFCC"
FontSize="{StaticResource FontSizeXXXL}"
VerticalTextAlignment="Center" />
<FlexLayout FlexLayout.Grow="1">
<Label Text="{Binding DistanceUnit}"
FontSize="{StaticResource FontSizeXS}"
TextColor="#FFF"
VerticalTextAlignment="Center"
Margin="15, 0, 0, 0" />
</FlexLayout>
</FlexLayout>
...
</AbsoluteLayout>
It uses FlexLayout to make the displayed distance value always horizontally centered and the distance unit set always 15px on the right.
Labels with pace and heart rate
Information about calculated pace and the current heart rate are provided by the Pace and the Bpm properties of the MainViewModel class.
~/Views/Workout/MainView.xaml
<AbsoluteLayout BackgroundColor="#7B7BB4">
...
<AbsoluteLayout AbsoluteLayout.LayoutBounds="59.5, 92.5, 120, 81" >
<Image Source="images/details_average_pace_icon.png"
AbsoluteLayout.LayoutFlags="XProportional"
AbsoluteLayout.LayoutBounds=".5, 0, 22, 22" />
<Label Text="{Binding Pace}"
TextColor="#FFF"
FontSize="{StaticResource FontSizeXXL}"
AbsoluteLayout.LayoutFlags="XProportional"
AbsoluteLayout.LayoutBounds=".5, 23, AutoSize, AutoSize" />
</AbsoluteLayout>
<BoxView AbsoluteLayout.LayoutBounds="179.5, 92.5, 1, 81"
BackgroundColor="#AAFFCC" />
<AbsoluteLayout AbsoluteLayout.LayoutBounds="180.5, 92.5, 120, 81">
<Image Source="images/details_intensity_icon.png"
AbsoluteLayout.LayoutFlags="XProportional"
AbsoluteLayout.LayoutBounds=".5, 0, 22, 22" />
<Label Text="{Binding Bpm, Converter={StaticResource BpmValueConverter}}"
TextColor="#FFF"
FontSize="{StaticResource FontSizeXXL}"
AbsoluteLayout.LayoutFlags="XProportional"
AbsoluteLayout.LayoutBounds=".5, 23, AutoSize, AutoSize" />
</AbsoluteLayout>
...
</AbsoluteLayout>
Both values are displayed in a similar way. The application uses two AbsoluteLayout elements placed next to each other, horizontally centered and separated by the BoxView element. Each of the AbsoluteLayout elements contains a Label element with binding to the appropriate property of the view model class and an Image element responsible for displaying corresponding icon.
But there is one small difference. The Bpm value is additionally processed with a converter, which returns ”–” string for any value less than or equal to 0.
~/Converters/BpmValueConverter.cs
using System;
using System.Globalization;
using Xamarin.Forms;
namespace Workout.Converters
{
/// <summary>
/// Class that converts bpm values which are lower than or equal to 0 to "--" string.
/// </summary>
public class BpmValueConverter : IValueConverter
{
#region methods
/// <summary>
/// Converts values which are lower than or equal to 0 to "--" string.
/// </summary>
/// <param name="value">The value produced by the binding source.</param>
/// <param name="targetType">The type of the binding target property.</param>
/// <param name="parameter">The converter parameter to use.</param>
/// <param name="culture">The culture to use in the converter.</param>
/// <returns>Converted value.</returns>
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return System.Convert.ToInt32(value) <= 0 ? "--" : value;
}
/// <summary>
/// Does nothing, but it must be defined, because it is in "IValueConverter" interface.
/// </summary>
/// <param name="value">The value produced by the binding source.</param>
/// <param name="targetType">The type of the binding target property.</param>
/// <param name="parameter">The converter parameter to use.</param>
/// <param name="culture">The culture to use in the converter.</param>
/// <returns>Converted value.</returns>
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}
}
In this way the application prevents displaying of Bpm values that are not relevant.
Image for GPS location status
To indicate the GPS location status the view supports with the IsGPSEnabled property of the MainViewModel class.
~/Views/Workout/MainView.xaml
<AbsoluteLayout BackgroundColor="#7B7BB4">
...
<Image Source="images/gps_indicator_off.png"
AbsoluteLayout.LayoutBounds="123, 47, AutoSize, AutoSize">
<Image.Triggers>
<DataTrigger TargetType="Image"
Binding="{Binding IsGPSEnabled}"
Value="true">
<Setter Property="Source"
Value="images/gps_indicator_on.png" />
</DataTrigger>
</Image.Triggers>
</Image>
...
</AbsoluteLayout>
It uses the Image element showing the GPS indicator in the OFF state by default. It also uses the DataTrigger triggered by changes of the IsGPSEnabled property and replacing the Source property of the Image to indicate the GPS ON state when the property is set to true.
Label with local time
To display information about local time the application uses the LocalTime property of the MainViewModel class.
~/Views/Workout/MainView.xaml
<AbsoluteLayout BackgroundColor="#7B7BB4">
...
<Label Text="{Binding LocalTime, StringFormat='{0:hh:mm}'}"
TextColor="#FFF"
FontSize="{StaticResource FontSizeXXXS}"
AbsoluteLayout.LayoutFlags="XProportional"
AbsoluteLayout.LayoutBounds=".5, 46, AutoSize, AutoSize" />
...
</AbsoluteLayout>
It uses StringFormat property of Binding to format provided DateTime data to the string expected by the app.
Background image
The Image element, the source of which is shown below, is responsible for displaying the view background.
~/Views/Workout/MainView.xaml
<AbsoluteLayout BackgroundColor="#7B7BB4">
...
<Image Source="images/workout_main_page_bg.png" />
...
</AbsoluteLayout>
It is transparent in the lower part, in a place designed for bpm indicator.
When combined with the background color of the AbsoluteLayout main element, the application screen produces the following effect.
Graphical indicator for bpm value
To display the graphical bpm indicator the application uses the Image element presented in the following snippet.
~/Views/Workout/MainView.xaml
<AbsoluteLayout BackgroundColor="#7B7BB4">
<Image Source="{Binding HeartRateIndicatorImage}" Rotation="{Binding HeartRateIndicatorRotation}"/>
...
</AbsoluteLayout>
Its appearance is influenced by two properties of the MainViewModel class, the HeartRateIndicatorImage and the HeartRateIndicatorRotation.
The value of both is calculated by the MainViewModel class at the time when the UpdateWorkoutProperties method is performed.
~/ViewModels/Workout/MainViewModel.cs
/// <summary>
/// Updates values of workout properties.
/// </summary>
/// <param name="data">Workout data.</param>
private void UpdateWorkoutProperties(WorkoutData data)
{
...
HeartRateIndicatorImage = "images/hrm_indicator/" + data.BpmRange.ToString() + ".png";
HeartRateIndicatorRotation = ((1 - data.NormalizedBpm) * 180).ToString();
}
The value of the HeartRateIndicatorImage property defines the appearance of the indicator. It can take the form of one of the six images according to the value of the BpmRange property, which represents the Bpm value expressed as integer from 0 to 5.
The value of the HeartRateIndicatorRotation property defines rotation of the indicator image. The rotation can take values from 0 to 180 degrees according to the value of the NormalizedBpm property, which represents the Bpm value expressed as integer from 0 to 1.
The following images show examples of the graphical bpm indicator.
The code behind of the MainView class overrides OnAppearing, OnBackButtonPressed and OnDisappearing methods.
When the MainView appears, the workout is started.
~/Views/Workout/MainView.xaml.cs
/// <summary>
/// Overrides method called when the page appears.
/// Starts workout.
/// </summary>
protected override void OnAppearing()
{
base.OnAppearing();
_viewModel.StartWorkout();
}
For this purpose the application executes the StartWorkout method of the view model class.
When the hardware back button is pressed, the application pauses the workout.
~/Views/Workout/MainView.xaml.cs
/// <summary>
/// Overrides method handling hardware "back" button press.
/// Pauses workout.
/// </summary>
protected override bool OnBackButtonPressed()
{
_viewModel.PauseWorkout();
return true;
}
For this purpose the StartWorkout method of the view model class is executed.
When the MainView disappears, the application executes Dispose method, which allows to release resources reserved by the MainViewModel class.
~/Views/Workout/MainView.xaml.cs
/// <summary>
/// Overrides method called when the page disappears.
/// Disposes binding context.
/// </summary>
protected override void OnDisappearing()
{
base.OnDisappearing();
if (BindingContext is IDisposable disposableBindingContext)
{
disposableBindingContext.Dispose();
BindingContext = null;
}
}
After the workout, the application uses the DetailsPageView class to show a summary view. This view displays data collected during the workout, including:
The following images show the header and all list items of the example summary view.
To access the DetailsPageViewModel class the application creates its instance and assigns it as a BindingContext every time the DetailsPageView is created.
~/Views/Workout/DetailsPageView.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:cui ="clr-namespace:Tizen.Wearable.CircularUI.Forms;assembly=Tizen.Wearable.CircularUI.Forms"
xmlns:models="clr-namespace:Workout.Models.Workout;assembly=Workout"
xmlns:viewModels="clr-namespace:Workout.ViewModels.Workout;assembly=Workout"
xmlns:tizen="clr-namespace:Xamarin.Forms.PlatformConfiguration.TizenSpecific;assembly=Xamarin.Forms.Core"
x:Class="Workout.Views.Workout.DetailsPageView"
NavigationPage.HasNavigationBar="False">
<ContentPage.BindingContext>
<viewModels:DetailsPageViewModel />
</ContentPage.BindingContext>
...
</ContentPage>
From now on, the view class can use all the properties, commands and methods exposed by this view model.
The page content is based on the AbsoluteLayout which contains only two elements.
~/Views/Workout/DetailsPageView.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:cui ="clr-namespace:Tizen.Wearable.CircularUI.Forms;assembly=Tizen.Wearable.CircularUI.Forms"
xmlns:models="clr-namespace:Workout.Models.Workout;assembly=Workout"
xmlns:viewModels="clr-namespace:Workout.ViewModels.Workout;assembly=Workout"
xmlns:tizen="clr-namespace:Xamarin.Forms.PlatformConfiguration.TizenSpecific;assembly=Xamarin.Forms.Core"
x:Class="Workout.Views.Workout.DetailsPageView"
NavigationPage.HasNavigationBar="False">
...
<ContentPage.Content>
<AbsoluteLayout>
<Image Source="images/workout_details_page_bg.png" />
<cui:CircleListView x:Name="listView"
AbsoluteLayout.LayoutFlags="All"
AbsoluteLayout.LayoutBounds="0, 0, 1, 1"
BackgroundColor="Transparent"
Header="{Binding HeaderData}">
...
</cui:CircleListView>
</AbsoluteLayout>
</ContentPage.Content>
</ContentPage>
There is a CircleListView element responsible for displaying data provided by the view model. It uses binding mechanism to access the HeaderData property of the DetailsPageViewModel and uses it as a data source for the header content. There is also an Image element that provides the background for mentioned list view and the whole page.
In addition to the header data source, the CircleListView element defines the header template. It is based on the AbsoluteLayout and contains three labels.
~/Views/Workout/DetailsPageView.xaml
<cui:CircleListView x:Name="listView"
AbsoluteLayout.LayoutFlags="All"
AbsoluteLayout.LayoutBounds="0, 0, 1, 1"
BackgroundColor="Transparent"
Header="{Binding HeaderData}">
<cui:CircleListView.HeaderTemplate>
<DataTemplate>
<AbsoluteLayout HeightRequest="360"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand">
<Label Text="{Binding LocalTime, StringFormat='{0:hh:mm}'}"
FontSize="{StaticResource FontSizeXXXS}"
TextColor="#FFF"
AbsoluteLayout.LayoutFlags="XProportional"
AbsoluteLayout.LayoutBounds=".5, 46, AutoSize, AutoSize" />
<Label Text="{Binding Title}"
FontSize="{StaticResource FontSizeL}"
TextColor="#FFF"
HorizontalTextAlignment="Center"
AbsoluteLayout.LayoutFlags="XProportional"
AbsoluteLayout.LayoutBounds=".5, 111, AutoSize, AutoSize" />
<Label Text="{Binding StartTime, StringFormat='{0:dd/MM/yyyy}'}"
FontSize="{StaticResource FontSizeXXXS}"
TextColor="#FFF"
AbsoluteLayout.LayoutFlags="XProportional"
AbsoluteLayout.LayoutBounds=".5, 228, AutoSize, AutoSize" />
</AbsoluteLayout>
</DataTemplate>
</cui:CircleListView.HeaderTemplate>
...
</cui:CircleListView>
Each of them is binded to the corresponding property of the DetailsHeaderData object provided as a data source of the list header. Thus, the following header information is displayed:
In addition to the data source for the header, the CircleListView also needs data source for displaying items.
~/Views/Workout/DetailsPageView.xaml
<cui:CircleListView x:Name="listView"
AbsoluteLayout.LayoutFlags="All"
AbsoluteLayout.LayoutBounds="0, 0, 1, 1"
BackgroundColor="Transparent"
Header="{Binding HeaderData}">
...
<cui:CircleListView.ItemsSource>
<x:Array Type="{x:Type models:DetailsItemData}">
<models:DetailsItemData Name="time"
Value="{Binding ElapsedTime}"
Icon="images/details_time_icon.png">
<models:DetailsItemData.ValueBounds>
<Rectangle X=".5" Y="193" Width="-1" Height="-1" />
</models:DetailsItemData.ValueBounds>
<models:DetailsItemData.NameBounds>
<Rectangle X=".5" Y="245" Width="-1" Height="-1" />
</models:DetailsItemData.NameBounds>
</models:DetailsItemData>
<models:DetailsItemData Name="distance"
Value="{Binding Distance}"
Icon="images/details_distance_icon.png">
<models:DetailsItemData.ValueBounds>
<Rectangle X=".5" Y="193" Width="-1" Height="-1" />
</models:DetailsItemData.ValueBounds>
<models:DetailsItemData.NameBounds>
<Rectangle X=".5" Y="245" Width="-1" Height="-1" />
</models:DetailsItemData.NameBounds>
</models:DetailsItemData>
<models:DetailsItemData Name="average pace"
Value="{Binding AveragePace}"
Icon="images/details_average_pace_icon.png">
<models:DetailsItemData.ValueBounds>
<Rectangle X=".5" Y="193" Width="-1" Height="-1" />
</models:DetailsItemData.ValueBounds>
<models:DetailsItemData.NameBounds>
<Rectangle X=".5" Y="245" Width="-1" Height="-1" />
</models:DetailsItemData.NameBounds>
</models:DetailsItemData>
<models:DetailsItemData Name="intensity"
Value="{Binding Intensity, Converter={StaticResource BpmRangeValueConverter}}"
Icon="images/details_intensity_icon.png"
IsActionButtonVisible="True">
<models:DetailsItemData.ValueBounds>
<Rectangle X=".5" Y="172" Width="-1" Height="-1" />
</models:DetailsItemData.ValueBounds>
<models:DetailsItemData.NameBounds>
<Rectangle X=".5" Y="224" Width="-1" Height="-1" />
</models:DetailsItemData.NameBounds>
</models:DetailsItemData>
</x:Array>
</cui:CircleListView.ItemsSource>
...
</cui:CircleListView>
It is defined in the XAML code as an Array of DetailsItemData objects and uses ElapsedTime, Distance, AveragePace and Intensity properties of the DetailsPageViewModel class to provide workout details data.
In addition, the number value responsible for displaying the workout intensity is converted to the appropriate string using the BpmRangeValueConverter class.
~/Converters/BpmRangeValueConverter.cs
using System;
using System.Collections.Generic;
using System.Globalization;
using Xamarin.Forms;
namespace Workout.Converters
{
/// <summary>
/// Class that converts Bpm range value to corresponding string.
/// </summary>
public class BpmRangeValueConverter : IValueConverter
{
#region fields
/// <summary>
/// Dictionary of Bpm range names.
/// </summary>
private readonly Dictionary<string, string> _bpmRangeNamesDictionary = new Dictionary<string, string>
{
{ "0", "rest" },
{ "1", "very light" },
{ "2", "light" },
{ "3", "moderate" },
{ "4", "hard" },
{ "5", "maximum" }
};
#endregion
#region methods
/// <summary>
/// Converts value to corresponding string.
/// </summary>
/// <param name="value">The value produced by the binding source.</param>
/// <param name="targetType">The type of the binding target property.</param>
/// <param name="parameter">The converter parameter to use.</param>
/// <param name="culture">The culture to use in the converter.</param>
/// <returns>Converted value.</returns>
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (_bpmRangeNamesDictionary.TryGetValue(value.ToString(), out string bpmRangeValue))
{
return bpmRangeValue;
}
else
{
return value;
}
}
/// <summary>
/// Does nothing, but it must be defined, because it is in "IValueConverter" interface.
/// </summary>
/// <param name="value">The value produced by the binding source.</param>
/// <param name="targetType">The type of the binding target property.</param>
/// <param name="parameter">The converter parameter to use.</param>
/// <param name="culture">The culture to use in the converter.</param>
/// <returns>Converted value.</returns>
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}
}
The template of the list item is also based on AbsoluteLayout, which is responsible for the layout of all its elements.
~/Views/Workout/DetailsPageView.xaml
<cui:CircleListView x:Name="listView"
AbsoluteLayout.LayoutFlags="All"
AbsoluteLayout.LayoutBounds="0, 0, 1, 1"
BackgroundColor="Transparent"
Header="{Binding HeaderData}">
...
<cui:CircleListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<AbsoluteLayout HeightRequest="360"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand">
...
</AbsoluteLayout>
</ViewCell>
</DataTemplate>
</cui:CircleListView.ItemTemplate>
</cui:CircleListView>
Each item displays the name of a specific workout property, its value and the corresponding icon. In addition, the last element of the list contains the OK button, which allows returning to the home screen of the application.
To display the item icon, the application uses an Image element, whose Source property is defined as a FileImageSource object.
~/Views/Workout/DetailsPageView.xaml
<AbsoluteLayout HeightRequest="360"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand">
<Image AbsoluteLayout.LayoutFlags="XProportional"
AbsoluteLayout.LayoutBounds=".5, 74, AutoSize, AutoSize">
<Image.Source>
<FileImageSource File="{Binding Icon}" />
</Image.Source>
</Image>
...
</AbsoluteLayout>
The File property of this object is binded to the Icon property of the DetailsItemData object provided as a data source for list items.
To display the item value, the application uses a Label element.
~/Views/Workout/DetailsPageView.xaml
<AbsoluteLayout HeightRequest="360"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand">
...
<Label Text="{Binding Value}"
FontSize="{StaticResource FontSizeM}"
TextColor="#FFF"
AbsoluteLayout.LayoutFlags="XProportional"
AbsoluteLayout.LayoutBounds="{Binding ValueBounds}">
</Label>
...
</AbsoluteLayout>
It uses the Value and ValueBounds properties provided by the data source to determine displayed value and the absolute position.
To display the item name, the application also uses a Label element, whose Text property is binded to the Name property of the DetailsItemData object.
~/Views/Workout/DetailsPageView.xaml
<AbsoluteLayout HeightRequest="360"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand">
...
<Label Text="{Binding Name}"
FontSize="{StaticResource FontSizeXXS}"
FontAttributes="Bold"
TextColor="#AAFFCC"
AbsoluteLayout.LayoutFlags="XProportional"
AbsoluteLayout.LayoutBounds="{Binding NameBounds}">
</Label>
...
</AbsoluteLayout>
Additionally, it uses the NameBounds properties provided by the data source to determine its the absolute position.
To display the item action button, the application uses a Button element.
~/Views/Workout/DetailsPageView.xaml
<AbsoluteLayout HeightRequest="360"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand">
...
<Button AbsoluteLayout.LayoutFlags="All"
AbsoluteLayout.LayoutBounds="0, 1, 1, .25"
Text="OK"
TextColor="#1B1B7D"
BackgroundColor="#AAFFCC"
Command="{Binding BindingContext.FinishCommand, Source={x:Reference listView}}"
IsVisible="{Binding IsActionButtonVisible}"
tizen:VisualElement.Style="bottom" />
</AbsoluteLayout>
This button is visible only when the IsActionButtonVisible property of the DetailsItemData object is set, what means that the last list item is displayed. Clicking this button executes the FinishCommand defined in the model view class.
The code behind of the DetailsPageView class uses its constructor to assign the BindingContext to the _viewModel field as well as to the BindingContext of all CircleListView items to enable them to use the data provided by the DetailsPageViewModel class.
~/Views/Workout/DetailsPageView.xaml.cs
/// <summary>
/// Initializes class instance.
/// </summary>
public DetailsPageView()
{
InitializeComponent();
_viewModel = BindingContext as DetailsPageViewModel;
foreach (BindableObject item in listView.ItemsSource)
{
item.BindingContext = _viewModel;
}
}
It also overrides OnBackButtonPressed and OnDisappearing methods.
When the hardware back button is pressed, the application executes the Finish method of the DetailsPageViewModel class.
~/Views/Workout/DetailsPageView.xaml.cs
/// <summary>
/// Overrides method handling hardware "back" button press.
/// Finishes workout.
/// Navigates to the home page.
/// </summary>
protected override bool OnBackButtonPressed()
{
_viewModel.Finish();
return true;
}
As a result the same action is performed as in the case of calling the FinishCommand.
When the DetailsPageView disappears, the application executes Dispose method, which allows to release resources reserved by the DetailsPageViewModel view model class.
~/Views/Workout/DetailsPageView.xaml.cs
/// <summary>
/// Overrides method called when the page disappears.
/// Disposes binding context.
/// </summary>
protected override void OnDisappearing()
{
base.OnDisappearing();
if (BindingContext is IDisposable disposableBindingContext)
{
disposableBindingContext.Dispose();
BindingContext = null;
}
}