概要

Xamarin.Formsを使ってアプリを開発するとき、UI作成にXAMLを使用する方は多いのではないでしょうか?僕もその1人です。

僕は開発時XAMLについて調べるとき、XAMLを「どのように」使うかについて触れた記事を読んできましたが、XAMLが「なぜ」そのように使えるのか、どういう仕組みで動いているのかについてはあまり考えてきませんでした。

そこでこの記事ではXAMLの「なぜ」や「基礎」の入り口になるような話を書きたいと思っています。基礎を徹底的に掘り下げるものでも、網羅するものでも、ありません。

付録として最後に「どのように」使うかが説明されたXamarin公式ページeXtensible Application Markup Language (XAML)の全訳を載せます。とても長いですが、「基本」事項(使い方を)概観するにはぴったりの記事です。

XAMLとは?

まず、XAMLとは何なのでしょうか?XamarinによるXamarin.Formsの公式ページでは次のように説明されています。

eXtensible Application Markup Language (XAML)

XAML is a declarative markup language that can be used to define user interfaces. The user interface is defined in an XML file using the XAML syntax, while runtime behavior is defined in a separate code-behind file.

(訳)XAMLはユーザインターフェースを定義する宣言的なマークアップ言語です。ユーザインターフェースはXAML構文を用いてXMLファイル内で定義され、実行時のふるまいは別のコードビハインドファイル内で定義されます。

また、以前参加した勉強会等では次のようにも説明されていました。

XAML入門 13 / 38 by Kazuki Ota さん

UI記述特化言語ではない オブジェクトのインスタンスを組み立てるための言語

XAML&Application Platform ~これまでとこれから~ 9 / 56 by Shinobu Takahashi さん

UIを定義するために特化した言語ではなく オブジェクトのインスタンスとオブジェクト間の階層構造を構築する定義型言語

XAMLで定義した内容はUIとして活用されるけれど、その本質は「オブジェクトのインスタンスの構築」にあるということでしょうか。

XAMLをコードで表現する

XAMLではこんな風に書きます。でも、これと同等の内容はC#のコードで表現できます。という説明を行う本を見たことがあります。例えばこのようなものです

XamlSamplePage.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:local="clr-namespace:XamlSample"
  x:Class="XamlSample.XamlSamplePage">
    <Label Text="Welcome to Xamarin Forms!" VerticalOptions="Center" HorizontalOptions="Center" />
</ContentPage>
XamlSamplePage.xaml.cs
using Xamarin.Forms;

namespace XamlSample
{
    public partial class XamlSamplePage : ContentPage
    {
        public XamlSamplePage()
        {
            InitializeComponent();
        }
    }
}

の組み合わせと、単独の

XamlSamplePage.xaml.cs
using Xamarin.Forms;

namespace XamlSample
{
    public partial class XamlSamplePage : ContentPage
    {
        public XamlSamplePage()
        {
            Content = new Label
            {
                Text = "Welcome to Xamarin Forms!",
                VerticalOptions = LayoutOptions.Center,
                HorizontalOptions = LayoutOptions.Center
            };
        }
    }
}

は実行したときに表示されるのはいずれも次のスクリーンショットのようになります。

Welcome to Xamarin Forms

ここで、もしかしたらXAML定義からC#コードに変換しているのではないか?InitializeComponent()が大事な機能を果たしているのではないか?という考えが頭をよぎります。

どのタイミングで、なにが、どうやってXAML定義を使用するのでしょうか?

XAMLG

ここで登場するのがXAMLGタスクです。

https://github.com/xamarin/Xamarin.Forms/blob/master/Xamarin.Forms.Build.Tasks/XamlGTask.cs

Xamarin.Formsの新規プロジェクトを作成し、上記のXAMLとコードビハインドを準備してビルドしてみましょう。

XamlSample/obj/Debug内に生成された成果物を探すと次のようなファイルを見つけることができます。

XamlSample.XamlSamplePage.xaml.g.cs
//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Runtime Version:4.0.30319.42000
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

namespace XamlSample {


    [global::Xamarin.Forms.Xaml.XamlFilePathAttribute("/Users/sugita-toshinori/dev/codes/xamarin/XamlSample/XamlSample/XamlSamplePage.xa" +
        "ml")]
    public partial class XamlSamplePage : global::Xamarin.Forms.ContentPage {

        [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Forms.Build.Tasks.XamlG", "0.0.0.0")]
        private void InitializeComponent() {
            global::Xamarin.Forms.Xaml.Extensions.LoadFromXaml(this, typeof(XamlSamplePage));
        }
    }
}

InitializeComponentメソッドが定義されており、LoadFromXamlが何かを行なってくれそうです。

これはどこに定義されていて、何を行なっているのでしょうか?

メソッドの詳細を追う前に、このメソッドが呼ばれるまでの大まかな流れを確認しましょう。

先ほども言及したXamarin公式ページでは次のように処理フローが説明されています。

At runtime, code in the particular platform project calls a LoadApplication method, passing to it a new instance of the App class in the PCL. The App class constructor instantiates XamlSamplesPage. The constructor of that class calls InitializeComponent, which then calls the LoadFromXaml method that extracts the XAML file (or its compiled binary) from the PCL. LoadFromXaml initializes all the objects defined in the XAML file, connects them all together in parent-child relationships, attaches event handlers defined in code to events set in the XAML file, and sets the resultant tree of objects as the content of the page.

(訳)実行時に、特定のプラットフォームプロジェクトのコードがLoadApplicationメソッドを呼び出し、PCLのAppクラスの新しいインスタンスをそのクラスに渡します。Appクラスのコンストラクタは、XamlSamplesPageをインスタンス化します。そのクラスのコンストラクタはInitializeComponentを呼び出し、次に、XAMLファイル(またはそのコンパイルされたバイナリ)をPCLから抽出するLoadFromXamlメソッドを呼び出します。LoadFromXamlは、XAMLファイルで定義されているすべてのオブジェクトを初期化し、それらをすべて親子関係で接続し、コードで定義されているイベントハンドラをXAMLファイルに設定されたイベントに関連付け、オブジェクトのツリーをページのコンテンツとして設定します。

これまで追ってきたように、ページをインスタンス化する際、コンストラクタでInitializeComponentを呼び出し、LoadFromXamlXAMLで定義されたすべてのオブジェクトを初期化するというのは仮説からも離れていなさそうです。

それでは、LoadFromXamlの定義を探してみましょう。

ここにありました。

https://github.com/xamarin/Xamarin.Forms/blob/master/Xamarin.Forms.Xaml/ViewExtensions.cs

ViewExtensions.cs
using System;

namespace Xamarin.Forms.Xaml
{
    public static class Extensions
    {
        public static TXaml LoadFromXaml<TXaml>(this TXaml view, Type callingType)
        {
            XamlLoader.Load(view, callingType);
            return view;
        }

        internal static TXaml LoadFromXaml<TXaml>(this TXaml view, string xaml)
        {
            XamlLoader.Load(view, xaml);
            return view;
        }
    }
}

XamlLoader.Loadで処理を行なっていそうです。また探してみましょう。

https://github.com/xamarin/Xamarin.Forms/blob/master/Xamarin.Forms.Xaml/XamlLoader.cs

XamlLoader.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Xml;
using Xamarin.Forms.Internals;

// (略)

namespace Xamarin.Forms.Xaml
{
    static class XamlLoader
    {
        public static void Load(object view, Type callingType)
        {
            var xaml = GetXamlForType(callingType);
            if (string.IsNullOrEmpty(xaml))
                throw new XamlParseException(string.Format("No embeddedresource found for {0}", callingType), new XmlLineInfo());
            Load(view, xaml);
        }

        public static void Load(object view, string xaml)
        {
            using (var textReader = new StringReader(xaml))
            using (var reader = XmlReader.Create(textReader))
            {
                while (reader.Read())
                {
                    //Skip until element
                    if (reader.NodeType == XmlNodeType.Whitespace)
                        continue;
                    if (reader.NodeType == XmlNodeType.XmlDeclaration)
                        continue;
                    if (reader.NodeType != XmlNodeType.Element) {
                        Debug.WriteLine("Unhandled node {0} {1} {2}", reader.NodeType, reader.Name, reader.Value);
                        continue;
                    }

                    var rootnode = new RuntimeRootNode (new XmlType (reader.NamespaceURI, reader.Name, null), view, (IXmlNamespaceResolver)reader);
                    XamlParser.ParseXaml (rootnode, reader);
                    Visit (rootnode, new HydrationContext {
                        RootElement = view,
#pragma warning disable 0618
                        ExceptionHandler = ResourceLoader.ExceptionHandler ?? (Internals.XamlLoader.DoNotThrowOnExceptions ? e => { }: (Action<Exception>)null)
#pragma warning restore 0618
                    });
                    break;
                }
            }
        }

        // (略)
    }

XmlReaderでXMLの各ノードをWhileでループしていく際にXamlParser.ParseXaml (rootnode, reader)しているのが気になります。

引数のrootnodeには1行手前でview(ここでの実体はXamlSamplePageのインスタンス)が渡されているので、XAMLの構造をViewのツリー構造にマッピングしているのではないかと想像できます。

https://github.com/xamarin/Xamarin.Forms/blob/master/Xamarin.Forms.Xaml/XamlParser.cs

XamlParser.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Xml;
using Xamarin.Forms.Internals;

namespace Xamarin.Forms.Xaml
{
    static class XamlParser
    {
        public const string XFUri = "http://xamarin.com/schemas/2014/forms";
        public const string X2006Uri = "http://schemas.microsoft.com/winfx/2006/xaml";
        public const string X2009Uri = "http://schemas.microsoft.com/winfx/2009/xaml";
        public const string McUri = "http://schemas.openxmlformats.org/markup-compatibility/2006";

        public static void ParseXaml(RootNode rootNode, XmlReader reader)
        {
            IList<KeyValuePair<string, string>> xmlns;
            var attributes = ParseXamlAttributes(reader, out xmlns);
            var prefixes = PrefixesToIgnore(xmlns);
            (rootNode.IgnorablePrefixes ?? (rootNode.IgnorablePrefixes=new List<string>())).AddRange(prefixes);
            rootNode.Properties.AddRange(attributes);
            ParseXamlElementFor(rootNode, reader);
        }

        static void ParseXamlElementFor(IElementNode node, XmlReader reader)
        {
            Debug.Assert(reader.NodeType == XmlNodeType.Element);

            var elementName = reader.Name;
            var isEmpty = reader.IsEmptyElement;

            if (isEmpty)
                return;

            while (reader.Read())
            {
                switch (reader.NodeType)
                {
                    case XmlNodeType.EndElement:
                        Debug.Assert(reader.Name == elementName); //make sure we close the right element
                        return;
                    case XmlNodeType.Element:
                        // 1. Property Element.
                        if (reader.Name.Contains("."))
                        {
                            XmlName name;
                            if (reader.Name.StartsWith(elementName + ".", StringComparison.Ordinal))
                                name = new XmlName(reader.NamespaceURI, reader.Name.Substring(elementName.Length + 1));
                            else //Attached DP
                                name = new XmlName(reader.NamespaceURI, reader.LocalName);

                            var prop = ReadNode(reader);
                            if (prop != null)
                                node.Properties.Add(name, prop);
                        }
                        // 2. Xaml2009 primitives, x:Arguments, ...
                        else if (reader.NamespaceURI == X2009Uri && reader.LocalName == "Arguments")
                        {
                            var prop = ReadNode(reader);
                            if (prop != null)
                                node.Properties.Add(XmlName.xArguments, prop);
                        }
                        // 3. DataTemplate (should be handled by 4.)
                        else if (node.XmlType.NamespaceUri == XFUri &&
                                 (node.XmlType.Name == "DataTemplate" || node.XmlType.Name == "ControlTemplate"))
                        {
                            var prop = ReadNode(reader, true);
                            if (prop != null)
                                node.Properties.Add(XmlName._CreateContent, prop);
                        }
                        // 4. Implicit content, implicit collection, or collection syntax. Add to CollectionItems, resolve case later.
                        else
                        {
                            var item = ReadNode(reader, true);
                            if (item != null)
                                node.CollectionItems.Add(item);
                        }
                        break;
                    case XmlNodeType.Whitespace:
                        break;
                    case XmlNodeType.Text:
                    case XmlNodeType.CDATA:
                        if (node.CollectionItems.Count == 1 && node.CollectionItems[0] is ValueNode)
                            ((ValueNode)node.CollectionItems[0]).Value += reader.Value.Trim();
                        else
                            node.CollectionItems.Add(new ValueNode(reader.Value.Trim(), (IXmlNamespaceResolver)reader));
                        break;
                    default:
                        Debug.WriteLine("Unhandled node {0} {1} {2}", reader.NodeType, reader.Name, reader.Value);
                        break;
                }
            }
        }

        // (省略)
    }
}

ParseXamlElementForNodeType毎にnode(rootNode)への追加の仕方が振り分けられています。

このXamlParser.ParseXamlで整理された要素は、先ほど見たXamlLoader.Load内の次のVisitメソッドでクラスとしてviewにセットされています。(急に雑)

答え合わせという訳でもありませんが、以下のUnit Testを見ることで、LoadFromXamlへの入力と関数を実行した結果へのイメージがより具体的になると思います。

こちらのテストなんていかがでしょうか?

インスタンス化したContentPageにXamlを渡してUIを組み立てています。

ブレークポイントを貼ってデバッグ実行すると、過程がより追いやすいです。

https://github.com/xamarin/Xamarin.Forms/blob/master/Xamarin.Forms.Xaml.UnitTests/LoaderTests.cs

LoaderTests.cs
 // (省略)
[Test]
        public void TestBindingModeAndConverter ()
        {
            var xaml = @"
                <ContentPage 
                xmlns=""http://xamarin.com/schemas/2014/forms""
                xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml""
                xmlns:local=""clr-namespace:Xamarin.Forms.Xaml.UnitTests;assembly=Xamarin.Forms.Xaml.UnitTests"">
                    <ContentPage.Resources>
                        <ResourceDictionary>
                            <local:ReverseConverter x:Key=""reverseConverter""/>
                        </ResourceDictionary>
                    </ContentPage.Resources>
                    <ContentPage.Content>
                        <StackLayout Orientation=""Vertical"">
                            <StackLayout.Children>
                                <Label x:Name=""label0"" Text=""{Binding Text, Converter={StaticResource reverseConverter}}""/>
                                <Label x:Name=""label1"" Text=""{Binding Text, Mode=TwoWay}""/>
                            </StackLayout.Children>
                        </StackLayout>
                    </ContentPage.Content>
                </ContentPage>";

            var contentPage = new ContentPage ();
            contentPage.LoadFromXaml (xaml);
            contentPage.BindingContext = new ViewModel { Text = "foobar" };
            var label0 = contentPage.FindByName<Label> ("label0");
            var label1 = contentPage.FindByName<Label> ("label1");
            Assert.AreEqual ("raboof", label0.Text);

            label1.Text = "baz";
            Assert.AreEqual ("baz", ((ViewModel)(contentPage.BindingContext)).Text);
        }
// (省略)

こちらはObsoleteなメソッド向けですね。
https://github.com/xamarin/Xamarin.Forms/blob/master/Xamarin.Forms.Xaml.UnitTests/XamlLoaderCreateTests.cs

XamlLoaderCreateTests.cs
 // (省略)
[TestFixture]
    public class XamlLoaderCreateTests
    {
        [Test]
        public void CreateFromXaml ()
        {
            var xaml = @"
                <ContentView xmlns=""http://xamarin.com/schemas/2014/forms""
                             xmlns:x=""http://schemas.microsoft.com/winfx/2009/xaml""
                             x:Class=""Xamarin.Forms.Xaml.UnitTests.FOO"">
                    <Label Text=""Foo""  x:Name=""label""/>
                </ContentView>";

            var view = XamlLoader.Create (xaml);
            Assert.That (view, Is.TypeOf<ContentView> ());
            Assert.AreEqual ("Foo", ((Label)((ContentView)view).Content).Text);
        }
// (省略)

一度このように追っておくと、InitializeComponentが見つからないようなエラーのときはInitializeComponent定義が想定通りの名前空間に生成されているか、InitializeComponentの前にXAMLで定義したコントロールにアクセスしようとしても無理などこれまで遭遇したかもしれないエラーを避けることができます。

また「使い方」として説明されていたことが実装としてどう実現されているのかがだいぶしっくりくるかもしれません。

付録は次のような「使い方」を説明する記事です。

  • XAMLの構文
  • マークアップ拡張
  • データバインディング
  • MVVM触り
  • XAMLコンパイル
  • XAML名前空間
  • バインダブルプロパティ
  • 添付プロパティ
  • リソースディクショナリー
  • XAML Standard

Xamarin.Formsリポジトリを登場するキーワードを検索するともちろん引っかかるので興味がある方は追ってみてください。

XAMLC

なんで実行時にXAMLを読み込むんだろう?先にコンパイルしておけばより高速にUIを表示できるのではないか?と考える人もいるかもしれません。

そこで XAMLCの登場です。

公式ページでは次のように説明されています。

XAML can be optionally compiled directly into intermediate language (IL) with the XAML compiler (XAMLC).

XAMLC offers a number of a benefits:

It performs compile-time checking of XAML, notifying the user of any errors.
It removes some of the load and instantiation time for XAML elements.
It helps to reduce the file size of the final assembly by no longer including .xaml files.
XAMLC is disabled by default to ensure backwards compatibility. It can be enabled at both the assembly and class level by adding the XamlCompilation attribute.

(訳)XAMLは、XAMLコンパイラ(XAMLC)で中間言語(IL)に直接コンパイルすることもできます。

XAMLCには多くの利点があります。

  • XAMLのコンパイル時チェックを実行し、ユーザーにエラーを通知します。
  • XAML要素のロードおよびインスタンス化時間の一部を削減します。
  • .xamlファイルを含まなくなることで、最終アセンブリのファイルサイズを縮小するのに役立ちます。

下位互換性を確保するため、XAMLCはデフォルトで無効になっています。XamlCompilation属性を追加することで、アセンブリレベルとクラスレベルの両方で有効にすることができます。

続きも翻訳しているので、気になる方はチェックしてみてください。

プレビュー版としてこの機能がリリースされたときに書かれたQiita記事では実行時にXAMLを読み込みUIを構築するケースとC#コードだけでUIを構築するケースとの比較記事もあり興味深いです。

[Xamarin.Forms 1.5.1-pre1] XamlC (XAMLプリコンパイル)

タスク本体はこのあたりを追ってください。
https://github.com/xamarin/Xamarin.Forms/blob/master/Xamarin.Forms.Build.Tasks/XamlCTask.cs

その他

XAMLの基礎を追っていくと、いつかは自作のプレビューアなどに行き着くのではないか…という期待を込めてこの記事のリンクを貼っておきます。

Xamarin.Forms 用のプレビューアをアルファ版で公開

感想

色々遠回り(次に紹介する日本語訳とか…)した割に、自分が知りたかったことの入り口は

