nuits.jp blog

C#, Xamarin, WPFを中心に書いています。Microsoft MVP for Development Technologies。

Visual Studio 2017 Update 2へUpdate3を適用する方法

普段Visual Studioの更新は、VSメニューバーの「ツール」>「拡張機能と更新プログラム」から入れられた気がするのですが、どうも今回は2017.08.15時点ではそこから上げられないようです?

f:id:nuitsjp:20170815094207p:plain

という訳で、Visual Studio Installerを手動で起動して(インストーラーはダウンロードしてもいいし、次のように検索しても出てくるはずです)更新すれば適用できるようです。

f:id:nuitsjp:20170815094350p:plain

インストーラーを起動したら、あとは更新ボタンを押すだけで現時点ではそんなに待たずにアップデートできました。

f:id:nuitsjp:20170815094427p:plain

それだけです。チャンチャン!

KAMISHIBAIセルフ プロモーション:一貫性を保ったイベント通知

f:id:nuitsjp:20170813232926p:plain

先日これまでのXamarin.Formsの経験や、PrismのPageNavigationServiceへの貢献を通して得たノウハウを整理して形にした、KAMISHIBAI for Xamarin.Formsをリリースしました。 前述のエントリーや、Github上のドキュメントでもKAMISHIBAIのメリットについては述べているのですが、少しずつ掘り下げて解説したいと思います。 まずは

  • 一貫性を保ったイベント通知

について説明したいと思います。これはXamarin.Formsを素でつかっていると非常に面倒な領域です。つぎの図を見てください。

f:id:nuitsjp:20170813233720p:plain

KAMISHIBAIではPageの状態遷移に伴ってつぎのようなイベントが発行されます。

イベント 想定する実装内容 パラメーター
OnInitialize リソースの初期化処理など
OnLoaded OnUnloadedで非活性化されるリソースの活性化 ×
OnUnloaded リソースの非活性化 ×
OnClosed リソースの解放 ×

これらのイベント通知があらゆるPageの状態遷移で保たれていることが、KAMISHIBAIの強みの一つです。
どういう事かというと、通常だと実現困難な次のようなケースでも、一貫して振る舞うということです。

  1. NavigationPageのNavigationBarやスワイプ、物理バックボタンによる戻る処理
  2. TabbedPageの選択Tabの変更
  3. CarouselPageの表示Pageの変更
  4. ModalStackの物理バックボタンによる戻る処理

これらを見ると、画面遷移イベントだけではない(INavigationが絡まないものはNavigationじゃないという意見もあるので)ため、画面遷移イベントというよりは、Page状態変更イベントといった方が良いでしょう。

1.~3.については、PageをPushする際にBehaviorを外部からインジェクションするという、私が勝手に「Injection Behavior of outside」と呼んでいるパターンを利用して実現しています。このあたりのコードです。

NavigationPage、TabbedPage、CarouselPageにはそれぞれ専用のBehaviorをインジェクションしています。例えばNavigationPageではPoppedイベントをハンドルして適切に処理するNavigationPageBehaviorをインジェクションしています。

また4.については、ApplicationのPopModalイベントをここらへんでハンドルして処理しています。

これらはほぼXamarin.Forms.INavigationを薄くラップするNavigatorクラスを中心に実現していますので、MVVM系の機能を利用せずとも、Navigatorクラス(とApplicationService)だけ利用するという選択肢もありです。これら(と周辺クラス群)が

  • 一貫性を保ったイベント通知
  • 複合Pageへの再帰的なイベント通知
  • 型安全な遷移パラメーター

を実現しているので、KAMISHIBAIの実装技術的な方面ではほぼ全てと言って良いでしょう。

このあたりの強い一貫性を保ったイベント通知は、アプリケーション構築の強い味方になると信じています。

Xamarin.Forms.GoogleMaps.Bindings 2.1.0の追加機能解説

