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

かずきのBlog@hatena

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

Xamarin.FormsでPrism.Formsを使ってPCLに非対応なライブラリを使う方法

Prism Xamarin

例えばMicrosoft.AzureのStorageのライブラリなんかがXamarin.AndroidとXamarin.iOSには対応してるけどPCLには対応してないといった感じです。

被害者がここに。

対応方法

ということで対応方法ですが、id:chomadoさんがちゃんと対応してました。

つまり、プラットフォーム固有機能を呼ぶときと同じ対応でOKということですね。

Prism.Formsでプラットフォーム固有処理を呼び出す方法はDependencyServiceを使うかIPlatformInitializerを使う方法の2通りがあります。

www.nuits.jp

特に理由がない限りは(例えば既存でDependencyServiceでガッツリ作ってるとか。まぁそれでも移行は楽ちんだからやった方がいいと思うけど)IPlatformInitializerを使うのがいいでしょう。柔軟ですし、早いですし。

ということで対応方法のステップ。

ステップ1

PCLのプロジェクトにインターフェースを切る。

public interface IHogeService
{
  Task<Hoge> HogeAsync();
}

ステップ2

NuGetからAndroidとiOSプロジェクトにライブラリを追加する。

ステップ3

AndroidかiOSプロジェクトのどちらかに、PCLに定義したインターフェースの実装を定義する。

public class HogeService : IHogeService
{
  public Task<Hoge> HogeAsync()
  {
    // ライブラリを使ったコード
  }
}

ステップ4

もう一方のプロジェクト(Androidにステップ3で実装したならiOS、iOSに実装したならAndroid)にリンクとしてステップ3で作ったファイルを追加。

これでソースコードの共有化を行います。

ステップ5

各プラットフォームのIPlatformInitializerの実装にサービスの登録を行う。Prism Template Packを使ってる場合は自動的にMainActivityAppDelegateに追加されています。

container.RegisterType<IHogeService, HogeService>(new ContainerControledLifetimeManager());

ステップ6

PCLで使いましょう。 ViewModelとかのコンストラクタにサービスを受け取るようにすることで、Androidで実行されたときはAndroid用のクラスが、iOSで実行されたときはiOS用のクラスがインジェクションされます。

まとめ

普通PCLか.NET Standard対応してるだろJKと思ってても、思わぬところで罠があったりするので頭の片隅に入れておきましょう。

Xamarin.Formsで画面レイアウトを作るためのレイアウトコントロールの必要最低限

Xamarin XAML

Xamarin.Formsで思い通りのレイアウトを組みたい。 そんな時には、レイアウト系のコントロールを押さえておくといいです。

公式ドキュメント

Layouts - Xamarin

Layoutコントロール

Xamarin.Formsでは、複数のコントロールを配置するときには、あらかじめ定義されている子要素をレイアウトする機能をもったコントロールに対して、コントロールを配置、もしくはレイアウトコントロールをネストさせて配置します。以下のコントロールが提供されています。

  • StackLayout
  • AbsoluteLayout
  • RelativeLayout
  • Grid
  • ScrillViewer

ここでは、おさえておくべき必要最低限のレイアウトコントロールとしてStackLayoutGridについて紹介したいと思います。

StackLayout

StackLayoutは、名前の通り子要素を縦方向、横方向に並べて配置するコントロールになります。デフォルトでは縦方向に子要素を並べて配置します。デフォルトの挙動を確認するための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="PrismUnityApp15.Views.MainPage"
             Title="MainPage">
  <StackLayout>
    <BoxView Color="Red" />
    <BoxView Color="Blue" />
    <BoxView Color="Green" />
    <BoxView Color="Aqua" />
  </StackLayout>
</ContentPage>

実行すると以下のような表示になります。

f:id:okazuki:20161208222547p:plain

縦と横の表示を切り替えるにはOrientationプロパティを指定します。VerticalがデフォルトでHorizontalを指定することで、横方向に並ぶようになります。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="PrismUnityApp15.Views.MainPage"
             Title="MainPage">
  <StackLayout Orientation="Horizontal">
    <BoxView Color="Red" />
    <BoxView Color="Blue" />
    <BoxView Color="Green" />
    <BoxView Color="Aqua" />
  </StackLayout>
</ContentPage>

実行すると以下のような表示になります。

f:id:okazuki:20161208222844p:plain

Spacing

Spacingプロパティを使うことでコントロールを並べるときの余白を指定することもできます。

