Condividi tramite


Teil 1/2: Xamarin Case Study: Smart Energy Management System by Levion

 

Wir wollen euch in den nächsten Monaten von ISVs und Startups berichten, mit denen wir an verschiedenen Technologien arbeiten und auf interessante technische Fragen aufstoßen. Solche Lösungen wollen wir mit euch teilen. Vielleicht hilft es euch bei euren Projekten oder inspiriert für neue Ansätze.

Levion Technologies GmbH ist ein Grazer Startup, das eine innovative Lösung im Bereich von Energie Management hat. Die hier zum Teil vorgestellte Lösung besitzt auch eine öffentliche API , sodass Entwickler weitere Lösungen auf ihr aufbauen können.

 

Intro

LEVION offers a Smart Energy Management System where users can montior and manage the energy profile and consumption of their electrical appliances at home.

In this development effort Microsoft supported LEVION to build a mobile application that connects with their SEMS control unit. The app was built with Xamarin in order to accelerate development for iOS, Android and Windows Phone and the app uses backend services in the form of Azure App Service and Redis Cache on Azure to store and visualize relevant data for their solution. In the first step the build for UWP platform is prioritized so the ISV could leverage their extensive .Net and UWP Skills. We further looked at the DevOps process together to create automated builds for their apps.

  • Key technologies used:
    • Xamarin
    • Template 10 (UWP)
    • MVVMlight by GalaSoft
    • Azure Web Apps & Redis Cache on Azure
    • VSTS for Build and Release Management and Source Control

Customer profile

Kernteam LEVION Technologies offers complete software and hardware solutions as well as consulting in projects. The foremost goal of this innovative company located in Austria is to provide cutting-edge technology in a simple and meaningful way, always adopting the users' point of view and the benefits of the product itself. Therefore, quality, usability and service play an important role in all decision-making processes.

SEMSLogo

Their main focus lies on the smart energy management system “SEMS”. It manages energy intelligently and opens a path for the meaningful use of renewable energies. Thanks to the smart connection of large electricity consumers, household appliances and photovoltaic systems the energy can always be consumed when it comes cheap and therefore is produced in excess. The central control unit SEM (short for: Smart Energy Manager) can be operated intuitively from the living room – completely without annoying cabling or major installation efforts. This way, the energy household can be optimized simple, smart and swift.

sems-schema-systemübersicht-2

Problem statement

Currently, LEVION offers the Smart Energy Management Systems SEMS with their Smart Energy Manager SEM as a control unit. SEMS allows customers to monitor and control the energy consumption of their connected electrical appliances and devices. Since the solution is rolled out in different parts of Austria, including rural areas where Internet connectivity among the households is not always given, it was important that the solution would work both with and without an internet connection.

LEVION wanted to enable their customers to monitor and control their energy consumption not only through their own custom control unit but also through a mobile application that is available on Android, iOS and Windows. Since LEVION focuses their technology skills on .NET and not Java or Swift/Objective C, they were considering of the developing the UWP version inhouse and outsourcing the Android and iOS development to agencies. We supported LEVION by introducing Xamarin as a cross-plattform native technology using .NET so that they opted for Xamarin as a way to develop a native application with a common codebase. This way they could not only leverage their .NET Skills to the fullest but also keep the iOS and Android developments inhouse. For the scope of this case study we have focused on the UWP version first.

Solution, steps, and delivery

Architecture

A technical overview of the Smart Energy Management Solution can be seen in the following architectural diagram.

ArchitekturFin

The solution revolves around the custom control unit of the SEMS System. This control unit connects directly to an Azure Web App to store persistent data in Azure Blob Storage and Azure SQL Database.

The mobile application should speak to the control unit and allow the user to not only monitor the energy consumption through the control unit, which is mounted at a fixed place in the house, but also anywhere with a mobile device. Since LEVION did not want to assume that their customer has an internet connection it was important that the mobile app connects to the control unit directly over sockets via TCP with UDP discovery. If there is internet connection, then the mobile app connects via a broker in Azure to the control unit. The broker is essentially an Azure Web App with a Redis Cache for performance improvements.

The connection to the control unit allows the mobile application to display current and recent energy data. For a future version the mobile app will also display historic energy data for which the mobile application will directly connect to the SEMS Service in Azure that returns the historic data from the Azure SQL DB and Blob Storage.

Xamarin Solution

Xamarin allows you to natively develop cross-platform apps where you can share code of your app logic in Portable Class Libraries or Shared Projects, which can be referenced in the native platform projects (iOS, Android, Windows UWP). The UI can be developed either natively in the platform projects or with Xamarin.Forms you can also share most of the UI Code.

Due to the fact, that LEVION uses a lot of custom controls in their UI, displaying diagrams of the energy consumption, we decided to share only the app logic and implement the Views for the App UI natively on each platform.

Below you can see the solution structure of the Xamarin app.

XamarinSolutionOverviewHighlighted

The portable class libraries are highlighted and these are the projects that are used in the following platform projects:

  • Levion.Sems.Mobile.UWP (Windows)
  • Levion.Sems.Mobile.Android
  • Levion.Sems.Mobile.iOs

