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

かずきのBlog@hatena

XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

Xamarin.Formsでグラフを描こう(OxyPlot)

Xamarin

WPFでグラフを描けるライブラリを探してたらOxyPlotというのを見つけました。 見つけたと思ったら、こいつXamarin.Formsでもできるぞ!?ということで試して見ました。

NuGetの追加

以下のパッケージを追加します。

  • OxyPlot.Xamarin.Forms

初期化

以下のコードをXamarin.Forms.Forms.Init()の後に追加します。

PlotViewRenderer.Init();

画面構築

画面にPlotView部品を置きます。そしてModelプロパティをバインドします。

<?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:local="clr-namespace:OxyApp"
    xmlns:oxy="clr-namespace:OxyPlot.Xamarin.Forms;assembly=OxyPlot.Xamarin.Forms"
    x:Class="OxyApp.OxyAppPage">
    <ContentPage.BindingContext>
        <local:OxyAppPageViewModel />
    </ContentPage.BindingContext>
    <oxy:PlotView Model="{Binding Model}" />
</ContentPage>

ViewModelの構築

ViewModelでグラフのモデルを構築します。

using System;
using OxyPlot;
using OxyPlot.Series;

namespace OxyApp
{
    public class OxyAppPageViewModel
    {
        public PlotModel Model { get; }

        public OxyAppPageViewModel()
        {
            this.Model = new PlotModel { Title = "Hello OxyPlot" };
            this.Model.Series.Add(new LineSeries
            {
                Points =
                {
                    new DataPoint(0, 10),
                    new DataPoint(1, 20),
                    new DataPoint(2, 15),
                    new DataPoint(3, 40),
                }
            });
        }
    }
}

Seriesにグラフの種類を表すSeries(今回の場合は折れ線グラフ)を追加していく形になります。DataPointをPointsに追加することで折れ線のデータを指定できます。

実行して動作確認

こんな感じで表示されます。

f:id:okazuki:20170116083107p:plain

感想

パッと触った感じ、なかなかいいんじゃないんでしょうか?? ライセンスもMITで扱いやすそう。

2016年に最も読まれた記事 TOP10 世間はDataTableと戦っている

Other

2017年も始まってしばらく経ったので2016年の振り返りをして見たいと思います。 ということで、2016年に最も読まれた記事TOP10でも見て見たいと思います。

1位

blog.okazuki.jp

あっはい。

世間は、まだDataTable全盛期なんでしょうか。2009年に書いてる記事がNo1でした。

2位

blog.okazuki.jp

WPF!!

DataTableと比べて大分進化した気がします。2014年の記事ですね。 コレクションをデータバインドするのは、基本ですので是非抑えたいところ。

3位

blog.okazuki.jp

スマホでListView的な表記が流行ってるとはいえDataGridの人気も根強いみたいです。 そういえば、エクセルソフトさんでDataGridのセッションしたりしたなぁ

4位

blog.okazuki.jp

小ネタですね。 まぁヒープに無慈悲にインスタンス作りまくるので、あまり大きなサイズのデータではやりたくないところですが、知りたいことって多いですよね。

5位

blog.okazuki.jp

Excelを出力したいというネタも根強いのでしょう。 何故かNPOIの公式サイトから、Japanese Documentationとしてリンクが貼られてるこのブログ記事が5位にランクインしました。

6位

blog.okazuki.jp

これは何ででしょうね? スクロールが上手くいかないことが多いんでしょうか。

7位

blog.okazuki.jp

WPFで複雑なレイアウトを組むならこのGridですよね。 ということで、こいつが7位にランクインしました。

8位

blog.okazuki.jp

ComboBoxも地味に使うコントロール。WPFではテンプレート使えばかなりリッチな見た目できますよね。

9位

blog.okazuki.jp

多分、Windows Formsから来た人がつまづくであろうポイントの1つですね。 これは重要なので抑えておきましょう!!

10位

blog.okazuki.jp

WPFもルーティングイベントとかあって特殊な感じですよね。 この挙動を知ってるか知らないかで細かな動きの制御ができるかどうかとかが関わって来たりします。

まとめ

来年はDataTableの記事を超えるものを書きたいな。

Microsoft Azure ServiceFabricで複数のサービスで設定値を共通化したい

Azure

同じDBに繋ぐときとか一か所で設定を終えたいですよね。 ということで手順を備忘録的に残しておこうと思います。基本的には以下に書いてあることです。

Service Fabric での複数の環境の管理 | Microsoft Docs

Step1

サービスのプロジェクト/PackageRoot/COnfig/Settings.xmlに設定値を書く。これは設定を共通化したいプロジェクトが2個あったら2個ともにやります。

