かずきのBlog@hatena

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

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

使い方

私の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を使ってみよう

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が作れない

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

stackoverflow.com

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

f:id:okazuki:20170103163143p:plain

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

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

www.slideshare.net

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

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

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

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

Xamarin.Forms入門のPDF作りました(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を使ってみよう

モバイル環境でのデータベースといったら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();
        }
    }
}

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

DIコンテナのUnityのLifetimeManagerを拡張して任意のタイミングでインスタンスの破棄をする

UnityのLifetimeManagerはシングルトンで管理するContainerControlledLifetimeManagerか、デフォルトの毎回newする‘PerResolveLifetimeManager‘か、スレッド単位のPerThreadLifetimeManagerが用意されています。あとマニアックなところだと、ExternallyControlledLifetimeManagerとかいう弱参照で管理されるものもあります。

今回は、LifetimeManagerを拡張して任意のタイミングで破棄できるLifetimeManagerを作ってみようと思います。

破棄を通知するインターフェースの定義

まず、オブジェクトが破棄されたことを通知するインターフェースを定義します。とりあえず今回はイベントの発火でライフサイクルの終了を通知するようにしてみようと思うので、シンプルにCompletedイベントだけを持ったインターフェースを定義しました。

interface ITransactionPolicy
{
    event EventHandler Completed;
}

ITransactionPolicyを使ったLifetimeManagerの定義

後は、ITransactionPolicyCompletedイベントが起きたらインスタンスを破棄するようにするLifetimeManagerの実装を作るだけです。

sealed class TransactionLifetimeManager : LifetimeManager, IDisposable
{
    private object value;

    public override object GetValue()
    {
        return value;
    }

    public override void RemoveValue()
    {
        this.Dispose();
    }

    public override void SetValue(object newValue)
    {
        value = newValue;
        var tx = value as ITransactionPolicy;
        if (tx != null)
        {
            tx.Completed += this.Tx_Completed;
        }
    }

    public void Dispose()
    {
        this.Dispose(true);
        GC.SuppressFinalize(this);
    }

    private void Dispose(bool disposable)
    {
        if (disposable)
        {
            (value as IDisposable)?.Dispose();
            this.value = null;
        }
    }


    private void Tx_Completed(object sender, EventArgs e)
    {
        ((ITransactionPolicy)this.value).Completed -= this.Tx_Completed;
        this.RemoveValue();
    }
}

使ってみよう

使い方は簡単です。ITransactionPolicyを実装してTransactionLifetimeManagerで登録するだけ。

// こっちはライフサイクルを制御する
class Person : ITransactionPolicy, IDisposable
{
    private Service Service { get; }
    public Person(Service service)
    {
        this.Service = service;
        Console.WriteLine("Person#Constructor");
    }

    public event EventHandler Completed;

    public void Complete()
    {
        this.Completed?.Invoke(this, EventArgs.Empty);
    }

    public void Dispose()
    {
        Console.WriteLine("Person#Dispose");
    }
}

// こっちは普通のクラス
class Service
{
    public Service()
    {
        Console.WriteLine("Service#Constructor");
    }
}

コンテナに登録して使ってみましょう。

var c = new UnityContainer();
// Serviceはシングルトン
c.RegisterType<Service>(new ContainerControlledLifetimeManager());
// Personは任意のタイミングで破棄
c.RegisterType<Person>(new TransactionLifetimeManager());

// とりあえずインスタンス取得
Console.WriteLine("Resolve<Person>()");
var p = c.Resolve<Person>();

// 2回目取得しても同じインスタンスが取れることを確認
Console.WriteLine("Resolve<Person>()");
Console.WriteLine(p == c.Resolve<Person>() ? "同じインスタンス" : "違うインスタンス");

// インスタンスを明示的に破棄
Console.WriteLine("RaiseCompleted");
p.Complete();

// 違うインスタンスが取れることを確認
Console.WriteLine("Resolve<Person>()");
Console.WriteLine(p == c.Resolve<Person>() ? "同じインスタンス" : "違うインスタンス");

// コンテナの破棄時にDisposeが呼ばれることも確認
Console.WriteLine("Container#Dispose");
c.Dispose();

実行すると以下のような出力が得られます。