昨日、Xamarin.Forms.GoogleMaps.Bindings 2.1.0のリリースを告知しましたが、あれだけだと不親切なので使い方が少し癖のある部分の解説を追記しておきます。

www.nuits.jp

追加した機能のうち、つぎの3つは少し使い方が特殊です。

  • Support MoveCamera
  • Support AnimateCamera
  • Support TakeSnapshot

とはいえ、どれか一つ覚えてしまえば基本的に他の機能も同じ方式に則っているので理解は容易でしょう。

というわけで、MoveCameraを例にとって説明します。

Mapの表示を位置や(三次元的な)角度を指定して、即座に変更するための機能がXamarin.Forms.GoogleMapsにはあります。
それをViewModelから利用するための機能を追加しました。

具体的にはViewModelに次のようなコードを追記します。

public MoveCameraRequest MoveCameraRequest { get; } = new MoveCameraRequest();

public Command MoveToTokyoCommand => new Command(() =>
{
    MoveCameraRequest.MoveCamera(CameraUpdateFactory.NewCameraPosition(
        new CameraPosition(
            new Position(35.681298, 139.766247), // Tokyo
            17d, // zoom
            45d, // bearing(rotation)
            60d // tilt
        ))); ;
});

MoveCameraRequestというプロパティを公開し、そのプロパティのMoveCameraメソッドを呼び出して利用します。
ちゃんと本家と同じく、Task<AnimationStatus>を返してくれます。
TakeSnapshotもちゃんとTask<Stream>を返してくれるため、投げっぱなしではなくちゃんとViewModel側で結果を受け取って処理できるようにしてあります。

もちろんこれだけではだめで、View側にBehaviorを追加して、このMoveCameraRequestからの要求を受け付けてMapへ伝達してあげる必要があります。

<googleMaps:Map>
    <googleMaps:Map.Behaviors>
        <bindings:MoveCameraBehavior Request="{Binding MoveCameraRequest}"/>
    </googleMaps:Map.Behaviors>
</googleMaps:Map>

こんな感じです。
簡単ですね。

つぎの機能にも簡単に触れておきましょうか。

  • Support InfoWindowClicked
  • Support InfoWindowLongClicked
  • Support MyLocationButtonClicked

これらにはMapのイベントを監視し、イベント発生時にイベントパラメーターを渡してCommandを発行するBehaviorとして追加しました。
今までも類似のものはありましたので説明は不要かもしれませんが。

具体的にはViewで次のように記述します。

<googleMaps:Map>
    <googleMaps:Map.Behaviors>
        <bindings:InfoWindowClickedToCommandBehavior Command="{Binding InfoWindowClickedCommand}"/>
        <bindings:InfoWindowLongClickedToCommandBehavior Command="{Binding InfoWindowLongClickedCommand}"/>
        <bindings:MyLocationButtonClickedToCommandBehavior Command="{Binding MyLocationButtonClicked}"/>
    </googleMaps:Map.Behaviors>
</googleMaps:Map>

特化型のEventToCommandBehaviorです。

てことで、簡単ですが今回はこれだけ。
それではまた!

Release Xamarin.Forms.GoogleMaps.Bindings 2.1.0

ちょっと放置状態になってしまっていたXamarin.Forms.GoogleMaps.Bindingsを、「だいたい」Xamarin.Forms.GoogleMaps 2.1.0まで対応しました。
未対応機能があれば、良かったら教えてください。

とりあえず、対応内容は以下の通りです。

  • Transition to .NET Standard 1.0
  • Support Xamarin.Forms.GoogleMaps 2.1.0
  • Support MoveCamera
  • Support AnimateCamera
  • Support TakeSnapshot
  • Support InfoWindowClicked
  • Support InfoWindowLongClicked
  • Support MyLocationButtonClicked

preview.nuget.org

KAMISHIBAI for Xamarin.Forms 1.1.0をリリースしました。

ServiceLocatorを追加して、Page生成のDependency Injection対応が可能となりました。