The shared logic of the app is divided into several portable class libraries:

  • Levion.SEMS.Mobile.Core contains all the models for the entities of the system.
  • Levion.SEMS.Mobile.DAL contains the logic for connecting to the broker and connecting directly to the control unit.
  • Levion.SEMS.Mobile.BI contains most of the app logic.
  • Levion.SEMS.Mobile.ViewLogic contains all ViewModels for the native platform Views.

For UWP we also have a helper project Levion.Lib.Mobile.UWP that only contains some custom UI control (hamburger menu and corresponding MenuNavigationService) that we will explain in the Challenges section.

Highlights and Challenges

MVVMlight, Template 10 and Xamarin

Since LEVION is already experienced with UWP Development they wanted to leverage Template10 as it already contains a good structure with services like Navigation Service and also ready-made UI controls. Especially the hamburger control works very well in Template 10 and they wanted to reuse this from Template 10.

LEVION also wanted to incorporate the MVVM pattern for their Xamarin solution. We discussed different frameworks and finally settled for MVVMlight by GalaSoft as it is a light framework and works well with Xamarin.

The challenge that we faced here was that Template 10 already comes with its own navigation service, as does MVVMlight. The hamburger menu control from Template 10 is tightly linked to its own navigation service. Since we did not want to implement our own interactive hamburger menu, what we did is take out the source code of the hamburger menu into Levion.SEMS.Lib.Mobile.UWP and hook it up with our custom navigation service that inherits from MVVMlight navigation service, so that it all plays nicely together.

This is the interface of our custom navigation service (PCL):

 using GalaSoft.MvvmLight.Views;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.UI.Xaml.Controls;

namespace Levion.Lib.Mobile.Uwp.Services
{
    /// <summary>
    /// Navigation service for menus.
    /// </summary>
    public interface IMenuNavigationService : INavigationService
    {
        Frame Content { get; }
    }
}

Here is the implementation (PCL):

 using GalaSoft.MvvmLight.Views;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace Levion.Lib.Mobile.Uwp.Services
{
    /// <summary>
    /// Navigation service for UWP applications with custom menu.
    /// </summary>
    public class MenuNavigationService : IMenuNavigationService
    {
        /// <summary>
        /// The key that is returned by the <see cref="CurrentPageKey"/> property
        /// when the current Page is the root page.
        /// </summary>
        public const string RootPageKey = "-- ROOT --";

        /// <summary>
        /// The key that is returned by the <see cref="CurrentPageKey"/> property
        /// when the current Page is not found.
        /// This can be the case when the navigation wasn't managed by this NavigationService,
        /// for example when it is directly triggered in the code behind, and the
        /// NavigationService was not configured for this page type.
        /// </summary>
        public const string UnknownPageKey = "-- UNKNOWN --";

        /// <summary>
        /// The pages by key.
        /// </summary>
        private readonly Dictionary<string, Type> _pagesByKey = new Dictionary<string, Type>();

        /// <summary>
        /// Initializes a new instance of the <see cref="MenuNavigationService"/> class.
        /// </summary>
        public MenuNavigationService()
        {
        }

        /// <summary>
        /// The key corresponding to the currently displayed page.
        /// </summary>
        public string CurrentPageKey
        {
            get
            {
                lock (_pagesByKey)
                {
                    var frame = this.GetFrame();

                    if (frame == null)
                    {
                        return null;
                    }

                    if (frame.BackStackDepth == 0)
                    {
                        return RootPageKey;
                    }

                    if (frame.Content == null)
                    {
                        return UnknownPageKey;
                    }

                    var currentType = frame.Content.GetType();

                    if (_pagesByKey.All(p => p.Value != currentType))
                    {
                        return UnknownPageKey;
                    }

                    var item = _pagesByKey.FirstOrDefault(
                        i => i.Value == currentType);

                    return item.Key;
                }
            }
        }

        /// <summary>
        /// Gets the content.
        /// </summary>
        /// <value>
        /// The content.
        /// </value>
        public Frame Content
        {
            get
            {
                return this.GetFrame();
            }
        }

        /// <summary>
        /// If possible, instructs the navigation service
        /// to discard the current page and display the previous page
        /// on the navigation stack.
        /// </summary>
        public void GoBack()
        {
            var frame = this.GetFrame();

            if (frame.CanGoBack)
            {
                frame.GoBack();
            }
        }

        /// <summary>
        /// Instructs the navigation service to display a new page
        /// corresponding to the given key. Depending on the platforms,
        /// the navigation service might have to be configured with a
        /// key/page list.
        /// </summary>
        /// <param name="pageKey">The key corresponding to the page
        /// that should be displayed.</param>
        public void NavigateTo(string pageKey)
        {
            this.NavigateTo(pageKey, null);
        }

        /// <summary>
        /// Instructs the navigation service to display a new page
        /// corresponding to the given key, and passes a parameter
        /// to the new page.
        /// Depending on the platforms, the navigation service might
        /// have to be Configure with a key/page list.
        /// </summary>
        /// <param name="pageKey">The key corresponding to the page
        /// that should be displayed.</param>
        /// <param name="parameter">The parameter that should be passed
        /// to the new page.</param>
        /// <exception cref="System.ArgumentException">pageKey</exception>
        public void NavigateTo(string pageKey, object parameter)
        {
            lock (_pagesByKey)
            {
                if (!_pagesByKey.ContainsKey(pageKey))
                {
                    throw new ArgumentException(
                        string.Format(
                            "No such page: {0}. Did you forget to call NavigationService.Configure?",
                            pageKey),
                        "pageKey");
                }


                ////var frame = this.rootFrame ?? ((Frame)Window.Current.Content);
                var frame = this.GetFrame();

                if (frame != null)
                {
                    frame.Navigate(_pagesByKey[pageKey], parameter);
                }
            }
        }

        /// <summary>
        /// Adds a key/page pair to the navigation service.
        /// </summary>
        /// <param name="key">The key that will be used later
        /// in the <see cref="NavigateTo(string)"/> or <see cref="NavigateTo(string, object)"/> methods.</param>
        /// <param name="pageType">The type of the page corresponding to the key.</param>
        public void Configure(string key, Type pageType)
        {
            lock (_pagesByKey)
            {
                if (_pagesByKey.ContainsKey(key))
                {
                    throw new ArgumentException("This key is already used: " + key);
                }

                if (_pagesByKey.Any(p => p.Value == pageType))
                {
                    throw new ArgumentException(
                        "This type is already configured with key " + _pagesByKey.First(p => p.Value == pageType).Key);
                }

                _pagesByKey.Add(
                    key,
                    pageType);
            }
        }

        /// <summary>
        /// Gets the frame.
        /// </summary>
        /// <returns>The current frame.</returns>
        protected virtual Frame GetFrame()
        {
            var current = Window.Current.Content;

            if (current == null)
            {
                return new Frame();
            }            
        }
    }
}