Resolve<Person>()
Service#Constructor
Person#Constructor
Resolve<Person>()
同じインスタンス
RaiseCompleted
Person#Dispose
Resolve<Person>()
Person#Constructor
違うインスタンス
Container#Dispose
Person#Dispose

ちゃんとServiceはシングルトンでPersonCompletedイベントの発火とともに削除されてることが確認できます。

注意点

スレッドセーフではないのでWebアプリケーションみたいに不特定多数のスレッドからコンテナにアクセスするような環境下では使わないほうが幸せです。クライアントアプリとかでUIスレッド上からしかコンテナにアクセスしないような環境下で使いましょう。

まとめ

Unityはいいぞ。

LifetimeManagerの拡張は、はじめてやったけど結構簡単でした。

追記

3年前にやってた。

code.msdn.microsoft.com

Xamarin.FormsでBehaviorのTriggerとActionを作る

Xamarin.Formsのドキュメント上は見つけれなかったけど、ソースコード的にはTriggerとActionがあったりします。

github.com

ただ、この人たちはBindingに対応してないという、ちょっと悲しい感じに仕上がってます。なので、XamarinのBehaviorをベースにBindingに対応したTriggerとActionを作ってみようと思います。

Behaviorの基本クラス

BindingContextを伝搬するBehaviorBase<T>クラスを作ります。

using System;
using Xamarin.Forms;

namespace PrismUnityApp16.Behaviors
{
    public class BehaviorBase<T> : Behavior<T>
        where T : BindableObject
    {
        protected T AssociatedObject { get; private set; }

        protected override void OnAttachedTo(T bindable)
        {
            base.OnAttachedTo(bindable);
            this.AssociatedObject = bindable;
            this.BindingContext = bindable.BindingContext;

            bindable.BindingContextChanged += this.Bindable_BindingContextChanged;
        }

        private void Bindable_BindingContextChanged(object sender, EventArgs e)
        {
            this.OnBindingContextChanged();
        }

        protected override void OnDetachingFrom(T bindable)
        {
            base.OnDetachingFrom(bindable);
            bindable.BindingContextChanged -= this.Bindable_BindingContextChanged;
        }

        protected override void OnBindingContextChanged()
        {
            base.OnBindingContextChanged();
            this.BindingContext = this.AssociatedObject.BindingContext;
        }
    }
}

Triggerの基本クラスを作る

次にTriggerの基本クラスを作ります。BehaviorBase<T>を継承してIActionインターフェースを抱え込む感じで作ります。ContentPropertyActionsを指定していい感じにXAMLで書けるようにもしておきましょう。

あと、IActionBindingContextを伝搬させるのも忘れないでやっておきます。

using System.Collections.Generic;
using System.Linq;
using Xamarin.Forms;

namespace PrismUnityApp16.Behaviors
{
    [ContentProperty("Actions")]
    public class TriggerBehaviorBase<T> : BehaviorBase<T>
        where T : BindableObject
    {
        public ICollection<IAction> Actions { get; } = new List<IAction>();

        protected void InvokeActions(object parameter)
        {
            foreach (var action in this.Actions.ToArray())
            {
                action.Execute(parameter);
            }
        }

        protected override void OnAttachedTo(T bindable)
        {
            base.OnAttachedTo(bindable);
            foreach (var action in this.Actions.ToArray())
            {
                action.BindingContext = this.BindingContext;
            }
        }

        protected override void OnDetachingFrom(T bindable)
        {
            base.OnDetachingFrom(bindable);
            foreach (var action in this.Actions.ToArray())
            {
                action.BindingContext = null;
            }
        }

        protected override void OnBindingContextChanged()
        {
            base.OnBindingContextChanged();

            foreach (var action in this.Actions.ToArray())
            {
                action.BindingContext = this.BindingContext;
            }
        }
    }
}

IActoinはこんな感じのシンプルなインターフェースです。

namespace PrismUnityApp16.Behaviors
{
    public interface IAction
    {
        object BindingContext { get; set; }

        void Execute(object parameter);
    }
}

使ってみよう

イベントをもとにActionを実行するEventTriggerBehaviorとCommandを実行するInvokeCommandActionを作ってみようと思います。

EventTriggerBehavior

さくっとリフレクションを使ってイベントを拾ってきて登録します。

using System;
using System.Reflection;
using Xamarin.Forms;