KAMISHIBAIをPrismと併用する記事を書かなきゃなと思っていたら、神のようなタイミングで記事化してくださった方がいらっしゃいまして。

blog.okazuki.jp

かずきさん、ありがとうございました!

その中で2点、ご指摘いただきました。

  1. Pageがデフォルトコンストラクタの存在が前提となっておりDIContainerからの生成に対応していない
  2. Prismと比較するとDeepLinkサポートがない

どちらも仰る通りで、1.はすぐ対応できることなので、対応してリリースしました。
例えば、Prism.Autofac.Formsを利用される場合、App.csに次のように記述することでPage生成をDI Containerへ移譲することが可能になります。

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

    protected override void OnInitialized()
    {
        InitializeComponent();

        ServiceLocator.SetLocator(type => Container.Resolve(type));
        ApplicationService.Initialize(this);
        ApplicationService.SetMainPage(new NavigationPage(new MainPage()), "Hello from Xamarin.Forms");
    }

    protected override void RegisterTypes()
    {
        var builder = new ContainerBuilder();
        builder.RegisterType<MainPage>();
        builder.RegisterType<SecondPage>();

        builder.Update(Container);
    }
}

ServiceLocator.SetLocatorで、Containerから生成するようにデリゲートを渡してあげればOKです。
あと、ContainerへのType登録ですが、RegisterTypeForNavigationではなく、ContainerBuilderを利用してRegisterTypeで行っています。
RegisterTypeForNavigationはPrismのNavigationServiceを利用するための拡張メソッドですので。

ちなみにServiceLocatorの名前がPageResolverとかじゃないのは理由があります。今度ViewModeLocatorを作ってDIと仲良くする方法を記事化しますがそこでも利用したいためです。

なお、Prismとの併用サンプルを一応こちらのリポジトリに「LiveWithPrism」というプロジェクトで作成してありますので、よかったらご覧ください。

github.com

なお、DeepLinkについてですが、ちょっと考えがあって現時点ではKAMISHIBAIではサポートしていませんが、不可能なわけではありません。 URIベースの指定は無理ですけどね。
その辺は、書き出すと長くなるのでまた今度きちんと文書化しようと思います。

というわけで、今日はここまで!

Xamarin.FormsでListView選択時にエレガントに詳細画面へ遷移する2つの方法 with 紙芝居 & BehaviorsPack

エレガントは言い過ぎ&タイトル長い上&ダイマでごめんなさい。

KAMISHIBAI for Xamarin.FormsとXamarin.Forms.BehaviorsPackの合わせ技でこんな簡単にListViewで選択されて画面遷移してパラメーター渡しするのが簡単に書けますよという紹介です。

f:id:nuitsjp:20170808094435g:plain

こんな感じのありがちなやつですが、これを至極簡単に書ける方法を紹介します。

サンプルの全コードはこちらのStylishListViewSampleをご覧ください。

さて、まず初期画面のViewModelをつぎのように記述します。

public class FruitsListPageViewModel : ViewModelBase
{
    public IReadOnlyList<Fruit> Fruits { get; }
        =  FruitsRepository.Fruits;

    public NavigationRequestCommand<Fruit> RequestDetail { get; }
        = new NavigationRequestCommand<Fruit>();
}

NavigationRequestCommandはICommandを実装したINavigationRequestの実装クラスです。
Command実行の受付と、画面遷移要求の発行を同時に行うやつです。
ユーザー操作から画面遷移までに、すべきことが少ない場合に使えます。(Actionをコンストラクタで登録して、簡単な処理を遷移前に実行する事も可能です。)

