第 3 部 XAML マークアップ拡張

XAML マークアップ拡張は、他のソースから間接的に参照されるオブジェクトまたは値にプロパティを設定できるようにする XAML の重要な機能です。 XAML マークアップ拡張は、オブジェクトを共有したり、アプリケーション全体で使用される定数を参照したりするために特に重要ですが、データ バインディングでその最も優れた有用性を発揮します。

XAML マークアップ拡張

一般に、XAML は、オブジェクトのプロパティに明示的な値 (文字列、数値、列挙メンバー、バックグラウンドで値に変換される文字列など) を設定するために使用します。

ただし、プロパティが別の場所で定義されている値を参照する必要がある場合や、ランタイムにコードによる処理が少し必要になる場合があります。 このような目的で、XAML マークアップ拡張機能を使用できます。

これらの XAML マークアップ拡張は XML の拡張機能ではありません。 XAML は完全に有効な XML です。 これらが "拡張" と呼ばれるのは、IMarkupExtension を実装するクラスのコードによってサポートされるためです。 独自のカスタム マークアップ拡張を記述できます。

多くの場合、XAML マークアップ拡張は、中かっこ { と } で区切られた属性設定として表示されるため、XAML ファイルですぐに認識できますが、マークアップでマークアップ拡張が従来の要素として表示される場合があります。

共有リソース

一部の XAML ページには、プロパティが同じ値に設定された複数のビューが含まれています。 たとえば、これらの Button オブジェクトのプロパティ設定の多くは同じです。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XamlSamples.SharedResourcesPage"
             Title="Shared Resources Page">

    <StackLayout>
        <Button Text="Do this!"
                HorizontalOptions="Center"
                VerticalOptions="CenterAndExpand"
                BorderWidth="3"
                Rotation="-15"
                TextColor="Red"
                FontSize="24" />

        <Button Text="Do that!"
                HorizontalOptions="Center"
                VerticalOptions="CenterAndExpand"
                BorderWidth="3"
                Rotation="-15"
                TextColor="Red"
                FontSize="24" />

        <Button Text="Do the other thing!"
                HorizontalOptions="Center"
                VerticalOptions="CenterAndExpand"
                BorderWidth="3"
                Rotation="-15"
                TextColor="Red"
                FontSize="24" />

    </StackLayout>
</ContentPage>

これらのプロパティのいずれかを変更する必要がある場合は、変更を 3 回ではなく 1 回だけ行った方がいいでしょう。 これがコードの場合は、定数と静的な読み取り専用オブジェクトを使用すると、このような値の一貫性を保ち、変更しやすくなる可能性があります。

XAML で一般的な解決策の 1 つは、このような値またはオブジェクトをリソース ディクショナリに格納することです。 VisualElement クラスは、ResourceDictionary 型の Resources という名前のプロパティを定義します。これは、string 型のキーとobject 型の値があるディクショナリです。 このディクショナリにオブジェクトを配置し、マークアップから参照できます。これはすべて XAML で実行できます。

ページでリソース ディクショナリを使用するには、Resources プロパティ要素タグのペアを含めます。 これらはページの上部に配置するのが最も便利です。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XamlSamples.SharedResourcesPage"
             Title="Shared Resources Page">

    <ContentPage.Resources>

    </ContentPage.Resources>
    ...
</ContentPage>

また、ResourceDictionary タグを明示的に含める必要もあります。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XamlSamples.SharedResourcesPage"
             Title="Shared Resources Page">

    <ContentPage.Resources>
        <ResourceDictionary>

        </ResourceDictionary>
    </ContentPage.Resources>
    ...
</ContentPage>