  • コンパイルしたときにできる成果物を見る
  • フレームワークやビルドタスクの関連しそうなソースコードを追う

をやるだけだったのが教訓的だったなぁと思いました。コード追いきれてないところはありつつ…。

動作の原理を追いたいときに必ずしもコード読めば自明ということばかりではないと思います。

でも人が説明する文章追ってばかりいないで、もう少し目の前で動いているコードを動かして仮説を作ったり、直接的に答えにたどりつくためのアプローチとっていかないと効率悪いなぁと書きながら反省しっぱなしでした。

付録: Xamarin公式XAMLドキュメントの全訳

eXtensible Application Markup Language (XAML)

XAMLはユーザインターフェースを定義する宣言的なマークアップ言語です。ユーザインターフェースはXAML構文を用いてXMLファイル内で定義され、実行時のふるまいは別のコードビハインドファイル内で定義されます。

タイトル
(アンカーリンク) 
元ページURL 概要
XAMLの基本   https://developer.xamarin.com/guides/xamarin-forms/xaml/xaml-basics/ XAMLを使用すると、開発者はコードではなくマークアップを使用してXamarin.Formsアプリケーションでユーザインターフェースを定義できます。XAMLはXamarin.Formsプログラムでは必須ではなく、ツールのように使用できます。視覚的に一貫性があり、同等のコードよりも簡潔であることがよくあります。XAMLは、一般的なModel-View-ViewModel(MVVM)アプリケーションアーキテクチャでの使用に特に適しています。XAMLは、XAMLベースのデータバインディングを介してViewModelコードにリンクされたビューを定義します。
XAMLコンパイル       https://developer.xamarin.com/guides/xamarin-forms/xaml/xamlc/ XAMLは、XAMLコンパイラ(XAMLC)で中間言語(IL)に直接コンパイルすることもできます。この記事では、XAMLCの使用方法と利点について説明します。
XAMLプレビューア https://developer.xamarin.com/guides/xamarin-forms/xaml/xaml-previewer/ Xamarin Evolve 2016で発表されたXAMLプレビューアは、Alphaチャンネルでのテストに使用できます。
XAML名前空間  https://developer.xamarin.com/guides/xamarin-forms/xaml/namespaces/ XAMLは、名前空間宣言にxmlns XML属性を使用します。この記事では、XAML名前空間の構文を紹介し、型にアクセスするためにXAML名前空間を宣言する方法を示します。
引数の受け渡し https://developer.xamarin.com/guides/xamarin-forms/xaml/passing-arguments/ XAMLを使用すると、デフォルトでないコンストラクタやファクトリメソッドに引数を渡すことができます。この記事では、コンストラクタに引数を渡したり、ファクトリメソッドを呼び出したり、ジェネリックな引数の型を指定するために使用できるXAML属性の使用方法について説明します。
バインダブルプロパティ https://developer.xamarin.com/guides/xamarin-forms/xaml/bindable-properties/ Xamarin.Formsでは、共通言語ランタイム(CLR)プロパティの機能がバインダブルプロパティによって拡張されています。バインダブルプロパティは特殊なタイプのプロパティです。プロパティの値はXamarin.Formsプロパティシステムによって追跡されます。この記事では、バインダブルプロパティの概要を紹介し、バインダブルプロパティを作成および使用する方法を示します。
添付プロパティ https://developer.xamarin.com/guides/xamarin-forms/xaml/attached-properties/ 添付プロパティはバインダブルプロパティの特殊な型であり、1つのクラスで定義されていますが他のオブジェクトに関連付けられています。XAMLでは、クラスとプロパティ名がピリオドで区切られた属性として認識されます。この記事では、添付プロパティの概要を説明し、それらを作成および使用する方法を示します。
リソースディクショナリー https://developer.xamarin.com/guides/xamarin-forms/xaml/resource-dictionaries/ XAMLリソースは、複数回使用できるオブジェクトの定義です。リソースディクショナリーを使用すると、リソースを単一の場所に定義し、Xamarin.Formsアプリケーション全体で再利用することができます。この資料では、リソースディクショナリーを作成および使用する方法と、リソースディクショナリーを別のリソースディクショナリーにマージする方法について説明します。

Xamarin.FormsのXAMLの基本

モバイルデバイス向けのクロスプラットフォームマークアップを始める

XAML(eXtensible Application Markup Language)を使用すると開発者はコードではなくマークアップを使用して、Xamarin.Formsアプリケーションでユーザインターフェースを定義できます。XAMLはXamarin.Formsプログラムでは必須ではなく、ツールのように使用できます。視覚的に一貫性があり、同等のコードよりも簡潔であることがよくあります。XAMLは、一般的なModel-View-ViewModel(MVVM)アプリケーションアーキテクチャでの使用に特に適しています。XAMLは、XAMLベースのデータバインディングを介してViewModelコードにリンクされたビューを定義します。

XAMLの基本のコンテンツ

XAMLの基本記事に加え、以下の本の各章をダウンロードできます。
Creating Mobile Apps with Xamarin.Forms Book

  • 第7章 XAMLとコード
  • 第8章 コードとXAMLの調和
  • 第10章 XAMLマークアップ拡張
  • 第18章 MVVM

概要

XAMLは、オブジェクトのインスタンス化と初期化、および親子階層内でのオブジェクトの整理のためのプログラミングコードの代替としてMicrosoftによって開発されたXMLベースの言語です。XAMLは.NETフレームワーク内のいくつかのテクノロジに採用されていますが、とりわけWindows Presentation Foundation(WPF)、Silverlight、WindowsRuntime、ユニバーサルWindowsプラットフォーム(UWP)内のユーザインターフェースのレイアウトを定義する上で大きな役割を果たしています 。

XAMLは、iOS、Android、およびUWPモバイルデバイス用のクロスプラットフォームネイティブベースのプログラミングインターフェースであるXamarin.Formsの一部です。Xamarin.Formsの開発者は、XAMLファイル内で、すべてのXamarin.Formsビュー、レイアウト、ページ、およびカスタムクラスを使用してユーザインターフェースを定義できます。XAMLファイルは、コンパイルするか、実行可能ファイルに埋め込むことができます。どちらの方法でも、XAML情報はビルド時に名前付きオブジェクトの検索、実行時にオブジェクトのインスタンス化と初期化、およびこれらのオブジェクトとプログラミングコード間のリンクを確立するために解析されます。

XAMLには、同等のコードに比べていくつかの利点があります。

  • XAMLは、同等のコードよりも簡潔で読みやすいことがよくあります。
  • XML固有の親子階層により、XAMLはユーザインターフェースオブジェクトの親子階層をより明確に視覚化することができます。
  • XAMLはプログラマが簡単に手書きすることができますが、視覚的なデザインツールでツールを作成して生成することもできます。

もちろん、主にマークアップ言語に内在する制限に関連する欠点もあります。

  • XAMLにはコードを含めることはできません。すべてのイベントハンドラは、コードファイルで定義する必要があります。
  • XAMLには繰り返し処理のループを含めることはできません。(ただし、ListViewなどのいくつかのXamarin.Formsビジュアルオブジェクトでは、ItemsSourceコレクション内のオブジェクトに基づいて複数の子を生成できます)
  • XAMLには条件付き処理を含めることはできません。(ただし、データバインディングは、条件付き処理を効果的に行えるようにするコードベースのバインディングコンバーターを参照できます)
  • XAMLは通常、パラメータのないコンストラクタを定義しないクラスはインスタンス化できません。(しかし、この制限を回避する方法があることもあります)
  • XAMLは一般的にメソッドを呼び出すことはできません。(この場合も、制限を克服できることがあります)

Xamarin.FormsアプリケーションでXAMLを生成するビジュアルデザイナーはまだありません。すべてのXAMLは手書きでなければなりませんが、XAMLプレビューアがあります。XAMLを初めて使用するプログラマは、特に明らかには正しくないアプリケーションを実行した後に、頻繁にアプリケーションをビルドして実行したいかもしれません。XAMLの多くの経験を持つ開発者でさえ、実験は報われることでしょう。

XAMLは基本的にXMLですが、XAMLには独自の構文機能があります。最も重要なものは次のとおりです。

  • プロパティ要素
  • 添付プロパティ
  • マークアップ拡張

これらの機能はXML拡張ではありません。XAMLは完全に法的なXMLです。しかし、これらのXAML構文機能はXMLを独自の方法で使用します。詳細は、以下の記事で説明します。これについてははMVVMを実装するためのXAMLの使用についての紹介があります。

要件

この記事では、Xamarin.Formsに精通していることを前提としています。An Introduction to Xamarin.Forms(リンク貼る)を読むことを強くお勧めします。

この記事では、XML名前空間宣言の使用方法、要素、タグ、属性という用語の理解を含む、XMLへのある程度の慣れを想定しています。
Xamarin.FormsとXMLに精通しているときは、第1部「XAML入門」を読んでください。

パート1 XAML入門

要素と属性を持つページの定義

Xamarin.Formsアプリケーションでは、XAMLは主にページのビジュアルコンテンツを定義するために使用されます。XAMLファイルは、常にマークアップのコードサポートを提供するC#コードファイルに関連付けられています。また、これらの2つのファイルは、子ビューとプロパティの初期化を含む新しいクラス定義に貢献します。XAMLファイル内で、クラスとプロパティはXML要素と属性で参照され、マークアップとコード間のリンクが確立されます。

ソリューションの作成

最初のXAMLファイルの編集を開始するには、Visual StudioまたはVisual Studio for Macを使用して、新しいXamarin.Formsソリューションを作成します。

Visual Studio for Macで、メニューからFile > New Solutionを選択します。New Projectダイアログで、左側にあるMultiplatform > Appを選択し、テンプレートリストからBlank Forms AppForms Appではありません)を選択します。

New Project Dialog 1

Nextをクリックします。

次のダイアログで、プロジェクトにXamlSamples(または好きなもの)の名前を付けます。Use Portable Class Libraryラジオボタンが選択されていること、Use XAML for user interface filesチェックボックスがオンになっていることを確認します。

New Project Dialog 2

Nextをクリックします。

次のダイアログではプロジェクトの場所を選択できます。

New Project Dialog 3

Createをクリックします。

このソリューションには、XamlSamplesポータブルクラスライブラリ(PCL)、XamlSamples.AndroidXamlSamples.iOSの3つのプロジェクトが作成されています。

XamlSamplesソリューションを作成したら、ソリューションのスタートアッププロジェクトとしてさまざまなプラットフォームプロジェクトを選択し、プロジェクトエミュレータまたは実際のデバイスのいずれかにプロジェクトテンプレートで作成した単純なアプリケーションをビルドして展開することで、開発環境をテストできます。

プラットフォーム固有のコードを記述する必要がない限り、共有のXamlSamples PCLプロジェクトはプログラミング時間をほとんど費やす場所です。これらの記事は、そのプロジェクトの外の話はしません。

XAMLファイルの構造

XamlSamplesポータブルクラスライブラリには、次の名前のファイルがあります。

  • XAMLファイルである App.xaml
  • XAMLファイルに関連付けられたC#コードビハインドファイルであるApp.xaml.cs

コードビハインドファイルを表示するには、App.xamlの横にある矢印をクリックする必要があります。

App.xamlApp.xaml.csの両方が、Applicationから派生したAppという名前のクラスに寄与します。XAMLファイルを持つ他のほとんどのクラスは、ContentPageから派生したクラスに寄与します。これらのファイルはXAMLを使用してページ全体のビジュアルコンテンツを定義します。これは、XamlSamplesプロジェクトの他の2つのファイルにも当てはまります。

  • XAMLファイルである XamlSamplesPage.xaml
  • XAMLファイルに関連付けられたC#コードビハインドファイルであるXamlSamplesPage.xaml.cs

XamlSamplesPage.xamlファイルは次のようになります。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XamlSamples"
             x:Class="XamlSamples.XamlSamplesPage">

    <Label Text="Welcome to Xamarin Forms!"
           VerticalOptions="Center"
           HorizontalOptions="Center" />

</ContentPage>

2つのXML名前空間(xmlns)宣言は、URIを参照しています。最初はXamarinのWebサイトに、もう1つはMicrosoftのものです。これらのURIが指しているものを確認する必要はありません。そこには何もありません。これらは単純にXamarinとMicrosoftが所有するURIであり、基本的にはバージョン識別子として機能します。

最初のXML名前空間宣言は、XAMLファイル内でプレフィックスなしで定義されたタグがXamarin.Formsのクラス(たとえばContentPage)を参照することを意味します。2番目の名前空間宣言はプレフィックスxを定義します。これは、XAML自体に固有でXAMLの他の実装でサポートされているいくつかの要素と属性に対して使用されます。ただし、これらの要素と属性は、URIに埋め込まれた年によって少し異なります。Xamarin.Formsは2009 XAML仕様をサポートしていますが、そのすべてはサポートしていません。

local名前空間宣言により、PCLプロジェクトから他のクラスにアクセスすることができます。

最初のタグの最後の部分で、xプレフィックスがClassという名前の属性に使用されます。このxプレフィックスの使用はXAML名前空間では事実上共通しているため、ClassなどのXAML属性はほとんどの場合x:Classと呼ばれます。

x:Class属性は、完全修飾された.NETクラス名、つまりXamlSamples名前空間のXamlSamplesPageクラスを指定します。つまり、このXAMLファイルは、x:Class属性が表示されるタグであるContentPageから派生したXamlSamples名前空間にXamlSamplesPageという名前の新しいクラスを定義します。

x:Class属性は、派生C#クラスを定義するためにXAMLファイルのルート要素にのみ使用できます。これは、XAMLファイルで定義された唯一の新しいクラスです。XAMLファイルに表示されるその他のものは、単に既存のクラスからインスタンス化され、初期化されます。

XamlSamplesPage.xaml.csファイルは次のようになります。

using Xamarin.Forms;

namespace XamlSamples
{
    public partial class XamlSamplesPage : ContentPage
    {
        public XamlSamplesPage()
        {
            InitializeComponent();
        }
    }
}

XamlSamplesPageクラスはContentPageから派生していますが、partialクラス定義に注目してください。XamlSamplesPageの別のpartialクラス定義を持つ別のC#ファイルがあるはずですが、どこでしょうか?そしてInitializeComponentメソッドとは何でしょうか?

Visual Studio for Macがプロジェクトをビルドすると、XAMLファイルが解析されてC#コードファイルが生成されます。XamlSamples\XamlSamples\obj\Debugディレクトリを見ると、XamlSamples.XamlSamplesPage.xaml.g.csという名前のファイルがあります。'g'は生成されたという意味です。これは、XamlSamplesPageコンストラクタから呼び出されたInitializeComponentメソッドの定義を含むXamlSamplesPageの他の部分クラス定義です。これらの2つのXamlSamplesPage部分クラス定義はまとめてコンパイルされます。XAMLがコンパイルされているかどうかによって、XAMLファイルまたはバイナリ形式のXAMLファイルが実行可能ファイルに埋め込まれます。

実行時に、特定のプラットフォームプロジェクトのコードがLoadApplicationメソッドを呼び出し、PCLのAppクラスの新しいインスタンスをそのクラスに渡します。Appクラスのコンストラクタは、XamlSamplesPageをインスタンス化します。そのクラスのコンストラクタはInitializeComponentを呼び出し、次に、XAMLファイル(またはそのコンパイルされたバイナリ)をPCLから抽出するLoadFromXamlメソッドを呼び出します。LoadFromXamlは、XAMLファイルで定義されているすべてのオブジェクトを初期化し、それらをすべて親子関係で接続し、コードで定義されているイベントハンドラをXAMLファイルに設定されたイベントに関連付け、オブジェクトのツリーをページのコンテンツとして設定します。

生成されたコードファイルでは通常は時間を費やす必要はありませんが、生成されたファイルのコードでは実行時の例外が発生することがあります。

このプログラムをコンパイルして実行すると、XAMLが示唆するように、Label要素がページの中央に表示されます。左からiOS、Android、Windows 10 Mobileです。

Default Xamarin.Forms display

より興味深いビジュアルを得るには、もっと興味深いXAMLが必要です。

準備

Windows用のVisual Studioで作成されたファイルとVisual Studio for Macでファイル名を一致させるには、XamlSamplesPage.xamlの名前をMainPage.xamlに、XamlSamplesPage.xaml.csの名前をMainPage.xaml.csに変更します。XamlSamplesPage.xamlファイル内で、XamlSamplesPageMainPageに変更します。XamlSamplesPage.xaml.csファイル内で、2回出現するXamlSamplesPageMainPageに変更します。
App.xaml.csファイル内で、宣言を変更します。

MainPage = new XamlSamplesPage();

MainPage = new MainPage();

続行する前に、プログラムがコンパイルおよびデプロイされることをテストします。

新しいXAMLページの追加

他のXAMLベースのContentPageクラスをプロジェクトに追加するには、XamlSamples PCLプロジェクトを選択し、File > New Fileメニュー項目を呼び出します。New Fileダイアログの左にあるFormsを選択し、Forms ContentPage Xaml(コード・オンリー・ページを作成するForms ContentPageでもページではないContent Viewでもありません)を選択します。ページに名前を付けます(例:HelloXamlPage)。

New File Dialog

HelloXamlPage.xamlとコードビハインドファイルHelloXamlPage.xaml.csの2ファイルがプロジェクトに追加されます。

ページコンテンツの設定

唯一のタグがContentPageとContentPage.Contentのタグであるように、HelloXamlPage.xamlファイルを編集します。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XamlSamples.HelloXamlPage">
    <ContentPage.Content>

    </ContentPage.Content>
</ContentPage>

ContentPage.Contentタグは、XAMLの一意の構文の一部です。最初は、無効なXMLと思われるかもしれませんが、有効です。ピリオドはXMLでは特殊文字ではありません。

ContentPage.Contentタグは、プロパティ要素タグと呼ばれます。ContentContentPageのプロパティであり、通常、単一のビューまたは子ビューを持つレイアウトに設定されます。通常、プロパティはXAMLの属性になりますが、複雑なオブジェクトにContent属性を設定するのは困難でしょう。そのため、プロパティは、クラス名とプロパティ名をピリオドで区切ったXML要素として表現されます。Contentプロパティは、ContentPage.Contentタグの間で次のように設定できます。

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

        <Label Text="Hello, XAML!"
               VerticalOptions="Center"
               HorizontalTextAlignment="Center"
               Rotation="-15"
               IsVisible="true"
               FontSize="Large"
               FontAttributes="Bold"
               TextColor="Blue" />

    </ContentPage.Content>
</ContentPage>

また、ルートタグにTitle属性が設定されていることにも注意してください。

この時点で、クラス、プロパティ、およびXMLの関係が明白でなければなりません。Xamarin.Formsクラス(ContentPageLabelなど)は、XML要素としてXAMLファイルに表示されます。ContentPageTitleLabelの7つのプロパティを含むそのクラスのプロパティは、通常、XML属性として表示されます。

これらのプロパティの値を設定するためのショートカットが多数存在します。たとえば、TitleTextのプロパティはString型、RotationDouble型、IsVisible(デフォルトではtrueで、ここではイラストレーション用にのみ設定されています)はBoolean型です。

Horizo​​ntalTextAlignmentプロパティの型はTextAlignmentで、列挙型です。任意の列挙型のプロパティに対して、必要なのはメンバ名だけです。

ただし、より複雑な型のプロパティでは、XAMLの解析にコンバーターが使用されます。これらはTypeConverterから派生したXamarin.Formsのクラスです。多くは公開クラスですが、一部は公開クラスではありません。この特定のXAMLファイルでは、これらのクラスのいくつかがバックグラウンドで役割を果たします。