<?xml version="1.0" encoding="utf-8" ?>
<Settings xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.microsoft.com/2011/01/fabric">
  <!-- これは StateManager のレプリケーターにより使用されます。-->
  <Section Name="ReplicatorConfig">
    <Parameter Name="ReplicatorEndpoint" Value="ReplicatorEndpoint" />
  </Section>
  <!-- これは StateManager のレプリケーション トラフィックをセキュリティで保護するために使用されます。-->
  <Section Name="ReplicatorSecurityConfig" />

  <!-- ここで、カスタム構成セクションとパラメーターを追加します。-->
  <!--
  <Section Name="MyConfigSection">
    <Parameter Name="MyParameter" Value="Value1" />
  </Section>
  -->

  <!-- これを追加 -->
  <Section Name="UserDatabase">
    <Parameter Name="ConnectionString" Value="" />
  </Section>
</Settings>

Valueにはなんか書いてもかまいませんが、どうせ後で上書きされるので適当な値を突っ込んでおきましょう。

Step2

アプリケーションのプロジェクトのApplicationPackageRoot/ApplicationManifest.xmlのServiceManifestImportのConfigOverridesで先ほどの設定をオーバーライドする定義を追加します。これも2サービスあったら2つぶんやります。

Valueのところでかっこで囲っているのがパラメータ名になります。

<ServiceManifestImport>
  <ServiceManifestRef ServiceManifestName="Stateful1Pkg" ServiceManifestVersion="1.0.0" />
  <ConfigOverrides>
    <!-- こんな感じ -->
    <ConfigOverride Name="Config">
      <Settings>
        <Section Name="UserDatabase">
          <Parameter Name="ConnectionString" Value="[ConnectionString]" />
        </Section>
      </Settings>
    </ConfigOverride>
  </ConfigOverrides>
  <!-- ServicePackage から ServiceManifest をインポートします。ServiceManifestName と ServiceManifestVersion は、
     ServiceManifest.xml ファイルで定義されている ServiceManifest 要素の [Name] 属性と [Version] 属性と 
     一致しなければなりません。-->
</ServiceManifestImport>

そして、同じファイルのParametersタグに先ほど追加したかっこで囲ったパラメータ名のパラメータを追加します。

<Parameters>
  <Parameter Name="Stateful1_MinReplicaSetSize" DefaultValue="3" />
  <Parameter Name="Stateful1_PartitionCount" DefaultValue="1" />
  <Parameter Name="Stateful1_TargetReplicaSetSize" DefaultValue="3" />
  <Parameter Name="Stateless1_InstanceCount" DefaultValue="-1" />
  <!-- これを追加 -->
  <Parameter Name="ConnectionString" DefaultValue="DefaultConnectionString" />
</Parameters>

Step3

アプリケーションのプロジェクトのAplicationParametersフォルダの下にある構成単位のxmlファイルの中身を構成に合わせて設定します。 Local.Node1.xmlやLocal.Node5.xmlの場合はローカルDBの接続文字列とか、Cloud.xmlはSQL Databaseとかいった感じです。

こんな風になります。

<?xml version="1.0" encoding="utf-8"?>
<Application xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" Name="fabric:/Application1" xmlns="http://schemas.microsoft.com/2011/01/fabric">
  <Parameters>
    <Parameter Name="Stateful1_PartitionCount" Value="1" />
    <Parameter Name="Stateful1_MinReplicaSetSize" Value="1" />
    <Parameter Name="Stateful1_TargetReplicaSetSize" Value="1" />
    <Parameter Name="Stateless1_InstanceCount" Value="1" />
    <!-- これを追加 -->
    <Parameter Name="ConnectionString" Value="Node1ConnectionString" />
  </Parameters>
</Application>

Step4

定義は完了です。あとはコードからの参照方法ですが以下のようなコードで出来ます。

this.Context
  .CodePackageActivationContext
  .GetConfigurationPackageObject("Config")
  .Settings
  .Sections["UserDatabase"]
  .Parameters["ConnectionString"]
  .Value

長いですね。記述場所はStatefulやStatelesの中になります。Contextにアクセスできれば何処で書いてもOKです。

かずきのXamarin.Forms入門を更新しました

Xamarin

すっかりDataTemplateSelectorについて書くのを忘れてたのでSlideShareのPDFとKindleを差し替えました。 Kindleは反映までに1日くらいかかるのと、おそらく既に購入された方には変更は届けないとAmazonが判断すると思います…(よっぽどインパクトの大きな変更でない限り更新は配らないらしいです。しおりとかのデータが全部飛んじゃうらしいので)

申し訳ありませんが、既にKindle版を購入されてる方はSlideShareから更新内容をご確認ください。

www.slideshare.net

www.amazon.co.jp

MacBook Pro (Late 2016) 15インチをXamarin開発用に買ったので感想

Other

使い方

私のMacBook Pro(Late 2016)の使い方は以下のような感じです。

Xamarin開発環境として

Xamarin StudioとVisual Studio for Macを入れてXamarinの開発環境とするために購入しました。 なので、この使い方がメインになります。

MacBook Proのメモリ16GBでSSD512GBモデルなので開発環境として申し分ない感じです。

Windowsとして

Windowsメインで使ってたのでParallelsを購入してWindows 10を入れました。 Parallelsって細かいところで色々気が利いてるので便利ですね。