<?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="PrismUnityApp15.Views.MainPage"
             Title="MainPage">
  <StackLayout Orientation="Horizontal"
               Spacing="20">
    <BoxView Color="Red" />
    <BoxView Color="Blue" />
    <BoxView Color="Green" />
    <BoxView Color="Aqua" />
  </StackLayout>
</ContentPage>

実行すると以下のようになります。

f:id:okazuki:20161208223029p:plain

コントロール間のスペースが広くなっていることが確認できます。

LayoutOptions

さらにコントロールのHorizontalOptionsプロパティとVerticalOptionsを使うことで子コントロールがStackLayout上で占める割合をある程度自由に設定することができます。

HorizontalOptionsプロパティとVerticalOptionsプロパティには以下の値が設定できます。

  • Start: 開始位置にレイアウトする
  • Center: 中央にレイアウトする
  • End: 終端にレイアウトする
  • Fill: いっぱいに広げる
  • StartAndExpand: 開始位置にレイアウトして余白を埋めつくす
  • CenterAndExpand: 中央にレイアウトして余白を埋め尽くす
  • EndAndExpand: 終了位置にレイアウトして余白を埋め尽くす
  • FillAndExpand: いっぱいに広げて余白を埋め尽くす

まず、Start, Center, End, Fillを見ていきます。

<?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="PrismUnityApp15.Views.MainPage"
             Title="MainPage">
  <StackLayout Orientation="Horizontal">
    <BoxView Color="Red" VerticalOptions="Start" />
    <BoxView Color="Blue" VerticalOptions="End" />
    <BoxView Color="Green" VerticalOptions="Center" />
    <BoxView Color="Aqua" VerticalOptions="Fill" /> <!-- Default -->
  </StackLayout>
</ContentPage>

実行すると以下のようになります。

f:id:okazuki:20161208223900p:plain

次にAndExpandがついているオプションについての挙動を見てみます。

<?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="PrismUnityApp15.Views.MainPage"
             Title="MainPage">
  <StackLayout>
    <BoxView Color="Red" VerticalOptions="StartAndExpand" />
    <BoxView Color="Blue" />
  </StackLayout>
</ContentPage>

実行すると、赤色が上端に配置されて余白を埋め尽くします。そして、青色が赤色が余白を埋め尽くすため下端に表示されます。実行結果を以下に示します。

f:id:okazuki:20161208224159p:plain

次に、EndAndExpandの挙動を確認します。

<?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="PrismUnityApp15.Views.MainPage"
             Title="MainPage">
  <StackLayout>
    <BoxView Color="Red" VerticalOptions="EndAndExpand" />
    <BoxView Color="Blue" />
  </StackLayout>
</ContentPage>

実行すると、赤色が余白を埋め尽くして下端に表示されます。その下に青色が表示されます。実行結果を以下に示します。

f:id:okazuki:20161208224411p:plain

次に、FillAndExpandの挙動を確認します。

<?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="PrismUnityApp15.Views.MainPage"
             Title="MainPage">
  <StackLayout>
    <BoxView Color="Red" VerticalOptions="FillAndExpand" />
    <BoxView Color="Blue" />
  </StackLayout>
</ContentPage>

実行すると、赤色が余白を埋め尽くす形でいっぱいに表示されて、残りの部分に青色が表示されます。実行結果を以下に示します。

f:id:okazuki:20161208224614p:plain

AndExpandのつくプロパティを複数使用した場合、余白は均等に割り振られます。以下のように2つのコントロールでFillAndExpandStartAndExpandを使用した場合は、上半分に赤色がいっぱいに表示され、下半分の開始位置に青色が配置されます。

<?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="PrismUnityApp15.Views.MainPage"
             Title="MainPage">
  <StackLayout>
    <BoxView Color="Red" VerticalOptions="FillAndExpand" />
    <BoxView Color="Blue" VerticalOptions="StartAndExpand"/>
  </StackLayout>
</ContentPage>

実行結果を以下に示します。

f:id:okazuki:20161208224850p:plain

複雑なレイアウト

StackLayoutLayoutOptionsを組み合わせることで複雑なレイアウトが実現可能です。以下に例を示します。イメージとしてはTwitterクライアントのような見た目をイメージしています。