  • VerticalOptionsプロパティ用のLayoutOptionsConverter
  • FontSizeプロパティ用のFontSizeConverter
  • TextColorプロパティ用のColorTypeConverter

これらのコンバータは、プロパティ設定の許容される構文を管理します。

ThicknessTypeConverterは、カンマで区切られた1つ、2つ、または4つの数値を処理できます。1つの番号が指定されている場合、それは4つの側面すべてに適用されます。2つの数字の場合、最初は左と右のパディング、もう1つは上と下です。4つの数字は左、上、右、下の順になります。

LayoutOptionsConverterは、LayoutOptions構造体のpublic staticフィールドの名前をLayoutOptions型の値に変換できます。

FontSizeConverterは、NamedSizeメンバーまたは数値フォントサイズを処理できます。

ColorTypeConverterは、アルファチャンネルの有無にかかわらず、数字の記号(#)が前に付いたColor構造体または16進数のRGB値のpublic staticフィールドの名前を受け入れます。アルファチャンネルのない構文は次のとおりです。

TextColor = "#rrggbb"

各小文字は16進数です。アルファチャンネルがどのように含まれているかは次のとおりです。

TextColor = "#aarrggbb">

アルファチャンネルの場合、FFは完全に不透明で00は完全に透明であることに注意してください。

他の2つのフォーマットでは、各チャンネルに対して1つの16進数字のみを指定できます。

TextColor="#rgb" TextColor="#argb"

これらの場合、数字を繰り返して値を形成します。たとえば、#CF3はRGBの色CC-FF-33です。

ページナビゲーション

XamlSamplesプログラムを実行すると、MainPageが表示されます。新しいHelloXamlPageを見るには、App.xaml.csファイルの新しいスタートアップページとして設定するか、MainPageから新しいページに遷移させます。

ナビゲーションを実装するには、まずApp.xaml.csコンストラクタのコードを変更して、NavigationPageオブジェクトを作成します。

public App()
{
    InitializeComponent();
    MainPage = new NavigationPage(new MainPage());
}

MainPage.xaml.csコンストラクタでは、シンプルなButtonを作成し、HelloXamlPageに移動するためにイベントハンドラを使用することができます。

public MainPage()
{
    InitializeComponent();

    Button button = new Button
    {
        Text = "Navigate!",
        HorizontalOptions = LayoutOptions.Center,
        VerticalOptions = LayoutOptions.Center
    };

    button.Clicked += async (sender, args) =>
    {
        await Navigation.PushAsync(new HelloXamlPage());
    };

    Content = button;
}

ページのContentプロパティを設定すると、XAMLファイルのContentプロパティの設定が置き換えられます。このプログラムの新しいバージョンをコンパイルしてデプロイすると、画面にボタンが表示されます。押すと、HelloXamlPageに遷移します。iPhone、Android、およびWindows 10 Mobileデバイスの結果ページは次のとおりです。

Rotated Label Text

iOSでは < Back ボタンを使用してメインページに戻ることができます。Androidではページの上部にある左矢印か画面下部にある矢印を使用します。Windows 10 Mobileではページ下部にある左矢印を使用します。

ラベルをレンダリングするさまざまな方法についてXAMLを自由に試してみてください。Unicode文字をテキストに埋め込む必要がある場合は、標準のXML構文を使用できます。たとえば、挨拶をスマートクオートで囲むには、次のようにします。

<Label Text="&#x201C;Hello, XAML!&#x201D;"  />

以下はその様子です。

Rotated Label Text with Unicode Characters

XAMLとコードの相互作用

HelloXamlPageサンプルには、ページ上の単一のラベルのみが含まれていますが、こういう状況はあまりありません。ほとんどのContentPage派生クラスは、StackLayoutのような何らかのレイアウトにContentプロパティを設定します。StackLayoutChildrenプロパティは、IList 型に定義されていますが、実際はElementCollection 型のオブジェクトであり、そのコレクションには複数のビューまたは他のレイアウトが設定されます。XAMLでは、これらの親子関係は通常のXML階層で確立されます。XamlPlusCodePageという名前の新しいページ用のXAMLファイルは次のとおりです。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XamlSamples.XamlPlusCodePage"
             Title="XAML + Code Page">
    <StackLayout>
        <Slider VerticalOptions="CenterAndExpand" />

        <Label Text="A simple Label"
               Font="Large"
               HorizontalOptions="Center"
               VerticalOptions="CenterAndExpand" />

        <Button Text="Click Me!"
                HorizontalOptions="Center"
                VerticalOptions="CenterAndExpand" />
    </StackLayout>
</ContentPage>

このXAMLファイルは構文的に完成しており、次のようになります。

Multiple Controls on a Page

しかし、あなたはきっとこのプログラムは機能的に不完全であると考えていることでしょう。おそらくSliderLabelに現在の値を表示させ、Buttonはおそらくプログラム内で何かを行うことを意図しています。

パート4 データバインディングの基礎で説明するように、Labelを使用してSlider値を表示するには、データバインディングを使用してXAMLで完全に処理できます。しかし、コードソリューションを最初に見ておくと便利です。それでも、ボタンクリックを処理するにはコードが必要です。つまり、XamlPlusCodePageのコードビハインドファイルには、SliderValueChangedイベントとButtonClickedイベントのハンドラが含まれている必要があります。それらを追加しましょう。

namespace XamlSamples
{
    public partial class XamlPlusCodePage
    {
        public XamlPlusCodePage()
        {
            InitializeComponent();
        }

        void OnSliderValueChanged(object sender, ValueChangedEventArgs args)
        {

        }

        void OnButtonClicked(object sender, EventArgs args)
        {

        }
    }
}

これらのイベントハンドラはpublicである必要はありません。

XAMLファイルに戻ると、SliderタグとButtonタグには、これらのハンドラを参照するValueChangedイベントとClickedイベントの属性が含まれている必要があります。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XamlSamples.XamlPlusCodePage"
             Title="XAML + Code Page">
    <StackLayout>
        <Slider VerticalOptions="CenterAndExpand"
                ValueChanged="OnSliderValueChanged" />

        <Label Text="A simple Label"
               Font="Large"
               HorizontalOptions="Center"
               VerticalOptions="CenterAndExpand" />

        <Button Text="Click Me!"
                HorizontalOptions="Center"
                VerticalOptions="CenterAndExpand"
                Clicked="OnButtonClicked" />
    </StackLayout>
</ContentPage>

イベントにハンドラを割り当てるのは、プロパティに値を代入するのと同じ構文です。

SliderValueChangedイベントのハンドラがLabelを使用して現在の値を表示する場合、ハンドラはコードからそのオブジェクトを参照する必要があります。

Labelにはx:Name属性で指定された名前が必要です。

<Label x:Name="valueLabel"
       Text="A simple Label"
       Font="Large"
       HorizontalOptions="Center"
       VerticalOptions="CenterAndExpand" />

x:Name属性のxプレフィックスは、この属性がXAMLに固有であることを示します。

x:Name属性に割り当てる名前は、C#変数名と同じ規則です。たとえば、文字またはアンダースコアで始まり、埋め込みスペースを含まないようにする必要があります。

ValueChangedイベントハンドラは、新しいSlider値を表示するようLabelを設定できるようになりました。新しい値はイベントの引数から取得できます。

void OnSliderValueChanged(object sender, ValueChangedEventArgs args)
{
    valueLabel.Text = args.NewValue.ToString("F3");
}

または、ハンドラは、sender引数からこのイベントを生成しているSliderオブジェクトを取得し、そこからValueプロパティを取得できます。

void OnSliderValueChanged(object sender, ValueChangedEventArgs args)
{
    valueLabel.Text = ((Slider)sender).Value.ToString("F3");
}

最初にプログラムを実行すると、ValueChangedイベントがまだ発生していないため、ラベルにSlider値が表示されません。しかし、Sliderを操作すると、値が表示されます。

Slider Value Displayed

次はButtonです。Clickedイベントに対する応答を、ボタンのTextで警告を表示してシミュレートしましょう。イベントハンドラは、sender引数をButtonに安全にキャストし、そのプロパティにアクセスできます。

async void OnButtonClicked(object sender, EventArgs args)
{
    Button button = (Button)sender;
    await DisplayAlert("Clicked!",
        "The button labeled '" + button.Text + "' has been clicked",
        "OK");
}

このメソッドはasyncとして定義されています。これは、DisplayAlertメソッドが非同期であり、前にawait演算子をおきます。その演算子はメソッド完了時に返します。このメソッドは、sender引数からイベントを発生させるButtonを取得するため、複数のボタンに対して同じハンドラを使用できます。

XAMLで定義されたオブジェクトがコードビハインドファイルで処理されるイベントを発生させ、XAMLで定義されたオブジェクトにコードビハインドファイルがx:Name属性で割り当てられた名前でアクセスできることがわかりました。これらはコードとXAMLが相互作用する2つの基本的な方法です。

新しく生成されたXamlPlusCode.xaml.g.csファイル(x:Name属性にプライベートフィールドとして割り当てられた任意の名前が含まれる)を調べることで、XAMLの動作に関する追加の洞察を得ることができます。そのファイルの簡略版は次のとおりです。

public partial class XamlPlusCodePage : ContentPage {

    private Label valueLabel;

    private void InitializeComponent() {
        this.LoadFromXaml(typeof(XamlPlusCodePage));
        valueLabel = this.FindByName<Label>("valueLabel");
    }
}

このフィールドの宣言により、あなたの管轄下のXamlPlusCodePagepartialクラスファイル内の任意の場所で変数を自由に使用することができます。実行時、フィールドはXAMLが解析された後に割り当てられます。これは、XamlPlusCodePageコンストラクターが開始されたときにvalueLabelフィールドがnullであり、InitializeComponentが呼び出された後に有効であることを意味します。

InitializeComponentがコントロールをコンストラクタに返すと、ページのビジュアルは、コードでインスタンス化され、初期化されたかのように構築されます。XAMLファイルはクラス内のどの役割も果たさなくなりました。たとえば、StackLayoutにビューを追加したり、ページのContentプロパティを完全に別のものに設定したりするなど、任意の方法でページ上のこれらのオブジェクトを操作できます。ページのContentプロパティとChildrenコレクションのレイアウトの項目を調べることで、「ツリー構造をたどる」ことができます。この方法でアクセスされるビューにプロパティを設定したり、イベントハンドラを動的に割り当てることができます。

どうぞ。それはあなたのページです。XAMLはコンテンツを構築するためのツールに過ぎません。

まとめ

ここでは、XAMLファイルとコードファイルがクラス定義にどのように寄与し、XAMLとコードファイルがどのように相互作用するかを見てきました。しかし、XAMLには独自の構文上の特徴があり、非常に柔軟に使用することができます。
パート2 XAML構文の本質で詳しく見て行きましょう。

パート2 XAML構文の本質

プロパティ要素と添付プロパティの操作

XAMLは主にオブジェクトのインスタンス化と初期化用に設計されています。しかし、多くの場合、XML文字列として簡単に表現できない複雑なオブジェクトにプロパティを設定する必要があります。また、あるクラスで定義されたプロパティを子クラスに設定する必要があることもあります。これらの2つプロパティ要素と添付プロパティのXAML構文の本質的な機能が必要です。

プロパティ要素

XAMLでは、クラスのプロパティは通常XML属性として設定されます。

<Label Text="Hello, XAML!"
       VerticalOptions="Center"
       FontAttributes="Bold"
       FontSize="Large"
       TextColor="Aqua" />

しかし、XAMLでプロパティを設定する別の方法があります。TextColorでこの代替方法を試すには、まず既存のTextColor設定を削除します。

<Label Text="Hello, XAML!"
       VerticalOptions="Center"
       FontAttributes="Bold"
       FontSize="Large" />

空要素のLabelタグを開始タグと終了タグに分けて開きます。

<Label Text="Hello, XAML!"
       VerticalOptions="Center"
       FontAttributes="Bold"
       FontSize="Large">

</Label>

次のように、これらの新しいタグの内容としてプロパティ値を設定します。

<Label Text="Hello, XAML!"
       VerticalOptions="Center"
       FontAttributes="Bold"
       FontSize="Large">
    <Label.TextColor>
        Aqua
    </Label.TextColor>
</Label>

TextColorプロパティを指定するこれらの2つの方法は機能的には同等ですが、同じプロパティに対して2つの方法を使用しないでください。プロパティを2回設定してしまい、あいまいになってしまうためです。

この新しい構文では、いくつかの便利な用語を紹介することができます。

  • Labelオブジェクト要素です。これはXML要素として表現されたXamarin.Formsオブジェクトです。
  • TextVerticalOptionsFontAttributesFontSizeプロパティ属性です。
  • 最後のスニペットでは、TextColorプロパティ要素になりました。これはXamarin.Formsプロパティですが、XML要素になりました。

プロパティ要素の定義は、最初はXML構文の違反と思われるかもしれませんが、そうではありません。ピリオドはXMLでは特別な意味を持ちません。XMLデコーダにとっては、Label.TextColorは単なる通常の子要素です。

しかし、XAMLでは、この構文は非常に特殊です。プロパティ要素の規則の1つは、Label.TextColorタグに何も表示されないことです。プロパティの値は、プロパティ要素の開始タグと終了タグの間のコンテンツとして常に定義されます。

複数のプロパティでプロパティ要素構文を使用できます。

<Label Text="Hello, XAML!"
       VerticalOptions="Center">
    <Label.FontAttributes>
        Bold
    </Label.FontAttributes>
    <Label.FontSize>
        Large
    </Label.FontSize>
    <Label.TextColor>
        Aqua
    </Label.TextColor>
</Label>

または、すべてのプロパティーに対してプロパティー要素構文を使用できます。

<Label>
    <Label.Text>
        Hello, XAML!
    </Label.Text>
    <Label.FontAttributes>
        Bold
    </Label.FontAttributes>
    <Label.FontSize>
        Large
    </Label.FontSize>
    <Label.TextColor>
        Aqua
    </Label.TextColor>
    <Label.VerticalOptions>
        Center
    </Label.VerticalOptions>
</Label>

最初に、プロパティ要素の構文は比較的簡単なものの、不要で冗長な置き換えのように見えるかもしれません。これらの例では確かです。

しかし、プロパティの値が複雑すぎて単純な文字列として表現できない場合は、プロパティ要素の構文が不可欠になります。プロパティ要素タグ内で、別のオブジェクトをインスタンス化してプロパティを設定することができます。たとえば、VerticalOptionsなどのプロパティをプロパティ設定でLayoutOptions値に明示的に設定することができます。

<Label>
    ...
    <Label.VerticalOptions>
        <LayoutOptions Alignment="Center" />
    </Label.VerticalOptions>
</Label>

別の例: GridにはRowDefinitionsColumnDefinitionsという2つのプロパティがあります。これら2つのプロパティは、RowDefinitionCollectionColumnDefinitionCollectionタイプです。これらはRowDefinitionColumnDefinitionオブジェクトのコレクションです。

RowDefinitionsコレクションとColumnDefinitionsコレクションのプロパティ要素タグを示す、GridDemoPageクラスのXAMLファイルの最初は次のとおりです。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XamlSamples.GridDemoPage"
             Title="Grid Demo Page">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
            <RowDefinition Height="100" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="100" />
        </Grid.ColumnDefinitions>
        ...
    </Grid>
</ContentPage>

自動サイズのセル、ピクセルの幅と高さのセル、および星の設定を定義するための省略形の構文に注目してください。

添付プロパティ

Gridは、行と列を定義するためにRowDefinitionsコレクションとColumnDefinitionsコレクションのプロパティ要素が必要であることがわかりました。しかし、プログラマがGridの子が置かれている行と列を指示する方法もなければなりません。

Gridの子のタグ内で、次の属性を使用してその子の行と列を指定します。

  • Grid.RowSpan
  • Grid.ColumnSpan

これら2つの属性のデフォルト値は1です。

完成したGridDemoPage.xamlファイルがこちらです。

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

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
            <RowDefinition Height="100" />
        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="100" />
        </Grid.ColumnDefinitions>

        <Label Text="Autosized cell"
               Grid.Row="0" Grid.Column="0"
               TextColor="White"
               BackgroundColor="Blue" />

        <BoxView Color="Silver"
                 HeightRequest="0"
                 Grid.Row="0" Grid.Column="1" />

        <BoxView Color="Teal"
                 Grid.Row="1" Grid.Column="0" />

        <Label Text="Leftover space"
               Grid.Row="1" Grid.Column="1"
               TextColor="Purple"
               BackgroundColor="Aqua"
               HorizontalTextAlignment="Center"
               VerticalTextAlignment="Center" />

        <Label Text="Span two rows (or more if you want)"
               Grid.Row="0" Grid.Column="2" Grid.RowSpan="2"
               TextColor="Yellow"
               BackgroundColor="Blue"
               HorizontalTextAlignment="Center"
               VerticalTextAlignment="Center" />

        <Label Text="Span two columns"
               Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2"
               TextColor="Blue"
               BackgroundColor="Yellow"
               HorizontalTextAlignment="Center"
               VerticalTextAlignment="Center" />

        <Label Text="Fixed 100x100"
               Grid.Row="2" Grid.Column="2"
               TextColor="Aqua"
               BackgroundColor="Red"
               HorizontalTextAlignment="Center"
               VerticalTextAlignment="Center" />

    </Grid>
</ContentPage>

Grid.RowGrid.Columnの0の設定は必須ではありませんが、一般的には分かりやすくするために含まれています。

3つのプラットフォームすべてでこのように見えます。

Grid Layout

これらのGrid.RowGrid.ColumnGrid.RowSpanGrid.ColumnSpanの各属性は、構文から判断すると、静的なフィールドかGridのプロパティであるように見えますが、興味深いことに、GridではRowColumnRowSpanColumnSpanという名前は定義されていません。

その代わり、GridではRowPropertyColumnPropertyRowSpanPropertyColumnSpanPropertyという4つのバインド可能なプロパティが定義されています。これらは、添付プロパティとして知られている特別なタイプのバインド可能なプロパティです。それらはGridクラスによって定義されますが、Gridの子に設定されます。

これらの添付プロパティをコードで使用する場合、GridクラスはSetRowGetColumnなどの静的メソッドを提供します。しかし、XAMLでは、これらの添付プロパティは、単純なプロパティ名を使用してGridの子に属性として設定されます。

添付プロパティは、XAMLファイルでは、クラスとプロパティ名をピリオドで区切った属性として認識することができます。あるクラス(この例ではGrid)で定義されているが、他のオブジェクト(この場合はGridの子)に添付されているため、添付プロパティと呼ばれます。レイアウト中、Gridはこれらの添付プロパティの値を調べて、各子をどこに配置するかを知ることができます。

AbsoluteLayoutクラスは、LayoutBoundsLayoutFlagsという2つの添付プロパティを定義します。AbsoluteLayoutの比率を維持した配置とサイジングの機能を使って実現したチェッカーボードパターンです

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

    <AbsoluteLayout BackgroundColor="#FF8080">
        <BoxView Color="#8080FF"
                 AbsoluteLayout.LayoutBounds="0.33, 0, 0.25, 0.25"
                 AbsoluteLayout.LayoutFlags="All" />

        <BoxView Color="#8080FF"
                 AbsoluteLayout.LayoutBounds="1, 0, 0.25, 0.25"
                 AbsoluteLayout.LayoutFlags="All" />

        <BoxView Color="#8080FF"
                 AbsoluteLayout.LayoutBounds="0, 0.33, 0.25, 0.25"
                 AbsoluteLayout.LayoutFlags="All" />

        <BoxView Color="#8080FF"
                 AbsoluteLayout.LayoutBounds="0.67, 0.33, 0.25, 0.25"
                 AbsoluteLayout.LayoutFlags="All" />

        <BoxView Color="#8080FF"
                 AbsoluteLayout.LayoutBounds="0.33, 0.67, 0.25, 0.25"
                 AbsoluteLayout.LayoutFlags="All" />

        <BoxView Color="#8080FF"
                 AbsoluteLayout.LayoutBounds="1, 0.67, 0.25, 0.25"
                 AbsoluteLayout.LayoutFlags="All" />

        <BoxView Color="#8080FF"
                 AbsoluteLayout.LayoutBounds="0, 1, 0.25, 0.25"
                 AbsoluteLayout.LayoutFlags="All" />

        <BoxView Color="#8080FF"
                 AbsoluteLayout.LayoutBounds="0.67, 1, 0.25, 0.25"
                 AbsoluteLayout.LayoutFlags="All" />

  </AbsoluteLayout>
</ContentPage>

このように表示されます。

Absolute Layout

このような実装をするにあたっては、XAMLでの実装は賢明でないと感じるかもしれません。確かに、LayoutBounds矩形の繰り返しと規則性は、コードでよりよく実現できることを示唆しています。

これは確かに正当な懸案事項であり、ユーザインターフェースの定義時にコードとマークアップの使用のバランスをとることには問題ありません。XAMLでビジュアルの一部を定義してから、コードビハインドファイルのコンストラクタを使用して、ループでより効率的に生成されるビジュアルを追加できます。

コンテントプロパティ

前の例では、StackLayoutGridAbsoluteLayoutオブジェクトはContentPageのContentプロパティに設定され、これらのレイアウトの子は実際にはChildrenコレクション内の項目です。しかし、これらのContentおよびChildrenプロパティはXAMLファイルにはありません。

XamlPlusCodeサンプルのように、Content要素とChildrenプロパティを明示的にプロパティ要素として含めることができます。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XamlSamples.XamlPlusCodePage"
             Title="XAML + Code Page">
    <ContentPage.Content>
        <StackLayout>
            <StackLayout.Children>
                <Slider VerticalOptions="CenterAndExpand"
                        ValueChanged="OnSliderValueChanged" />

                <Label x:Name="valueLabel"
                       Text="A simple Label"
                       FontSize="Large"
                       HorizontalOptions="Center"
                       VerticalOptions="CenterAndExpand" />

                <Button Text="Click Me!"
                      HorizontalOptions="Center"
                      VerticalOptions="CenterAndExpand"
                      Clicked="OnButtonClicked" />
            </StackLayout.Children>
        </StackLayout>
    </ContentPage.Content>
</ContentPage>

XAMLファイルでこれらのプロパティ要素が不要なのはなぜでしょうか?

XAMLで使用するためにXamarin.Formsで定義された要素は、そのクラスのContentProperty属性にフラグが立てられた1つのプロパティを持つことができます。オンラインのXamarin.FormsドキュメントでContentPageクラスを調べると、次の属性が見つかります。

[Xamarin.Forms.ContentProperty("Content")]
public class ContentPage : TemplatedPage

これはContentプロパティ要素タグは必須ではないという意味です。ContentPageタグの先頭と末尾の間に表示されるXMLコンテンツは、Contentプロパティに割り当てられているものとみなされます。

StackLayoutGridAbsoluteLayoutRelativeLayoutはすべてLayout から派生し、Xamarin.FormsドキュメントでLayout を調べると、別のContentProperty属性が見つかります。

[Xamarin.Forms.ContentProperty("Children")]
public abstract class Layout<T> : Layout ...

これにより、レイアウトのコンテンツを明示的なChildrenプロパティ要素タグなしでChildrenコレクションに自動的に追加することができます。

他のクラスにもContentProperty属性定義があります。たとえば、LabelContentプロパティはTextです。他のAPIドキュメントをチェックしてみてください。

OnPlatformとプラットフォームの違い

シングルページアプリケーションでは、iOSのステータスバーを上書きしないように、ページのPaddingプロパティを設定するのが一般的です。コードでは、この目的でDevice.RuntimePlatformプロパティを使用できます。

if (Device.RuntimePlatform == Device.iOS)
{
    Padding = new Thickness(0, 20, 0, 0);
}

また、OnPlatformクラスとOnクラスを使用して、XAMLでも同様のことを行うことができます。まず、ページの上部にあるPaddingプロパティのプロパティ要素を含めます。

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

    <ContentPage.Padding>

    </ContentPage.Padding>
    ...
</ContentPage>

これらのタグには、OnPlatformタグが含まれています。OnPlatformはジェネリッククラスです。ジェネリック型の引数を指定する必要があります。この場合は、Paddingプロパティの型であるThicknessを指定します。幸い、x:TypeArgumentsというジェネリック引数を定義するためのXAML属性があります。これは設定しているプロパティの型と一致する必要があります。

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

    <ContentPage.Padding>
        <OnPlatform x:TypeArguments="Thickness">

        </OnPlatform>
    </ContentPage.Padding>
  ...
</ContentPage>

OnPlatformには、OnオブジェクトのIListであるPlatformsという名前のプロパティがあります。そのプロパティのプロパティ要素タグを使用します。

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

    <ContentPage.Padding>
        <OnPlatform x:TypeArguments="Thickness">
            <OnPlatform.Platforms>

            </OnPlatform.Platforms>
        </OnPlatform>
    </ContentPage.Padding>
  ...
</ContentPage>

On要素を追加します。各要素には、Platformプロパティと、Thicknessプロパティのマークアップ用のValueプロパティを設定します。

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

    <ContentPage.Padding>
        <OnPlatform x:TypeArguments="Thickness">
            <OnPlatform.Platforms>
                <On Platform="iOS" Value="0, 20, 0, 0" />
                <On Platform="Android" Value="0, 0, 0, 0" />
                <On Platform="UWP" Value="0, 0, 0, 0" />
            </OnPlatform.Platforms>
        </OnPlatform>
    </ContentPage.Padding>
  ...
</ContentPage>

このマークアップは単純化することができます。 OnPlatformContentプロパティはPlatformsであるため、これらのプロパティ要素タグは削除できます。

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

    <ContentPage.Padding>
        <OnPlatform x:TypeArguments="Thickness">
            <On Platform="iOS" Value="0, 20, 0, 0" />
            <On Platform="Android" Value="0, 0, 0, 0" />
            <On Platform="UWP" Value="0, 0, 0, 0" />
        </OnPlatform>
    </ContentPage.Padding>
  ...
</ContentPage>

OnPlatformプロパティはIList 型であるため、値が同じ場合は複数のプラットフォームを含めることができます。

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

    <ContentPage.Padding>
        <OnPlatform x:TypeArguments="Thickness">
            <On Platform="iOS" Value="0, 20, 0, 0" />
            <On Platform="Android, UWP" Value="0, 0, 0, 0" />
        </OnPlatform>
    </ContentPage.Padding>
  ...
</ContentPage>

AndroidとWindowsはPaddingのデフォルト値に設定されているので、そのタグは削除できます。

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

    <ContentPage.Padding>
        <OnPlatform x:TypeArguments="Thickness">
            <On Platform="iOS" Value="0, 20, 0, 0" />
        </OnPlatform>
    </ContentPage.Padding>
  ...
</ContentPage>

これは、XAMLでプラットフォーム依存のPaddingプロパティを設定する標準的な方法です。Valueの設定を単一の文字列で表すことができない場合は、プロパティの要素を定義できます。

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

    <ContentPage.Padding>
        <OnPlatform x:TypeArguments="Thickness">
            <On Platform="iOS">
                <On.Value>
                    0, 20, 0, 0
                </On.Value>
            </On>
        </OnPlatform>
    </ContentPage.Padding>
  ...
</ContentPage>

まとめ

プロパティ要素と添付プロパティに関して基本的なXAML構文の多くが確立されています。しかし、リソースディクショナリーなどの間接的な方法でオブジェクトにプロパティを設定する必要がある場合もあります。このアプローチは、次の部で説明されています。

パート3 XAMLマークアップ拡張

一般化されたオブジェクト参照構文の探索

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

XAMKマークアップ拡張

一般に、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="Large" />

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

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

    </StackLayout>
</ContentPage>

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

XAMLでは、そのような値やオブジェクトをリソースディクショナリーに格納することが一般的な解決策の1つです。VisualElementクラスはResourceDictionary型のResourcesという名前のプロパティを定義します。これは、string型のキーとobject型の値を持つ辞書です。この辞書にオブジェクトを配置し、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>

    </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の静的フィールドを使用する方がはるかに一般的ですが、今回はプロパティを設定する方が便利です。

これらのボタンのHorizo​​ntalOptionsプロパティとVerticalOptionsプロパティをこれらのリソースに設定する必要があります。これはStaticResource XAMLマークアップ拡張で行います

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

StaticResourceマークアップ拡張子は、常に中括弧で区切られ、辞書キーが含まれています。

StaticResourceという名前は、Xamarin.FormsでもサポートされているDynamicResourceと区別されます。DynamicResourceは実行時に変更される可能性のある値に関連付けられた辞書キー用であるのに対し、StaticResourceはページ上の要素が構築されたときに、一度だけ辞書から要素にアクセスします。

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="Large" />

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

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

FontSizeプロパティには少し問題があります。このプロパティはdouble型であると定義されています。プロパティをLargeなどのNamedSize列挙体のメンバに設定すると、FontSizeConverterクラスは背後で動作し、Device.GetNamedSizedメソッドを使用してプラットフォーム依存の値に変換します。

しかし、フォントサイズのリソースをdoubleとして定義して値を "Large"に設定することはできません。XAMLパーサーがリソースを処理するときに、値がフォントサイズとして使用されることはわかりません。

解決策は、x:String型を使用してリソースを文字列として定義することです。

<x:String x:Key="fontSize">Large</x:String>

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:String x:Key="fontSize">Large</x:String>
        </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>

スクリーンショットで一貫したスタイリングとプラットフォームに依存するスタイリングを見てみましょう。

Styled Controls

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

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

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

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

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

x:Staticマークアップ拡張

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

  • public staticフィールド
  • public staticプロパティ
  • public constフィールド
  • 列挙メンバー

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ファイルには、Xamarin.FormsクラスにアクセスするためのXML名前空間宣言と、XAMLに固有のタグと属性を参照するためのXML名前空間宣言の2つのXML名前空間宣言が含まれています。

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

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

xmlns:local="clr-namespace:XamlSamples"

また、PCLが参照するアセンブリ内の.NET名前空間のXML名前空間宣言を定義することもできます。たとえば、かつてmscorlibアセンブリにおいて「Microsoft Common Object Runtime Library」を意味したの標準.NET System名前空間のsysプレフィックスが今では「多言語標準共通オブジェクトランタイムライブラリ」を意味します。これは別のアセンブリなので、アセンブリ名(この場合はmscorlib)も指定する必要があります。

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

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

そうです、コロンはclr-namespaceの後に続きますが、等号はアセンブリの後に続きます。構文はこのようにして意図的に定義されました。大部分のXML名前空間宣言は、httpなどのURIスキーム名を開始するURIを参照し、その後には常にコロンが続きます。この文字列のclr-namespace部分は、その慣習を模倣するためのものです。

これらの名前空間宣言は、両方ともStaticConstantsPageサンプルに含まれています。BoxViewの次元はMath.PIとMath.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=mscorlib"
             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の画面に対するサイズは、プラットフォームによって異なります。

Controls using x:Static Markup Extension

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

いくつかのマークアップ拡張はXAMLに固有であり、Xamarin.Forms XAMLファイルでサポートされています。これらのうちのいくつかは頻繁には使用されませんが、必要なときには不可欠です。

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

ConstraintExpressionマークアップ拡張

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

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

<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ファイルにマークアップ拡張を入力するときは、プロパティの値を引用符で囲みます。誘惑に負けないでください!

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

Relative Layout using Constraints

まとめ

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

パート4 データバインディングの基礎

XAMLファイルのプロパティのリンク

データバインディングを使用すると、2つのオブジェクトのプロパティがリンクされ、一方を変更すると他方が変更されます。これは非常に価値あるツールです。データバインディングをコードで完全に定義できますが、XAMLはショートカットと利便性を提供します。したがって、Xamarin.Formsの最も重要なマークアップ拡張の1つはBindingです。

データバインディング

データバインディングは、ソースターゲットと呼ばれる2つのオブジェクトのプロパティを接続します。コードでは、2つのステップが必要です。ターゲットオブジェクトのBindingContextプロパティは、ソースオブジェクトに設定する必要があります。ターゲットオブジェクトのプロパティをソースオブジェクトのプロパティにバインドするには、ターゲットオブジェクトに対してSetBindingメソッド(Bindingクラスと組み合わせて使用​​されることが多い)を呼び出す必要があります。

ターゲットプロパティはバインダブルプロパティでなければなりません。つまり、ターゲットオブジェクトはBindableObjectから派生する必要があります。オンラインのXamarin.Formsドキュメントでは、どのプロパティがバインダブルプロパティであるかを説明しています。TextなどのLabelのプロパティは、バインダブルプロパティTextPropertyに関連付けられています。

マークアップでは、コードに必要な同じ2つの手順も実行する必要があります。ただし、Bindingマークアップ拡張がSetBinding呼び出しとBindingクラスの代わりに使用される点が異なります。

しかし、XAMLでデータバインディングを定義する場合、ターゲットオブジェクトのBindingContextを設定する方法は複数あります。場合によっては、StaticResourceまたはx:Staticマークアップ拡張を使用するコードビハインドファイルから設定されることもあります。また、BindingContextのプロパティ要素タグの内容として設定されることもあります。

バインディングは、プログラムのビジュアルを基になるデータモデルと結びつけるために最も頻繁に使用されます。特に、パート5 データバインディングからMVVMへで議論するように、MVVM(Model-View-ViewModel)アプリケーション・アーキテクチャーを実現する際に通常は使用されますが、他のシナリオでも使用できます。

ビューからビューへのバインディング

同じページ上の2つのビューのプロパティをリンクするデータバインディングを定義できます。この場合、x:Referenceマークアップ拡張を使用してターゲットオブジェクトのBindingContextを設定します。

次の例では、Sliderと2つのLabelビューが含まれているXAMLファイルがあり、そのうちの1つはSlider値で回転し、もう1つはSlider値を表示します。

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

    <StackLayout>
        <Label Text="ROTATION"
               BindingContext="{x:Reference Name=slider}"
               Rotation="{Binding Path=Value}"
               FontAttributes="Bold"
               FontSize="Large"
               HorizontalOptions="Center"
               VerticalOptions="CenterAndExpand" />

        <Slider x:Name="slider"
                Maximum="360"
                VerticalOptions="CenterAndExpand" />

        <Label BindingContext="{x:Reference slider}"
               Text="{Binding Value, StringFormat='The angle is {0:F0} degrees'}"
               FontAttributes="Bold"
               FontSize="Large"
               HorizontalOptions="Center"
               VerticalOptions="CenterAndExpand" />
    </StackLayout>
</ContentPage>

Sliderには、x:Referenceマークアップ拡張を使用して2つのLabelビューによって参照されるx:Name属性が含まれています。

x:Referenceバインディング拡張では、Nameという名前のプロパティを定義して、参照される要素(この場合はslider)の名前を設定します。しかし、x:Referenceマークアップ拡張を定義するReferenceExtensionクラスでは、明示的に必須ではないことを意味するNameContentProperty属性も定義されています。最初のx:Referenceには "Name ="が含まれていますが、2番目にはありません。

BindingContext="{x:Reference Name=slider}"

BindingContext="{x:Reference slider}"

Bindingマークアップ拡張機能自体には、BindingBaseクラスやBindingクラスと同様に、いくつかのプロパティがあります。BindingContentPropertyPathですが、パスがBindingマークアップ拡張の最初の項目である場合は、マークアップ拡張の "Path ="部分を省略できます。最初の例には "Path ="がありますが、2番目の例では省略しています。

Rotation="{Binding Path=Value}"

Text="{Binding Value, StringFormat='The angle is {0:F0} degrees'}"

プロパティは、すべて1行にすることも、複数行に分割することもできます。

Text="{Binding Value, 
               StringFormat='The angle is {0:F0} degrees'}"

都合のいいように書いてください。

2番目のBindingマークアップ拡張のStringFormatプロパティに注目してください。Xamarin.Formsでは、バインディングは暗黙の型変換を実行しません。文字列以外のオブジェクトを文字列として表示する必要がある場合は、型変換を提供するか、StringFormatを使用する必要があります。背後では、静的なString.Formatメソッドを使用してStringFormatを実装します。これは潜在的に問題です。.NETの書式指定には中括弧が含まれていますが、これはマークアップ拡張の区切りにも使用されるためです。これにより、XAMLパーサーを混乱させる危険があります。回避するには、書式設定文字列全体をシングルクオートで囲みます。

Text="{Binding Value, StringFormat='The angle is {0:F0} degrees'}"

実行中のプログラムは次のとおりです。

View-to-View Bindings

バインディングモード

1つビューは、複数のプロパティにデータバインディングを設定することができます。しかし、各ビューにはBindingContextが1つしかないため、そのビューの複数のデータバインディングはすべて同じオブジェクトのすべてのプロパティを参照する必要があります。

この問題やその他の問題を解決するために、BindingMode列挙体のメンバに設定されているModeプロパティがあります。