個人的には、アプリ仮想化ができる点(Coherence)が好きです。 バックグラウンドでWindowsが動いていて、アプリをまるでMac上で動かしてるみたいなことができます。 私は、これでVisual Studio 2015をマック上で使っています。

管理者権限で起動したいとか言った特別な要件がない限りは割と満足できる使い勝手です。

入れたもの

インストールしたソフトウェアは以下のようなものになります。

  • Xamarin Studio
  • Visual Studio for Mac
  • Visual Studio Code
  • TweetDeck
  • Office
  • Slack
  • LINE
  • Kindle
  • Remote Desktop
  • SourceTree
  • Minecraft

ParallelsのWindowsには以下のようなものを入れています。

  • Visual Studio 2015 Update3
  • SourceTree
  • Office

本当にWindowsが使いたいときは、Surface Bookを持ってるのでそっちを使うので、Parallels上のWindowsは、あくまで開発環境といった感じです。

不満なところ

現時点使っていて不満に感じるところをつらつらとまず書いていこうと思います。

英語キーボードなので…

私は、1年前にSurface Bookをアメリカで買った時から英字配列のキーボードを使うようになりました。 そのためMacBook Proも英字配列で購入しました。 オプションとして英字配列を選ぶことのできるMacBook Pro素晴らしいと思いました。

まぁそんなわけで英字配列使ってると不便なことがあります。

