読者です 読者をやめる 読者になる 読者になる

ぴよぴよエンジニアの日記

ぴよぴよエンジニアの日記です 技術系のことや日常のことをつぶやきます

「JXUGC #22 最新事例&お前のアプリを説明してもらおうの会」で発表してきました

Xamarin C# Android iOS

「JXUGC #22 最新事例&お前のアプリを説明してもらおうの会」で発表してきました.

jxug.connpass.com


技術的に深い話ができなさそうだったので、メインの内容は Xamarin.Traditional vs. Xamarin.Forms と各種ライブラリの紹介になっております.

www.slideshare.net

資料をアップしておりますので、よろしければご覧ください.


今回の発表は自分の中で反省が多かったので今後に活かしたいと思います...;


スタッフの皆様、ビデオ配信班の皆様お疲れ様でした!

またよろしくお願いします!

WPF の OxyPlot グラフを Xamarin.Forms に移植してみた

Xamarin WPF OxyPlot C# Android

WPF で OxyPlot を用いて作ったグラフを Xamarin.Forms に移植してみました.


santea.hateblo.jp

Xamarin.Forms で OxyPlot を試してみた記事はこちらになります.
OxyPlot for Xamarin.Forms はプレリリースパッケージです.ご注意ください.





WPF の実装

f:id:Santea:20170113133903j:plain

こちらが WPF で作ったグラフです.
OxyPlot for WPF を用いてグラフを描画しています.



<Page x:Class="Mobiquitous2016App.Views.Pages.ECGsPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      xmlns:oxy="http://oxyplot.org/wpf"
      xmlns:vm="clr-namespace:Mobiquitous2016App.ViewModels.PageViewModels"
      d:DesignHeight="1000"
      d:DesignWidth="1000"
      Background="{DynamicResource MaterialDesignPaper}"
      FontFamily="{StaticResource MaterialDesignFont}"
      mc:Ignorable="d">

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <oxy:PlotView Grid.Row="0"
                      Grid.Column="0"
                      Model="{Binding PlotModelConvertLoss}" />

        <oxy:PlotView Grid.Row="1"
                      Grid.Column="0"
                      Model="{Binding PlotModelAirResistance}" />

        <oxy:PlotView Grid.Row="0"
                      Grid.Column="1"
                      Model="{Binding PlotModelRollingResistance}" />

        <oxy:PlotView Grid.Row="1"
                      Grid.Column="1"
                      Model="{Binding PlotModelRegeneLoss}" />

    </Grid>
</Page>

こちらが View になります.
グラフを描画する Control が OxyPlot の PlotView になります.
四辺の PlotView それぞれに OxyPlot の PlotModel をバインドしています.



using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Diagnostics;
using Livet;
using Livet.Commands;
using Livet.Messaging;
using Livet.Messaging.IO;
using Livet.EventListeners;
using Livet.Messaging.Windows;

using Mobiquitous2016App.Models;
using Mobiquitous2016App.Models.GraphModels;
using Mobiquitous2016App.ViewModels.WindowViewModels;
using OxyPlot;
using OxyPlot.Axes;
using OxyPlot.Series;

namespace Mobiquitous2016App.ViewModels.PageViewModels
{
    // ReSharper disable once InconsistentNaming
    public class ECGsPageViewModel : ViewModel
    {
        private readonly IList<GraphDatum> _graphData;

        private readonly double _maximum;

        #region PlotModelConvertLoss変更通知プロパティ
        private PlotModel _PlotModelConvertLoss;

        public PlotModel PlotModelConvertLoss
        {
            get
            { return _PlotModelConvertLoss; }
            set
            {
                if (_PlotModelConvertLoss == value)
                    return;
                _PlotModelConvertLoss = value;
                RaisePropertyChanged();
            }
        }
        #endregion

        #region PlotModelAirResistance変更通知プロパティ
        private PlotModel _PlotModelAirResistance;

        public PlotModel PlotModelAirResistance
        {
            get
            { return _PlotModelAirResistance; }
            set
            {
                if (_PlotModelAirResistance == value)
                    return;
                _PlotModelAirResistance = value;
                RaisePropertyChanged();
            }
        }
        #endregion

        #region PlotModelRollingResistance変更通知プロパティ
        private PlotModel _PlotModelRollingResistance;

        public PlotModel PlotModelRollingResistance
        {
            get
            { return _PlotModelRollingResistance; }
            set
            {
                if (_PlotModelRollingResistance == value)
                    return;
                _PlotModelRollingResistance = value;
                RaisePropertyChanged();
            }
        }
        #endregion

        #region PlotModelRegeneLoss変更通知プロパティ
        private PlotModel _PlotModelRegeneLoss;

        public PlotModel PlotModelRegeneLoss
        {
            get
            { return _PlotModelRegeneLoss; }
            set
            {
                if (_PlotModelRegeneLoss == value)
                    return;
                _PlotModelRegeneLoss = value;
                RaisePropertyChanged();
            }
        }
        #endregion