さまざまな種類のオブジェクトと値をリソース ディクショナリに追加できるようになりました。 これらの型はインスタンス化できなくてはなりません。 たとえば、抽象クラスにすることはできません。 この型には、パラメーターのない公開用コンストラクターが必要です。 各項目には、x:Key 属性で指定されたディクショナリ キーが必要です。 次に例を示します。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XamlSamples.SharedResourcesPage"
             Title="Shared Resources Page">

    <ContentPage.Resources>
        <ResourceDictionary>
            <LayoutOptions x:Key="horzOptions"
                           Alignment="Center" />

            <LayoutOptions x:Key="vertOptions"
                           Alignment="Center"
                           Expands="True" />
        </ResourceDictionary>
    </ContentPage.Resources>
    ...
</ContentPage>

これら 2 つの項目は構造体型 LayoutOptions の値であり、それぞれに一意のキーと 1 つまたは 2 つのプロパティが設定されています。 コードとマークアップでは、LayoutOptions の静的フィールドを使用する方がはるかに一般的ですが、ここではプロパティを設定する方が便利です。

次に、これらのボタンの HorizontalOptions および VerticalOptions プロパティをこれらのリソースに設定する必要があります。これは StaticResource XAML マークアップ拡張を使用して行われます。

<Button Text="Do this!"
        HorizontalOptions="{StaticResource horzOptions}"
        VerticalOptions="{StaticResource vertOptions}"
        BorderWidth="3"
        Rotation="-15"
        TextColor="Red"
        FontSize="24" />

StaticResource マークアップ拡張は常に中かっこで区切られ、ディクショナリ キーが含まれます。

名前 StaticResourceDynamicResource と区別されます。これも、Xamarin.Forms でサポートされています。 DynamicResource は、ランタイム中に変更される可能性のある値に関連付けられているディクショナリ キー用です。一方、StaticResource は、ページ上の要素が構築されたときに 1 回だけディクショナリから要素にアクセスします。

BorderWidth プロパティの場合は、ディクショナリに double を格納する必要があります。 XAML は、x:Doublex:Int32 のような一般的なデータ型のタグを簡単に定義します。

<ContentPage.Resources>
    <ResourceDictionary>
        <LayoutOptions x:Key="horzOptions"
                       Alignment="Center" />

        <LayoutOptions x:Key="vertOptions"
                       Alignment="Center"
                       Expands="True" />

        <x:Double x:Key="borderWidth">
            3
        </x:Double>
    </ResourceDictionary>
</ContentPage.Resources>

3 行に配置する必要はありません。 この回転角度に対するこのディクショナリ エントリは、最大でも 1 行のみを占めます。

<ContentPage.Resources>
    <ResourceDictionary>
        <LayoutOptions x:Key="horzOptions"
                       Alignment="Center" />

        <LayoutOptions x:Key="vertOptions"
                       Alignment="Center"
                       Expands="True" />

         <x:Double x:Key="borderWidth">
            3
         </x:Double>

        <x:Double x:Key="rotationAngle">-15</x:Double>
    </ResourceDictionary>
</ContentPage.Resources>

これらの 2 つのリソースは、LayoutOptions 値と同じ方法で参照できます。

<Button Text="Do this!"
        HorizontalOptions="{StaticResource horzOptions}"
        VerticalOptions="{StaticResource vertOptions}"
        BorderWidth="{StaticResource borderWidth}"
        Rotation="{StaticResource rotationAngle}"
        TextColor="Red"
        FontSize="24" />

Color 型のリソースの場合、これらの型の属性を直接割り当てるときに使用するものと同じ文字列表現を使用できます。 型コンバーターは、リソースの作成時に呼び出されます。 Color 型のリソースを次に示します。

<Color x:Key="textColor">Red</Color>

多くの場合、プログラムは、FontSize プロパティを Large などの NamedSize 列挙型のメンバーに設定します。 FontSizeConverter クラスはバックグラウンドで動作し、Device.GetNamedSized メソッドを使用してそれをプラットフォーム依存の値に変換します。 ただし、フォントサイズ リソースを定義する場合は、次に x:Double 型として示すように、数値を使用するほうが理にかなっています。

<x:Double x:Key="fontSize">24</x:Double>

これで、Text を除くすべてのプロパティがリソース設定で定義されました。