  • Default
  • OneWay — 値はソースからターゲットに転送されます
  • OneWayToSource — 値はターゲットからソースに転送されます
  • TwoWay — 値はソースとターゲットの間で双方向に転送されます

次のプログラムは、OneWayToSourceおよびTwoWayバインディングモードの一般的な使用方法の1つを示しています。4つのSliderビューは、ラベルのScaleRotateRotateXRotateYプロパティを制御するためのものです。最初は、Labelのこれら4つのプロパティがデータバインディングターゲットである必要があるように見えます。これは、それぞれがSliderによって設定されているためです。しかし、LabelBindingContextは1つのオブジェクトにすぎず、4つの異なるスライダがあります。

そのため、すべてのバインディングは一見逆方向に設定されています。4つのスライダーのそれぞれのBindingContextLabelに設定され、バインディングはスライダーのValueプロパティに設定されます。OneWayToSourceモードとTwoWayモードを使用することで、これらのValueプロパティで、LabelScaleRotateRotateXRotateYプロパティであるソースプロパティを設定できます。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XamlSamples.SliderTransformsPage"
             Padding="5"
             Title="Slider Transforms Page">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>

        <!-- Scaled and rotated Label -->
        <Label x:Name="label"
               Text="TEXT"
               HorizontalOptions="Center"
               VerticalOptions="CenterAndExpand" />

        <!-- Slider and identifying Label for Scale -->
        <Slider x:Name="scaleSlider"
                BindingContext="{x:Reference label}"
                Grid.Row="1" Grid.Column="0"
                Maximum="10"
                Value="{Binding Scale, Mode=TwoWay}" />

        <Label BindingContext="{x:Reference scaleSlider}"
               Text="{Binding Value, StringFormat='Scale = {0:F1}'}"
               Grid.Row="1" Grid.Column="1"
               VerticalTextAlignment="Center" />

        <!-- Slider and identifying Label for Rotation -->
        <Slider x:Name="rotationSlider"
                BindingContext="{x:Reference label}"
                Grid.Row="2" Grid.Column="0"
                Maximum="360"
                Value="{Binding Rotation, Mode=OneWayToSource}" />

        <Label BindingContext="{x:Reference rotationSlider}"
               Text="{Binding Value, StringFormat='Rotation = {0:F0}'}"
               Grid.Row="2" Grid.Column="1"
               VerticalTextAlignment="Center" />

        <!-- Slider and identifying Label for RotationX -->
        <Slider x:Name="rotationXSlider"
                BindingContext="{x:Reference label}"
                Grid.Row="3" Grid.Column="0"
                Maximum="360"
                Value="{Binding RotationX, Mode=OneWayToSource}" />

        <Label BindingContext="{x:Reference rotationXSlider}"
               Text="{Binding Value, StringFormat='RotationX = {0:F0}'}"
               Grid.Row="3" Grid.Column="1"
               VerticalTextAlignment="Center" />

        <!-- Slider and identifying Label for RotationY -->
        <Slider x:Name="rotationYSlider"
                BindingContext="{x:Reference label}"
                Grid.Row="4" Grid.Column="0"
                Maximum="360"
                Value="{Binding RotationY, Mode=OneWayToSource}" />

