Development

Step 1: Create 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, we 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
    • Simalarly, add new folders named ViewModels and Services

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


Step 2: Add the 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.


Step 3: Implement services - part 1

First, we 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, we will add GoToGuidePage method that will display GuidePage. We will also create a body of GoToSquatCounterPage method, which will be completed in 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 we need to do is modify App.cs constructor to display the guide page. At the code below, we 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 build and run application we should see following screen.


Step 4: Implement services - part 2

Now, we will implement a service for the Pressure Sensor. It will allow us to obtain pressure data from 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 our PressureSensorService.cs class. Before we start implementing the pressure sensor, we 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, we need to add PressureSensor instance which allows us to obtain pressure data.

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

    • NotSupportedMsg - this field have 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, we 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. In this step, we will 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 we need to do is release the resources obtained by PressureSensor. To achieve this we will use Dispose pattern. This step is very important for our application. If we 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();
            }
        }
    }   
    

Step 5: Implement services - part 3

The data we receive from the sensor must be processed in order to obtain the necessary information from it. Now we can create our 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, we need to add 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, we will 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. In this step, we will 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, we will create 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;
                }
            }
        }
    }   
    

Step 6: Implement viewmodels - part 1

In order to show all the data to the user we 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. In this step, we will create our 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, we 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, we 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. In this step, we will 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, we need to release 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;
                }
            }   
    

Step 7: Implement viewmodels - part 2

In this step, we 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, we will implement 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();
            }
        }
    }   
    

Step 8: UI - Implement guide pages

It is time to deal with the user interface. In this part, we 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 we 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. Now, we 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: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, we need to 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. We also need to 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, we can 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. We need modify GuideClockPage.xaml.cs in similar way as like in Step 8-4.

    using Tizen.Wearable.CircularUI.Forms;
    
    namespace SquatCounter.Views
    {
        public partial class GuideClockPage : CirclePage
        {
            public GuideClockPage()
            {
                InitializeComponent();
            }
        }
    }   
    
  7. Now, we have implemented GuideStepPage to introduce the user on how to do squats and the GuideClockPage. In addition, we 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 build and run our app we will see four pages.

    • Step pages

    • Clock page


Step 9: UI - Implement squat counter 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, we 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, we need to 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. We also need to 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, we 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());
            }
        }
    }
    

We have finished our 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 activity. Now, you can develop your own Squat Counter app by yourself! But, if you're having trouble, you may check out the link below.

Squat Counter Complete Code173.64 KB