WindowsはAlt + `が半角・全角の切り替えなのですがMacはCtrl + Spaceが切り替えに割り当たってます。 そのため、Parallels上のWindowsとMacで同じハードで同じデスクトップ上でアプリを使ってるのに半角・全角の切り替えが統一されてない!という状況になってます。 まぁ、これは誰が悪いわけでもないんですが不便ですね。

Minecraft

マウスホイールでのアイテムの切り替えがWindowsと逆なんですよ…。 マウスの設定でスクロールを逆にすると全体に効いてくるので変えるわけにもいかない。 まぁそんなところですね。

その他

まぁ郷に入れば郷に従えなので、他に不満点はないですね。MacっていいOSなんだなと思います。

便利なところ

ということで便利だと感じてるところを書いてみたいと思います。

仮想デスクトップ

アプリを全画面にして気持ちよく別デスクトップに割当たるという動きは気持ちいいです。

タッチバー

思ったほど悪くないです。

個人に依存するとは思うのですが私はF1〜F12は、もともと割と目を落として目視で押してたみたいなのでFnキーを押してから目視で選んで押すので割と間に合ってます。 まぁ押す機会自体少ないですしね。

Visual StudioやXamarin Studioでデバッグ実行するときくらいしか押してない感じ。

Safariを使ってると割とよくできていて、検索バーへのフォーカスの移動やタブ増やしたりとか、お気に入りへのアクセスとか簡単にできるのがいいです。 まぁショートカットの方が早いですが、検索バーへの移動ショートカットを未だに発見できていないのでタッチバー使ってます。 (タッチバーでできるから調べようという気力も起きない)

あと、音量の調整や画面輝度の調整がタッチバーでやれるのもなかなかいい感じです。 トラックパッドでバーをちまちまやるよりタッチでささっとやるのが気持ちいいですね。

文字変換

デフォルトの文字変換って勝手に変換してくれるんですよね。 これが最初は気持ち悪かったけど、慣れたらWindowsで変換忘れてしまうようになってしまいました。 これは、気持ちいいですね。

思ったよりない

便利なところはそれくらいでしょうか。

思ったこと

WindowsもMacもOSとして割と歴史あるものなので、それぞれ使いやすくなってて作法を知れば便利なようにできてるんだなと思いました。 仮想デスクトップについては、Macの方が古くからできているのでこなれてるなぁという印象です。

まぁMacのVMをWindows機の上に立てていいようなライセンスになったらMacBookとはさようならしそうだけど、そんな未来はないよね。 ということでXamarinやるならMacいるんだよね。 それが最大の不満点かも(不満点に書けばよかったか)

まぁそんなことで、久しぶりにプログラミングネタじゃない記事でした。

Prism.FormsでAutofacを使ってみよう

Prism Xamarin

Xamarin Studioベースで話を進めます。(Visual Studioでもだいたい同じになると思うけど)

まずForms Appを新規作成してPCLで作ります。

NuGetから以下のパッケージを追加します。

  • Prism.Autofac.Forms

Views名前空間を作って、そこにMainPage.xamlを作成します。XAMLはいつも通り、ViewModelLocatorを設定しておきます。 今回は、ViewModelにMessageプロパティがあることを前提に作って見ました。あとでViewModelも作ります。

<?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:mvvm="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
    mvvm:ViewModelLocator.AutowireViewModel="true"
    x:Class="PrismAutofac.Views.MainPage">
    <Label Text="{Binding Message}"
        HorizontalOptions="Center"
        VerticalOptions="Center" />
</ContentPage>

ViewModels名前空間を作って以下のようなViewModelも作ります。ここら辺は普通のPrismの世界ですね。

using System;
using Prism.Mvvm;

namespace PrismAutofac.ViewModels
{
    public class MainPageViewModel : BindableBase
    {
        public string Message => "Hello world powered by Prism.Autofac.Forms";
    }
}

そして、App.xaml.csを以下の内容に書き換えます。

AutofacはUnityと違って登録されてないクラスのインスタンスは作ってくれないので、Unityでは必要のなかったViewModelの登録を行っています。と言ってもAssemblyをスキャンしてViewModelを一括登録する感じなので楽チンですけどね。

using Prism.Autofac;
using Prism.Autofac.Forms;
using Autofac;
using Xamarin.Forms;
using PrismAutofac.Views;
using PrismAutofac.ViewModels;
using System.Reflection;

namespace PrismAutofac
{
    public partial class App : PrismApplication
    {
        protected override void OnInitialized()
        {
            InitializeComponent();

            this.NavigationService.NavigateAsync("MainPage");
        }

        protected override void RegisterTypes()
        {
            // ViewModelの登録
            var containerUpdater = new ContainerBuilder();
            containerUpdater
                .RegisterAssemblyTypes(typeof(App).GetTypeInfo().Assembly)
                .Where(x => x.IsInNamespace("PrismAutofac.ViewModels"))
                .Where(x => x.Name.EndsWith("ViewModel"))
                .AsSelf();
            containerUpdater.Update(this.Container);

            // Viewの登録
            this.Container.RegisterTypeForNavigation<MainPage>();
        }
    }
}

追記 2017/01/09

nuitsさんから指摘いただきました。

ということなので、こういう感じでOKな上に、将来的にはこの手間もいらなくなるとか。

using Prism.Autofac;
using Prism.Autofac.Forms;
using Autofac;
using Xamarin.Forms;
using PrismAutofac.Views;
using PrismAutofac.ViewModels;
using System.Reflection;

namespace PrismAutofac
{
    public partial class App : PrismApplication
    {
        protected override void OnInitialized()
        {
            InitializeComponent();

            this.NavigationService.NavigateAsync("MainPage");
        }

        protected override void RegisterTypes()
        {
            // Viewの登録
            this.Container.RegisterTypeForNavigation<MainPage>();

            var containerUpdater = new ContainerBuilder();
            // 登録されてない型もコンテナで作成する
            containerUpdater.RegisterSource(new Autofac.Features.ResolveAnything.AnyConcreteTypeNotAlreadyRegisteredSource());

            // Serviceの登録
            containerUpdater
                .RegisterAssemblyTypes(typeof(App).GetTypeInfo().Assembly)
                .Where(x => x.IsInNamespace("PrismAutofac.Services"))
                .Where(x => x.Name.EndsWith("Service"))
                .AsImplementedInterfaces()
                .SingleInstance();
            containerUpdater.Update(this.Container);
        }
    }
}

個人的に納得いかない挙動が1つあって、Viewの登録を最後に持ってくるとアプリが死ぬようになることですかね。

追記ここまで

あとは、PrismApplicationを継承するようにしてるのでApp.xamlの内容もそれに合わせて書き換えます。

<?xml version="1.0" encoding="utf-8"?>
<prism:PrismApplication xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:prism="clr-namespace:Prism.Autofac;assembly=Prism.Autofac.Forms"
    x:Class="PrismAutofac.App">
    <Application.Resources>
        <!-- Application resource dictionary -->
    </Application.Resources>
</prism:PrismApplication>

実行すると、以下のようにいい感じに表示されます。

f:id:okazuki:20170108220649p:plain

サービスとかの登録もしてみよう

サービスの登録もついでにして見ましょう。以下のようなIMessageServiceというインターフェースを定義します。

using System;
namespace PrismAutofac.Services
{
    public interface IMessageService
    {
        string GetMessage();
    }
}

実装も適当にやります。

using System;
namespace PrismAutofac.Services
{
    public class MessageService : IMessageService
    {
        public string GetMessage() => "Xamarin.Forms and Prism.Autofac.Forms.";
    }
}

App.xaml.csでサービスの登録をシングルトンで行います。これもサービスが増えて来たときのことを考えてAssemblyをスキャンしてやってしまいましょう。

using Prism.Autofac;
using Prism.Autofac.Forms;
using Autofac;
using Xamarin.Forms;
using PrismAutofac.Views;
using PrismAutofac.ViewModels;
using System.Reflection;

namespace PrismAutofac
{
    public partial class App : PrismApplication
    {
        protected override void OnInitialized()
        {
            InitializeComponent();

            this.NavigationService.NavigateAsync("MainPage");
        }

        protected override void RegisterTypes()
        {
            // ViewModelの登録
            var containerUpdater = new ContainerBuilder();
            containerUpdater
                .RegisterAssemblyTypes(typeof(App).GetTypeInfo().Assembly)
                .Where(x => x.IsInNamespace("PrismAutofac.ViewModels"))
                .Where(x => x.Name.EndsWith("ViewModel"))
                .AsSelf();

            // Serviceの登録
            containerUpdater
                .RegisterAssemblyTypes(typeof(App).GetTypeInfo().Assembly)
                .Where(x => x.IsInNamespace("PrismAutofac.Services"))
                .Where(x => x.Name.EndsWith("Service"))
                .AsImplementedInterfaces()
                .SingleInstance();
            containerUpdater.Update(this.Container);

            // Viewの登録
            this.Container.RegisterTypeForNavigation<MainPage>();
        }
    }
}

MainPageViewModelを、IMessageServiceを使うように書き換えます。

using System;
using Prism.Mvvm;
using PrismAutofac.Services;

namespace PrismAutofac.ViewModels
{
    public class MainPageViewModel : BindableBase
    {
        private IMessageService MessageService { get; }

        public MainPageViewModel(IMessageService messageService)
        {
            this.MessageService = messageService;
        }

        public string Message => this.MessageService.GetMessage();
    }
}

実行すると、ちゃんとViewModelにMessageServiceがインジェクションされていることが確認できます。

f:id:okazuki:20170108221600p:plain

まとめ

Unityがいなくなっても平気ではありそうだなぁと思った今日この頃でした。(Unityには頑張って欲しい)

MacのXamarin.iOSでSegueが作れない

Xamarin

Ctrl + Dragでページを結んでもSegueが作れないというかメニューが一瞬出て消えるっていう動きをしていました。ググってみるとStackoverflowが引っかかりました。

stackoverflow.com

トラックパッドの設定で「強めのクリックと触覚フィードバック」のチェックを入れるとメニューが消えるみたいです。何じゃそりゃ。

f:id:okazuki:20170103163143p:plain

かずきのXamarin.Forms入門のKindle版だしました(Prismもあるよ)

Xamarin Prism

先日SlideShareに公開したXamarin.FormsのPDFですが

www.slideshare.net

こちらは、SlideShareにログインすると無料でダウンロードできます。

それに加えて、Kindleで販売も始めました。こちらはKindleで見たい人向けです1250円になります。 因みに無料で配ってる関係で、売り上げの35%しか入ってこないので本を買うよりもお寿司奢ってくれる方が喜びます?

かずきのXamarin.Forms入門(Kindle)

ということでよいお年を!!

Xamarin.Forms入門のPDF作りました(Prismもあるよ)

Xamarin Prism

SlideShareでログインしてダウンロードできるようにしています。是非ダウンロードしてみてください!200ページちょっとあります。

www.slideshare.net

目次

1    はじめに
1.1 ターゲットプラットフォーム
1.2 Xamarin.Formsとは
2   Hello world
2.1.1   実行して動作確認
3   XAML
3.1 XAMLとC#のコードの対比
3.1.1   XAMLの基本
3.2 XAMLの応用
3.2.1   添付プロパティ
3.2.2   マークアップ拡張
3.2.3   StaticResource
3.2.4   x:Static
3.2.5   TypeConverter
3.2.6   データバインディング
4   Xamarin.Formsのコントロール
4.1 BindableObject
4.1.1   バインダブルプロパティ
4.1.2   添付プロパティ
4.2 レイアウトコントロール
4.2.1   StackLayout
4.2.2   Grid
4.2.3   AbsoluteLayout
4.2.4   RelativeLayout
4.2.5   ScrollView
4.2.6   余白の制御
4.3 一般的なコントロール
4.3.1   Label
4.3.2   ActivityIndicator
4.3.3   BoxView
4.3.4   Button
4.3.5   DatePicker
4.3.6   Editor
4.3.7   Entry
4.3.8   Image
4.3.9   ListView
4.3.10  OpenGLView
4.3.11  Picker
4.3.12  ProgressBar
4.3.13  SearchBar
4.3.14  Slider
4.3.15  Stepper
4.3.16  Switch
4.3.17  TableView
4.3.18  TimePicker
4.3.19  WebView
4.3.20  Map
4.3.21  CarouselView
4.4 ページ
4.4.1   Page
4.4.2   ContentPage
4.4.3   MasterDetailPage
4.4.4   NavigationPage
4.4.5   TabbedPage
4.4.6   CarouselPage
5   スタイル
6   ジェスチャー
6.1 TapGestureRecognizer
6.2 PinchGestureRecognizer
6.3 PanGestureRecognizer
7   アニメーション
7.1 Xamarin.Formsのコントロールの移動や拡大縮小、回転
7.2 シンプルなアニメーション
7.3 イージング
8   ビヘイビア
9   トリガー・アクション
9.1 PropertyTrigger
9.2 DataTrigger
9.3 EventTrigger
9.4 MultiTrigger
10  メッセージングセンター
11  プラットフォーム固有機能
11.1    Deviceクラス
11.1.1  Idiom
11.1.2  OS
11.1.3  OnPlatform
11.1.4  Styles
11.1.5  GetNamedSize
11.1.6  OpenUri
11.1.7  StartTimer
11.1.8  BeginInvokeOnMainThread
11.2    DependencyService
11.3    Effect
11.4    CustomRenderer
11.5    Plugin
11.6    ネイティブのビュー
12  永続化
12.1    ApplicationクラスのProperties
12.2    ローカルファイル
12.3    SQLite
13  Prism
13.1    Prismの機能
13.2    MVVM開発のサポート
13.3    Dependency Injection
13.4    Xamarin.Forms組み込みのCommandよりも高機能のCommand
13.5    Page Dialog Service
13.6    ページナビゲーション
13.7    MessageingCenterよりも高機能なメッセージング機能
13.8    ロギング
14  まとめ

Xamarin.Forms + PrismでSQLiteを使ってみよう

Xamarin

モバイル環境でのデータベースといったらSQLiteがデファクト!ということでXamarin.Forms + Prism.Formsの環境で試してみましょう。

NuGetパッケージの導入

使用するパッケージはSQLite-net-pclです。(似た名前のが多いので注意)

www.nuget.org

プラットフォーム固有処理を作成

残念なことにPCLに閉じて完結という感じではなさそうです。 コネクションの作成時にパスを渡すのですが、このパスがプラットフォーム固有文字列になるので以下のようにSQLiteConnectionを返すインターフェースを定義していい感じにやる必要があります。

using SQLite;

namespace PrismUnityApp17.Services
{
    public interface ISQLiteConnectionProvider
    {
        SQLiteConnection GetConnection();
    }
}

Androidの実装

Personalフォルダをとってきて、そこにファイルを作る感じにします。

using PrismUnityApp17.Services;
using SQLite;
using System.IO;

namespace PrismUnityApp17.Droid.Services
{
    public class SQLiteConnectionProvider : ISQLiteConnectionProvider
    {
        private SQLiteConnection Connection { get; set; }

        public SQLiteConnection GetConnection()
        {
            if (this.Connection != null) { return this.Connection; }

            var path = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal);
            path = Path.Combine(path, "database.db3");
            return this.Connection = new SQLiteConnection(path);
        }
    }
}

iOSの実装

iOSはLibraryフォルダに作る感じです。(ちょっとAndroidに比べてめんどい)

using PrismUnityApp17.Services;
using SQLite;
using System;
using System.IO;

namespace PrismUnityApp17.iOS.Services
{
    public class SQLiteConnectionProvider : ISQLiteConnectionProvider
    {
        private SQLiteConnection Connection { get; set; }

        public SQLiteConnection GetConnection()
        {
            if (this.Connection != null) { return this.Connection; }

            var path = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
            path = Path.Combine(path, "..", "Library", "database.db3");
            return this.Connection = new SQLiteConnection(path);
        }
    }
}

悩み

SQLiteConnectionIDispoasbleなのでDisposeしないとなのですが、毎回やるのとstaticに持ってて、使いまわすのどっちが正解なのか悩んでます…。とりあえず今回の例ではアプリ内で1つのコネクションにしてます。

PlatformInitializerへの登録

上記で作成したクラスをIPlatformInitializerで登録します。

Android

MainActivityの下に定義されてるので以下のように追加します。

public class AndroidInitializer : IPlatformInitializer
{
    public void RegisterTypes(IUnityContainer container)
    {
        container.RegisterType<ISQLiteConnectionProvider, SQLiteConnectionProvider>(new ContainerControlledLifetimeManager());
    }
}

iOS

iOSはAppDelegateの下に定義されているので、そこにも追加します。

public class iOSInitializer : IPlatformInitializer
{
    public void RegisterTypes(IUnityContainer container)
    {
        container.RegisterType<ISQLiteConnectionProvider, SQLiteConnectionProvider>(new ContainerControlledLifetimeManager());
    }
}

テーブルの定義

こんな感じでテーブルを表すクラスを定義します。

using SQLite;

namespace PrismUnityApp17.Businesses
{
    public class TodoItem
    {
        [PrimaryKey]
        [AutoIncrement]
        public int Id { get; set; }
        [NotNull]
        public string Title { get; set; }
    }
}

そして、テーブルにアクセスするためのクラスを作ります。

using PrismUnityApp17.Businesses;
using SQLite;
using System.Collections.Generic;
using System.Linq;

namespace PrismUnityApp17.Services
{
    public interface ITodoItemService
    {
        IEnumerable<TodoItem> GetAll();
        TodoItem GetById(int id);
        void Update(TodoItem todoItem);
        void Insert(TodoItem todoItem);
        void Delete(int id);
    }

    public class TodoItemService : ITodoItemService
    {
        private ISQLiteConnectionProvider ConnectionProvider { get; }
        private SQLiteConnection Connection { get; }

        public TodoItemService(ISQLiteConnectionProvider connectionProvider)
        {
            this.ConnectionProvider = connectionProvider;
            this.Connection = this.ConnectionProvider.GetConnection();
            this.Connection.CreateTable<TodoItem>();
        }

        public void Delete(int id)
        {
            this.Connection.Delete<TodoItem>(id);
        }

        public IEnumerable<TodoItem> GetAll()
        {
            return this.Connection.Table<TodoItem>().ToList();
        }

        public TodoItem GetById(int id)
        {
            return this.Connection.Table<TodoItem>().FirstOrDefault(x => x.Id == id);
        }

        public void Insert(TodoItem todoItem)
        {
            this.Connection.Insert(todoItem);
        }

        public void Update(TodoItem todoItem)
        {
            this.Connection.Update(todoItem);
        }
    }
}

画面とVMを作ろう

あとは、これを使う画面を作るだけです。とりあえず追加と削除を。

using Prism.Commands;
using Prism.Mvvm;
using Prism.Navigation;
using PrismUnityApp17.Businesses;
using PrismUnityApp17.Services;
using System.Collections.Generic;

namespace PrismUnityApp17.ViewModels
{
    public class MainPageViewModel : BindableBase, INavigationAware
    {
        private ITodoItemService TodoItemService { get; }

        private IEnumerable<TodoItem> todoItems;

        public IEnumerable<TodoItem> TodoItems
        {
            get { return this.todoItems; }
            set { this.SetProperty(ref this.todoItems, value); }
        }

        private string inputText;

        public string InputText
        {
            get { return this.inputText; }
            set { this.SetProperty(ref this.inputText, value); }
        }

        public DelegateCommand AddCommand { get; }

        public DelegateCommand<TodoItem> DeleteCommand { get; }

        public MainPageViewModel(ITodoItemService todoItemService)
        {
            this.TodoItemService = todoItemService;

            this.AddCommand = new DelegateCommand(this.AddTodoItem, () => !string.IsNullOrEmpty(this.InputText))
                .ObservesProperty(() => this.InputText);

            this.DeleteCommand = new DelegateCommand<TodoItem>(this.DeleteTodoItem);
        }

        private void DeleteTodoItem(TodoItem todoItem)
        {
            this.TodoItemService.Delete(todoItem.Id);
            this.TodoItems = this.TodoItemService.GetAll();
        }

        private void AddTodoItem()
        {
            this.TodoItemService.Insert(new TodoItem { Title = this.InputText });
            this.InputText = "";
            this.TodoItems = this.TodoItemService.GetAll();
        }

        public void OnNavigatedFrom(NavigationParameters parameters)
        {

        }

        public void OnNavigatedTo(NavigationParameters parameters)
        {
            this.TodoItems = this.TodoItemService.GetAll();
        }
    }
}

XAMLはこんな感じで。

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Class="PrismUnityApp17.Views.MainPage"
             Title="MainPage"
             x:Name="Page">
  <ContentPage.Padding>
    <OnPlatform x:TypeArguments="Thickness"
                iOS="0,20,0,0" />
  </ContentPage.Padding>
  <ContentPage.ToolbarItems>
    <ToolbarItem Text="Add"
                 Command="{Binding AddCommand}" />
  </ContentPage.ToolbarItems>
  <StackLayout>
    <Entry Text="{Binding InputText, Mode=TwoWay}" />
    <ListView ItemsSource="{Binding TodoItems}"
              VerticalOptions="FillAndExpand">
      <ListView.ItemTemplate>
        <DataTemplate>
          <ViewCell>
            <ViewCell.ContextActions>
              <MenuItem Text="Delete"
                        Command="{Binding BindingContext.DeleteCommand, Source={x:Reference Page}}"
                        CommandParameter="{Binding}" />
            </ViewCell.ContextActions>
            <Label Text="{Binding Title}" />
          </ViewCell>
        </DataTemplate>
      </ListView.ItemTemplate>
    </ListView>
  </StackLayout>
</ContentPage>

仕上げにApp.xaml

ITodoItemServiceの登録や、ページの登録(NavigationPageの登録など)をやります。

using Microsoft.Practices.Unity;
using Prism.Unity;
using PrismUnityApp17.Services;
using PrismUnityApp17.Views;
using Xamarin.Forms;

namespace PrismUnityApp17
{
    public partial class App : PrismApplication
    {
        public App(IPlatformInitializer initializer = null) : base(initializer) { }

        protected override async void OnInitialized()
        {
            InitializeComponent();

            await this.NavigationService.NavigateAsync("NavigationPage/MainPage");
        }

        protected override void RegisterTypes()
        {
            this.Container.RegisterTypeForNavigation<MainPage>();
            this.Container.RegisterTypeForNavigation<NavigationPage>();

            this.Container.RegisterType<ITodoItemService, TodoItemService>(new ContainerControlledLifetimeManager());
        }
    }
}

これで、追加と削除ができるアプリが出来上がり。意外とお手軽ですね。

Async対応

Async対応版に改造してみます。

ISQLiteConnectionProviderを改造

SQLiteAsyncConnectionを返すようにします。

using SQLite;

namespace PrismUnityApp17.Services
{
    public interface ISQLiteConnectionProvider
    {
        SQLiteAsyncConnection GetConnection();
    }
}

プラットフォーム固有実装でもSQLiteAsyncConnectionを返すようにします。

Android

using PrismUnityApp17.Services;
using SQLite;
using System.IO;

namespace PrismUnityApp17.Droid.Services
{
    public class SQLiteConnectionProvider : ISQLiteConnectionProvider
    {
        private SQLiteAsyncConnection Connection { get; set; }

        public SQLiteAsyncConnection GetConnection()
        {
            if (this.Connection != null) { return this.Connection; }

            var path = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal);
            path = Path.Combine(path, "database.db3");
            return this.Connection = new SQLiteAsyncConnection(path);
        }
    }
}

iOS

using PrismUnityApp17.Services;
using SQLite;
using System;
using System.IO;

namespace PrismUnityApp17.iOS.Services
{
    public class SQLiteConnectionProvider : ISQLiteConnectionProvider
    {
        private SQLiteAsyncConnection Connection { get; set; }

        public SQLiteAsyncConnection GetConnection()
        {
            if (this.Connection != null) { return this.Connection; }

            var path = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
            path = Path.Combine(path, "..", "Library", "database.db3");
            return this.Connection = new SQLiteAsyncConnection(path);
        }
    }
}

TodoItemServiceの非同期化

TodoItemServiceを非同期に書き換えます。

using PrismUnityApp17.Businesses;
using SQLite;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace PrismUnityApp17.Services
{
    public interface ITodoItemService
    {
        Task<IEnumerable<TodoItem>> GetAllAsync();
        Task<TodoItem> GetByIdAsync(int id);
        Task UpdateAsync(TodoItem todoItem);
        Task InsertAsync(TodoItem todoItem);
        Task DeleteAsync(TodoItem todoItem);
    }

    public class TodoItemService : ITodoItemService
    {
        private ISQLiteConnectionProvider ConnectionProvider { get; }
        private SQLiteAsyncConnection Connection { get; }

        public TodoItemService(ISQLiteConnectionProvider connectionProvider)
        {
            this.ConnectionProvider = connectionProvider;
            this.Connection = this.ConnectionProvider.GetConnection();
        }

        public async Task DeleteAsync(TodoItem todoItem)
        {
            await this.Connection.CreateTableAsync<TodoItem>();
            await this.Connection.DeleteAsync(todoItem);
        }

        public async Task<IEnumerable<TodoItem>> GetAllAsync()
        {
            await this.Connection.CreateTableAsync<TodoItem>();
            return await this.Connection.Table<TodoItem>().ToListAsync();
        }

        public async Task<TodoItem> GetByIdAsync(int id)
        {
            await this.Connection.CreateTableAsync<TodoItem>();
            return await this.Connection.Table<TodoItem>().Where(x => x.Id == id).FirstOrDefaultAsync();
        }

        public async Task InsertAsync(TodoItem todoItem)
        {
            await this.Connection.CreateTableAsync<TodoItem>();
            await this.Connection.InsertAsync(todoItem);
        }

        public async Task UpdateAsync(TodoItem todoItem)
        {
            await this.Connection.CreateTableAsync<TodoItem>();
            await this.Connection.UpdateAsync(todoItem);
        }
    }
}

ViewModelの非同期対応

最後にViewModelを非同期対応にします。

using Prism.Commands;
using Prism.Mvvm;
using Prism.Navigation;
using PrismUnityApp17.Businesses;
using PrismUnityApp17.Services;
using System.Collections.Generic;

namespace PrismUnityApp17.ViewModels
{
    public class MainPageViewModel : BindableBase, INavigationAware
    {
        private ITodoItemService TodoItemService { get; }

        private IEnumerable<TodoItem> todoItems;

        public IEnumerable<TodoItem> TodoItems
        {
            get { return this.todoItems; }
            set { this.SetProperty(ref this.todoItems, value); }
        }

        private string inputText;

        public string InputText
        {
            get { return this.inputText; }
            set { this.SetProperty(ref this.inputText, value); }
        }

        public DelegateCommand AddCommand { get; }

        public DelegateCommand<TodoItem> DeleteCommand { get; }

        public MainPageViewModel(ITodoItemService todoItemService)
        {
            this.TodoItemService = todoItemService;

            this.AddCommand = new DelegateCommand(this.AddTodoItem, () => !string.IsNullOrEmpty(this.InputText))
                .ObservesProperty(() => this.InputText);

            this.DeleteCommand = new DelegateCommand<TodoItem>(this.DeleteTodoItem);
        }

        private async void DeleteTodoItem(TodoItem todoItem)
        {
            await this.TodoItemService.DeleteAsync(todoItem);
            this.TodoItems = await this.TodoItemService.GetAllAsync();
        }

        private async void AddTodoItem()
        {
            await this.TodoItemService.InsertAsync(new TodoItem { Title = this.InputText });
            this.InputText = "";
            this.TodoItems = await this.TodoItemService.GetAllAsync();
        }

        public void OnNavigatedFrom(NavigationParameters parameters)
        {

        }

        public async void OnNavigatedTo(NavigationParameters parameters)
        {
            this.TodoItems = await this.TodoItemService.GetAllAsync();
        }
    }
}

これでばっちり!非同期でも動きますね。