        <Label BindingContext="{x:Reference rotationYSlider}"
               Text="{Binding Value, StringFormat='RotationY = {0:F0}'}"
               Grid.Row="4" Grid.Column="1"
               VerticalTextAlignment="Center" />
    </Grid>
</ContentPage>

3つのSliderビューのバインディングはOneWayToSourceです。つまり、Sliderの値によって、そのBindingContextのプロパティが変更されます。それはlabelという名前のLabelです。これら3つのSliderビューは、LabelRotateRotateXRotateYプロパティを変更します。

しかし、ScaleプロパティのバインディングはTwoWayです。これは、Scaleプロパティのデフォルト値が1で、TwoWayバインディングを使用するとSliderの初期値が0ではなく1に設定されるためです。そのバインディングがもしOneWayToSourceの場合、ScaleプロパティはSliderのデフォルト値から最初は0に設定されます。ラベルは表示されず、ユーザーに混乱を招く可能性があります。

Backwards Bindings

バインディングとコレクション

テンプレート化されたListViewよりもXAMLとデータバインディングの機能が優れていることはまだ何も示されていません。

ListViewIEnumerable型のItemsSourceプロパティを定義し、そのコレクション内のアイテムを表示します。これらのアイテムは、どのタイプのオブジェクトでもかまいません。デフォルトでは、ListViewは各項目のToStringメソッドを使用してその項目を表示します。場合によってはこれが必要な場合もありますが、多くの場合、ToStringはオブジェクトの完全修飾クラス名のみを返します。

しかし、ListViewコレクションのアイテムは、Cellから派生したクラスを含むtemplateを使用して任意の方法で表示できます。テンプレートはListViewのすべてのアイテムに対して複製され、テンプレートに設定されているデータバインディングは個々の複製に転送されます。

非常に多くの場合、ViewCellクラスを使用してこれらの項目のカスタムセルを作成する必要があります。このプロセスはコードで行うとやや乱雑になりますが、XAMLでは非常に簡単に実現できます。

XamlSamplesプロジェクトにはNamedColorというクラスが含まれています。各NamedColorオブジェクトは、文字列型のNameプロパティとFriendlyNameプロパティ、およびColor型のColorプロパティを持ちます。さらに、NamedColorには、Xamarin.FormsのColorクラスで定義された色に対応する、Color型の静的読み取り専用フィールドが141あります。静的コンストラクタは、これらの静的フィールドに対応するNamedColorオブジェクトを含むIEnumerableコレクションを作成し、そのpublic static Allプロパティに割り当てます。

静的なNamedColor.AllプロパティをListViewItemsSourceに設定するには、x:Staticマークアップ拡張を使用するのが簡単です。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XamlSamples;assembly=XamlSamples"
             x:Class="XamlSamples.ListViewDemoPage"
             Title="ListView Demo Page">

    <ListView ItemsSource="{x:Static local:NamedColor.All}" />

</ContentPage>

結果を表示すると、各項目が本当にXamlSamples.NamedColor型であることを確認できます。

Binding to a Collection

情報は多くありませんが、ListViewはスクロール可能で選択可能です。

アイテムのテンプレートを定義するには、ItemTemplateプロパティをプロパティ要素として分割し、DataTemplateに設定してから、ViewCellを参照する必要があります。ViewCellViewプロパティには、各アイテムを表示するための1つ以上のビューのレイアウトを定義することができます。ここに簡単な例があります。

<ListView ItemsSource="{x:Static local:NamedColor.All}">
    <ListView.ItemTemplate>
        <DataTemplate>
            <ViewCell>
                <ViewCell.View>
                    <Label Text="{Binding FriendlyName}" />
                </ViewCell.View>
            </ViewCell>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

Label要素は、ViewCellViewプロパティに設定されています。 ViewプロパティはViewCellのコンテンツプロパティであるため、ViewCell.Viewタグは不要です。このマークアップは、各NamedColorオブジェクトのFriendlyNameプロパティを表示します。

Binding to a Collection with a DataTemplate

改善されましたね。今必要なのは、詳細情報と実際の色を使ってアイテムテンプレートを飾ることだけです。このテンプレートをサポートするために、ページのリソースディクショナリーにいくつかの値とオブジェクトが定義されています。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XamlSamples"
             x:Class="XamlSamples.ListViewDemoPage"
             Title="ListView Demo Page">

    <ContentPage.Resources>
        <ResourceDictionary>
            <OnPlatform x:Key="boxSize"
                        x:TypeArguments="x:Double">
                <On Platform="iOS, Android, UWP" Value="50" />
            </OnPlatform>

            <OnPlatform x:Key="rowHeight"
                        x:TypeArguments="x:Int32">
                <On Platform="iOS, Android, UWP" Value="60" />
            </OnPlatform>

            <local:DoubleToIntConverter x:Key="intConverter" />

        </ResourceDictionary>
    </ContentPage.Resources>

    <ListView ItemsSource="{x:Static local:NamedColor.All}">
        <ListView.ItemTemplate>
            <DataTemplate>
                <ViewCell>
                    <StackLayout Padding="5, 5, 0, 5"
                                 Orientation="Horizontal"
                                 Spacing="15">

                        <BoxView WidthRequest="{StaticResource boxSize}"
                                 HeightRequest="{StaticResource boxSize}"
                                 Color="{Binding Color}" />

                        <StackLayout Padding="5, 0, 0, 0"
                                     VerticalOptions="Center">

                            <Label Text="{Binding FriendlyName}"
                                   FontAttributes="Bold"
                                   FontSize="Medium" />

                            <StackLayout Orientation="Horizontal"
                                         Spacing="0">
                                <Label Text="{Binding Color.R,
                                       Converter={StaticResource intConverter},
                                       ConverterParameter=255,
                                       StringFormat='R={0:X2}'}" />

                                <Label Text="{Binding Color.G,
                                       Converter={StaticResource intConverter},
                                       ConverterParameter=255,
                                       StringFormat=', G={0:X2}'}" />

                                <Label Text="{Binding Color.B,
                                       Converter={StaticResource intConverter},
                                       ConverterParameter=255,
                                       StringFormat=', B={0:X2}'}" />
                            </StackLayout>
                        </StackLayout>
                    </StackLayout>
                </ViewCell>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</ContentPage>

OnPlatformを使用してBoxViewのサイズとListView行の高さを定義することに注目してください。3つのプラットフォームの値はすべて同じですが、マークアップを他の値に合わせてディスプレイを微調整することも簡単にできます。

バインディング値のコンバーター

さきほどのListView Demo XAMLファイルには、Xamarin.Forms Color構造体の個々のRGBプロパティが表示されます。これらのプロパティはdouble型で、範囲は0〜1です。16進数の値を表示する場合は、 "X2"書式指定でStringFormatを使用するだけではできません。それは整数値だけで動作しますが、double値には255を掛ける必要があります。

この小さな問題は、バインディングコンバーターとも呼ばれるバリューコンバーターで解決されました。これは、IValueConverterインターフェースを実装するクラスのため、ConvertConvertBackという2つのメソッドがあります。Convertメソッドは、値がソースからターゲットに転送されるときに呼び出されます。ConvertBackメソッドは、OneWayToSourceまたはTwoWayバインディングでターゲットからソースへの転送に呼び出されます。

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

namespace XamlSamples
{
    class DoubleToIntConverter : IValueConverter
    {
        public object Convert(object value, Type targetType,
                              object parameter, CultureInfo culture)
        {
            double multiplier;

            if (!Double.TryParse(parameter as string, out multiplier))
                multiplier = 1;

            return (int)Math.Round(multiplier * (double)value);
        }

        public object ConvertBack(object value, Type targetType,
                                  object parameter, CultureInfo culture)
        {
            double divider;

            if (!Double.TryParse(parameter as string, out divider))
                divider = 1;

            return ((double)(int)value) / divider;
        }
    }
}

バインディングはソースからターゲットへの一方向のため、ConvertBackメソッドはこのプログラムでは役割を果たしません。

バインディングはバインディングコンバーターをConverterプロパティで参照します。バインディングコンバーターは、ConverterParameterプロパティで指定されたパラメーターを受け入れることもできます。汎用性を担保するために、このようにして乗数を指定します。バインディングコンバータは、コンバータパラメータに有効なdouble値をチェックします。

コンバータはリソースディクショナリーでインスタンス化されるため、複数のバインディング間で共有できます。

<local:DoubleToIntConverter x:Key="intConverter" />

3つのデータバインディングがこの1つのインスタンスを参照します。Bindingマークアップ拡張に埋め込みStaticResourceマークアップ拡張が含まれていることに注目してください。

<Label Text="{Binding Color.R,
                      Converter={StaticResource intConverter},
                      ConverterParameter=255,
                      StringFormat='R={0:X2}'}" />

こちらが結果です。

Binding to a Collection with a DataTemplate and Converters

ListViewは、基になるデータで動的に発生する可能性のある変更を処理するのにかなり洗練されていますが、特定の手順を実行する場合にのみ必要です。ListViewItemsSourceプロパティに割り当てられたアイテムのコレクションが実行時に変更された場合、つまりコレクションにアイテムが追加または削除され得る場合は、これらのアイテムのObservableCollectionクラスを使用します。ObservableCollectionINotifyCollectionChangedインターフェースを実装し、ListViewCollectionChangedイベントのハンドラをインストールします。

実行時にアイテム自体のプロパティが変更された場合、コレクション内のアイテムはINotifyPropertyChangedインターフェースを実装し、PropertyChangedイベントを使用してプロパティ値の変更を通知する必要があります。これはこのシリーズの次のパートで説明されています。パート5 データバインディングからMVVMへ

まとめ

データバインディングは、ページ内の2つのオブジェクト間、またはビジュアルオブジェクトと元になるデータ間でプロパティをリンクする強力なメカニズムを提供します。しかし、アプリケーションがデータソースの処理を開始すると、著名なアプリケーションアーキテクチャパターンが有用なパラダイムとして浮上し始めます。これについては、パート5 データバインディングからMVVMへを参照してください。

パート5 データバインディングからMVVMへ

Model-View-ViewModelアーキテクチャの概要

Model-View-ViewModel(MVVM)アーキテクチャパターンは、XAMLを念頭に置いて考案されました。このパターンソフトウェアの階層を3つに分離します。ビューと呼ばれるXAMLユーザインターフェース、Modelと呼ばれる基になるデータ、ViewModelと呼ばれるViewとModelの間の仲介者です。ViewとViewModelは、多くの場合、XAMLファイルで定義されたデータバインディングを介して接続されます。ViewのBindingContextは通常、ViewModelのインスタンスです。

シンプルなViewModel

ViewModelsの導入として、最初にViewModelsが1つもないプログラムを見てみましょう。これまでに、新しいXML名前空間宣言を定義して、XAMLファイルが他のアセンブリのクラスを参照できるようにする方法を見てきました。SystemネームスペースのXML名前空間宣言を定義するプログラムは次のとおりです。

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

このプログラムでは、x:Staticを使用して静的なDateTime.Nowプロパティから現在の日付と時刻を取得し、そのDateTime値をStackLayoutBindingContextに設定できます。

<StackLayout BindingContext="{x:Static sys:DateTime.Now}" >

BindingContextは非常に特殊なプロパティです。要素にBindingContextを設定すると、その要素のすべての子によって継承されます。つまり、StackLayoutのすべての子には同じBindingContextがあり、そのオブジェクトのプロパティへの単純なバインディングを含めることができます。One-Shot DateTimeプログラムでは、2つの子にDateTime値のプロパティへのバインディングが含まれていますが、他の2つの子にはバインディングパスがないように見えます。これは、DateTime値自体がStringFormatに使用されていることを意味します。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:sys="clr-namespace:System;assembly=mscorlib"
             x:Class="XamlSamples.OneShotDateTimePage"
             Title="One-Shot DateTime Page">

    <StackLayout BindingContext="{x:Static sys:DateTime.Now}"
                 HorizontalOptions="Center"
                 VerticalOptions="Center">

        <Label Text="{Binding Year, StringFormat='The year is {0}'}" />
        <Label Text="{Binding StringFormat='The month is {0:MMMM}'}" />
        <Label Text="{Binding Day, StringFormat='The day is {0}'}" />
        <Label Text="{Binding StringFormat='The time is {0:T}'}" />

    </StackLayout>
</ContentPage>

もちろん、大きな問題は、ページが最初に作成されたときに日付と時刻が一度設定され、変更されないことです。

View Displaying Date and Time

XAMLファイルには、現在の時刻を常に示す時計が表示されますが、手助けするためのコードが必要です。MVVMの観点から考えると、ModelとViewModelは完全にコードで記述されたクラスです。Viewは、多くの場合、データバインディングを通じてViewModelで定義されたプロパティを参照するXAMLファイルです。

適切に設計すると、ModelはViewModelを知らず、ViewModelはViewをしらない状態になります。しかし、非常に多くの場合、プログラマは、ViewModelによって公開されるデータ型を、特定のユーザインターフェースに関連付けられたデータ型に調整します。たとえば、モデルが8ビット文字のASCII文字列を含むデータベースにアクセスする場合、ViewModelは、ユーザインターフェースでのUnicodeの排他使用に対応するために、これらの文字列をUnicode文字列に変換する必要があります。

MVVMの単純な例(ここに示すようなもの)では、モデルはまったくなく、パターンにはデータバインディングとリンクされたViewModelだけが含まれます。

ここでは、DateTimeという名前のプロパティを持つ時計用のViewModelがありますが、毎秒DateTimeプロパティが更新されます。

using System;
using System.ComponentModel;
using Xamarin.Forms;

namespace XamlSamples
{
    class ClockViewModel : INotifyPropertyChanged
    {
        DateTime dateTime;

        public event PropertyChangedEventHandler PropertyChanged;

        public ClockViewModel()
        {
            this.DateTime = DateTime.Now;

            Device.StartTimer(TimeSpan.FromSeconds(1), () =>
                {
                    this.DateTime = DateTime.Now;
                    return true;
                });
        }

        public DateTime DateTime
        {
            set
            {
                if (dateTime != value)
                {
                    dateTime = value;

                    if (PropertyChanged != null)
                    {
                        PropertyChanged(this, new PropertyChangedEventArgs("DateTime"));
                    }
                }
            }
            get
            {
                return dateTime;
            }
        }
    }
}

ViewModelsは一般的にINotifyPropertyChangedインタフェースを実装しています。つまり、プロパティのいずれかが変更されたときにクラスがPropertyChangedイベントを発生させます。Xamarin.Formsのデータバインディングメカニズムは、このPropertyChangedイベントにハンドラを添付します。したがって、プロパティが変更されたときに通知を受けて、ターゲットを新しい値で更新し続けることができます。

このViewModelに基づく時計は、次のように簡単にすることができます。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XamlSamples;assembly=XamlSamples"
             x:Class="XamlSamples.ClockPage"
             Title="Clock Page">

    <Label Text="{Binding DateTime, StringFormat='{0:T}'}"
           FontSize="Large"
           HorizontalOptions="Center"
           VerticalOptions="Center">
        <Label.BindingContext>
            <local:ClockViewModel />
        </Label.BindingContext>
    </Label>
</ContentPage>

ClockViewModelがプロパティ要素タグを使用してLabelBindingContextにどのように設定されているかに注目してください。また、ClockViewModelResourcesコレクションでインスタンス化し、StaticResourceマークアップ拡張を介してBindingContextに設定することもできます。コードビハインドファイルでもViewModelをインスタンス化できます。

LabelTextプロパティのBindingマークアップ拡張は、DateTimeプロパティをフォーマットします。このように表示されます。

View Displaying Date and Time via ViewModel

プロパティをピリオドで区切って、ViewModelのDateTimeプロパティの個々のプロパティにアクセスすることもできます。

<Label Text="{Binding DateTime.Second, StringFormat='{0}'}"  >

インタラクティブMVVM

MVVMは、もとになるデータモデルに基づくインタラクティブビューの双方向データバインディングでよく使用されます。

Color値をHueSaturationLuminosityの値に変換するHslViewModelというクラスがあります。その逆も同様です。

using System;
using System.ComponentModel;
using Xamarin.Forms;

namespace XamlSamples
{
    public class HslViewModel : INotifyPropertyChanged
    {
        double hue, saturation, luminosity;
        Color color;

        public event PropertyChangedEventHandler PropertyChanged;

        public double Hue
        {
            set
            {
                if (hue != value)
                {
                    hue = value;
                    OnPropertyChanged("Hue");
                    SetNewColor();
                }
            }
            get
            {
                return hue;
            }
        }

        public double Saturation
        {
            set
            {
                if (saturation != value)
                {
                    saturation = value;
                    OnPropertyChanged("Saturation");
                    SetNewColor();
                }
            }
            get
            {
                return saturation;
            }
        }

        public double Luminosity
        {
            set
            {
                if (luminosity != value)
                {
                    luminosity = value;
                    OnPropertyChanged("Luminosity");
                    SetNewColor();
                }
            }
            get
            {
                return luminosity;
            }
        }

        public Color Color
        {
            set
            {
                if (color != value)
                {
                    color = value;
                    OnPropertyChanged("Color");

                    Hue = value.Hue;
                    Saturation = value.Saturation;
                    Luminosity = value.Luminosity;
                }
            }
            get
            {
                return color;
            }
        }

        void SetNewColor()
        {
            Color = Color.FromHsla(Hue, Saturation, Luminosity);
        }

        protected virtual void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

HueSaturationLuminosityのプロパティを変更すると、Colorプロパティが変更され、Colorを変更すると、他の3つのプロパティが変更されます。これは、プロパティが実際に変更されない限りクラスがPropertyChangedイベントを呼び出さないことを除いて、無限ループのように見えるかもしれません。そうでなけば制御不能なフィードバックループを終了させます。

次のXAMLファイルには、ColorプロパティがViewModelのColorプロパティにバインドされたBoxViewと、HueSaturationLuminosityプロパティにバインドされた3つのSliderおよび3つのLabelビューが含まれています。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XamlSamples;assembly=XamlSamples"
             x:Class="XamlSamples.HslColorScrollPage"
             Title="HSL Color Scroll Page">
    <ContentPage.BindingContext>
        <local:HslViewModel Color="Aqua" />
    </ContentPage.BindingContext>

    <StackLayout Padding="10, 0">
        <BoxView Color="{Binding Color}"
                 VerticalOptions="FillAndExpand" />

        <Label Text="{Binding Hue, StringFormat='Hue = {0:F2}'}"
               HorizontalOptions="Center" />

        <Slider Value="{Binding Hue, Mode=TwoWay}" />

        <Label Text="{Binding Saturation, StringFormat='Saturation = {0:F2}'}"
               HorizontalOptions="Center" />

        <Slider Value="{Binding Saturation, Mode=TwoWay}" />

        <Label Text="{Binding Luminosity, StringFormat='Luminosity = {0:F2}'}"
               HorizontalOptions="Center" />

        <Slider Value="{Binding Luminosity, Mode=TwoWay}" />
    </StackLayout>
</ContentPage>

Labelのバインディングは、デフォルトのOneWayです。値を表示するのにだけ必要です。しかし、各SliderのバインディングはTwoWayです。これにより、SliderをViewModelから初期化することができます。ViewModelがインスタンス化されると、ColorプロパティがBlueに設定されていることに注意してください。しかし、Sliderを変更すると、ViewModelのプロパティの新しい値を設定する必要があり、ViewModelは新しい色を計算します。

MVVM using Two-Way Data Bindings

ViewModelでのコマンド

多くの場合、MVVMパターンはデータ項目の操作に制限されています。ViewのユーザインターフェースオブジェクトはViewModelのデータオブジェクトに対応しています。

しかし、ViewにViewModelのさまざまなアクションをトリガーするボタンが含まれている必要があることがあります。しかし、ViewModelにはボタンのClickedハンドラが含まれてはいけません。これは、ViewModelを特定のユーザインターフェースのパラダイムに結びつけてしまうためです。

ViewModelを特定のユーザインターフェースオブジェクトからより独立させ、かつViewModel内でメソッドを呼び出せるようにするために、commandインタフェースが存在します。このコマンドインタフェースは、Xamarin.Formsの以下の要素でサポートされています。

  • Button
  • MenuItem
  • ToolbarItem
  • SearchBar
  • TextCell (と ImageCell)
  • ListView
  • TapGestureRecognizer

SearchBarListView要素を除いて、これらの要素は2つのプロパティを定義します。

  • System.Windows.Input.ICommand型のCommand
  • Object型のCommandParameter

SearchBarSearchCommandSearchCommandParameterプロパティを定義し、ListViewICommand型RefreshCommandプロパティを定義します。

ICommandインターフェースは、2つのメソッドと1つのイベントを定義します。

