Create a Squat Counter App


Objective

Learn how to create a squat counter application using pressure sensor from Tizen.Sensor.API for your Galaxy Watch.

Overview

This application counts squats based on pressure changes in your Galaxy Watch.

After setting up your environment, the development part of this Code Lab is divided into:

  1. Application Logic - there is an explanation and implementation of the application logic
  2. User Interface - shows how to create the application interface and animate it according to the collected data

Remember, before you start, you should be familiar with basics of C# and Xamarin.Forms.

Set up your environment

You will need the following:

  • Visual Studio 2019 (including Xamarin package/component)
  • Visual Studio Tools for Tizen (Visual Studio extension)
  • Samsung Galaxy Watch or Emulator (Tizen 5.5↑)

Create a new project

  1. Open Visual Studio IDE to create a new Tizen Wearable Xaml App.

    • When the start window appears, choose Create a new project. Alternatively, you can do it via File > New > Project.

    • In the new project window, set up the fields based on the following screen. Choose Tizen Wearable Xaml App and press the Next button.

    • In the Configure your new project, set project name as SquatCounter and press the Create button.

  2. Add/remove files.

    • Right-click on MainPage.xaml and select delete from the context menu
    • Right-click on App.xaml and delete it
    • Right-click on SquatCounter project, select Add > New Item from the context menu, choose C# class and name it App.cs. After adding the file, you need to enter following code to App.cs:
    
    using Xamarin.Forms;
    using Xamarin.Forms.Xaml;
    
    namespace SquatCounter
    {
        public class App : Application
        {
            public App()
            {
    
            }
        }
    }
    
    
  3. Create folder structure.

    • Right-click on SquatCounter project, select Add > New Folder from the context menu and name it Views.
    • Similarly, add new folders named ViewModels and Services.

    After passing through all the steps, the structure of the project should look like the following image.

Add assets

  1. Download and unpack the assets for the application.

    It consists of two folders.

    Inside, you may see these images:

    • background image file for counting squats page (res/squat_background.png)

    • three image files for guide pages (res/step_1.png, res/step_2.png, res/step_3.png)

    • image file for clock step page (res/clock.png)

    • image file of application icon (shared/res/SquatCounter.png)

  2. Select the unpacked folders, that is res and shared, and copy them with context menu or press Ctrl + C. Next, make a left mouse click on the project name SquatCounter in the Solution Explorer and press Ctrl + V, to paste copied folders directly to the project.

  3. Confirm the pop-ups to replace the content of the res and shared folders of the project.

Implement Services - part 1

First, you will develop the application logic.

Let's start with the implementation of the page navigation service based on singleton design pattern. This service will provide navigation between pages across the application.

  1. Go to the Solution Explorer, right-click on folder Services, select Add > New Item from the context menu, choose class and name it PageNavigationService.cs.

  2. Let's modify our class based on principles of the singleton design pattern:

    
    namespace SquatCounter.Services
    {
        public sealed class PageNavigationService
        {
            private static PageNavigationService _instance;
    
            public static PageNavigationService Instance
            {
                get => _instance ?? (_instance = new PageNavigationService());
            }
        }
    }   
    
    
  3. Right-click on the Views folder, select Add > New from the context menu, choose Content page and name it GuidePage.xaml.

  4. Next, you will add GoToGuidePage method that will display GuidePage. You will also create a body of GoToSquatCounterPage method, which will be completed later on:

    
    using SquatCounter.Views;
    using Xamarin.Forms;
    
    namespace SquatCounter.Services
    {
        public sealed class PageNavigationService
        {
            private static PageNavigationService _instance;
    
            public static PageNavigationService Instance
            {
                get => _instance ?? (_instance = new PageNavigationService());
            }
    
            public void GoToGuidePage()
            {
                Application.Current.MainPage = new NavigationPage(new GuidePage());
            }
    
            public void GoToSquatCounterPage()
            {
    
            }
        }
    }   
    
    
  5. The last thing you need to do is modify App.cs constructor to display the guide page. At the code below, you have added compile-time checking on all XAML files within namespace. It will help us to notice errors, while compile-time improves performance of the views and reduce size of our app:

    
    using SquatCounter.Services;
    using Xamarin.Forms;
    using Xamarin.Forms.Xaml;
    
    [assembly: XamlCompilation(XamlCompilationOptions.Compile)]
    namespace SquatCounter
    {
        public class App : Application
        {
            public App()
            {
                PageNavigationService.Instance.GoToGuidePage();
            }
        }
    }
    
    
  6. After you build and run the application, you should see the following screen.

Implement Services - part 2