そしてXAMLをこんな感じで書きます。

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage ...>
    <ContentPage.BindingContext>
        <viewModels:FruitsListPageViewModel/>
    </ContentPage.BindingContext>
    <ContentPage.Behaviors>
        <mvvm:PushAsync Request="{Binding RequestDetail}" 
                        x:TypeArguments="views:FruitDetailPage" />
    </ContentPage.Behaviors>
    <ListView ItemsSource="{Binding Fruits}">
        <ListView.Behaviors>
            <behaviorsPack:SelectedItemBehavior 
                Command="{Binding RequestDetail}"/>
        </ListView.Behaviors>
        <ListView.ItemTemplate>
            <DataTemplate>
                <TextCell Text="{Binding Name}" TextColor="{Binding Color}"/>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</ContentPage>

ListViewのBehaviorsに登録されているSelectedItemBehaviorをまず見てください。

<behaviorsPack:SelectedItemBehavior Command="{Binding RequestDetail}"/>

SelectedItemBehaviorは行が選択された時に、良い感じで選択されたアイテムを渡しつつCommandを実行してくれる自分的にはうれしい奴です(再選択問題なんかも対応してありますよ)。
ListViewで行が選択されたら、選択された行にBindされているFruitを引数に、RequestDetailのExecuteを呼び出してくれます。

つづいてPageのBehaviorsを見てください。

<mvvm:PushAsync Request="{Binding RequestDetail}" 
                x:TypeArguments="views:FruitDetailPage" />

RequestDetailの要求を受けてFruitDetailPageにPushAsyncで遷移してね、と宣言されています。
これでListViewでいずれかが選択されたら、選択されたFruitを渡して画面遷移します。

あとは受け取る側を次のように実装します。