<?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="PrismUnityApp15.Views.MainPage"
             Title="MainPage">
  <StackLayout>
    <!-- つぶやき1 -->
    <StackLayout Orientation="Horizontal"
                 VerticalOptions="Start">
      <BoxView Color="Red" />
      <StackLayout HorizontalOptions="FillAndExpand">
        <StackLayout Orientation="Horizontal">
          <StackLayout Orientation="Vertical"
                       HorizontalOptions="FillAndExpand">
            <StackLayout Orientation="Horizontal">
              <Label Text="@okazuki" />
              <Label Text="かずき@MBPぽちった"
                     HorizontalOptions="FillAndExpand" />
            </StackLayout>
            <Label Text="XXXXXXXXXXXXXXXXXXXX" />
          </StackLayout>
        </StackLayout>
        <StackLayout Orientation="Horizontal"
                     HorizontalOptions="EndAndExpand">
          <Button Text="Like" 
                  HorizontalOptions="End" />
          <Button Text="RT" 
                  HorizontalOptions="End" />
          <Button Text="Quote" 
                  HorizontalOptions="End" />
        </StackLayout>
      </StackLayout>
    </StackLayout>

    <!-- つぶやき2 -->
    <StackLayout Orientation="Horizontal"
                 VerticalOptions="Start">
      <BoxView Color="Red" />
      <StackLayout HorizontalOptions="FillAndExpand">
        <StackLayout Orientation="Horizontal">
          <StackLayout Orientation="Vertical"
                       HorizontalOptions="FillAndExpand">
            <StackLayout Orientation="Horizontal">
              <Label Text="@okazuki" />
              <Label Text="かずき@MBPぽちった"
                     HorizontalOptions="FillAndExpand" />
            </StackLayout>
            <Label Text="YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY" />
          </StackLayout>
        </StackLayout>
        <StackLayout Orientation="Horizontal"
                     HorizontalOptions="EndAndExpand">
          <Button Text="Like" 
                  HorizontalOptions="End" />
          <Button Text="RT" 
                  HorizontalOptions="End" />
          <Button Text="Quote" 
                  HorizontalOptions="End" />
        </StackLayout>
      </StackLayout>
    </StackLayout>
  </StackLayout>
</ContentPage>

実行結果を以下に示します。

f:id:okazuki:20161208230620p:plain

このように、StackLayoutLayoutOptionsを組み合わせてネストさせることで、かなりのレイアウトを組むことができます。

Grid

次に、Gridについて説明します。Gridは、名前の通り格子状にレイアウトの中を区切って、そこにコントロールを配置していきます。行の定義はGridRowDefinitionsプロパティに対してRowDefinitionを追加することで定義できます。列の定義はColumnDefinitionsプロパティに対してColumnDefinitionを追加することで定義できます。デフォルトでは均等に幅と高さが割り振られます。

コントロールの配置はGrid.Row添付プロパティで行(0オリジン)を指定してGrid.Columnプロパティで列(0オリジン)を指定します。

3x3の格子状に画面を区切った場合のコード例を以下に示します。

<?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="PrismUnityApp15.Views.MainPage"
             Title="MainPage">
  <Grid>
    <!-- 行の定義 -->
    <Grid.RowDefinitions>
      <RowDefinition />
      <RowDefinition />
      <RowDefinition />
    </Grid.RowDefinitions>    
    <!-- 列の定義 -->
    <Grid.ColumnDefinitions>
      <ColumnDefinition />
      <ColumnDefinition />
      <ColumnDefinition />
    </Grid.ColumnDefinitions>
    
    <BoxView Color="Red" /> <!-- デフォルトは0,0に配置される -->
    <BoxView Color="Blue" 
             Grid.Row="0"
             Grid.Column="1" />
    <BoxView Color="Aqua" 
             Grid.Row="0"
             Grid.Column="2" />
    <BoxView Color="Maroon" 
             Grid.Row="1"
             Grid.Column="0" />
    <BoxView Color="Navy" 
             Grid.Row="1"
             Grid.Column="1" />
    <BoxView Color="Silver" 
             Grid.Row="1"
             Grid.Column="2" />
    <BoxView Color="Purple" 
             Grid.Row="2"
             Grid.Column="0" />
    <BoxView Color="Lime" 
             Grid.Row="2"
             Grid.Column="1" />
    <BoxView Color="Yellow" 
             Grid.Row="2"
             Grid.Column="2" />
             
  </Grid>
</ContentPage>

実行結果を以下に示します。

f:id:okazuki:20161208231350p:plain

幅と高さの指定

RowDefinitionにはHeightプロパティで行の高さを指定できます。ColumnDefinitionにはWidthプロパティで列の幅を指定できます。指定方法には、数字を指定して固定幅を指定する方法と、1*, 2*のように*を使用して余白を占める割合を比率で指定する方法とAutoを指定して中身のサイズに合わせる方法があります。

コード例を以下に示します。