namespace PrismUnityApp16.Behaviors
{
    public class EventTriggerBehavior : TriggerBehaviorBase<View>
    {
        public static readonly BindableProperty EventNameProperty = BindableProperty
            .Create(nameof(EventName), typeof(string), typeof(EventTriggerBehavior));

        private Delegate EventHandler { get; set; }

        private EventInfo EventInfo { get; set; }

        public string EventName
        {
            get { return (string)this.GetValue(EventNameProperty); }
            set { this.SetValue(EventNameProperty, value); }
        }

        protected override void OnAttachedTo(View bindable)
        {
            base.OnAttachedTo(bindable);
            if (string.IsNullOrEmpty(this.EventName))
            {
                return;
            }

            this.EventInfo = this.AssociatedObject.GetType().GetRuntimeEvent(this.EventName);
            if (this.EventInfo == null)
            {
                throw new InvalidOperationException($"{this.EventName} is not found.");
            }

            var methodInfo = typeof(EventTriggerBehavior).GetTypeInfo().GetDeclaredMethod(nameof(OnEvent));
            this.EventHandler = methodInfo.CreateDelegate(this.EventInfo.EventHandlerType, this);
            this.EventInfo.AddEventHandler(bindable, this.EventHandler);
        }

        private void OnEvent(object sender, object args)
        {
            this.InvokeActions(args);
        }

        protected override void OnDetachingFrom(View bindable)
        {
            base.OnDetachingFrom(bindable);
            this.EventInfo.RemoveEventHandler(bindable, this.EventHandler);
        }
    }
}

InvokeCommandAction

BindableObjectから継承してIActionを実装します。Commandを実行する感じに書きましょう。

using System.Windows.Input;
using Xamarin.Forms;

namespace PrismUnityApp16.Behaviors
{
    public class InvokeCommandAction : BindableObject, IAction
    {
        public static readonly BindableProperty CommandProperty = BindableProperty
            .Create(nameof(Command), typeof(ICommand), typeof(EventTriggerBehavior));
        public static readonly BindableProperty CommandParameterProperty = BindableProperty
            .Create(nameof(CommandParameter), typeof(object), typeof(EventTriggerBehavior));
        public static readonly BindableProperty ConverterProperty = BindableProperty
            .Create(nameof(Converter), typeof(IValueConverter), typeof(EventTriggerBehavior));

        public ICommand Command
        {
            get { return (ICommand)this.GetValue(CommandProperty); }
            set { this.SetValue(CommandProperty, value); }
        }
        public object CommandParameter
        {
            get { return this.GetValue(CommandParameterProperty); }
            set { this.SetValue(CommandParameterProperty, value); }
        }
        public IValueConverter Converter
        {
            get { return (IValueConverter)this.GetValue(ConverterProperty); }
            set { this.SetValue(ConverterProperty, value); }
        }


        public void Execute(object parameter)
        {
            var p = this.CommandParameter;

            if (p == null)
            {
                p = this.Converter?.Convert(parameter, typeof(object), null, null);
            }

            if (this.Command?.CanExecute(p) ?? false)
            {
                this.Command.Execute(p);
            }
        }
    }
}

使ってみよう

使い方は簡単。例えばButtonClickedイベントと紐づける場合はこんな感じ。

<?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:controls="clr-namespace:PrismUnityApp16.Controls"
             xmlns:behaviors="clr-namespace:PrismUnityApp16.Behaviors"
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Class="PrismUnityApp16.Views.MainPage"
             Title="MainPage">
  <StackLayout HorizontalOptions="Center" VerticalOptions="Center">
    <Button Text="OK">
      <Button.Behaviors>
        <behaviors:EventTriggerBehavior EventName="Clicked">
          <behaviors:InvokeCommandAction Command="{Binding HelloCommand}" />
        </behaviors:EventTriggerBehavior>
      </Button.Behaviors>
    </Button>
  </StackLayout>
</ContentPage>

ViewModel側はこんな感じです。

using Prism.Commands;
using Prism.Mvvm;
using Prism.Navigation;
using System.Diagnostics;

namespace PrismUnityApp16.ViewModels
{
    public class MainPageViewModel : BindableBase, INavigationAware
    {
        public DelegateCommand HelloCommand { get; }

        public MainPageViewModel()
        {
            this.HelloCommand = new DelegateCommand(() => Debug.WriteLine("Clicked"));
        }