In the platform project for UWP we then get the actual Frame.

 using Levion.Lib.Mobile.Uwp.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace Levion.Sems.Mobile.Uwp.Services
{
    /// <summary>
    /// Navigation service to navigate the views.
    /// </summary>
    public class NavigationService : MenuNavigationService
    {
        /// <summary>
        /// Gets the frame.
        /// </summary>
        /// <returns>
        /// The current frame.
        /// </returns>
        protected override Frame GetFrame()
        {
            var current = Window.Current.Content;

            if (current == null)
            {
                return new Frame();
            }

            // Get frame from hamburger menu.
            if (current is Views.Shell)
            {
                Views.Shell converted = current as Views.Shell;
                var menu = converted.Content as Lib.Mobile.Uwp.Controls.HamburgerMenu;
                return (Frame)menu.ContentFrame;
            }
            else
            {
                return current as Frame;
            }
        }
    }
}

In the OnLaunched() Method of the UWP Project we have the following instantiation for the Navigation Service:

             if (rootFrame == null)
            {
                // Create a Frame to act as the navigation context and navigate to the first page
                rootFrame = new Frame();

                rootFrame.NavigationFailed += OnNavigationFailed;

                if (e.PreviousExecutionState == ApplicationExecutionState.Terminated)
                {
                    //TODO: Load state from previously suspended application
                }

                // Place the frame in the current Window
                Window.Current.Content = new Views.Shell(ServiceLocator.Current.GetInstance<IMenuNavigationService>());
            }

A further adaption was necessary to fix the use of a Pivot control in combination with the hamburger menu. Since on Windows it uses a Pivot that was not responsive, we created our own style version of the Pivot that is responsive regarding the element titles. Here is the corresponding XAML code that is found in a LevionStyles.xaml file for the AdaptivePivot control. We are only posting the relevant portion, because mostly our AdaptivePivot corresponds to the normal Pivot style on UWP. The interesting part here is the NarrowState Visual State.

 <VisualStateGroup x:Name="AdaptiveHeader">
                                <VisualState x:Name="NarrowState">
                                    <VisualState.StateTriggers>
                                        <AdaptiveTrigger MinWindowWidth="0"/>
                                    </VisualState.StateTriggers>
                                    <VisualState.Setters>
                                        <Setter Target="HeaderClipper.Margin" Value="48,0,0,0"/>
                                    </VisualState.Setters>
                                </VisualState>
                                <VisualState x:Name="NormalState">
                                    <VisualState.StateTriggers>
                                        <AdaptiveTrigger MinWindowWidth="512"/>
                                    </VisualState.StateTriggers>
                                    <VisualState.Setters>
                                        <Setter Target="HeaderClipper.Margin" Value="0,0,0,0"/>
                                    </VisualState.Setters>
                                </VisualState>
                                <VisualState x:Name="WideState">
                                    <VisualState.StateTriggers>
                                        <AdaptiveTrigger MinWindowWidth="1200"/>
                                    </VisualState.StateTriggers>
                                    <VisualState.Setters>
                                        <Setter Target="HeaderClipper.Margin" Value="0,0,0,0"/>
                                    </VisualState.Setters>
                                </VisualState>
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups>

To be continued …