<?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="PrismUnityApp15.Views.MainPage"
             Title="MainPage">
  <Grid>
    <!-- 行の定義 -->
    <Grid.RowDefinitions>
      <RowDefinition Height="15" /> <!-- 固定 -->
      <RowDefinition Height="1*" /> <!-- 余白を1対2に分割 -->
      <RowDefinition Height="2*" />
    </Grid.RowDefinitions>    
    <!-- 列の定義 -->
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="Auto" /> <!-- 中身の幅に応じて設定 -->
      <ColumnDefinition Width="*" /> <!-- デフォルト値は*(1*と同じ意味) -->
      <ColumnDefinition />
    </Grid.ColumnDefinitions>
    
    <BoxView Color="Red" /> <!-- デフォルトは0,0に配置される -->
    <BoxView Color="Blue" 
             Grid.Row="0"
             Grid.Column="1" />
    <BoxView Color="Aqua" 
             Grid.Row="0"
             Grid.Column="2" />
    <BoxView Color="Maroon" 
             Grid.Row="1"
             Grid.Column="0" />
    <BoxView Color="Navy" 
             Grid.Row="1"
             Grid.Column="1" />
    <BoxView Color="Silver" 
             Grid.Row="1"
             Grid.Column="2" />
    <BoxView Color="Purple" 
             Grid.Row="2"
             Grid.Column="0" />
    <BoxView Color="Lime" 
             Grid.Row="2"
             Grid.Column="1" />
    <BoxView Color="Yellow" 
             Grid.Row="2"
             Grid.Column="2" />
             
  </Grid>
</ContentPage>

実行結果を以下に示します。

f:id:okazuki:20161208231859p:plain

複数行・複数列の占有

今までは格子状に区切った中の1つのセルに1つのコントロールを置いてきました。Gridコントロールでは、これに加えて複数のセルにまたがる形でコントロールを配置することができます。Grid.RowSpan添付プロパティと、Grid.ColumnSpan添付プロパティを使用することで行の占有数と列の占有数を指定できます。

コード例を以下に示します。

<?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="PrismUnityApp15.Views.MainPage"
             Title="MainPage">
  <Grid>
    <!-- 行の定義 -->
    <Grid.RowDefinitions>
      <RowDefinition />
      <RowDefinition />
      <RowDefinition />
    </Grid.RowDefinitions>    
    <!-- 列の定義 -->
    <Grid.ColumnDefinitions>
      <ColumnDefinition />
      <ColumnDefinition />
      <ColumnDefinition />
    </Grid.ColumnDefinitions>
    
    <BoxView Color="Red" 
             Grid.RowSpan="2"
             Grid.ColumnSpan="3"/>
    <BoxView Color="Yellow" 
             Grid.Row="2"
             Grid.Column="1"
             Grid.ColumnSpan="2" />
             
  </Grid>
</ContentPage>

実行すると以下のようになります。

f:id:okazuki:20161208232709p:plain

複雑なレイアウト

Gridを使うことで複雑なレイアウトを組むことが簡単にできます。StackLayoutで示した表示と同じような表示をGridでもやってみたいと思います。

<?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="PrismUnityApp15.Views.MainPage"
             Title="MainPage">
  <StackLayout VerticalOptions="Start">
    <!-- つぶやき1 -->
    <Grid>
      <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />        
        <RowDefinition Height="Auto" />        
        <RowDefinition Height="Auto" />        
      </Grid.RowDefinitions>
      <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition />
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition Width="Auto"/>
      </Grid.ColumnDefinitions>
      <BoxView Color="Red"
               Grid.RowSpan="3" />
      <StackLayout Orientation="Horizontal"
                   Grid.Column="1"
                   Grid.ColumnSpan="4">
        <Label Text="@okazuki" />
        <Label Text="かずき@MBPぽちった" />
      </StackLayout>
      <Label Text="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
             Grid.Row="1"
             Grid.Column="1"
             Grid.ColumnSpan="4" />
      <Button Text="Like"
              Grid.Row="2"
              Grid.Column="2" />
      <Button Text="RT"
              Grid.Row="2"
              Grid.Column="3" />
      <Button Text="Quote"
              Grid.Row="2"
              Grid.Column="4" />
    </Grid>
    
    <!-- つぶやき2 -->
    <Grid>
      <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />        
        <RowDefinition Height="Auto" />        
        <RowDefinition Height="Auto" />        
      </Grid.RowDefinitions>
      <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition />
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition Width="Auto"/>
      </Grid.ColumnDefinitions>
      <BoxView Color="Red"
               Grid.RowSpan="3" />
      <StackLayout Orientation="Horizontal"
                   Grid.Column="1"
                   Grid.ColumnSpan="4">
        <Label Text="@okazuki" />
        <Label Text="かずき@MBPぽちった" />
      </StackLayout>
      <Label Text="YYYYYYYYYYYYYYYYYYYYYYYYYYYYYY"
             Grid.Row="1"
             Grid.Column="1"
             Grid.ColumnSpan="4" />
      <Button Text="Like"
              Grid.Row="2"
              Grid.Column="2" />
      <Button Text="RT"
              Grid.Row="2"
              Grid.Column="3" />
      <Button Text="Quote"
              Grid.Row="2"
              Grid.Column="4" />
    </Grid>
  </StackLayout>