<Button Text="Do this!"
        HorizontalOptions="{StaticResource horzOptions}"
        VerticalOptions="{StaticResource vertOptions}"
        BorderWidth="{StaticResource borderWidth}"
        Rotation="{StaticResource rotationAngle}"
        TextColor="{StaticResource textColor}"
        FontSize="{StaticResource fontSize}" />

また、リソース ディクショナリ内の OnPlatform を使用して、プラットフォームのさまざまな値を定義することもできます。 OnPlatform オブジェクトをさまざまなテキストの色のリソース ディクショナリに含める方法を次に示します。

<OnPlatform x:Key="textColor"
            x:TypeArguments="Color">
    <On Platform="iOS" Value="Red" />
    <On Platform="Android" Value="Aqua" />
    <On Platform="UWP" Value="#80FF80" />
</OnPlatform>

OnPlatform は、x:Key 属性 (ディクショナリ内のオブジェクトであるため) と、x:TypeArguments 属性 (ジェネリック クラスであるため) の両方を取得することに注意してください。 iOSAndroidUWP の各属性は、オブジェクトの初期化時に Color 値に変換されます。

6 つの共有値にアクセスする 3 つのボタンを含む最終的な完全な XAML ファイルを次に示します。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XamlSamples.SharedResourcesPage"
             Title="Shared Resources Page">

    <ContentPage.Resources>
        <ResourceDictionary>
            <LayoutOptions x:Key="horzOptions"
                           Alignment="Center" />

            <LayoutOptions x:Key="vertOptions"
                           Alignment="Center"
                           Expands="True" />

            <x:Double x:Key="borderWidth">3</x:Double>

            <x:Double x:Key="rotationAngle">-15</x:Double>

            <OnPlatform x:Key="textColor"
                        x:TypeArguments="Color">
                <On Platform="iOS" Value="Red" />
                <On Platform="Android" Value="Aqua" />
                <On Platform="UWP" Value="#80FF80" />
            </OnPlatform>

            <x:Double x:Key="fontSize">24</x:Double>
        </ResourceDictionary>
    </ContentPage.Resources>

    <StackLayout>
        <Button Text="Do this!"
                HorizontalOptions="{StaticResource horzOptions}"
                VerticalOptions="{StaticResource vertOptions}"
                BorderWidth="{StaticResource borderWidth}"
                Rotation="{StaticResource rotationAngle}"
                TextColor="{StaticResource textColor}"
                FontSize="{StaticResource fontSize}" />

        <Button Text="Do that!"
                HorizontalOptions="{StaticResource horzOptions}"
                VerticalOptions="{StaticResource vertOptions}"
                BorderWidth="{StaticResource borderWidth}"
                Rotation="{StaticResource rotationAngle}"
                TextColor="{StaticResource textColor}"
                FontSize="{StaticResource fontSize}" />

        <Button Text="Do the other thing!"
                HorizontalOptions="{StaticResource horzOptions}"
                VerticalOptions="{StaticResource vertOptions}"
                BorderWidth="{StaticResource borderWidth}"
                Rotation="{StaticResource rotationAngle}"
                TextColor="{StaticResource textColor}"
                FontSize="{StaticResource fontSize}" />

    </StackLayout>
</ContentPage>

スクリーンショットでは、一貫性のあるスタイル設定と、プラットフォームに依存するスタイル設定が確認されます。

スタイル付きコントロール

ページの上部に Resources のコレクションを定義するのが最も一般的ですが、Resources プロパティが VisualElement によって定義されていることに注意してください。また、ページ上の他の要素に Resources のコレクションを含めることができます。 たとえば、次の例で StackLayout に 1 つ追加してみます。

<StackLayout>
    <StackLayout.Resources>
        <ResourceDictionary>
            <Color x:Key="textColor">Blue</Color>
        </ResourceDictionary>
    </StackLayout.Resources>
    ...
</StackLayout>