        public ECGsPageViewModel(GraphWindowViewModel parentViewModel)
        {
            _graphData = parentViewModel.GraphDataList;

            _maximum = new double[]
            {
                parentViewModel.GraphDataList.Max(v => v.ConvertLoss),
                parentViewModel.GraphDataList.Max(v => v.AirResistance),
                parentViewModel.GraphDataList.Max(v => v.RollingResistance),
                parentViewModel.GraphDataList.Max(v => v.RegeneLoss)
            }.Max();

            Initialize();
        }

        public void Initialize()
        {
            PlotModelConvertLoss = CreatePlotModel("ConvertLoss");
            PlotModelAirResistance = CreatePlotModel("AirResistance");
            PlotModelRollingResistance = CreatePlotModel("RollingResistance");
            PlotModelRegeneLoss = CreatePlotModel("RegeneLoss");
        }

        private PlotModel CreatePlotModel(string propertyName)
        {
            string title = null;
            switch (propertyName)
            {
                case "ConvertLoss":
                    title = "Convert loss";
                    break;
                case "AirResistance":
                    title = "Air resistance";
                    break;
                case "RollingResistance":
                    title = "Rolling resistance";
                    break;
                case "RegeneLoss":
                    title = "Regene loss";
                    break;
            }

            var model = new PlotModel
            {
                Subtitle = title,
                PlotMargins = new OxyThickness(double.NaN, double.NaN, 80, double.NaN)
            };

            var colorAxis = new LinearColorAxis
            {
                HighColor = OxyColors.Gray,
                LowColor = OxyColors.Black,
                Position = AxisPosition.Right,
                MajorStep = 0.02,
                Minimum = 0,
                Maximum = _maximum,
                Unit = "kWh",
                AxisTitleDistance = 0
            };
            model.Axes.Add(colorAxis);

            var xAxis = new LinearAxis
            {
                Title = "Transit time",
                Unit = "s",
                Position = AxisPosition.Bottom
            };
            model.Axes.Add(xAxis);

            var yAxis = new LinearAxis
            {
                Title = "Lost energy",
                Unit = "kWh"
            };
            model.Axes.Add(yAxis);

            var scatterSeries = new ScatterSeries();

            foreach (var datum in _graphData)
            {
                scatterSeries.Points.Add(new ScatterPoint(datum.TransitTime, datum.LostEnergy)
                {
                    Value = (float)typeof(GraphDatum).GetProperty(propertyName).GetValue(datum)
                });
            }

            model.Series.Add(scatterSeries);

            return model;
        }
    }
}

WPF では MVVM アーキテクチャとして Livet を用いています.



scatterSeries.Points.Add(new ScatterPoint(datum.TransitTime, datum.LostEnergy)
{
    Value = (float)typeof(GraphDatum).GetProperty(propertyName).GetValue(datum)
});

ScatterSeries に ScatterPoint を追加する部分では、メソッドの引数として与えた propertyName で値を取り出しています.
ScatterPoint の Value プロパティにバインドができれば ViewModel がもっと簡素になるのですが、バインドできなかったためこのようにしています.


以上が WPF の実装になります.



Xamarin.Forms の実装

f:id:Santea:20170113141847p:plain

こちらが Xamarin.Forms で作ったグラフです.
OxyPlot for Xamarin.Forms を用いてグラフを描画しています.



<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
             xmlns:forms="clr-namespace:OxyPlot.Xamarin.Forms;assembly=OxyPlot.Xamarin.Forms"
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Class="TOD2017MobileApp.Views.ECGsDemoPage">

  <Grid>
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="*" />
      <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
      <RowDefinition Height="*" />
      <RowDefinition Height="*" />
    </Grid.RowDefinitions>

    <forms:PlotView Grid.Row="0"
              Grid.Column="0"
              Model="{Binding PlotModelConvertLoss.Value}" />

    <forms:PlotView Grid.Row="1"
              Grid.Column="0"
              Model="{Binding PlotModelAirResistance.Value}" />

    <forms:PlotView Grid.Row="0"
              Grid.Column="1"
              Model="{Binding PlotModelRollingResistance.Value}" />

    <forms:PlotView Grid.Row="1"
              Grid.Column="1"
              Model="{Binding PlotModelRegeneLoss.Value}" />

  </Grid>

</ContentPage>

こちらが View になります.
WPF との違いは Page が ContentPage になっているところ、OxyPlot の名前空間、PlotModel のバインディングが ReactiveProperty になっているところです.
WPF とほとんど変わらないのが見て取れると思います.



using Prism.Commands;
using Prism.Mvvm;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using OxyPlot;
using OxyPlot.Axes;
using OxyPlot.Series;
using Plugin.Geolocator.Abstractions;
using Prism.Navigation;
using Reactive.Bindings;
using TOD2017MobileApp.Models;

namespace TOD2017MobileApp.ViewModels
{
    public class ECGsDemoPageViewModel : BindableBase
    {
        private readonly ECGModel _ecgModel;
        private readonly double _maximum;

        public ReactiveProperty<PlotModel> PlotModelConvertLoss { get; set; }
        public ReactiveProperty<PlotModel> PlotModelAirResistance { get; set; }
        public ReactiveProperty<PlotModel> PlotModelRollingResistance { get; set; }
        public ReactiveProperty<PlotModel> PlotModelRegeneLoss { get; set; }