Now, you will implement a service for the pressure sensor. It will allow you to obtain pressure data from an instance of the pressure sensor and this data will be used later to count squats.

  1. Go to the Solution Explorer, right-click on Services folder, select Add > New Item from the context menu, choose class and name it PressureSensorService.cs.

  2. Let's modify PressureSensorService.cs class. Before you start implementing the pressure sensor, you need to change class access modifier to public and create public constructor with empty body:

    
    namespace SquatCounter.Services
    {
        public class PressureSensorService
        {
            public PressureSensorService()
            {
    
            }
        }
    }   
    
    
  3. Next, add PressureSensor instance which allows you to obtain pressure data:

    
    using Tizen.Sensor;
    
    namespace SquatCounter.Services
    {
        public class PressureSensorService
        {
            private readonly PressureSensor _pressureSensor;
    
            public PressureSensorService()
            {
    
            }
        }
    }   
    
    
  4. Before you create the instance of PressureSensor, add the following properties and method:

    • NotSupportedMsg - this field has a string message which will be displayed if PressureSensor is not supported
    • Interval - this field describes frequency in milliseconds of calling callback
    • PressureSensorUpdated - this is the callback method that will be called every Interval frequency
    
    using Tizen.Sensor;
    
    namespace SquatCounter.Services
    {
        public class PressureSensorService
        {
            private readonly PressureSensor _pressureSensor;
    
            private const string NotSupportedMsg = "Pressure sensor is not supported on your device.";
    
            private const int Interval = 10;
    
            public PressureSensorService()
            {
    
            }
    
            private void PressureSensorUpdated(object sender, PressureSensorDataUpdatedEventArgs e)
            {
    
            }
        }
    }   
    
    
  5. In this step, you will create and start instance of PressureSensor in the constructor body of PressureSensorService:

    
    using Tizen.Sensor;
    using System;
    
    namespace SquatCounter.Services
    {
        public class PressureSensorService
        {
            private PressureSensor _pressureSensor;
    
            private const string NotSupportedMsg = "Pressure sensor is not supported on your device.";
    
            private const int Interval = 10;
    
            public PressureSensorService()
            {
                if (!PressureSensor.IsSupported)
                {
                    throw new NotSupportedException(NotSupportedMsg);
                }
    
                _pressureSensor = new PressureSensor();
                _pressureSensor.DataUpdated += PressureSensorUpdated;
                _pressureSensor.Interval = Interval;
                _pressureSensor.Start();
            }
    
            private void PressureSensorUpdated(object sender, PressureSensorDataUpdatedEventArgs e)
            {
    
            }
        }
    }
    
    
  6. Here, create an event handler, which is invoked whenever PressueSensorUpdate callback has been raised:

    
    using System;
    using Tizen.Sensor;
    
    namespace SquatCounter.Services
    {
        public class PressureSensorService
        {
            private PressureSensor _pressureSensor;
    
            private const string NotSupportedMsg = "Pressure sensor is not supported on your device.";
    
            public event EventHandler<float> ValueUpdated;
    
            private const int Interval = 10;
    
            public PressureSensorService()
            {
                if (!PressureSensor.IsSupported)
                {
                    throw new NotSupportedException(NotSupportedMsg);
                }
    
                _pressureSensor = new PressureSensor();
                _pressureSensor.DataUpdated += PressureSensorUpdated;
                _pressureSensor.Interval = Interval;
                _pressureSensor.Start();
            }
    
            private void PressureSensorUpdated(object sender, PressureSensorDataUpdatedEventArgs e)
            {
                ValueUpdated?.Invoke(this, e.Pressure);
            }
        }
    }  
    
    
  7. The last thing that to do is release the resources obtained by PressureSensor. To achieve this, use Dispose pattern. This step is very important for our application. If you do not release the resources, then the app may crash:

    
    namespace SquatCounter.Services
    {
        public class PressureSensorService : IDisposable
        {
            private PressureSensor _pressureSensor;
    
            private const string NotSupportedMsg = "Pressure sensor is not supported on your device.";
    
            public event EventHandler<float> ValueUpdated;
    
            private const int Interval = 10;
    
            public PressureSensorService()
            {
                if (!PressureSensor.IsSupported)
                {
                    throw new NotSupportedException(NotSupportedMsg);
                }
    
                _pressureSensor = new PressureSensor();
                _pressureSensor.DataUpdated += PressureSensorUpdated;
                _pressureSensor.Interval = Interval;
                _pressureSensor.Start();
            }
    
            private void PressureSensorUpdated(object sender, PressureSensorDataUpdatedEventArgs e)
            {
                ValueUpdated?.Invoke(this, e.Pressure);
            }
    
            public void Dispose()
            {
                _pressureSensor?.Stop();
                _pressureSensor.DataUpdated -= PressureSensorUpdated;
                _pressureSensor?.Dispose();
            }
        }
    }  
    
    

Implement Services - part 3