  • void Execute(object arg)
  • bool CanExecute(object arg)
  • event EventHandler CanExecuteChanged

ViewModelは、ICommand型のプロパティを定義できます。これらのプロパティは、各Buttonやその他の要素のCommandプロパティ、またはおそらくこのインターフェースを実装するカスタムビューにバインドできます。オプションで、このViewModelプロパティにバインドされている個々のButtonオブジェクト(またはその他の要素)を識別するためにCommandParameterプロパティを設定できます。内部的には、ユーザーがButtonをタップしてCommandParameterExecuteメソッドに渡すと、ButtonExecuteメソッドを呼び出します。

CanExecuteメソッドとCanExecuteChangedイベントは、Buttonタップが現在無効である場合に使用されます。この場合、Button自体を無効にする必要があります。Buttonは、Commandプロパティが最初に設定され、CanExecuteChangedイベントが発生するたびにCanExecuteを呼び出します。CanExecutefalseを返すと、Buttonは自身を無効にし、Execute呼び出しを生成しません。

ViewModelにコマンドを追加する際の助けとして、Xamarin.FormsはICommandを実装する2つのクラスを定義しています。CommandCommandTExecuteCanExecuteの引数の型)です。これらの2つのクラスは、いくつかのコンストラクタと、ViewModelがCanExecuteChangedイベントを発生させるCommandオブジェクトを強制的に呼び出すことができるChangeCanExecuteメソッドを定義します。

電話番号を入力するための単純なキーパッドのViewModelがあります。ExecuteメソッドとCanExecuteメソッドは、コンストラクタ内でラムダ関数として定義されていることに注意してください。

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

namespace XamlSamples
{
    class KeypadViewModel : INotifyPropertyChanged
    {
        string inputString = "";
        string displayText = "";
        char[] specialChars = { '*', '#' };

        public event PropertyChangedEventHandler PropertyChanged;

        // Constructor
        public KeypadViewModel()
        {
            AddCharCommand = new Command<string>((key) =>
                {
                    // Add the key to the input string.
                    InputString += key;
                });

            DeleteCharCommand = new Command(() =>
                {
                    // Strip a character from the input string.
                    InputString = InputString.Substring(0, InputString.Length - 1);
                },
                () =>
                {
                    // Return true if there's something to delete.
                    return InputString.Length > 0;
                });
        }

        // Public properties
        public string InputString
        {
            protected set
            {
                if (inputString != value)
                {
                    inputString = value;
                    OnPropertyChanged("InputString");
                    DisplayText = FormatText(inputString);

                    // Perhaps the delete button must be enabled/disabled.
                    ((Command)DeleteCharCommand).ChangeCanExecute();
                }
            }

            get { return inputString; }
        }

        public string DisplayText
        {
            protected set
            {
                if (displayText != value)
                {
                    displayText = value;
                    OnPropertyChanged("DisplayText");
                }
            }
            get { return displayText; }
        }

        // ICommand implementations
        public ICommand AddCharCommand { protected set; get; }

        public ICommand DeleteCharCommand { protected set; get; }

        string FormatText(string str)
        {
            bool hasNonNumbers = str.IndexOfAny(specialChars) != -1;
            string formatted = str;

            if (hasNonNumbers || str.Length < 4 || str.Length > 10)
            {
            }
            else if (str.Length < 8)
            {
                formatted = String.Format("{0}-{1}",
                                          str.Substring(0, 3),
                                          str.Substring(3));
            }
            else
            {
                formatted = String.Format("({0}) {1}-{2}",
                                          str.Substring(0, 3),
                                          str.Substring(3, 3),
                                          str.Substring(6));
            }
            return formatted;
        }

        protected void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

このViewModelは、AddCharCommandプロパティがCommandParameterによって識別されるいくつかのボタン(またはコマンドインターフェースを持つもの)のCommandプロパティにバインドされていることを前提としています。これらのボタンは、InputStringプロパティに文字を追加し、DisplayTextプロパティの電話番号として書式設定されます。

DeleteCharCommandという名前のICommand型の2番目のプロパティもあります。これはバックスペースボタンにバインドされていますが、削除する文字がない場合はボタンを無効にする必要があります。

次のキーパッドは視覚的に洗練されていません。その代わりに、マークアップはコマンド・インターフェースの使用をより明確に示すために最小限に抑えられています。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XamlSamples;assembly=XamlSamples"
             x:Class="XamlSamples.KeypadPage"
             Title="Keypad Page">

    <Grid HorizontalOptions="Center"
          VerticalOptions="Center">
        <Grid.BindingContext>
            <local:KeypadViewModel />
        </Grid.BindingContext>

        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="80" />
            <ColumnDefinition Width="80" />
            <ColumnDefinition Width="80" />
        </Grid.ColumnDefinitions>

        <!-- Internal Grid for top row of items -->
        <Grid Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>

            <Frame Grid.Column="0"
                   OutlineColor="Accent">
                <Label Text="{Binding DisplayText}" />
            </Frame>

            <Button Text="⇦"
                    Command="{Binding DeleteCharCommand}"
                    Grid.Column="1"
                    BorderWidth="0" />
        </Grid>

        <Button Text="1"
                Command="{Binding AddCharCommand}"
                CommandParameter="1"
                Grid.Row="1" Grid.Column="0" />

        <Button Text="2"
                Command="{Binding AddCharCommand}"
                CommandParameter="2"
                Grid.Row="1" Grid.Column="1" />

        <Button Text="3"
                Command="{Binding AddCharCommand}"
                CommandParameter="3"
                Grid.Row="1" Grid.Column="2" />

        <Button Text="4"
                Command="{Binding AddCharCommand}"
                CommandParameter="4"
                Grid.Row="2" Grid.Column="0" />

        <Button Text="5"
                Command="{Binding AddCharCommand}"
                CommandParameter="5"
                Grid.Row="2" Grid.Column="1" />

        <Button Text="6"
                Command="{Binding AddCharCommand}"
                CommandParameter="6"
                Grid.Row="2" Grid.Column="2" />

        <Button Text="7"
                Command="{Binding AddCharCommand}"
                CommandParameter="7"
                Grid.Row="3" Grid.Column="0" />

        <Button Text="8"
                Command="{Binding AddCharCommand}"
                CommandParameter="8"
                Grid.Row="3" Grid.Column="1" />

        <Button Text="9"
                Command="{Binding AddCharCommand}"
                CommandParameter="9"
                Grid.Row="3" Grid.Column="2" />

        <Button Text="*"
                Command="{Binding AddCharCommand}"
                CommandParameter="*"
                Grid.Row="4" Grid.Column="0" />

        <Button Text="0"
                Command="{Binding AddCharCommand}"
                CommandParameter="0"
                Grid.Row="4" Grid.Column="1" />

        <Button Text="#"
                Command="{Binding AddCharCommand}"
                CommandParameter="#"
                Grid.Row="4" Grid.Column="2" />
    </Grid>
</ContentPage>

このマークアップに表示される最初のButtonCommandプロパティは、DeleteCharCommandにバインドされています。残りはボタン面に表示される文字と同じCommandParameterを持つAddCharCommandにバインドされます。プログラムの実行結果は次のとおりです。

Calculator using MVVM and Commands

非同期メソッドの呼び出し

コマンドは、非同期メソッドを呼び出すこともできます。これは、Executeメソッドを指定するときにasyncキーワードとawaitキーワードを使用することによって実現されます。

DownloadCommand = new Command (async () => await DownloadAsync ());

これは、DownloadAsyncメソッドがTaskであり、待機する必要があることを示します。

async Task DownloadAsync ()
{
    await Task.Run (() => Download ());
}

void Download ()
{
    ...
}

ナビゲーションメニューの実装

この一連の記事のすべてのソースコードを含むXamlSamplesプログラムは、そのホームページにViewModelを使用します。このViewModelは、各サンプルページのタイプ、タイトル、および短い説明を含むTypeTitleDescriptionという3つのプロパティを持つ短いクラスの定義です。さらに、ViewModelは、プログラム内のすべてのページのコレクションであるAllという名前の静的プロパティを定義します。

public class PageDataViewModel
{
    public PageDataViewModel(Type type, string title, string description)
    {
        Type = type;
        Title = title;
        Description = description;
    }

    public Type Type { private set; get; }

    public string Title { private set; get; }

    public string Description { private set; get; }

    static PageDataViewModel()
    {
        All = new List<PageDataViewModel>
        {
            // Part 1. Getting Started with XAML
            new PageDataViewModel(typeof(HelloXamlPage), "Hello, XAML",
                                  "Display a Label with many properties set"),

            new PageDataViewModel(typeof(XamlPlusCodePage), "XAML + Code",
                                  "Interact with a Slider and Button"),

            // Part 2. Essential XAML Syntax
            new PageDataViewModel(typeof(GridDemoPage), "Grid Demo",
                                  "Explore XAML syntax with the Grid"),

            new PageDataViewModel(typeof(AbsoluteDemoPage), "Absolute Demo",
                                  "Explore XAML syntax with AbsoluteLayout"),

            // Part 3. XAML Markup Extensions
            new PageDataViewModel(typeof(SharedResourcesPage), "Shared Resources",
                                  "Using resource dictionaries to share resources"),

            new PageDataViewModel(typeof(StaticConstantsPage), "Static Constants",
                                  "Using the x:Static markup extensions"),

            new PageDataViewModel(typeof(RelativeLayoutPage), "Relative Layout",
                                  "Explore XAML markup extensions"),

            // Part 4. Data Binding Basics
            new PageDataViewModel(typeof(SliderBindingsPage), "Slider Bindings",
                                  "Bind properties of two views on the page"),

            new PageDataViewModel(typeof(SliderTransformsPage), "Slider Transforms",
                                  "Use Sliders with reverse bindings"),

            new PageDataViewModel(typeof(ListViewDemoPage), "ListView Demo",
                                  "Use a ListView with data bindings"),

            // Part 5. From Data Bindings to MVVM
            new PageDataViewModel(typeof(OneShotDateTimePage), "One-Shot DateTime",
                                  "Obtain the current DateTime and display it"),

            new PageDataViewModel(typeof(ClockPage), "Clock",
                                  "Dynamically display the current time"),

            new PageDataViewModel(typeof(HslColorScrollPage), "HSL Color Scroll",
                                  "Use a view model to select HSL colors"),

            new PageDataViewModel(typeof(KeypadPage), "Keypad",
                                  "Use a view model for numeric keypad logic")
        };
    }

    public static IList<PageDataViewModel> All { private set; get; }
}

MainPageのXAMLファイルは、ItemsSourceプロパティがAllプロパティに設定され、各ページのTitleプロパティとDescriptionプロパティを表示するTextCellを含むListBoxを定義します。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XamlSamples"
             x:Class="XamlSamples.MainPage"
             Padding="5, 0"
             Title="XAML Samples">

    <ListView ItemsSource="{x:Static local:PageDataViewModel.All}"
              ItemSelected="OnListViewItemSelected">
        <ListView.ItemTemplate>
            <DataTemplate>
                <TextCell Text="{Binding Title}"
                          Detail="{Binding Description}" />
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</ContentPage>

ページはスクロール可能なリストで表示されます。

Scrollable list of pages

コードビハインドファイルのハンドラは、ユーザが項目を選択するとトリガされます。ハンドラはListBoxSelectedItemプロパティをnullに戻し、選択したページをインスタンス化してそのページに移動します。

private async void OnListViewItemSelected(object sender, SelectedItemChangedEventArgs args)
{
    (sender as ListView).SelectedItem = null;

    if (args.SelectedItem != null)
    {
        PageDataViewModel pageData = args.SelectedItem as PageDataViewModel;
        Page page = (Page)Activator.CreateInstance(pageData.Type);
        await Navigation.PushAsync(page);
    }
}

まとめ

XAMLは、特にデータバインディングとMVVMを使用する場合に、Xamarin.Formsアプリケーションでユーザインターフェースを定義するための強力なツールです。その結果、きれいで、エレガントで、ツールで表現可能なユーザインターフェースが表現され、コード内のすべてのバックグラウンドサポートが得られます。

XAMLコンパイル

XAMLコンパイラを使用してXamarin.Formsアプリケーションのパフォーマンスを向上させる

XAMLは、XAMLコンパイラ(XAMLC)で中間言語(IL)に直接コンパイルすることもできます。

XAMLCには多くの利点があります。

  • XAMLのコンパイル時チェックを実行し、ユーザーにエラーを通知します。
  • XAML要素のロードおよびインスタンス化時間の一部を削減します。
  • .xamlファイルを含まなくなることで、最終アセンブリのファイルサイズを縮小するのに役立ちます。

下位互換性を確保するため、XAMLCはデフォルトで無効になっています。XamlCompilation属性を追加することで、アセンブリレベルとクラスレベルの両方で有効にすることができます。

次のコード例は、アセンブリレベルでXAMLCを有効にする方法を示しています。

using Xamarin.Forms.Xaml;
...
[assembly: XamlCompilation (XamlCompilationOptions.Compile)]
namespace PhotoApp
{
  ...
}

この例では、PhotoApp名前空間に含まれるすべてのXAMLのコンパイル時チェックが実行され、実行時ではなくコンパイル時にXAMLエラーが報告されます。XamlCompilation属性のassemblyプレフィックスは、属性がアセンブリ全体に適用されることを指定します。

次のコード例は、クラスレベルでXAMLCを有効にする方法を示しています。

using Xamarin.Forms.Xaml;
...
[XamlCompilation (XamlCompilationOptions.Compile)]
public class HomePage : ContentPage
{
  ...
}

この例では、HomePageクラスのXAMLのコンパイル時チェックが実行され、コンパイルプロセスの一部としてエラーがレポートされます。

XamlCompilationOptions列挙体は、Xamarin.Forms.Xaml名前空間にあり、使用するためにインポートする必要があります。

Xamarin.Forms向けのXAMLプレビューア

タイピング時にレンダリングされるXamarin.Formsレイアウトを見てみましょう

要件

プロジェクトには、XAMLプレビューアが機能するための最新のXamarin.Forms NuGetパッケージが必要です。Androidアプリをプレビューするには、JDK 1.8 x64が必要です。

リリースノートにはさらに詳しい情報があります。

入門

MacでVisual Studio for Macを使用する場合

Previewボタンは、XAMLファイルを右クリックし、Open With > XAML Viewerを選択することで、エディタに表示できます。 XAMLドキュメントウィンドウの右上隅にあるPreviewボタンを押すと、プレビューウィンドウが表示または非表示になります。

ListView control preview in Visual Studio for Mac

WindowsでVisual Studioを使用する場合

Visual StudioのView > Other Windows > Xamarin.Forms Previewerメニューを選択して、プレビューウィンドウを開きます。Window > New Vertical Tab Groupメニューを使ってサイドバイサイドで配置します。

ListView control preview in Visual Studio

XAMLプレビューオプション

プレビューペインの上部にあるオプションは次のとおりです。

  • Phone – 電話機サイズの画面でレンダリングします
  • Tablet – タブレットサイズのスクリーンでレンダリングします(ペインの右下にズームコントロールがあります)
  • Android – Androidバージョンの画面を表示します
  • iOS – iOSバージョンの画面を表示します
  • Portait (アイコン) – プレビューに縦向きを使用します
  • Landscape (アイコン) – プレビューに横向きを使用します

デザイン時データの追加

一部のレイアウトは、ユーザインターフェースコントロールにバインドされたデータなしでは視覚化しにくい場合があります。プレビューをより便利にするには、コードビハインドまたはXAMLを使用してバインディングコンテキストをハードコードすることによって、いくつかの静的データをコントロールに割り当てます。

XAMLの静的なViewModelにバインドする方法については、James Montemagnoのブログ記事のデザイン時データの追加を参照してください。

トラブルシューティング

問題が発生した場合は、以下のissueとXamarin Forumsを確認してください。

XAMLプレビューが表示されない

以下を確認してください。

  • XAMLファイルをプレビューする前にプロジェクトをビルド(コンパイル)する必要があります。
  • デザイナーエージェントは、XAMLファイルを初めてプレビューするときにセットアップする必要があります。準備が整うまで、進行状況インジケータが進行状況メッセージと共にプレビューアに表示されます。
  • XAMLファイルを閉じてもう一度開きます。

無効なXAML:プレビューを作成する前にAndroidプロジェクトをビルドする必要があります

XAMLプレビューアでは、ページを表示する前にプロジェクトをビルドする必要があります。プレビューペインの上部に以下のエラーが表示された場合は、アプリケーションをリビルドしてもう一度やり直してください。

Error message: project must be built first

XAML名前空間

型を参照するためのXAML名前空間宣言

XAMLは、名前空間宣言にxmlns XML属性を使用します。この、記事では、XAML名前空間の構文を紹介し、型にアクセスするためにXAML名前空間を宣言する方法を示します。

概要

XAMLファイルのルート要素内には常に2つのXAML名前空間宣言があります。1つ目は、次のXAMLコードの例に示すように、デフォルトの名前空間を定義します。

xmlns="http://xamarin.com/schemas/2014/forms"

デフォルトの名前空間では、XAMLファイル内で定義されたプレフィックスのない要素は、ContentPageなどのXamarin.Formsクラスを参照します。

2番目の名前空間宣言では、次のXAMLコードの例に示すように、xプレフィックスが使用されます。

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

XAMLは、プレフィックスを使用して、デフォルト以外の名前空間を宣言します。プレフィックスは、名前空間内の型を参照するときに使用されます。x名前空間宣言は、プレフィックスxを持つXAML内で定義された要素が、XAMLに固有の要素および属性(特に2009 XAML仕様)に使用されることを指定します。

次の表は、Xamarin.Formsでサポートされているx名前空間の属性の概要を示しています。

構成 説明
x:Arguments デフォルト以外のコンストラクタ、またはファクトリメソッドオブジェクト宣言のコンストラクタ引数を指定します。
x:Class XAMLで定義されたクラスの名前空間とクラス名を指定します。クラス名はコードビハインドファイルのクラス名と一致する必要があります。この構文は、XAMLファイルのルート要素にのみ記述できます。
x:FactoryMethod オブジェクトを初期化するために使用できるファクトリメソッドを指定します。
x:Key ResourceDictionary内の各リソースの一意のユーザー定義キーを指定します。キーの値はXAMLリソースを取得するために使用され、通常はStaticResourceマークアップ拡張の引数として使用されます。
x:Name XAML要素の実行時オブジェクト名を指定します。x:Nameの設定は、コード内で変数を宣言するのと似ています。
x:TypeArguments ジェネリック型のコンストラクタへのジェネリック型の引数を指定します。

x:Argumentsx:FactoryMethodx:TypeArgumentsの各属性の詳細については、XAMLでの引数の受け渡しを参照してください。

XAMLでは、名前空間宣言は親要素から子要素に継承されます。したがって、XAMLファイルのルート要素に名前空間を定義すると、そのファイル内のすべての要素が名前空間宣言を継承します。

型の名前空間宣言

XAMLで型を参照するには、接頭辞付きのXAML名前空間を宣言し、CLR(Common Language Runtime)名前空間名を指定する名前空間宣言と、オプションでアセンブリ名を指定します。これは、名前空間宣言内の次のキーワードの値を定義することによって実現されます。