</ContentPage>

実行結果を以下に示します。

f:id:okazuki:20161208233621p:plain

余白

最後に余白について説明します。余白の設定は、PaddingプロパティとMarginプロパティがあります。Paddingプロパティがコントロールの外側に余白を設けます。Marginプロパティがコントロールの内側に余白を設けます。プロパティの設定方法は、4方向に同一の値を設定する方法と、左右と上下にそれぞれ同じ値を設定する方法と、上下左右に個別の値を設定する方法があります。

10のように単一の値を設定すると上下左右にすべて10の余白が設定されます。10,5のように2つの値を設定すると左右に10、上下に5の余白が設定されます。5,10,15,20のように4つの値を設定すると左、上、右、下の順番で余白が設定されます。

コード例を以下に示します。

<?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="PrismUnityApp15.Views.MainPage"
             Title="MainPage"
             Padding="20">
  <Grid Margin="10,5">
    <BoxView Color="Red" />
  </Grid>
</ContentPage>

ページのPaddingに20を設定して上下左右に20の余白を持たせています。さらにGridでMarginを指定して左右に10、上下に5の余白を持たせています。

実行結果を以下に示します。

f:id:okazuki:20161208234344p:plain

まとめ

Xamarin.Formsには、いくつかのレイアウトコントロールが定義されていますが、基本的なレイアウトに関していうとStackLayoutGridを組み合わせることで大体実現可能です。まず、とっかかりとしてこの2つのレイアウトコントロールの使い方をマスターして思い通りのページを作れるようになりましょう。

Xamarin.Formsでタブページを使う

Xamarin

TabbedPageを使うといいということみたいです。

TabbedPage内にはタブとして表示したいページを置いて行って、タブのタイトルにはPageのTitleが表示されるという動きをしています。 意外と簡単だった。

Prismを使ってNavigationPage内にTabbedPageをネストして初期表示ページを指定して画面遷移するには以下のような感じのコードでOK.

using Prism.Unity;
using PrismUnityApp14.Views;
using Xamarin.Forms;

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

        protected override void OnInitialized()
        {
            InitializeComponent();

            NavigationService.NavigateAsync("NavigationPage/MainPage/AboutPage");
        }

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

MainPageがTabbedPageを拡張したページで以下のようなXAMLを書いています。

<?xml version="1.0" encoding="utf-8" ?>
<TabbedPage 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:Views="clr-namespace:PrismUnityApp14.Views"
            prism:ViewModelLocator.AutowireViewModel="True"
            x:Class="PrismUnityApp14.Views.MainPage"
            Title="SampleApp">
  <Views:HomePage />
  <Views:AboutPage />
</TabbedPage>

実行すると以下のような感じ。

f:id:okazuki:20161206123916p:plain

いい感じですね。

Prism.Formsで使うときの注意点

タブ切り替えではタブ内ページのViewModelで実装しているINavigationAwareのOnNavigatedFromとOnNavigatedToは動かないので、自分でどうにかしないといけない点が要注意です。

今後のバージョンで改善されるといいな(そんなIssueを見た気がする)

Xamarin.FormsでLabelに下線を引きたい

Xamarin

デフォルトで引けないんですね。知らなかった。

Effectを使おう

ということでカスタムレンダラー案件かなと思ったらEffectでいけるっぽいです。

Android

Androidに以下のようなクラスを追加します。

using Android.Widget;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;

[assembly: ResolutionGroupName("Sample")]
[assembly: ExportEffect(typeof(PrismUnityApp13.Droid.UnderlineEffect), "UnderlineEffect")]
namespace PrismUnityApp13.Droid
{
    public class UnderlineEffect : PlatformEffect
    {