        public ECGsDemoPageViewModel()
        {
            PlotModelConvertLoss = new ReactiveProperty<PlotModel>();
            PlotModelAirResistance = new ReactiveProperty<PlotModel>();
            PlotModelRollingResistance = new ReactiveProperty<PlotModel>();
            PlotModelRegeneLoss = new ReactiveProperty<PlotModel>();
            AtentionText = new ReactiveProperty<string>();
            
            _ecgModel = ECGModel.GetECGModel(new SemanticLink{ SemanticLinkId = 196 });

            _maximum = new double[]
            {
                _ecgModel.GraphData.Max(v => v.ConvertLoss),
                _ecgModel.GraphData.Max(v => v.AirResistance),
                _ecgModel.GraphData.Max(v => v.RollingResistance),
                _ecgModel.GraphData.Max(v => v.RegeneLoss)
            }.Max();

            PlotModelConvertLoss.Value = CreatePlotModel("ConvertLoss");
            PlotModelAirResistance.Value = CreatePlotModel("AirResistance");
            PlotModelRollingResistance.Value = CreatePlotModel("RollingResistance");
            PlotModelRegeneLoss.Value = CreatePlotModel("RegeneLoss");
        }

        private PlotModel CreatePlotModel(string propertyName)
        {
            string title = null;
            switch (propertyName)
            {
                case "ConvertLoss":
                    title = "Convert loss";
                    break;
                case "AirResistance":
                    title = "Air resistance";
                    break;
                case "RollingResistance":
                    title = "Rolling resistance";
                    break;
                case "RegeneLoss":
                    title = "Regene loss";
                    break;
            }

            var model = new PlotModel
            {
                Subtitle = title,
                PlotMargins = new OxyThickness(double.NaN, double.NaN, 80, double.NaN)
            };

            var colorAxis = new LinearColorAxis
            {
                HighColor = OxyColors.Gray,
                LowColor = OxyColors.Black,
                Position = AxisPosition.Right,
                MajorStep = 0.02,
                Minimum = 0,
                Maximum = _maximum,
                Unit = "kWh",
                AxisTitleDistance = 0
            };
            model.Axes.Add(colorAxis);

            var xAxis = new LinearAxis
            {
                Title = "Transit time",
                Unit = "s",
                Position = AxisPosition.Bottom
            };
            model.Axes.Add(xAxis);

            var yAxis = new LinearAxis
            {
                Title = "Lost energy",
                Unit = "kWh"
            };
            model.Axes.Add(yAxis);

            var scatterSeries = new ScatterSeries();

            foreach (var datum in _ecgModel.GraphData)
            {
                scatterSeries.Points.Add(new ScatterPoint(datum.TransitTime, datum.LostEnergy)
                {
                    Value = (float)typeof(GraphDatum).GetRuntimeProperty(propertyName).GetValue(datum)
                });
            }

            model.Series.Add(scatterSeries);

            return model;
        }
    }
}

こちらが Xamarin.Forms の ViewModel です.
MVVM アーキテクチャとして Prism.Forms を用いています.



scatterSeries.Points.Add(new ScatterPoint(datum.TransitTime, datum.LostEnergy)
{
    Value = (float)typeof(GraphDatum).GetRuntimeProperty(propertyName).GetValue(datum)
});

ScatterSeries に ScatterPoint を追加する部分では、Type.GetProperty から Type.GetRuntimeProperty に変更しています.


以上が Xamarin.Forms の実装になります.



WPF と Xamarin.Forms の比較

WPF と Xamarin.Forms で View、ViewModel のコード変更量を視覚的に比較してみます.
コード変更箇所がオレンジのラインです.

f:id:Santea:20170113152936p:plain

View の変更点としては OxyPlot の名前空間バインディングの値です.

WPF の Page と Xamarin.Forms の ContentPage で違いはありますが、テンプレートから作成すれば変更する必要がないので除外しています.

コード変更量としてはごくごく少ない量であることが見て取れます.


f:id:Santea:20170113153216p:plain

ViewModel の変更点としては、ReactiveProperty の使用と、Type.GetProperty から Type.GetRuntimeProperty への変更です.

こちらもごくごく少ないコード変更量であることが分かります.


私の実装の場合、WPF は Livet を使用、Xamarin.Forms は Prism.Forms + ReactiveProperty を使用するように、MVVM アーキテクチャの利用に違いがありましたが、WPF も Prism を用いる実装にすればさらに移植が楽になると思われます.



まとめ

OxyPlot のように、WPF と Xamarin.Forms で共通に利用できるライブラリがあれば、たとえグラフのような複雑な View であっても、WPF から Xamarin.Forms に簡単に移植が行えます.

XAML を共通化できること、バインディングにより ViewModel を共通化できることが非常に効率的です.


既存の WPF アプリケーションからモバイルアプリへの移植を考える場合、共通に使えるライブラリがあれば比較的簡単に移植ができる例の紹介でした.


以上です.