  • clr-namespace:using: ー XAML要素として公開する型を含むアセンブリ内で宣言されたCLR名前空間。このキーワードは必須です。
  • assembly= ー 参照されるCLR名前空間を含むアセンブリ。この値は、ファイル拡張子のないアセンブリの名前です。アセンブリへのパスは、アセンブリを参照するXAMLファイルを含むプロジェクトファイル内の参照として設定する必要があります。clr-namespaceの値が型を参照するアプリケーションコードと同じアセンブリ内にある場合、このキーワードは省略できます。

clr-namespaceもしくはusingトークンと値を分離する文字はコロンですが、assemblyトークンとその値を分離する文字は等号です。2つのトークンの間に使用する文字はセミコロンです。

次のコード例は、XAML名前空間宣言を示しています。

<ContentPage ... xmlns:local="clr-namespace:HelloWorld" ...>
  ...
</ContentPage>

あるいは、これは次のように書くことができます。

<ContentPage ... xmlns:local="using:HelloWorld" ...>
  ...
</ContentPage>

localプレフィックスは、名前空間内の型がアプリケーションに対してローカルであることを示すために使用される規則です。型が別のアセンブリにある場合は、次のXAMLコードの例に示すように、アセンブリ名を名前空間宣言にも定義する必要があります。

<ContentPage ... xmlns:behaviors="clr-namespace:Behaviors;assembly=BehaviorsLibrary" ...>
  ...
</ContentPage>

次のXAMLコード例に示すように、インポートされた名前空間から型のインスタンスを宣言するときに、名前空間プレフィックスが指定されます。

<ListView ...>
  <ListView.Behaviors>
    <behaviors:EventToCommandBehavior EventName="ItemSelected" ... />
  </ListView.Behaviors>
</ListView>

まとめ

この記事では、XAML名前空間の構文を紹介し、型にアクセスするためにXAML名前空間を宣言する方法を示しました。XAMLは名前空間宣言にxmlns XML属性を使用し、プレフィックス付きのXAML名前空間を宣言することによってXAMLで型を参照できます。

XAMLでの引数の受け渡し

この記事では、デフォルト以外のコンストラクターに引数を渡したり、ファクトリーメソッドを呼び出したり、汎用引数の型を指定するために使用できるXAML属性の使用方法について説明します。

概要

引数を必要とするコンストラクタを使用してオブジェクトをインスタンス化するか、静的な作成メソッドを呼び出す必要があることがよくあります。これは、x:Argumentおよびx:FactoryMethod属性を使用してXAMLで実現できます。

  • x:Argument属性は、デフォルト以外のコンストラクタ、またはファクトリーメソッドオブジェクト宣言のコンストラクタ引数を指定するために使用されます。詳細については、コンストラクタ引数の受け渡しを参照してください。
  • x:FactoryMethod属性は、オブジェクトの初期化に使用できるファクトリメソッドを指定するために使用されます。詳細については、ファクトリーメソッドの呼び出しを参照してください。

さらに、x:TypeArguments属性を使用して、ジェネリック型のコンストラクタへのジェネリック型引数を指定できます。詳細については、ジェネリック型引数の指定を参照してください。

コンストラクタ引数の受け渡し

引数は、x:Argument属性を使用して、デフォルト以外のコンストラクタに渡すことができます。各コンストラクタ引数は、引数の型を表すXML要素内で区切られなければなりません。Xamarin.Formsは、基本型に対して次の要素をサポートしています。

  • x:Object
  • x:Boolean
  • x:Byte
  • x:Int16
  • x:Int32
  • x:Int64
  • x:Single
  • x:Double
  • x:Decimal
  • x:Char
  • x:String
  • x:TimeSpan
  • x:Array
  • x:DateTime

次のコード例は、x:Arguments属性と3つのColorコンストラクタを使用する方法を示しています。

<BoxView HeightRequest="150" WidthRequest="150" HorizontalOptions="Center">
  <BoxView.Color>
    <Color>
      <x:Arguments>
        <x:Double>0.9</x:Double>
      </x:Arguments>
    </Color>
  </BoxView.Color>
</BoxView>
<BoxView HeightRequest="150" WidthRequest="150" HorizontalOptions="Center">
  <BoxView.Color>
    <Color>
      <x:Arguments>
        <x:Double>0.25</x:Double>
        <x:Double>0.5</x:Double>
        <x:Double>0.75</x:Double>
      </x:Arguments>
    </Color>
  </BoxView.Color>
</BoxView>
<BoxView HeightRequest="150" WidthRequest="150" HorizontalOptions="Center">
  <BoxView.Color>
    <Color>
      <x:Arguments>
        <x:Double>0.8</x:Double>
        <x:Double>0.5</x:Double>
        <x:Double>0.2</x:Double>
        <x:Double>0.5</x:Double>
      </x:Arguments>
    </Color>
  </BoxView.Color>
</BoxView>

x:Argumentsタグ内の要素の数とこれらの要素の型は、Colorコンストラクタの1つと一致する必要があります。1つのパラメータを持つColorコンストラクタでは、0(黒)から1(白)までのグレースケール値が必要です。3つのパラメータを持つColorコンストラクタでは、0〜1の範囲の赤、緑、青の値が必要です。4つのパラメータを持つColorコンストラクタは、4つ目のパラメータとしてアルファチャンネルを追加します。

次のスクリーンショットは、指定された引数値を持つ各Colorコンストラクタを呼び出した結果を示しています。

BoxView.Color specified with x:Arguments

ファクトリーメソッドの呼び出し

ファクトリーメソッドは、x:FactoryMethod属性を使用してメソッドの名前を指定し、x:Arguments属性を使用して引数を指定することで、XAMLで呼び出すことができます。ファクトリーメソッドは、メソッドを定義するクラスまたは構造体と同じ型のオブジェクトまたは値を返すpublic staticメソッドです。

Color構造体にはいくつかのファクトリメソッドが定義されています。次のコード例は、これらの3つを呼び出す方法を示しています。

<BoxView HeightRequest="150" WidthRequest="150" HorizontalOptions="Center">
  <BoxView.Color>
    <Color x:FactoryMethod="FromRgba">
      <x:Arguments>
        <x:Int32>192</x:Int32>
        <x:Int32>75</x:Int32>
        <x:Int32>150</x:Int32>
        <x:Int32>128</x:Int32>
      </x:Arguments>
    </Color>
  </BoxView.Color>
</BoxView>
<BoxView HeightRequest="150" WidthRequest="150" HorizontalOptions="Center">
  <BoxView.Color>
    <Color x:FactoryMethod="FromHsla">
      <x:Arguments>
        <x:Double>0.23</x:Double>
        <x:Double>0.42</x:Double>
        <x:Double>0.69</x:Double>
        <x:Double>0.7</x:Double>
      </x:Arguments>
    </Color>
  </BoxView.Color>
</BoxView>
<BoxView HeightRequest="150" WidthRequest="150" HorizontalOptions="Center">
  <BoxView.Color>
    <Color x:FactoryMethod="FromHex">
      <x:Arguments>
        <x:String>#FF048B9A</x:String>
      </x:Arguments>
    </Color>
  </BoxView.Color>
</BoxView>

x:Argumentsタグ内の要素の数とこれらの要素の型は、呼び出されるファクトリーメソッドの引数と一致する必要があります。FromRgbaファクトリーメソッドには、赤、緑、青、およびアルファ値をそれぞれ表す0〜255の4つのInt32パラメータが必要です。FromHslaファクトリーメソッドは、色相、彩度、明度、およびアルファ値をそれぞれ表す0〜1の4つのDoubleパラメータを必要とします。FromHexファクトリーメソッドには、16進数(A)のRGBカラーを表すStringが必要です。

次のスクリーンショットは、指定された引数値を持つ各Colorファクトリーメソッドを呼び出した結果を示しています。

BoxView.Color specified with x:FactoryMethod and x:Arguments

ジェネリック型引数の指定

ジェネリック型のコンストラクタのジェネリック型引数は、次のコード例に示すように、x:TypeArguments属性を使用して指定できます。

<ContentPage ...>
  <StackLayout>
    <StackLayout.Margin>
      <OnPlatform x:TypeArguments="Thickness">
        <On Platform="iOS" Value="0,20,0,0" />
        <On Platform="Android" Value="5, 10" />
        <On Platform="WinPhone, Windows" Value="10" />
      </OnPlatform>
    </StackLayout.Margin>
  </StackLayout>
</ContentPage>

OnPlatformクラスはジェネリッククラスであり、ターゲットタイプと一致するx:TypeArguments属性でインスタンス化する必要があります。Onクラスでは、Platform属性は単一のstring値、または複数のカンマ区切りのstring値を受け入れることができます。この例では、StackLayout.Marginプロパティがプラットフォーム固有のThicknessに設定されています。

まとめ

この記事では、デフォルト以外のコンストラクターに引数を渡したり、ファクトリーメソッドを呼び出したり、ジェネリック引数の型を指定するために使用できるXAML属性の使用方法について説明しました。

バインダブルプロパティ

バインディングを許可するプロパティの作成

Xamarin.Formsでは、共通言語ランタイム(CLR)プロパティの機能がバインダブルプロパティによって拡張されています。バインダブルプロパティは特殊なタイプのプロパティです。プロパティの値はXamarin.Formsプロパティシステムによって追跡されます。この記事では、バインダブルプロパティの概要を紹介し、バインダブルプロパティを作成、使用する方法を示します。

概要

バインダブルプロパティは、プロパティをフィールドでサポートするのではなく、BindableProperty型でプロパティをサポートすることにより、CLRプロパティの機能を拡張します。バインダブルプロパティの目的は、親子関係で設定されたデータバインディング、スタイル、テンプレート、値をサポートするプロパティシステムを提供することです。さらに、バインダブルプロパティは、デフォルト値、プロパティ値の検証、プロパティの変更を監視するコールバックを提供できます。

プロパティは、以下の機能の1つ以上をサポートするために、バインダブルプロパティとして実装する必要があります。

  • データバインディングの有効なtargetプロパティとして機能
  • styleを使用してプロパティを設定
  • プロパティの型のデフォルトとは異なるデフォルトのプロパティー値の提供
  • プロパティの値の検証

Xamarin.Formsのバインダブルプロパティなプロパティの例には、Label.TextButton.BorderRadiusStackLayout.Orientationがあります。各バインダブルプロパティには、同じクラスで公開されるバインダブルプロパティの識別子であるBindableProperty型の対応するpublic static readonlyプロパティがあります。たとえば、Label.Textプロパティの対応するバインド可能なプロパティ識別子はLabel.TextPropertyです。

バインダブルプロパティの作成と使用

バインダブルプロパティを作成するプロセスは、次のとおりです。

  1. BindableProperty.Createメソッドオーバーロードのいずれかを使用してBindablePropertyインスタンスを作成します。
  2. BindablePropertyインスタンスのプロパティアクセサを定義します。

すべてのBindablePropertyインスタンスは、UIスレッドで作成する必要があります。これは、UIスレッドで実行されるコードだけがBindablePropertyの値を取得または設定できることを意味します。しかし、BindablePropertyインスタンスは、Device.BeginInvokeOnMainThreadメソッドを使用してUIスレッドにマーシャリングすることによって、他のスレッドからアクセスできます。

プロパティの作成

BindablePropertyインスタンスを作成するには、含まれるクラスがBindableObjectクラスから派生する必要があります。しかし、BindableObjectクラスはクラス階層の上位です。そのため、ユーザインターフェース機能に使用されるクラスの大半はBindablePropertyをサポートしています。

バインダブルプロパティは、BindableProperty型のpublic static readonlyプロパティを宣言することによって作成できます。バインダブルプロパティは、BindableProperty.Createメソッドのオーバーロードのいずれかの戻り値に設定する必要があります。宣言は、BindableObject派生クラスの本体内にある必要がありますが、メンバー定義の外にある必要があります。

BindablePropertyを作成する際には、少なくとも次のパラメータとともに識別子を指定する必要があります。

  • BindablePropertyの名前
  • プロパティの型
  • 所有オブジェクトの型
  • プロパティーのデフォルト値。これにより、プロパティが設定されていない場合に常に特定のデフォルト値が返され、プロパティの型のデフォルト値と異なる可能性があります。デフォルト値は、バインダブルプロパティに対してClearValueメソッドが呼び出されたときに復元されます。

次のコードは、4つの必須パラメータの識別子と値を持つバインダブルプロパティの例を示しています。

public static readonly BindableProperty EventNameProperty =
  BindableProperty.Create ("EventName", typeof(string), typeof(EventToCommandBehavior), null);

これにより、string型のEventNameという名前のBindablePropertyインスタンスが作成されます。プロパティーはEventToCommandBehaviorクラスが所有し、デフォルト値はnullです。バインダブルプロパティの命名規則は、バインダブルプロパティ識別子が、Createメソッドで指定されたプロパティ名と一致し、 "Property"が追加されている必要があります。したがって、上記の例では、バインダブルプロパティ識別子はEventNamePropertyです。

オプションで、BindablePropertyインスタンスを作成するときに、次のパラメータを指定できます。

  • バインドモード。これは、プロパティ値の変更が伝播する方向を指定するために使用されます。デフォルトのバインディングモードでは、変更がsourceからtargetに伝播します。
  • プロパティ値が設定されたときに呼び出される検証デリゲート。詳細については、バリデーションコールバックを参照してください。
  • プロパティ値が変更されたときに呼び出されるデリゲートを変更するプロパティ。このデリゲートには、デリゲートを変更したプロパティと同じシグネチャがあります。
  • プロパティ値が変更されたときに呼び出される強制値デリゲート。詳細については、強制値コールバックを参照してください。
  • デフォルトのプロパティ値を初期化するために使用されるFunc。詳細については、Funcを使用したデフォルト値の作成を参照してください。

アクセサの作成

バインダブルプロパティにアクセスするためにプロパティ構文を使用するには、プロパティアクセサが必要です。Getアクセサは、対応するバインド可能なプロパティに含まれる値を返す必要があります。これは、GetValueメソッドを呼び出して、値を取得するバインダブルプロパティ識別子を渡し、結果を必要な型にキャストすることで実現できます。Setアクセサは、対応するバインダブルプロパティの値を設定する必要があります。これは、SetValueメソッドを呼び出し、値を設定するバインダブルプロパティ識別子と設定する値を渡すことで実現できます。

次のコード例は、EventNameバインダブルプロパティのアクセサを示しています。

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

バインダブルプロパティの使用

バインダブルプロパティが作成されると、XAMLまたはコードから使用される可能性があります。XAMLでは、プレフィックスを持つ名前空間を宣言し、CLR名前空間の名前を示す名前空間宣言と、オプションでアセンブリ名を指定することで実現します。詳細については、XAML名前空間を参照してください。

次のコード例は、カスタム型を参照するアプリケーションコードと同じアセンブリ内で定義された、バインダブルプロパティを含むカスタム型のXAML名前空間を示しています。

<ContentPage ... xmlns:local="clr-namespace:EventToCommandBehavior" ...>
  ...
</ContentPage>

名前空間宣言は、次のXAMLコード例に示すように、EventNameバインダブルプロパティを設定するときに使用されます。

<ListView ...>
  <ListView.Behaviors>
    <local:EventToCommandBehavior EventName="ItemSelected" ... />
  </ListView.Behaviors>
</ListView>

同等のC#コードを次のコード例に示します。

var listView = new ListView ();
listView.Behaviors.Add (new EventToCommandBehavior {
  EventName = "ItemSelected",
  ...
});

高度なシナリオ

BindablePropertyインスタンスを作成するときに、高度なバインダブルプロパティシナリオを有効にするために設定できるオプションのパラメータがいくつかあります。このセクションでは、これらのシナリオについて説明します。

プロパティの変更の検出

BindableProperty.CreateメソッドのpropertyChangedパラメーターを指定することにより、静的なプロパティ変更のコールバックメソッドをバインダブルプロパティに登録できます。指定可能なコールバックメソッドは、バインダブルプロパティの値が変更されたときに呼び出されます。

次のコード例は、EventNameバインダブルプロパティがOnEventNameChangedメソッドをプロパティ変更コールバックメソッドとして登録する方法を示しています。

public static readonly BindableProperty EventNameProperty =
  BindableProperty.Create (
    "EventName", typeof(string), typeof(EventToCommandBehavior), null, propertyChanged: OnEventNameChanged);
...

static void OnEventNameChanged (BindableObject bindable, object oldValue, object newValue)
{
  // Property changed implementation goes here
}

property-changedコールバックメソッドでは、BindableObjectパラメータは、所有しているクラスのどのインスタンスが変更を報告したかを示すために使用され、2つのobjectパラメータの値はバインド可能プロパティの古い値と新しい値を表します。

バリデーションコールバック

staticバリデーションコールバックメソッドは、BindableProperty.CreateメソッドのvalidateValueパラメーターを指定することによりバインダブルプロパティと一緒に登録できます。指定可能なコールバックメソッドは、バインダブルプロパティの値が設定されているときに呼び出されます。

次のコード例は、AngleのバインダブルプロパティがバリデーションコールバックメソッドとしてIsValidValueメソッドを登録する方法を示しています。

public static readonly BindableProperty AngleProperty =
  BindableProperty.Create ("Angle", typeof(double), typeof(HomePage), 0.0, validateValue: IsValidValue);
...

static bool IsValidValue (BindableObject view, object value)
{
  double result;
  bool isDouble = double.TryParse (value.ToString (), out result);
  return (result >= 0 && result <= 360);
}

バリデーションコールバックには値が与えられ、その値がプロパティに対して有効であればtrueをそれ以外の場合にはfalseを返します。バリデーションコールバックがfalseを返す場合、例外が発生します。これは開発者が処理する必要があります。バリデーションコールバックメソッドの典型的な使用法は、バインダブルプロパティが設定されているときに、int値またはdouble値に制約することです。たとえば、IsValidValueメソッドは、プロパティ値が0〜360の範囲内のdoubleであることをチェックします。

強制値コールバック

static強制値コールバックメソッドは、BindableProperty.CreateメソッドのcoerceValueパラメーターを指定することによって、バインダブルプロパティと一緒に登録できます。指定可能なコールバックメソッドは、バインダブルプロパティの値が変更されたときに呼び出されます。

強制値コールバックは、プロパティの値が変更されたときにバインダブルプロパティの再評価を強制するために使用されます。たとえば、強制値のコールバックを使用して、1つのバインダブルプロパティの値が別のバインダブルプロパティの値よりも大きくないようにすることができます。

次のコード例は、AngleのバインダブルプロパティがCoerceAngleメソッドを強制値コールバックメソッドとして登録する方法を示しています。

public static readonly BindableProperty AngleProperty = BindableProperty.Create (
  "Angle", typeof(double), typeof(HomePage), 0.0, coerceValue: CoerceAngle);
public static readonly BindableProperty MaximumAngleProperty = BindableProperty.Create (
  "MaximumAngle", typeof(double), typeof(HomePage), 360.0);
...

static object CoerceAngle (BindableObject bindable, object value)
{
  var homePage = bindable as HomePage;
  double input = (double)value;

  if (input > homePage.MaximumAngle) {
    input = homePage.MaximumAngle;
  }

  return input;
}

CoerceAngleメソッドはMaximumAngleプロパティの値をチェックし、Angleプロパティの値がそれより大きい場合、値をMaximumAngleプロパティの値に強制します。

Funcを使用したデフォルト値の作成

Funcを使用すると、次のコード例に示すように、バインダブルプロパティのデフォルト値を初期化できます。

public static readonly BindableProperty SizeProperty =
  BindableProperty.Create ("Size", typeof(double), typeof(HomePage), 0.0,
  defaultValueCreator: bindable => Device.GetNamedSize (NamedSize.Large, (Label)bindable));

defaultValueCreatorパラメータは、Device.GetNamedSizeメソッドを呼び出すFuncに設定され、ネイティブプラットフォームのLabelで使用されるフォントの名前付きサイズを表すdoubleを返します。

まとめ

この記事では、バインダブルプロパティの概要を紹介し、バインダブルプロパティを作成、使用する方法を示しました。バインダブルプロパティは特殊なタイプのプロパティです。プロパティの値はXamarin.Formsプロパティシステムによって追跡されます。

添付プロパティ

あらゆるオブジェクトで設定可能なグローバルプロパティの作成

添付プロパティは、特殊型のバインダブルプロパティであり、1つのクラスで定義されていますが他のオブジェクトに関連付けられています。XAMLでは、クラスとプロパティ名がピリオドで区切られた属性として認識されます。この記事では、添付されたプロパティの概要を紹介し、それらを作成、使用する方法を示します。

概要

添付プロパティを使用すると、オブジェクトは自分のクラスで定義されていないプロパティの値を割り当てることができます。例えば、子要素は添付されたプロパティを使用して、親要素にそれらがどのようにユーザインターフェースに提示されるかを知らせることができます。Gridコントロールを使用すると、Grid.RowGrid.Columnの添付プロパティを設定することで、子の行と列を指定できます。Grid.RowGrid.Columnは、Grid自体ではなく、Gridの子である要素に設定されているため、添付プロパティです。

バインダブルプロパティは、次のシナリオでは添付プロパティとして実装する必要があります。