        protected override void OnAttached()
        {
            var textView = this.Control as TextView;
            if (textView == null) { return; }

            textView.PaintFlags = textView.PaintFlags | Android.Graphics.PaintFlags.UnderlineText;
        }

        protected override void OnDetached()
        {
        }
    }
}

iOS

iOSは動作確認取れる環境はないのですが、UILabelに対して以下のようにすると下線が引けるっぽい?

using Foundation;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;

[assembly: ResolutionGroupName("Sample")]
[assembly: ExportEffect(typeof(PrismUnityApp13.iOS.UnderlineEffect), "UnderlineEffect")]
namespace PrismUnityApp13.iOS
{
    public class UnderlineEffect : PlatformEffect
    {
        protected override void OnAttached()
        {
            var label = this.Control as UILabel;
            if (label == null) { return; }

            var text = label.Text;
            label.AttributedText = new NSAttributedString(
                text,
                underlineStyle: NSUnderlineStyle.Single);
        }

        protected override void OnDetached()
        {
        }
    }
}

PCL

そして、PCLでEffectを作ります。

using Xamarin.Forms;

namespace PrismUnityApp13
{
    public class UnderlineEffect : RoutingEffect
    {
        public UnderlineEffect() : base("Sample.UnderlineEffect")
        {
        }
    }
}

XAML

そして、XAMLでLabelにEffectを追加します。

<Label Text="{Binding Title}">
  <Label.Effects>
    <local:UnderlineEffect />
  </Label.Effects>
</Label>

実行

Androidしか実行環境持ってないのでiOS試せてないのですが、Androidでは以下のように下線が引かれました。

f:id:okazuki:20161205233948p:plain

まとめ

Effect初めて触ってみたけど簡単お手軽な感じですね。いいかも。

Visual Studio Moble Center触ってみた

Azure Xamarin

Visual Studio Mobile Centerの申し込みをしたら、先日招待メールもらったので触ってみました。

初期画面

ログインするとこんな感じの画面になります。

f:id:okazuki:20161204101229p:plain

アプリの作成

Add a new appというボタンがあるので押すと、アプリの作成画面になります。

f:id:okazuki:20161204101401p:plain

OSの選択が排他式になってるのでXamarinでAndroid/iOS両対応とかしてるときにはもにょっとした感じになりながらアプリを作ることになりますね。とりあえず、XamarinでAndroidアプリ開発してるということでAdd new appしてしまいましょう。

そうすると、クラッシュレポートとか分析?系の機能を有効化するためにアプリに組み込まないといけないコードが表示されます。ここで表示されてるGUIDがアプリの秘密のキーっぽいので、管理には気を付けた方がよさそうですね。

f:id:okazuki:20161204101953p:plain

実際に組み込んでみよう

Prism Template Packでアプリを作って早速やってみたいと思います。

Mobile Center Analyticsで検索しろって書いてありますが、検索した結果上から3番目にそれっぽいのが出てきました。 Microsoft.Azure.Mobile.Analyticsがそれっぽいです。

次に、Mobile Center Crashesで検索して上から2番目に出てくるMicrosoft.Azure.Mobile.Crashesをインストールします。

AppクラスのOnInitializedメソッドのNavigateAsyncメソッドの呼び出しの上らへんにコードを挿入します。

protected override void OnInitialized()
{
    InitializeComponent();
    MobileCenter.Start(typeof(Analytics), typeof(Crashes));
    NavigationService.NavigateAsync("MainPage?title=Hello%20from%20Xamarin.Forms");
}

そして、MainActivityOnCreateメソッドの中でConfigureメソッドを呼び出しましょう。

protected override void OnCreate(Bundle bundle)
{
    TabLayoutResource = Resource.Layout.tabs;
    ToolbarResource = Resource.Layout.toolbar;

    base.OnCreate(bundle);
    MobileCenter.Configure("d59f9362-351e-4602-9355-6f56cbf4f412");
    global::Xamarin.Forms.Forms.Init(this, bundle);
    LoadApplication(new App(new AndroidInitializer()));
}

続いてiOSプロジェクトのAppDelegate.csの中のFinishedLaunchingメソッドの中にも同じコードを埋め込みましょう。

public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
    global::Xamarin.Forms.Forms.Init();
    MobileCenter.Configure("d59f9362-351e-4602-9355-6f56cbf4f412");
    LoadApplication(new App(new iOSInitializer()));

    return base.FinishedLaunching(app, options);
}

分析レポートの確認

アプリを起動して、Mobile Centerの左側のAnalyticsをみてみましょう。以下のようなレポートが表示されます。 アプリ1つしか起動してないので、Active usersが1になってるのが確認できますね。