The data received from the sensor must be processed in order to obtain the necessary information from it. Now, create the counting squats logic with the following points:

  • Read the first 10 pressure readings from PressureCounterService.
  • Determine our calibration upper and lower threshold pressure at starting point.
  • Every next pressure reading calculate a moving average of the 10 last readings.
  • If the computed average is between upper and lower threshold, then count the squat.
  1. Go to the Solution Explorer, right-click on Services folder, select Add > New Item from the context menu, choose C# class and name it SquatCounterService.cs.

  2. To obtain data from PressureSensorService, add the following things:

    • private field _pressureSensorService
    • callback method PressureSensorUpdated for value update in PressureSensorService
    • create instance of PressureSensorService in body of SquatCounterService constructor and subscribe updated event by our callback method
    
    namespace SquatCounter.Services
    {
        public class SquatCounterService
        {
            private PressureSensorService _pressureService;
    
            public SquatCounterService()
            {
                _pressureService = new PressureSensorService();
                _pressureService.ValueUpdated += PressureSensorUpdated;
            }
    
            private void PressureSensorUpdated(object sender, float pressure)
            {
    
            }
        }
    }   
    
    
  3. Next, add the methods and fields for service calibration and calculate stable average, CalculateStableAverage method:

    
    using System.Collections.Generic;
    using System.Linq;
    
    namespace SquatCounter.Services
    {
        public class SquatCounterService
        {
            private const float Accuracy = 0.030F;
            private const int WindowSize = 10;
    
            private PressureSensorService _pressureService;
            private Queue<float> _pressureWindow;
    
            private float _upperThreshold;
            private float _lowerThreshold;
            private bool _valueExceededUpperThreshold;
            private bool _isServiceCalibrated;
    
            public SquatCounterService()
            {
                _pressureWindow = new Queue<float>();
    
                _pressureService = new PressureSensorService();
                _pressureService.ValueUpdated += PressureSensorUpdated;
            }
    
            private void PressureSensorUpdated(object sender, float pressure)
            {
    
            }
    
            private void CalibrateService()
            {
                float average = CalculateStableAverage();
                _upperThreshold = average + Accuracy;
                _lowerThreshold = average - Accuracy;
                _isServiceCalibrated = true;
            }
    
            private float CalculateStableAverage()
            {
                float minPressure = _pressureWindow.Min();
                float maxPressure = _pressureWindow.Max();
                float sum = _pressureWindow.Sum(); 
                return (sum - minPressure - maxPressure) / (WindowSize - 2);
            }
        }   
        
    
  4. Here, add previously created method to calibrate SquatCounter after PressureWindow reach the first 10 readings:

    
    using System.Collections.Generic;
    using System.Linq;
    
    namespace SquatCounter.Services
    {
        public class SquatCounterService
        {
            private const float Accuracy = 0.030F;
            private const int WindowSize = 10;
    
            private PressureSensorService _pressureService;
            private Queue<float> _pressureWindow;
    
            private float _upperThreshold;
            private float _lowerThreshold;
            private bool _valueExceededUpperThreshold;
            private bool _isServiceCalibrated;
    
            public SquatCounterService()
            {
                _pressureWindow = new Queue<float>();
    
                _pressureService = new PressureSensorService();
                _pressureService.ValueUpdated += PressureSensorUpdated;
            }
    
            private void PressureSensorUpdated(object sender, float pressure)
            {
                _pressureWindow.Enqueue(pressure);
    
                if (_pressureWindow.Count < WindowSize)
                {
                    return;
                }
                else if (_pressureWindow.Count == WindowSize && !_isServiceCalibrated)
                {
                    CalibrateService();
                }
    
                _pressureWindow.Dequeue();
            }
    
            private void CalibrateService()
            {
                float average = CalculateStableAverage();
                _upperThreshold = average + Accuracy;
                _lowerThreshold = average - Accuracy;
                _isServiceCalibrated = true;
            }
    
            private float CalculateStableAverage()
            {
                float minPressure = _pressureWindow.Min();
                float maxPressure = _pressureWindow.Max();
                float sum = _pressureWindow.Sum(); 
                return (sum - minPressure - maxPressure) / (WindowSize - 2);
            }
        }
    }  
    
    
  5. Next, create a method that analyze our pressure window to detect hit to the starting calibration. This method will be a rising event and pass counted squats every time when squat is detected:

    
    using System;
    using System.Collections.Generic;
    using System.Linq;
    
    namespace SquatCounter.Services
    {
        public class SquatCounterService
        {
            private const float Accuracy = 0.030F;
            private const int WindowSize = 10;
    
            private PressureSensorService _pressureService;
            private Queue<float> _pressureWindow;
    
            private float _upperThreshold;
            private float _lowerThreshold;
            private bool _valueExceededUpperThreshold;
            private bool _isServiceCalibrated;
    
            public event EventHandler<int> SquatsUpdated;
    
            public int SquatsCount { get; private set; }
    
            public SquatCounterService()
            {
                _pressureWindow = new Queue<float>();
    
                _pressureService = new PressureSensorService();
                _pressureService.ValueUpdated += PressureSensorUpdated;
            }
    
            private void PressureSensorUpdated(object sender, float pressure)
            {
                _pressureWindow.Enqueue(pressure);
    
                if (_pressureWindow.Count < WindowSize)
                {
                    return;
                }
                else if (_pressureWindow.Count == WindowSize && !_isServiceCalibrated)
                {
                    CalibrateService();
                }
    
                AnalyzeNewWindow();
    
                _pressureWindow.Dequeue();
            }
    
            private void CalibrateService()
            {
                float average = CalculateStableAverage();
                _upperThreshold = average + Accuracy;
                _lowerThreshold = average - Accuracy;
                _isServiceCalibrated = true;
            }
    
            private float CalculateStableAverage()
            {
                float minPressure = _pressureWindow.Min();
                float maxPressure = _pressureWindow.Max();
                float sum = _pressureWindow.Sum(); 
                return (sum - minPressure - maxPressure) / (WindowSize - 2);
            }
    
            private void AnalyzeNewWindow()
            {
                float average = CalculateStableAverage();
    
                if (average <= _upperThreshold && average >= _lowerThreshold && _valueExceededUpperThreshold)
                {
                    _valueExceededUpperThreshold = false;
                    SquatsCount++;
                    SquatsUpdated.Invoke(this, SquatsCount);
                }
                else if (average > _upperThreshold)
                {
                    _valueExceededUpperThreshold = true;
                }
            }
        }
    }  
    
    
  6. Add stop, start, reset methods and implement dispose pattern to release managed resources:

    
    using System;
    using System.Collections.Generic;
    using System.Linq;
    
    namespace SquatCounter.Services
    {
        public class SquatCounterService : IDisposable
        {
            private const float Accuracy = 0.030F;
            private const int WindowSize = 10;
    
            private PressureSensorService _pressureService;
            private Queue<float> _pressureWindow;
    
            private float _upperThreshold;
            private float _lowerThreshold;
            private bool _valueExceededUpperThreshold;
            private bool _isServiceCalibrated;
    
            public event EventHandler<int> SquatsUpdated;
    
            public int SquatsCount { get; private set; }
    
            public SquatCounterService()
            {
                _pressureWindow = new Queue<float>();
    
                _pressureService = new PressureSensorService();
                _pressureService.ValueUpdated += PressureSensorUpdated;
            }
    
            public void Start()
            {
                _pressureService.ValueUpdated += PressureSensorUpdated;
            }
    
            public void Stop()
            {
                _pressureService.ValueUpdated -= PressureSensorUpdated;
            }
    
            public void Reset()
            {
                SquatsCount = 0;
                SquatsUpdated.Invoke(this, SquatsCount);
            }
    
            public void Dispose()
            {
                _pressureService.ValueUpdated -= PressureSensorUpdated;
                _pressureService?.Dispose();
            }
    
            private void PressureSensorUpdated(object sender, float pressure)
            {
                _pressureWindow.Enqueue(pressure);
    
                if (_pressureWindow.Count < WindowSize)
                {
                    return;
                }
                else if (_pressureWindow.Count == WindowSize && !_isServiceCalibrated)
                {
                    CalibrateService();
                }
    
                AnalyzeNewWindow();
    
                _pressureWindow.Dequeue();
            }
    
            private void CalibrateService()
            {
                float average = CalculateStableAverage();
                _upperThreshold = average + Accuracy;
                _lowerThreshold = average - Accuracy;
                _isServiceCalibrated = true;
            }
    
            private float CalculateStableAverage()
            {
                float minPressure = _pressureWindow.Min();
                float maxPressure = _pressureWindow.Max();
                float sum = _pressureWindow.Sum(); 
                return (sum - minPressure - maxPressure) / (WindowSize - 2);
            }
    
            private void AnalyzeNewWindow()
            {
                float average = CalculateStableAverage();
    
                if (average <= _upperThreshold && average >= _lowerThreshold && _valueExceededUpperThreshold)
                {
                    _valueExceededUpperThreshold = false;
                    SquatsCount++;
                    SquatsUpdated.Invoke(this, SquatsCount);
                }
                else if (average > _upperThreshold)
                {
                    _valueExceededUpperThreshold = true;
                }
            }
        }
    }   
    
    

Implement ViewModels - part 1

In order to show all the data to the user, you need to add a view model, which acts as a connection between the model and the view. It’s responsible for the presentation logic.

  1. Create a base view model. Go to the Solution Explorer, right-click on ViewModels folder, select Add > New Item from the context menu, choose C# class and name it ViewModelBase.cs.

  2. Next, add following code to ViewModelBase class:

    
    using System.ComponentModel;
    using System.Runtime.CompilerServices;
    
    namespace SquatCounter.ViewModels
    {
        public class ViewModelBase : INotifyPropertyChanged
        {
            public event PropertyChangedEventHandler PropertyChanged;
    
            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;
            }
    
            protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
            {
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    } 
    
    
  3. Now, you can create our squat counter view model. Go to the Solution Explorer, right-click on folder ViewModels, select Add > New Item from the context menu, choose C# class and name it SquatCounterPageViewModel.cs. Created class must inherit from the ViewModelBase class:

    
    namespace SquatCounter.ViewModels
    {
        public class SquatCounterPageViewModel : ViewModelBase
        {
    
        }
    }
    
    
  4. Let's create our instance of SquatCounterService, subscribe to service event and create property for counting squats:

    
    using SquatCounter.Services;
    
    namespace SquatCounter.ViewModels
    {
        public class SquatCounterPageViewModel : ViewModelBase
        {
            private SquatCounterService _squatService;
            private int _squatsCount;
    
            public int SquatsCount
            {
                get => _squatsCount;
                set => SetProperty(ref _squatsCount, value);
            }
    
            public SquatCounterPageViewModel()
            {
                _squatService = new SquatCounterService();
                _squatService.SquatsUpdated += ExecuteSquatsUpdatedCallback;
            }
    
            private void ExecuteSquatsUpdatedCallback(object sender, int squatsCount)
            {
                SquatsCount = squatsCount;
            }
        }
    }  
    
    
  5. Next, you need to implement timer to count, create a method for subscribing timer tick and property for elapsed time:

    
    using SquatCounter.Services;
    using System.Timers;
    using System;
    
    namespace SquatCounter.ViewModels
    {
        public class SquatCounterPageViewModel : ViewModelBase
        {
            private SquatCounterService _squatService;
            private int _squatsCount;
    
            private Timer _timer;
            private int _seconds;
            private string _time;
    
            public int SquatsCount
            {
                get => _squatsCount;
                set => SetProperty(ref _squatsCount, value);
            }
    
            public string Time
            {
                get => _time;
                set => SetProperty(ref _time, value);
            }
    
            public SquatCounterPageViewModel()
            {
                _seconds = 0;
                _time = "00:00";
    
                _squatService = new SquatCounterService();
                _squatService.SquatsUpdated += ExecuteSquatsUpdatedCallback;
    
                _timer = new Timer(1000);
                _timer.Elapsed += TimerTick;
                _timer.Start();
            }
    
            private void ExecuteSquatsUpdatedCallback(object sender, int squatsCount)
            {
                SquatsCount = squatsCount;
            }
    
            private void TimerTick(object sender, EventArgs e)
            {
                _seconds++;
                Time = TimeSpan.FromSeconds(_seconds).ToString("mm\\:ss");
            }
        }
    } 
    
    
  6. Implement commands for reset and change state of SquatCounterService:

    
    using SquatCounter.Services;
    using System.Timers;
    using System;
    using System.Windows.Input;
    using Xamarin.Forms;
    
    namespace SquatCounter.ViewModels
    {
        public class SquatCounterPageViewModel : ViewModelBase
        {
            private SquatCounterService _squatService;
            private int _squatsCount;
    
            private Timer _timer;
            private int _seconds;
            private string _time;
    
            private bool _isCouting;
    
            public int SquatsCount
            {
                get => _squatsCount;
                set => SetProperty(ref _squatsCount, value);
            }
    
            public string Time
            {
                get => _time;
                set => SetProperty(ref _time, value);
            }
    
            public bool IsCounting
            {
                get => _isCouting;
                set => SetProperty(ref _isCouting, value);
            }
    
            public ICommand ChangeServiceStateCommand { get; }
    
            public ICommand ResetCommand { get; }
    
            public SquatCounterPageViewModel()
            {
                _seconds = 0;
                _time = "00:00";
    
                _squatService = new SquatCounterService();
                _squatService.SquatsUpdated += ExecuteSquatsUpdatedCallback;
    
                _timer = new Timer(1000);
                _timer.Elapsed += TimerTick;
                _timer.Start();
    
                ChangeServiceStateCommand = new Command(ExecuteChangeServiceStateCommand);
                ResetCommand = new Command(ExecuteResetCommand);
            }
    
            private void ExecuteSquatsUpdatedCallback(object sender, int squatsCount)
            {
                SquatsCount = squatsCount;
            }
    
            private void TimerTick(object sender, EventArgs e)
            {
                _seconds++;
                Time = TimeSpan.FromSeconds(_seconds).ToString("mm\\:ss");
            }
    
            private void ExecuteChangeServiceStateCommand()
            {
                if (IsCounting)
                {
                    _timer.Stop();
                    _squatService.Stop();
                }
                else
                {
                    _timer.Start();
                    _squatService.Start();
                }
    
                IsCounting = !IsCounting;
            }
    
            private void ExecuteResetCommand()
            {
                _squatService.Reset();
                Time = "00:00";
                _seconds = 0;
            }
        }
    }  
    
    
  7. Now, release the resources after page will be popped. Without this, the app could crash:

    
            public SquatCounterPageViewModel()
            {
                _seconds = 0;
                _time = "00:00";
    
                _squatService = new SquatCounterService();
                _squatService.SquatsUpdated += ExecuteSquatsUpdatedCallback;
    
                _timer = new Timer(1000);
                _timer.Elapsed += TimerTick;
                _timer.Start();
    
                ChangeServiceStateCommand = new Command(ExecuteChangeServiceStateCommand);
                ResetCommand = new Command(ExecuteResetCommand);
    
                if (Application.Current.MainPage is NavigationPage navigationPage)
                {
                    navigationPage.Popped += OnPagePopped;
                }
            }
    
            private void OnPagePopped(object sender, NavigationEventArgs e)
            {
                _timer.Elapsed -= TimerTick;
                _timer.Close();
                _squatService.SquatsUpdated -= ExecuteSquatsUpdatedCallback;
                _squatService.Dispose();
    
                if (Application.Current.MainPage is NavigationPage navigationPage)
                {
                    navigationPage.Popped -= OnPagePopped;
                }
            }   
            
    

Implement ViewModels - part 2

In this step, you will implement GuideViewModel. It provides logic to present each step of squat.

  1. Go to the Solution Explorer, right-click on ViewModels folder, select Add > New Item from the context menu, choose C# class and name GuidePageViewModel.cs. Created class must inherit from the ViewModelBase class:

    
    namespace SquatCounter.ViewModels
    {
        public class GuidePageViewModel : ViewModelBase
        {
        }
    }  
    
    
  2. Here, implement a command to move to the next page and property image path for the current page guide:

    
    using SquatCounter.Services;
    using System.Windows.Input;
    using Xamarin.Forms;
    
    namespace SquatCounter.ViewModels
    {
        public class GuidePageViewModel : ViewModelBase
        {
            public string ImagePath { get; set; }
            public ICommand GoToSquatCounterPageCommand { get; set; }
    
            public GuidePageViewModel()
            {
                GoToSquatCounterPageCommand = new Command(ExecuteGoToSquatCounterPageCommand);
            }
    
            public void ExecuteGoToSquatCounterPageCommand()
            {
                PageNavigationService.Instance.GoToSquatCounterPage();
            }
        }
    }   
    
    

Implement Guide pages

It is time to deal with the user interface. In this part, you will create a full UI of the application including Guide pages and Counting Squat page and fill them with data from the viewmodel using binding.

The implementation will consist in modification of the GuidePage, which will display the next pages of squats and clock page. To achieve this you will use Tizen extension to Xamarin.Forms - Cirucular UI.

  1. Right-click on the Views folder, select Add > New Item from the context menu, choose content page and name it GuideStepPage.xaml. Then, paste the following code to the created page:

    
    <?xml version="1.0" encoding="utf-8" ?>
    <cui:CirclePage 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"
                    x:Class="SquatCounter.Views.GuideStepPage">
    
        <cui:CirclePage.Content>
    
        </cui:CirclePage.Content>
    
    </cui:CirclePage> 
    
    
  2. Next, bind the image path to image to set our step page:

    
    <?xml version="1.0" encoding="utf-8" ?>
    <cui:CirclePage 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"
                    x:Class="SquatCounter.Views.GuideStepPage">
    
        <cui:CirclePage.Content>
            <AbsoluteLayout>
                <Image AbsoluteLayout.LayoutBounds="0, 0, 360, 360"
                       Source="{Binding ImagePath}" />
            </AbsoluteLayout>
        </cui:CirclePage.Content>
    
    </cui:CirclePage>   
    
    
  3. Modify file GuideStepPage.xaml.cs with following code:

    
    using Tizen.Wearable.CircularUI.Forms;
    
    namespace SquatCounter.Views
    {
        public partial class GuideStepPage : CirclePage
        {
            public GuideStepPage()
            {
                InitializeComponent();
            }
        }
    }   
    
    
  4. Right-click on the Views folder, select Add > New Item from the context menu, choose content page and name it GuideClockPage.xaml. Now, paste the code below to the created page:

    
    <?xml version="1.0" encoding="utf-8" ?>
    <cui:CirclePage 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"
                 x:Class="SquatCounter.Views.GuideClockPage">
    
        <cui:CirclePage.Content>
    
        </cui:CirclePage.Content>
    
    </cui:CirclePage>   
    
    
  5. The only thing that makes GuideStepPage different from GuideClockPage is the command that calls up transition as a gesture to the SquatCounterPage. Let's bind the command and image path to the page:

    
    <?xml version="1.0" encoding="utf-8" ?>
    <cui:CirclePage 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"
                    x:Class="SquatCounter.Views.GuideClockPage">
    
        <cui:CirclePage.Content>
            <AbsoluteLayout>
                <Image AbsoluteLayout.LayoutBounds="0, 0, 360, 360"
                       Source="{Binding ImagePath}">
                    <Image.GestureRecognizers>
                        <TapGestureRecognizer Command="{Binding GoToSquatCounterPageCommand}">
                        </TapGestureRecognizer>
                    </Image.GestureRecognizers>
                </Image>
            </AbsoluteLayout>
        </cui:CirclePage.Content>
    
    </cui:CirclePage>
    
    
  6. Modify GuideClockPage.xaml.cs similarly, as seen in previous steps:

    
    using Tizen.Wearable.CircularUI.Forms;
    
    namespace SquatCounter.Views
    {
        public partial class GuideClockPage : CirclePage
        {
            public GuideClockPage()
            {
                InitializeComponent();
            }
        }
    }   
    
  7. Now, you have implemented GuideStepPage to introduce the user on how to do squats and the GuideClockPage. In addition, you can modify the GuidePage.xaml file with the setting pages:

    
    <?xml version="1.0" encoding="utf-8" ?>
    <cui:IndexPage 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:viewModels="clr-namespace:SquatCounter.ViewModels;assembly=SquatCounter"
                   xmlns:views="clr-namespace:SquatCounter.Views;assembly=SquatCounter"
                   x:Class="SquatCounter.Views.GuidePage"
                   NavigationPage.HasNavigationBar="False">
    
        <cui:IndexPage.Children>
            <views:GuideStepPage>
                <views:GuideStepPage.BindingContext>
                    <viewModels:GuidePageViewModel ImagePath="step_1.png" />
                </views:GuideStepPage.BindingContext>
            </views:GuideStepPage>
    
            <views:GuideStepPage>
                <views:GuideStepPage.BindingContext>
                    <viewModels:GuidePageViewModel ImagePath="step_2.png" />
                </views:GuideStepPage.BindingContext>
            </views:GuideStepPage>
    
            <views:GuideStepPage>
                <views:GuideStepPage.BindingContext>
                    <viewModels:GuidePageViewModel ImagePath="step_3.png" />
                </views:GuideStepPage.BindingContext>
            </views:GuideStepPage>
    
            <views:GuideClockPage>
                <views:GuideClockPage.BindingContext>
                    <viewModels:GuidePageViewModel ImagePath="clock.png" />
                </views:GuideClockPage.BindingContext>
            </views:GuideClockPage>
        </cui:IndexPage.Children>
    
    </cui:IndexPage>   
    
    
  8. Last thing to do is to modify GuidePage.xaml.cs file:

    
    using Tizen.Wearable.CircularUI.Forms;
    
    namespace SquatCounter.Views
    {
        public partial class GuidePage : IndexPage
        {
            public GuidePage()
            {
                InitializeComponent();
            }
        }
    }   
    
    
  9. After you build and run the app, you will see four pages.

    • Step pages

    • Clock page

Implement SquatCounter page

Let's create SquatCounterPage, which will be responsible for displaying counted time and squats.

  1. Right-click on the Views folder, select Add > New Item from the context menu, choose content page and name it SquatCounterPage.xaml. Now, you can paste the following code to the created page:

    
    <?xml version="1.0" encoding="utf-8" ?>
    <cui:CirclePage xmlns="http://xamarin.com/schemas/2014/forms"
                    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                    xmlns:viewModels="clr-namespace:SquatCounter.ViewModels;assembly=SquatCounter"
                    xmlns:cui ="clr-namespace:Tizen.Wearable.CircularUI.Forms;assembly=Tizen.Wearable.CircularUI.Forms"
                    x:Class="SquatCounter.Views.SquatCounterPage"
                    NavigationPage.HasNavigationBar="False">
    
        <cui:CirclePage.BindingContext>
            <viewModels:SquatCounterPageViewModel />
        </cui:CirclePage.BindingContext>
    
        <cui:CirclePage.Content>
            <AbsoluteLayout>
                <Image AbsoluteLayout.LayoutBounds="0, 0, 360, 360"
                       Source="squat_background.png" />
                <Label AbsoluteLayout.LayoutBounds="130, 28, 100, 47"
                       TextColor="#77A6D2"
                       FontFamily="BreezeSans"
                       FontSize="Large"
                       HorizontalTextAlignment="Center"
                       Text="Squats" />
                <Label AbsoluteLayout.LayoutBounds="118, 90, 123, 72"
                       TextColor="#FFFEFE"
                       FontSize="28.50"
                       FontFamily="BreezeSans"
                       HorizontalTextAlignment="Center"
                       Text="{Binding SquatsCount}" />
                <Label AbsoluteLayout.LayoutBounds="148, 184, 64, 27"
                       FontFamily="BreezeSans"
                       FontSize="Small"
                       HorizontalTextAlignment="Center"
                       TextColor="#77A6D2"
                       Text="Time" />
                <Label AbsoluteLayout.LayoutBounds="133, 228, 94, 29"
                       TextColor="#FFFEFE"
                       FontFamily="BreezeSans"
                       FontSize="Large"
                       HorizontalTextAlignment="Center"
                       Text="{Binding Time}" />
                <AbsoluteLayout.GestureRecognizers>
                    <TapGestureRecognizer Command="{Binding ChangeServiceStateCommand}" />
                </AbsoluteLayout.GestureRecognizers>
            </AbsoluteLayout>
        </cui:CirclePage.Content>
    
    </cui:CirclePage>  
    
    
  2. Here, add action button from Circular UI to reset timer and counted squats:

    
    <?xml version="1.0" encoding="utf-8" ?>
    <cui:CirclePage xmlns="http://xamarin.com/schemas/2014/forms"
                    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                    xmlns:viewModels="clr-namespace:SquatCounter.ViewModels;assembly=SquatCounter"
                    xmlns:cui ="clr-namespace:Tizen.Wearable.CircularUI.Forms;assembly=Tizen.Wearable.CircularUI.Forms"
                    x:Class="SquatCounter.Views.SquatCounterPage"
                    NavigationPage.HasNavigationBar="False">
    
        <cui:CirclePage.BindingContext>
            <viewModels:SquatCounterPageViewModel />
        </cui:CirclePage.BindingContext>
    
        <cui:CirclePage.Content>
            <AbsoluteLayout>
                <Image AbsoluteLayout.LayoutBounds="0, 0, 360, 360"
                       Source="squat_background.png" />
                <Label AbsoluteLayout.LayoutBounds="130, 28, 100, 47"
                       TextColor="#77A6D2"
                       FontFamily="BreezeSans"
                       FontSize="Large"
                       HorizontalTextAlignment="Center"
                       Text="Squats" />
                <Label AbsoluteLayout.LayoutBounds="118, 90, 123, 72"
                       TextColor="#FFFEFE"
                       FontSize="28.50"
                       FontFamily="BreezeSans"
                       HorizontalTextAlignment="Center"
                       Text="{Binding SquatsCount}" />
                <Label AbsoluteLayout.LayoutBounds="148, 184, 64, 27"
                       FontFamily="BreezeSans"
                       FontSize="Small"
                       HorizontalTextAlignment="Center"
                       TextColor="#77A6D2"
                       Text="Time" />
                <Label AbsoluteLayout.LayoutBounds="133, 228, 94, 29"
                       TextColor="#FFFEFE"
                       FontFamily="BreezeSans"
                       FontSize="Large"
                       HorizontalTextAlignment="Center"
                       Text="{Binding Time}" />
                <AbsoluteLayout.GestureRecognizers>
                    <TapGestureRecognizer Command="{Binding ChangeServiceStateCommand}" />
                </AbsoluteLayout.GestureRecognizers>
            </AbsoluteLayout>
        </cui:CirclePage.Content>
    
        <cui:CirclePage.ActionButton>
            <cui:ActionButtonItem Text="RESET"
                                  BackgroundColor="#2a537d"
                                  Command="{Binding ResetCommand}"/>
        </cui:CirclePage.ActionButton>
    
    </cui:CirclePage>   
    
    
  3. Let's implement trigger to disable the action button while time is counted:

    
    <?xml version="1.0" encoding="utf-8" ?>
    <cui:CirclePage xmlns="http://xamarin.com/schemas/2014/forms"
                    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                    xmlns:viewModels="clr-namespace:SquatCounter.ViewModels;assembly=SquatCounter"
                    xmlns:cui ="clr-namespace:Tizen.Wearable.CircularUI.Forms;assembly=Tizen.Wearable.CircularUI.Forms"
                    x:Class="SquatCounter.Views.SquatCounterPage"
                    NavigationPage.HasNavigationBar="False">
    
        <cui:CirclePage.BindingContext>
            <viewModels:SquatCounterPageViewModel />
        </cui:CirclePage.BindingContext>
    
        <cui:CirclePage.Content>
            <AbsoluteLayout>
                <Image AbsoluteLayout.LayoutBounds="0, 0, 360, 360"
                       Source="squat_background.png" />
                <Label AbsoluteLayout.LayoutBounds="130, 28, 100, 47"
                       TextColor="#77A6D2"
                       FontFamily="BreezeSans"
                       FontSize="Large"
                       HorizontalTextAlignment="Center"
                       Text="Squats" />
                <Label AbsoluteLayout.LayoutBounds="118, 90, 123, 72"
                       TextColor="#FFFEFE"
                       FontSize="28.50"
                       FontFamily="BreezeSans"
                       HorizontalTextAlignment="Center"
                       Text="{Binding SquatsCount}" />
                <Label AbsoluteLayout.LayoutBounds="148, 184, 64, 27"
                       FontFamily="BreezeSans"
                       FontSize="Small"
                       HorizontalTextAlignment="Center"
                       TextColor="#77A6D2"
                       Text="Time" />
                <Label AbsoluteLayout.LayoutBounds="133, 228, 94, 29"
                       TextColor="#FFFEFE"
                       FontFamily="BreezeSans"
                       FontSize="Large"
                       HorizontalTextAlignment="Center"
                       Text="{Binding Time}" />
                <AbsoluteLayout.GestureRecognizers>
                    <TapGestureRecognizer Command="{Binding ChangeServiceStateCommand}" />
                </AbsoluteLayout.GestureRecognizers>
            </AbsoluteLayout>
        </cui:CirclePage.Content>
    
        <cui:CirclePage.ActionButton>
            <cui:ActionButtonItem Text="RESET"
                                  BackgroundColor="#2a537d"
                                  Command="{Binding ResetCommand}"/>
        </cui:CirclePage.ActionButton>
    
        <cui:CirclePage.Triggers>
            <DataTrigger TargetType="cui:CirclePage"
                         Binding="{Binding IsCounting}"
                         Value="True">
                <Setter Property="ActionButton">
                    <Setter.Value>
                        <cui:ActionButtonItem IsVisible="False" />
                    </Setter.Value>
                </Setter>
            </DataTrigger>
        </cui:CirclePage.Triggers>
    
    </cui:CirclePage>   
    
    
  4. Modify SquatCounterPage.xaml.cs:

    
    using Tizen.Wearable.CircularUI.Forms;
    
    namespace SquatCounter.Views
    {
        public partial class SquatCounterPage : CirclePage
        {
            public SquatCounterPage()
            {
                InitializeComponent();
            }
        }
    }   
    
    
  5. At the last step, you need to implement GoToSquatCounterPage method, which will push our page:

    
    using SquatCounter.Views;
    using Xamarin.Forms;
    
    namespace SquatCounter.Services
    {
        public sealed class PageNavigationService
        {
            private static PageNavigationService _instance;
    
            public static PageNavigationService Instance
            {
                get => _instance ?? (_instance = new PageNavigationService());
            }
    
            public void GoToGuidePage()
            {
                Application.Current.MainPage = new NavigationPage(new GuidePage());
            }
    
            public async void GoToSquatCounterPage()
            {
                await Application.Current.MainPage.Navigation.PushAsync(new SquatCounterPage());
            }
        }
    }
    
    

You have finished the squat counter app! Now, after you build and run app, you can go to SquatCounterPage from GuideClockPage.

You're done!

Congratulations! You have successfully achieved the goal of this Code Lab. Now, you can develop your own squat counter app by yourself! If you're having trouble, you may download this file:

Squat Counter Complete Code
(173.64 KB)