  • 定義クラス以外のクラスでプロパティ設定メカニズムを使用できるようにする必要があるとき。
  • クラスが他のクラスと容易に統合される必要のあるサービスを表す場合。

バインダブルプロパティの詳細については、バインダブルプロパティを参照してください。

添付プロパティの作成と使用

添付プロパティを作成するプロセスは次のとおりです。

  1. CreateAttachedメソッドオーバーロードのいずれかを使用してBindablePropertyインスタンスを作成します。
  2. static GetPropertyNameSetPropertyNameメソッドを、添付プロパティのアクセサとして提供します。

プロパティの作成

他の型で使用するために添付プロパティを作成する場合、そのプロパティが作成されるクラスはBindableObjectから派生する必要はありません。ただし、アクセサのtargerプロパティは、BindableObjectであるか、BindableObject派生する必要があります。

BindableProperty型のpublic static readonlyプロパティを宣言することによって、添付プロパティを作成できます。バインダブルプロパティは、BindableProperty.CreateAttachedメソッドオーバーロードの1つの戻り値に設定する必要があります。宣言は所有クラスの本体内にある必要がありますが、メンバー定義の外にある必要があります。

次のコードは、添付プロパティの例を示しています。

public static readonly BindableProperty HasShadowProperty =
  BindableProperty.CreateAttached ("HasShadow", typeof(bool), typeof(ShadowEffect), false);

これにより、bool型のHasShadowという名前の添付プロパティが作成されます。プロパティはShadowEffectクラスが所有し、デフォルト値はfalseです。添付されたプロパティの命名規則は、添付されたプロパティ識別子が、CreateAttachedメソッドで指定されたプロパティ名と "Property"が付加されたものとが一致しなければならないことです。したがって、上記の例では、添付プロパティ識別子はHasShadowPropertyです。

作成時に指定できるパラメータを含む、バインダブルプロパティの作成の詳細については、バインダブルプロパティの作成と使用を参照してください。

アクセサの作成

静的なGetPropertyNameSetPropertyNameメソッドは、添付プロパティのアクセサとして必要です。そうでない場合、プロパティシステムは添付プロパティを使用できなくなります。GetPropertyNameアクセサは、次のシグネチャに準拠する必要があります。

public static valueType GetPropertyName(BindableObject target)

GetPropertyNameアクセサは、添付プロパティの対応するBindablePropertyフィールドに含まれる値を返す必要があります。これは、GetValueメソッドを呼び出し、値を取得するバインダブルプロパティ識別子を渡し、結果の値を必要な型にキャストすることで実現できます。

SetPropertyNameアクセサは、次のシグネチャに準拠する必要があります。

public static void SetPropertyName(BindableObject target, valueType value)

SetPropertyNameアクセサは、添付プロパティの対応するBindablePropertyフィールドの値を設定する必要があります。これは、SetValueメソッドを呼び出し、値を設定するバインダブルプロパティ識別子と設定する値を渡すことで実現できます。

どちらのアクセサでも、targetオブジェクトはBindableObjectであるか、BindableObjectから派生している必要があります。

次のコード例は、HasShadow添付プロパティのアクセサを示しています。

public static bool GetHasShadow (BindableObject view)
{
  return (bool)view.GetValue (HasShadowProperty);
}

public static void SetHasShadow (BindableObject view, bool value)
{
  view.SetValue (HasShadowProperty, value);
}

添付プロパティの使用

添付されたプロパティが作成されると、XAMLまたはコードから使用される可能性があります。XAMLでは、プレフィックス付きの名前空間を宣言し、CLR(Common Language Runtime)名前空間名を示す名前空間宣言と、オプションでアセンブリ名を指定してこれを実現します。詳細については、XAML名前空間を参照してください。

次のコード例は、カスタム型を参照するアプリケーションコードと同じアセンブリ内で定義された、関連付けられたプロパティを含むカスタム型のXAML名前空間を示しています。

<ContentPage ... xmlns:local="clr-namespace:EffectsDemo" ...>
  ...
</ContentPage>

XAMLの次のコード例に示すように、特定のコントロールの添付プロパティを設定するときに、名前空間宣言が使用されます。

<Label Text="Label Shadow Effect" local:ShadowEffect.HasShadow="true" />

同等のC#コードを次のコード例に示します。

var label = new Label { Text = "Label Shadow Effect" };
ShadowEffect.SetHasShadow (label, true);

Style付きの添付プロパティの使用

添付プロパティは、スタイルによってコントロールに追加することもできます。次のXAMLコード例は、Labelコントロールに適用できる、HasShadow添付プロパティを使用する明示的なスタイルを示しています。

<Style x:Key="ShadowEffectStyle" TargetType="Label">
  <Style.Setters>
    <Setter Property="local:ShadowEffect.HasShadow" Value="true" />
  </Style.Setters>
</Style>

Styleは、次のコード例に示すように、StaticResourceマークアップ拡張を使用してStyleプロパティにStyleインスタンスを設定することによって、Labelに適用できます。

<Label Text="Label Shadow Effect" Style="{StaticResource ShadowEffectStyle}" />

スタイルの詳細については、Styleを参照してください。

高度なシナリオ

添付プロパティを作成するときは、高度な添付プロパティのシナリオを有効にするために設定できるいくつかのオプションのパラメータがあります。これには、プロパティ変更の検出、プロパティ値の検証、プロパティ値の強制変換が含まれます。詳細については、高度なシナリオを参照してください。

まとめ

この記事では、添付プロパティの概要と、それらを作成、使用する方法を示しました。添付プロパティは、特別の種類のバインダブルプロパティです。クラス内で定義されていますが、他のオブジェクトに関連付けられています。XAMLでは、クラスとプロパティ名がピリオドで区切られた属性として認識されます。

リソースディクショナリー

XAMLリソースの再利用

XAMLリソースは、複数回使用できるオブジェクトの定義です。ResourceDictionaryを使用すると、リソースを単一の場所に定義し、Xamarin.Formsアプリケーション全体で再利用することができます。この記事では、ResourceDictionaryの作成と使用方法、マージする方法について説明します。

概要

ResourceDictionaryは、Xamarin.Formsアプリケーションで使用されるリソースのリポジトリです。ResourceDictionaryに格納される一般的なリソースには、stylecontrol templatesdata templates、色、コンバータが含まれます。

XAMLでは、リソースはResourceDictionaryで定義され、StaticResourceマークアップ拡張を使用して取得され、要素に適用されます。C#では、リソースはResourceDictionaryで定義され、文字列ベースのインデクサを使用して取得され、要素に適用されます。しかし、ResourceDictionaryからリソースを最初に取得することなく、ビジュアル要素のプロパティにリソースを簡単に割り当てることができるため、C#でResourceDictionaryを使用する利点はほとんどありません。

ResourceDictionaryの作成と使用

リソースは、ページまたはコントロールのResourcesコレクション、またはアプリケーションのResourcesコレクションに添付されたResourceDictionaryで定義できます。ResourceDictionaryを定義する場所を選択すると、使用できる場所に影響があります。

  • コントロールレベルで定義されたResourceDictionaryのリソースは、コントロールとその子にのみ適用できます。
  • ページレベルで定義されたResourceDictionary内のリソースは、ページおよびその子にのみ適用できます。
  • アプリケーションレベルで定義された ResourceDictionaryのリソースは、アプリケーション全体に適用できます。

次のXAMLコード例は、アプリケーションレベルのResourceDictionaryで定義されたリソースを示しています。

<Application ...>
    <Application.Resources>
        <ResourceDictionary>
            <Color x:Key="PageBackgroundColor">Yellow</Color>
            <Color x:Key="HeadingTextColor">Black</Color>
            <Color x:Key="NormalTextColor">Blue</Color>
            <Style x:Key="LabelPageHeadingStyle" TargetType="Label">
                <Setter Property="FontAttributes" Value="Bold" />
                <Setter Property="HorizontalOptions" Value="Center" />
                <Setter Property="TextColor" Value="{StaticResource HeadingTextColor}" />
            </Style>
        </ResourceDictionary>
    </Application.Resources>
</Application>

このResourceDictionaryは、3つのColorリソースとStyleリソースを定義します。 XAML Appクラスの作成の詳細については、App Classを参照してください。

各リソースにはx:Key属性を使用して指定されたキーがあり、ResourceDictionaryで説明的なキーが与えられます。このキーは、コントロールレベルResourceDictionary
で定義された追加のリソースを示す次のXAMLコードの例に示すように、StaticResourceマークアップ拡張によってResourceDictionaryからリソースを取得するために使用されます。

<StackLayout Margin="0,20,0,0">
  <StackLayout.Resources>
    <ResourceDictionary>
      <Style x:Key="LabelNormalStyle" TargetType="Label">
        <Setter Property="TextColor" Value="{StaticResource NormalTextColor}" />
      </Style>
      <Style x:Key="MediumBoldText" TargetType="Button">
        <Setter Property="FontSize" Value="Medium" />
        <Setter Property="FontAttributes" Value="Bold" />
      </Style>
    </ResourceDictionary>
  </StackLayout.Resources>
  <Label Text="ResourceDictionary Demo" Style="{StaticResource LabelPageHeadingStyle}" />
    <Label Text="This app demonstrates consuming resources that have been defined in resource dictionaries."
           Margin="10,20,10,0"
           Style="{StaticResource LabelNormalStyle}" />
    <Button Text="Navigate"
            Clicked="OnNavigateButtonClicked"
            TextColor="{StaticResource NormalTextColor}"
            Margin="0,20,0,0"
            HorizontalOptions="Center"
            Style="{StaticResource MediumBoldText}" />
</StackLayout>

最初のLabelインスタンスは、アプリケーションレベルのResourceDictionary
で定義されているLabelPageHeadingStyleリソースを取得および使用します。2番目のLabelインスタンスは、コントロールレベルResourceDictionaryで定義されたLabelNormalStyleリソースを取得して使用します。同様に、Buttonインスタンスは、アプリケーションレベルのResourceDictionaryで定義されたNormalTextColorリソース、およびコントロールレベルのResourceDictionaryで定義されたMediumBoldTextリソースを取得および使用します。この結果、次のスクリーンショットが表示されます。

Consuming ResourceDictionary Resources

注意: 単一ページに固有のリソースは、アプリケーションレベルのリソースディクショナリーに含めてはいけません。そのようなリソースは、ページで必要とされるときではなく、アプリケーションの起動時に解析されるためです。詳細については、アプリケーションリソースディクショナリーサイズを小さくするを参照してください。

リソースの上書き

ResourceDictionaryリソースがx:Key属性値を共有する場合、ビュー階層の下位に定義されたリソースは、上位に定義されたリソースよりも優先されます。たとえば、アプリケーションレベルでPageBackgroundColorリソースをBlueに設定しても、ページレベルのPageBackgroundColorリソースがYellowに設定されていると上書きされます。同様に、ページレベルのPageBackgroundColorリソースは、コントロールレベルのPageBackgroundColorリソースによってオーバーライドされます。

<ContentPage ... BackgroundColor="{StaticResource PageBackgroundColor}">
    <ContentPage.Resources>
        <ResourceDictionary>
            <Color x:Key="PageBackgroundColor">Blue</Color>
            <Color x:Key="NormalTextColor">Yellow</Color>
        </ResourceDictionary>
    </ContentPage.Resources>
    <StackLayout Margin="0,20,0,0">
        ...
        <Label Text="ResourceDictionary Demo" Style="{StaticResource LabelPageHeadingStyle}" />
        <Label Text="This app demonstrates consuming resources that have been defined in resource dictionaries."
               Margin="10,20,10,0"
               Style="{StaticResource LabelNormalStyle}" />
        <Button Text="Navigate"
                Clicked="OnNavigateButtonClicked"
                TextColor="{StaticResource NormalTextColor}"
                Margin="0,20,0,0"
                HorizontalOptions="Center"
                Style="{StaticResource MediumBoldText}" />
    </StackLayout>
</ContentPage>

アプリケーションレベルで定義された元のPageBackgroundColorおよびNormalTextColorインスタンスは、ページレベルで定義されたPageBackgroundColorおよびNormalTextColorインスタンスによってオーバーライドされます。したがって、次のスクリーンショットに示すように、ページの背景色は青色になり、ページ上のテキストは黄色になります。

Overriding ResourceDictionary Resources

ただし、BarBackgroundColorプロパティがアプリケーションレベルのResourceDictionaryで定義されたPageBackgroundColorリソースの値に設定されているため、NavigationPageのバックグラウンドバーはまだ黄色です。

マージドリソースディクショナリー

マージドリソースディクショナリーは、1つ以上のResourceDictionary
インスタンスを別のインスタンスにマージします。これは、ResourceDictionary.MergedDictionariesプロパティを、アプリケーション、ページ、またはコントロールレベルにマージされる1つ以上のResourceDictionaryに設定することによって実行されます。

注意: ResourceDictionary型には、現在のResourceDictionaryインスタンスにマージされるMergedWithプロパティの値として指定されたResourceDictionaryを使用して、単一のResourceDictionaryを別のResourceDictionaryにマージするために使用できるMergedWithプロパティもあります。MergedWithプロパティを使用してマージすると、x:Key属性値を共有する現在のResourceDictionaryのすべてのリソースが、マージ対象のResourceDictionaryのリソースと置き換えられます。ただし、MergedWithプロパティはXamarin.Formsの今後のリリースで非推奨になります。したがって、MergedDictionariesプロパティを使用してResourceDictionaryインスタンスをマージすることをお勧めします。

次のXAMLコード例は、単一のリソースを含むMyResourceDictionaryという名前のResourceDictionaryを示しています。

<ResourceDictionary xmlns="http://xamarin.com/schemas/2014/forms"
                    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                    x:Class="ResourceDictionaryDemo.MyResourceDictionary">
    <DataTemplate x:Key="PersonDataTemplate">
        <ViewCell>
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="0.5*" />
                    <ColumnDefinition Width="0.2*" />
                    <ColumnDefinition Width="0.3*" />
                </Grid.ColumnDefinitions>
                <Label Text="{Binding Name}" TextColor="{StaticResource NormalTextColor}" FontAttributes="Bold" />
                <Label Grid.Column="1" Text="{Binding Age}" TextColor="{StaticResource NormalTextColor}" />
                <Label Grid.Column="2" Text="{Binding Location}" TextColor="{StaticResource NormalTextColor}" HorizontalTextAlignment="End" />
            </Grid>
        </ViewCell>
    </DataTemplate>
</ResourceDictionary>

MyResourceDictionaryは、任意のアプリケーション、ページ、またはコントロールレベルのResourceDictionaryにマージできます。次のXAMLコード例は、MergedDictionariesプロパティを使用して、ページレベルのResourceDictionaryにマージされていることを示しています。

<ContentPage ...>
    <ContentPage.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <local:MyResourceDictionary />
                <!-- Add more resource dictionaries here -->
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </ContentPage.Resources>
    ...
</ContentPage>

マージされたResourceDictionaryリソースが同じx:Key属性値を共有する場合、Xamarin.Formsは次のリソース優先順位を使用します。

  1. リソースディクショナリーのローカルリソース
  2. MergedWithプロパティを使用してマージされたリソースディクショナリーに含まれるリソース
  3. MergedDictionariesコレクションを使用してマージされたリソースディクショナリーに含まれるリソースを、MergedDictionariesプロパティにリストされている順序で返します。

注意: アプリケーションに複数の大きなリソース辞書が含まれている場合、リソース辞書を検索することは計算集約的な作業になります。したがって、不要な検索を避けるために、アプリケーション内の各ページがそのページに適したリソース辞書のみを使用するようにしてください。

まとめ

この記事では、ResourceDictionaryを作成および使用する方法、リソースディクショナリーをマージする方法について説明しました。ResourceDictionaryを使用すると、リソースを単一の場所に定義し、Xamarin.Formsアプリケーション全体で再利用することができます。

XAML Standard(プレビュー)

Xamarin.FormsでXAML Standardプレビューを調べる方法

Preview

Xamarin.FormsでXAML Standardを試すには、次の手順に従ってください。

  1. プレビューNuGetパッケージをこちらからダウンロードします。
  2. Xamarin.Forms.Alias NuGetパッケージをXamarin.Forms PCL、.NET Standard、およびプラットフォームプロジェクトに追加します。
  3. Alias.Init()でパッケージを初期化します。
  4. xmlns:a="clr-namespace:Xamarin.Forms.Alias;assembly=Xamarin.Forms.Alias"を参照するxmlns:aを追加します。
  5. XAMLの型を使用します。 - 詳細については、コントロールリファレンスを参照してください。

次のXAMLは、Xamarin.Forms ContentPageで使用されているXAML Standardコントロールの一部を示しています。

<?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:a="clr-namespace:Xamarin.Forms.Alias;assembly=Xamarin.Forms.Alias"
  x:Class="XAMLStandardSample.ItemsPage"
  Title="{Binding Title}" x:Name="BrowseItemsPage">
    <ContentPage.ToolbarItems>
        <ToolbarItem Text="Add" Clicked="AddItem_Clicked" />
    </ContentPage.ToolbarItems>
    <ContentPage.Content>
        <a:StackPanel>
            <ListView x:Name="ItemsListView" ItemsSource="{Binding Items}" VerticalOptions="FillAndExpand" HasUnevenRows="true" RefreshCommand="{Binding LoadItemsCommand}" IsPullToRefreshEnabled="true" IsRefreshing="{Binding IsBusy, Mode=OneWay}" CachingStrategy="RecycleElement" ItemSelected="OnItemSelected">
                <ListView.ItemTemplate>
                    <DataTemplate>
                        <ViewCell>
                            <StackLayout Padding="10">
                                <a:TextBlock Text="{Binding Text}" LineBreakMode="NoWrap" Style="{DynamicResource ListItemTextStyle}" FontSize="16" />
                                <a:TextBlock Text="{Binding Description}" LineBreakMode="NoWrap" Style="{DynamicResource ListItemDetailTextStyle}" FontSize="13" />
                            </StackLayout>
                        </ViewCell>
                    </DataTemplate>
                </ListView.ItemTemplate>
            </ListView>
        </a:StackPanel>
    </ContentPage.Content>
</ContentPage>

注意: XAML Standardコントロールのxmlns a:プレフィックスが必要なのは、現在のプレビューの制限事項です。

XAML Standard(プレビュー)コントロール

Xamarin.FormsでXAML Standardプレビューを調べる方法

Preview

このページには、プレビューで使用できるXAML Standardコントロールと、同等のXamarin.Formsコントロールを一覧表示しています。
また、XAML Standardに新しいプロパティと列挙名を持つコントロールの一覧もあります。

コントロール

XAMARIN.FORMS XAML STANDARD
Frame Border
Picker ComboBox
ActivityIndicator ProgressRing
StackLayout StackPanel
Label TextBlock
Entry TextBox
Switch ToggleSwitch
ContentView UserControl

プロパティと列挙型

XAMARIN.FORMS
CONTROLS WITH UPDATED PROPERTIES
XAMARIN.FORMS
PROPERTY OR ENUM
XAML STANDARD
EQUIVALENT
Button, Entry, Label, DatePicker, Editor, SearchBar, TimePicker TextColor Foreground
VisualElement BackgroundColor Background *
Picker, Button BorderColor, OutlineColor BorderBrush
Button BorderWidth BorderThickness
ProgressBar Progress Value
Button, Entry, Label, Editor, SearchBar, Span, Font FontAttributes
Bold, Italic, None
FontStyle
Italic, Normal
Button, Entry, Label, Editor, SearchBar, Span, Font FontAttributes FontWeights *
Bold, Normal
InputView Keyboard
Default, Url, Number, Telephone, Text, Chat, Email
InputScopeNameValue *
Default, Url, Number, TelephoneNumber, Text, Chat, EmailNameOrAddress
StackPanel StackOrientation Orientation *

注意: *が付いている項目は、現在のプレビューでは不完全です。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.