f:id:okazuki:20161204103631p:plain

クラッシュレポートの確認

次にクラッシュレポートを確認してみます。 MainPageViewModelに確実にクラッシュするコマンドを用意します。

public DelegateCommand CrashCommand { get; }

public MainPageViewModel()
{
    this.CrashCommand = new DelegateCommand(() => { throw new InvalidOperationException("error"); });
}

そして、画面にこのコマンドに紐づいたボタンを配置します。

<?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="HelloWorldApp.Views.MainPage"
             Title="MainPage">
  <StackLayout HorizontalOptions="Center" VerticalOptions="Center">
    <Label Text="{Binding Title}" />
    <Button Text="Crash"
            Command="{Binding CrashCommand}" />
  </StackLayout>
</ContentPage>

あと、多分これがいるんだと思われるメソッドをOnNavigatedToあたりに仕込んでおきます。(Appクラスでもいいかな)

public void OnNavigatedTo(NavigationParameters parameters)
{
    if (parameters.ContainsKey("title"))
        Title = (string)parameters["title"] + " and Prism";

    Crashes.GenerateTestCrash();
}

GenerateTestCrashでレポートしてくれるんだと思います。

そんなこんなでボタンを押してクラッシュさせて起動させたりっていうのを繰り返してるとそのうちCrashesのところに以下のようにレポートが表示されます。

f:id:okazuki:20161204110122p:plain

f:id:okazuki:20161204110228p:plain

配布

配布もできます。 やってみましょう。

DistributionからDistribute new releaseあたりをぽちっとしましょう。APKファイルを登録して、なんかコメント書いていきます。

f:id:okazuki:20161204111038p:plain

配布先のグループを選んで(このグループにメンバー追加する方法がわからん…)

f:id:okazuki:20161204111356p:plain

最後にDistributeボタンを押しましょう。

f:id:okazuki:20161204111446p:plain

メールが届きます。 メールのInstallボタンを押すとMobile Centerへのサインインを求められます。サインインをすると、インストール画面が出てきます。

f:id:okazuki:20161204112111j:plain

インストールをタップするとダウンロードが走ります。

そしてインストールをしようとすると、パッケージの解析に失敗とか出てインストールまでいけませんでした…アップロードしたapkがよくなかったのかな…。

まとめ

このほかにMobile CenterではXamarin Test CloudやAzure Mobile Appsの機能がインテグレートされています。そちらは、別途機会があれば紹介したいと思います。 最後にインストール成功までいけなくてもやっとした感じになりましたが、触ってみた感じは以上です。

Xamarin.Formsでボタンの2度押しをReactivePropertyを使って抑止してみよう

ReactiveProperty Prism Xamarin

お題の通りです。

こんな感じでReactiveCommandを普通に使うと連打すると2重で画面遷移したりします。

using Prism.Mvvm;
using Prism.Navigation;
using Reactive.Bindings;
using System;

namespace PrismUnityApp12.ViewModels
{
    public class MainPageViewModel : BindableBase, INavigationAware
    {
        private string _title;
        public string Title
        {
            get { return _title; }
            set { SetProperty(ref _title, value); }
        }

        public ReactiveCommand NavigateCommand { get; }

        public MainPageViewModel(INavigationService navigationService)
        {
            this.NavigateCommand = new ReactiveCommand();
            this.NavigateCommand.Subscribe(async _ => await navigationService.NavigateAsync("NextPage"));
        }

        public void OnNavigatedFrom(NavigationParameters parameters)
        {

        }

        public void OnNavigatedTo(NavigationParameters parameters)
        {
            if (parameters.ContainsKey("title"))
                Title = (string)parameters["title"] + " and Prism";
        }
    }
}

解決策は簡単で2重起動防止機能が組み込まれてるAsyncReactiveCommandを使うだけでOKです。

using Prism.Mvvm;
using Prism.Navigation;
using Reactive.Bindings;
using System;

namespace PrismUnityApp12.ViewModels
{
    public class MainPageViewModel : BindableBase, INavigationAware
    {
        private string _title;
        public string Title
        {
            get { return _title; }
            set { SetProperty(ref _title, value); }
        }

        public AsyncReactiveCommand NavigateCommand { get; }

        public MainPageViewModel(INavigationService navigationService)
        {
            this.NavigateCommand = new AsyncReactiveCommand();
            this.NavigateCommand.Subscribe(async _ => await navigationService.NavigateAsync("NextPage"));
        }

        public void OnNavigatedFrom(NavigationParameters parameters)
        {

        }