ボタンのテキストの色が青になったことがわかります。 基本的に、XAML パーサーは、StaticResource マークアップ拡張を検出するたびに、ビジュアル ツリーを検索し、そのキーを含む最初に検出された ResourceDictionary を使用します。

リソース ディクショナリに格納されるオブジェクトの最も一般的な種類の 1 つは Xamarin.FormsStyle で、これはプロパティ設定のコレクションを定義します。 スタイルについては、スタイルに関する記事で説明します。

XAML を初めて使用する開発者は、LabelButton などのビジュアル要素を ResourceDictionary に配置できるだろうかと思う場合があります。 確かに可能ではありますが、あまり意味はありません。 ResourceDictionary の目的は、オブジェクトを共有することです。 ビジュアル要素は共有できません。 同じインスタンスを 1 つのページに 2 回表示することはできません。

x:Static マークアップ拡張

名前は似ていますが、x:StaticStaticResource は大きく異なります。 StaticResource はリソース ディクショナリからオブジェクトを返しますが、x:Static は次のいずれかにアクセスします。

  • パブリック静的フィールド
  • パブリック静的プロパティ
  • パブリック定数フィールド
  • 列挙メンバー。

StaticResourceマークアップ拡張は、リソース ディクショナリを定義する XAML 実装でサポートされていますが、x:Staticx プレフィックスから明らかなように XAML の組み込み部分です。

次に、x:Static が静的フィールドと列挙型メンバーを明示的に参照する方法を示す例をいくつか示します。

<Label Text="Hello, XAML!"
       VerticalOptions="{x:Static LayoutOptions.Start}"
       HorizontalTextAlignment="{x:Static TextAlignment.Center}"
       TextColor="{x:Static Color.Aqua}" />

ここまでは、関心する要素はあまりありません。 ただし、x:Static マークアップ拡張では、独自のコードから静的フィールドまたはプロパティを参照することもできます。 たとえば、アプリケーション全体で複数のページで使用する可能性のあるいくつかの静的フィールドが含まれる AppConstants クラスを次に示します。

using System;
using Xamarin.Forms;

namespace XamlSamples
{
    static class AppConstants
    {
        public static readonly Thickness PagePadding;

        public static readonly Font TitleFont;

        public static readonly Color BackgroundColor = Color.Aqua;

        public static readonly Color ForegroundColor = Color.Brown;

        static AppConstants()
        {
            switch (Device.RuntimePlatform)
            {
                case Device.iOS:
                    PagePadding = new Thickness(5, 20, 5, 0);
                    TitleFont = Font.SystemFontOfSize(35, FontAttributes.Bold);
                    break;

                case Device.Android:
                    PagePadding = new Thickness(5, 0, 5, 0);
                    TitleFont = Font.SystemFontOfSize(40, FontAttributes.Bold);
                    break;

                case Device.UWP:
                    PagePadding = new Thickness(5, 0, 5, 0);
                    TitleFont = Font.SystemFontOfSize(50, FontAttributes.Bold);
                    break;
            }
        }
    }
}

XAML ファイルでこのクラスの静的フィールドを参照するには、XAML ファイル内で、このファイルが配置されている場所をなんらかの方法で指定する必要があります。 これを行うには、XML 名前空間を宣言します。

標準の Xamarin.Forms XAML テンプレートの一部として作成された XAML ファイルには、2 つの XML 名前空間の宣言が含まれていることを思い出してください。1 つは Xamarin.Forms クラスにアクセスするための宣言で、もう 1 つは XAML に組み込まれているタグと属性を参照するための宣言です。

xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"

他のクラスにアクセスするには、XML 名前空間を追加で宣言する必要があります。 追加の XML 名前空間宣言ごとに、新しいプレフィックスが定義されます。 AppConstants などの共有アプリケーション .NET Standard ライブラリにローカルなクラスにアクセスするために、XAML プログラマは多くの場合、プレフィックス local を使用します。 名前空間宣言は、CLR (共通言語ランタイム) 名前空間名 (.NET 名前空間名とも呼ばれます) を示す必要があります。これは、C# namespace 定義または using ディレクティブに表示される名前です。