        public void OnNavigatedFrom(NavigationParameters parameters)
        {

        }

        public void OnNavigatedTo(NavigationParameters parameters)
        {
        }
    }
}

実行してボタンを押すとデバッグウィンドウの出力にClickedと表示されます。

まとめ

デフォで用意しといてくれ。

Xamarin.FormsのBehaviorをBinding可能にする

デフォルトだとBindingできないんですよ。ということでソリューションは以下のようにします。

github.com

BindingContextが伝搬しないなら伝搬させればいいじゃない?ということで手動でせっせと伝搬させてます。

これくらいデフォルトで面倒見てほしかった…。

Xamarin.FormsのListViewでタップされた項目をスマートにViewModelに渡す方法

EventToCommandBehaviorを使います。コードはこちらを参考に。

github.com

この時、こういうBehaviorを作っておくとListViewの選択がされなくなって捗ります。

using Xamarin.Forms;

namespace PrismUnityApp2
{
    public class NotSelectableListViewBehavior : Behavior<ListView>
    {
        protected override void OnAttachedTo(ListView bindable)
        {
            base.OnAttachedTo(bindable);

            bindable.ItemSelected += this.Bindable_ItemSelected;
        }

        protected override void OnDetachingFrom(ListView bindable)
        {
            base.OnDetachingFrom(bindable);
            bindable.ItemSelected -= this.Bindable_ItemSelected;
        }

        private void Bindable_ItemSelected(object sender, SelectedItemChangedEventArgs e)
        {
            ((ListView)sender).SelectedItem = null;
        }
    }
}

SelectedItemをItemSelectedでnullにしてるのですが、こうすることで選択時に色がつかなくなります。 色がついてると同じアイテムを2回選択したときにイベントが飛ばないので強制的に選択をしないようにしているという感じですね。

あとは、SelectedItemChangedEventArgsから選択項目を抜き出すConverterを作って

using System;
using System.Globalization;
using Xamarin.Forms;

namespace PrismUnityApp2
{
    public class SelectedItemChangedEventArgsToSelectedItemConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var args = (SelectedItemChangedEventArgs)value;
            return args.SelectedItem;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}

こんな風にListViewに対して設定すればOKです。

<?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:local="clr-namespace:PrismUnityApp2"
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Class="PrismUnityApp2.Views.MainPage"
             Title="MainPage">
  <ContentPage.Resources>
    <ResourceDictionary>
      <local:SelectedItemChangedEventArgsToSelectedItemConverter x:Key="SelectedItemChangedEventArgsToSelectedItemConverter" />
    </ResourceDictionary>
  </ContentPage.Resources>
  <Grid>
    <ListView ItemsSource="{Binding People}">
      <ListView.Behaviors>
        <local:NotSelectableListViewBehavior />
        <local:EventToCommandBehavior EventName="ItemSelected"
                                      Converter="{StaticResource SelectedItemChangedEventArgsToSelectedItemConverter}"
                                      Command="{Binding SelectedCommand}"/>
      </ListView.Behaviors>
    </ListView>
  </Grid>
</ContentPage>

大事なのは、ViewModel側のCommandでnullは受け付けないようにするところです。これをやらないと無駄にコマンドが実行されます。 (初回表示のときや、ItemSelectedイベントでnullをセットしたときとかにイベントが発生するので)

using Prism.Commands;
using Prism.Mvvm;
using Prism.Navigation;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace PrismUnityApp2.ViewModels
{
    public class MainPageViewModel : BindableBase, INavigationAware
    {
        public IEnumerable<Person> People { get; } = Enumerable.Range(1, 100)
            .Select(x => new Person { Name = $"tanaka {x}" })
            .ToArray();

        public DelegateCommand<Person> SelectedCommand { get; }

        public MainPageViewModel()
        {
            this.SelectedCommand = new DelegateCommand<Person>(
                x => Debug.WriteLine($"{x.Name} selected."), 
                x => x != null);
        }

        public void OnNavigatedFrom(NavigationParameters parameters)
        {
        }

        public void OnNavigatedTo(NavigationParameters parameters)
        {
        }
    }

    public class Person
    {
        public string Name { get; set; }
    }
}

以上、簡単にですが最近得たノウハウ。