        public void OnNavigatedTo(NavigationParameters parameters)
        {
            if (parameters.ContainsKey("title"))
                Title = (string)parameters["title"] + " and Prism";
        }
    }
}

これでOK。このCommandを紐づけがボタンを連打しても2重で画面遷移したりしなくなりました。名前の通りasyncに対応してるからばっちりですね。

ReactiveProperty 3.4.0をリリースしました

ReactiveProperty

www.nuget.org

  • Reactive Extensionsを3.1.0から3.1.1に更新しています。
  • DependencyObject(UWP & WPF)に以下の拡張メソッドを追加しました
    • ObserveDependencyProperty: DependencyPropertyの変更を監視するIObservable<Unit>を返します。
    • ToReadOnlyReactiveProperty: DependencyPropertyからReadOnlyReactivePropertyを生成します。
    • ToReactiveProperty: DependencyPropertyからReactivePropertyを生成します。

Xamarinで処理中を表すインジケーターを出したい

Xamarin

先日Cognitive Serviceを使った笑顔判定機を作りました。

こいつですが、Web上のAPIを呼び出して回線状況に応じては、そこそこ時間がかかるにも関わらず、処理中を示すUIが表示されていませんでした。

Xamarin.Formsでは、ActivityIndicatorクラスを使うことで、簡単にインジケーターを出すことができます。

写真はおっさんの顔になってしまうので省きますが…

使い方は簡単です。画面の全体を覆うようにGridを置いて、そこにメインコンテンツとActivityIndicatorを置くだけです。そして、ActivityIndicatorIsRunningプロパティにTrueかFalseを設定することで表示・非表示を切り替えます。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="SmileXamarinApp.Views.MainPage"
             Title="MainPage">
  <ContentPage.ToolbarItems>
    <ToolbarItem Text="写真"
                 Command="{Binding TakePhotoCommand}" />
  </ContentPage.ToolbarItems>
  <Grid>
    <StackLayout HorizontalOptions="Center" VerticalOptions="Center">
      <Image Source="{Binding ImageSource}" />
    </StackLayout>
    <ActivityIndicator IsRunning="{Binding IsBusy}" />
  </Grid>
</ContentPage>

IsBusyがVMに追加したプロパティでAPI呼び出し中だけTrueになるように制御しておきます。

using Microsoft.ProjectOxford.Face;
using Plugin.Media;
using Prism.Commands;
using Prism.Mvvm;
using Prism.Navigation;
using Prism.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Xamarin.Forms;

namespace SmileXamarinApp.ViewModels
{
    public class MainPageViewModel : BindableBase, INavigationAware
    {
        private ImageSource imageSource;

        public ImageSource ImageSource
        {
            get { return this.imageSource; }
            set { this.SetProperty(ref this.imageSource, value); }
        }

        private IPageDialogService PageDialogService { get; }

        public DelegateCommand TakePhotoCommand { get; }

        private bool isBusy;

        public bool IsBusy
        {
            get { return this.isBusy; }
            set { this.SetProperty(ref this.isBusy, value); }
        }

        public MainPageViewModel(IPageDialogService pageDialogService)
        {
            this.PageDialogService = pageDialogService;
            this.TakePhotoCommand = new DelegateCommand(async() => await this.TakePhotoAsync());
        }

        private async Task TakePhotoAsync()
        {
            var file = await CrossMedia.Current.TakePhotoAsync(new Plugin.Media.Abstractions.StoreCameraMediaOptions
            {
                DefaultCamera = Plugin.Media.Abstractions.CameraDevice.Front,
                AllowCropping = false,
            });
            if (file == null) { return; }

            this.ImageSource = ImageSource.FromStream(() => file.GetStream());

            try
            {
                this.IsBusy = true;
                var client = new FaceServiceClient("Your API Key");
                var result = await client.DetectAsync(file.GetStream(), returnFaceAttributes: new[]
                {
                    FaceAttributeType.Smile,
                });
                if (!result.Any()) { return; }
                await this.PageDialogService.DisplayAlertAsync("Smile point", $"Your smile point is {result.First().FaceAttributes.Smile * 100}", "OK");
            }
            finally
            {
                this.IsBusy = false;
            }

        }

        public void OnNavigatedFrom(NavigationParameters parameters)
        {
        }

        public void OnNavigatedTo(NavigationParameters parameters)
        {
        }
    }
}

これで、処理中なのかなどうなのかな?という疑問を持たなくてすむようになりました。コードは同じくGitHubのリポジトリにあげています。

github.com