xmlns:local="clr-namespace:XamlSamples"

.NET Standard ライブラリが参照する任意のアセンブリで、.NET 名前空間の XML 名前空間宣言を定義することもできます。 たとえば、次に示すのは標準の .NET System 名前空間の sys プレフィックスです。これは、netstandard アセンブリにあります。 これは別のアセンブリであるため、アセンブリ名 (この場合 netstandard) も指定する必要があります。

xmlns:sys="clr-namespace:System;assembly=netstandard"

キーワード clr-namespace の後には、コロン、.NET 名前空間名、セミコロン、キーワード assembly、等号、アセンブリ名が続きます。

clr-namespace の後にコロンが続き、assembly の後には等号が続きます。 構文は意図的にこのように定義されています。ほとんどの XML 名前空間宣言は、http などの URI スキーム名を開始する URI を参照します。これには、常にコロンが続きます。 この文字列の clr-namespace の部分は、その規則を模倣することを目的としています。

これらの名前空間宣言はどちらも、StaticConstantsPage サンプルに含まれています。 BoxView ディメンションは Math.PIMath.E に設定されていますが、係数 100 でスケーリングされます。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XamlSamples"
             xmlns:sys="clr-namespace:System;assembly=netstandard"
             x:Class="XamlSamples.StaticConstantsPage"
             Title="Static Constants Page"
             Padding="{x:Static local:AppConstants.PagePadding}">

    <StackLayout>
       <Label Text="Hello, XAML!"
              TextColor="{x:Static local:AppConstants.BackgroundColor}"
              BackgroundColor="{x:Static local:AppConstants.ForegroundColor}"
              Font="{x:Static local:AppConstants.TitleFont}"
              HorizontalOptions="Center" />

      <BoxView WidthRequest="{x:Static sys:Math.PI}"
               HeightRequest="{x:Static sys:Math.E}"
               Color="{x:Static local:AppConstants.ForegroundColor}"
               HorizontalOptions="Center"
               VerticalOptions="CenterAndExpand"
               Scale="100" />
    </StackLayout>
</ContentPage>

画面を基準にした結果の BoxView のサイズはプラットフォームによって異なります。

x:Static マークアップ拡張を使用するコントロール

その他の標準マークアップ拡張

いくつかのマークアップ拡張は XAML に組み込まれており、Xamarin.Forms XAML ファイルでサポートされています。 これらの一部はあまり使用されませんが、必要な場合は不可欠です。

  • プロパティに既定で null 以外の値があり、それを null に設定する場合は、{x:Null} マークアップ拡張に設定します。
  • プロパティが Type 型の場合は、マークアップ拡張 {x:Type someClass} を使用して Type オブジェクトに割り当てることができます。
  • x:Array マークアップ拡張を使用して、XAML で配列を定義できます。 このマークアップ拡張には、配列内の要素の型を示す、Type という名前の必須の属性があります。
  • Binding マークアップ拡張については、「パート 4. データ バインディングの基礎」で説明します。
  • RelativeSource マークアップ拡張については、相対バインディングに関するページで説明します。

ConstraintExpression マークアップ拡張

マークアップ拡張はプロパティを持つことができますが、XML 属性のように設定されません。 マークアップ拡張では、プロパティ設定はコンマで区切られ、中かっこ内に引用符は表示されません。

これは、RelativeLayout クラスで使用される ConstraintExpression という名前の Xamarin.Forms マークアップ拡張を使用して示すことができます。 子ビューの場所またはサイズは、定数として指定することも、親ビューやその他の名前付きビューを基準にして指定することもできます。 ConstraintExpression の構文を使用すると、別のビューのプロパティを Factor 倍にし、Constant を追加して、ビューの位置またはサイズを設定できます。 それ以上に複雑なものにはコードが必要です。