public class FruitDetailPageViewModel 
    : ViewModelBase, IPageInitializeAware<Fruit>
{
    public void OnInitialize(Fruit fruit) => Fruit = fruit;

OnInitializeメソッドのパラメーターとして型安全に受け取れます。受け取り後、このコードではFruitプロパティに値を設定しています。
良くないですか?

…ん?良くない?パラメーターはID渡しして、遷移先で値は取り直したいと?
OK、そのパターンを紹介しましょう。

初期画面のViewModelを次のように修正します。

// public NavigationRequestCommand<Fruit> RequestDetail { get; } 
//    = new NavigationRequestCommand<Fruit>();
public NavigationRequestCommand<int> RequestDetail { get; } 
    = new NavigationRequestCommand<int>();

型パラメーターをFruitからintに変更しただけです。
そして、XAMLのSelectedItemBehaviorでFruitのIdをCommandに渡すよう、次のように修正します。

before

<behaviorsPack:SelectedItemBehavior 
    Command="{Binding RequestDetail}"/>

after

<!-- 
<behaviorsPack:SelectedItemBehavior 
    Command="{Binding RequestDetail}" PropertyPath="Id"/>

最期に、受け取るところを受け取ったIdから値を再取得するように修正しましょう。

//public class FruitDetailPageViewModel
//    : ViewModelBase, IPageInitializeAware<Fruit>
public class FruitDetailPageViewModel 
    : ViewModelBase, IPageInitializeAware<int>
{
    // public void OnInitialize(Fruit fruit) => Fruit = fruit;
    public void OnInitialize(int id) => 
        Fruit = FruitsRepository.FindById(id);

これでOKです。

いや~、Xamarin.Forms.BehaviorsPackと紙芝居便利だな~(棒

続きを読む

KAMISHIBAI for Xamarin.Forms 1.0.0をリリースしました。

ここしばらく作っては消しを繰り返していた、お手製画面遷移ライブラリ

f:id:nuitsjp:20170807125900p:plain

をリリースしました。コードとドキュメントはGithub上に公開しています。利用の際はNuGetからインストールして利用してください。

目指したのは

  1. Xamarin.Formsで可能なあらゆる画面遷移が実現可能
  2. タブ切り替えを含むPageが切り替わるタイミングで、一貫性のある画面遷移イベントが通知され
  3. 型安全に画面遷移パラメーターを受け渡せて
  4. ちゃんとViewとViewModelが分離できる

画面遷移ライブラリです。ちょっと胡散臭いですね。

実際にコードを見ていただいた方が良いでしょう。DateTime型を引数に画面遷移するシナリオを考えます。まずはViewModelです。

public INavigationRequest<DateTime> RequestSecondPage { get; } = 
    new NavigationRequest<DateTime>();

public Task Navigate(DateTime selectedDate)
{
    return RequestSecondPage.RaiseAsync(selectedDate);
}

見ての通り、ViewModelからはパラメーターを渡して遷移要求をあげるだけで、遷移処理そのものは記述しません。
遷移処理はXAMLに書きます(C#で書いても良いですが)。

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage ...>
    <ContentPage.Behaviors>
        <mvvm:PushModalAsync Request="{Binding RequestSecondPage}" 
                             x:TypeArguments="views:SecondPage"/>
    </ContentPage.Behaviors>

PushModalAsyncビヘイビアに注目してください。
RequestSecondPageから遷移要求を受け、SecondPageへ、PushModalAsyncで画面遷移する事が理解できるでしょう。

そして値を受け取るのは、遷移イベントの通知で受け取ります。

    public class SecondPageViewModel : IPageInitializeAware<DateTime>, INotifyPropertyChanged
    {
        ...
        public void OnInitialize(DateTime selectedDate)
        {
            SelectedDate = selectedDate;
        }

ここまでで興味をもっていただけた方は、良かったらチュートリアルをお試しください。

なお大雑把なアーキテクチャモデルは次のとおりです。

f:id:nuitsjp:20170807164150p:plain

NavigatorクラスがXamarin.Forms.INavigationをラップし、イベント通知と遷移パラメーターの受け渡しを実現しています。
画面遷移はViewModel層でNavigationRequestを利用して画面遷移要求をView層にあげる事から始まります。
NavigationRequestはPushAsyncやPopAsyncなど、NavigationBehaviorを継承したBehaviorにバインドされており、NavigationRequestからの要求を受けて、BehaviorがNavigatorクラスを呼び出して画面遷移します。

流れ的には以下のようなイメージです。

f:id:nuitsjp:20170807171121p:plain

NavigationBehaviorを継承して独自の遷移処理を実装すれば、アーキテクチャ上、Xamarin.Formsで実装可能な画面遷移は全て実現可能です。

アーキテクチャについては、もう少し詳細な情報をこちらに記載しています。

ところで本当に「Xamarin.Formsで可能なあらゆる画面遷移が実現可能」なのでしょうか?
ということで、ちょっと前に感動したMasterDetailPageの設計パターンに適用してみました。元記事はこちらになります。

qiita.com

こんなMasterDetailPageを作ります。

f:id:nuitsjp:20170807161006g:plain

  1. DrawerからHome以外を選択して画面遷移した後、戻るボタンでHomeへ戻る
  2. DrawerからHome以外を複数回選択して画面を切り替えても、戻るボタンを押すとHomeへ戻る
  3. その間、左上のボタンはずっとハンバーガーメニューの状態になっている
  4. Homeを表示した状態で戻るボタンを押すと、Androidのホームに戻る
  5. Drawerから選択した画面から、ドリルダウンで詳細へ遷移すると左上のボタンは矢印ボタンになる

Xamarin.FormsのデフォルトのMasterDetailPageだとちょっと面倒な仕様ですが、Detail側でNavigationPageを2段階にネストして利用することで実現されています。
詳細な解説は先に紹介したQiitaの記事をご覧ください。
Qiitaの記事ではView層でガッツリ実装されていますが、これをKAMISHIBAIを適用しつつ、ViewとViewModelを分離します。

分離して実装したサンプルコードがこちらにあります。

github.com

ここのSpecialMasterDetailPageソリューションを参照してください。
ちゃんとViewとViewModelが分離されているのが見て取れると思います。

解説は次回にでも行いたいと思います。

それでは今回はここまで。良かったら使ってください!