次に例を示します。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XamlSamples.RelativeLayoutPage"
             Title="RelativeLayout Page">

    <RelativeLayout>

        <!-- Upper left -->
        <BoxView Color="Red"
                 RelativeLayout.XConstraint=
                     "{ConstraintExpression Type=Constant,
                                            Constant=0}"
                 RelativeLayout.YConstraint=
                     "{ConstraintExpression Type=Constant,
                                            Constant=0}" />
        <!-- Upper right -->
        <BoxView Color="Green"
                 RelativeLayout.XConstraint=
                     "{ConstraintExpression Type=RelativeToParent,
                                            Property=Width,
                                            Factor=1,
                                            Constant=-40}"
                 RelativeLayout.YConstraint=
                     "{ConstraintExpression Type=Constant,
                                            Constant=0}" />
        <!-- Lower left -->
        <BoxView Color="Blue"
                 RelativeLayout.XConstraint=
                     "{ConstraintExpression Type=Constant,
                                            Constant=0}"
                 RelativeLayout.YConstraint=
                     "{ConstraintExpression Type=RelativeToParent,
                                            Property=Height,
                                            Factor=1,
                                            Constant=-40}" />
        <!-- Lower right -->
        <BoxView Color="Yellow"
                 RelativeLayout.XConstraint=
                     "{ConstraintExpression Type=RelativeToParent,
                                            Property=Width,
                                            Factor=1,
                                            Constant=-40}"
                 RelativeLayout.YConstraint=
                     "{ConstraintExpression Type=RelativeToParent,
                                            Property=Height,
                                            Factor=1,
                                            Constant=-40}" />

        <!-- Centered and 1/3 width and height of parent -->
        <BoxView x:Name="oneThird"
                 Color="Red"
                 RelativeLayout.XConstraint=
                     "{ConstraintExpression Type=RelativeToParent,
                                            Property=Width,
                                            Factor=0.33}"
                 RelativeLayout.YConstraint=
                     "{ConstraintExpression Type=RelativeToParent,
                                            Property=Height,
                                            Factor=0.33}"
                 RelativeLayout.WidthConstraint=
                     "{ConstraintExpression Type=RelativeToParent,
                                            Property=Width,
                                            Factor=0.33}"
                 RelativeLayout.HeightConstraint=
                     "{ConstraintExpression Type=RelativeToParent,
                                            Property=Height,
                                            Factor=0.33}"  />

        <!-- 1/3 width and height of previous -->
        <BoxView Color="Blue"
                 RelativeLayout.XConstraint=
                     "{ConstraintExpression Type=RelativeToView,
                                            ElementName=oneThird,
                                            Property=X}"
                 RelativeLayout.YConstraint=
                     "{ConstraintExpression Type=RelativeToView,
                                            ElementName=oneThird,
                                            Property=Y}"
                 RelativeLayout.WidthConstraint=
                     "{ConstraintExpression Type=RelativeToView,
                                            ElementName=oneThird,
                                            Property=Width,
                                            Factor=0.33}"
                 RelativeLayout.HeightConstraint=
                     "{ConstraintExpression Type=RelativeToView,
                                            ElementName=oneThird,
                                            Property=Height,
                                            Factor=0.33}"  />
    </RelativeLayout>
</ContentPage>

おそらく、このサンプルから学べる最も重要なレッスンは、マークアップ拡張の構文です。マークアップ拡張の中かっこ内に引用符を含める必要はありません。 XAML ファイルにマークアップ拡張を入力するときに、プロパティの値を引用符で囲みたくなるのは自然なことです。 その誘惑に抵抗しましょう。

実行されているプログラムを次に示します。

制約を使用した相対レイアウト

まとめ

ここに示す XAML マークアップ拡張は、XAML ファイルに重要なサポートを提供します。 しかし、おそらく最も価値のある XAML マークアップ拡張は Binding です。これについては、このシリーズの次のパートである「パート 4. データ バインディングの基礎」で説明します。