Xamarin.Forms には無い XamlReader なんですが、動的に XAML をロードすることができるので結構便利です、って話を少し(まあ、XAML のパーサーは作ったので、ロードしてやればいいだけなんですけど)。
XamlReader.Load メソッドを使うと XAML 文字列を動的に読み込むことができます。XML を作るときは実は Visual Basic が便利なんですけど、今回は C# で XElement を使って構築します。
XElement で XAML を作成する
こんな風に、チェックボックス(CheckBox)を縦に並べた画面を作ります。これはアンケートツクレールから自動生成するために動的に作っています。
XElement CheckboxToXaml(ExElement el)
{
string qtext = ((ExElement)(el * "td" % "class" == "qtext")).Value;
string qbody = ((ExElement)(el * "td" % "class" == "qbody")).Value;
var lst = el * "input" % "type" == "checkbox";
var panel = new XElement("StackPanel", new XAttribute("Grid.Row", "1"));
var hub = new XElement("HubSection",
new XAttribute("Width", "400"),
new XAttribute("Header", qtext),
new XElement("DataTemplate",
new XElement("Grid",
new XElement("Grid.RowDefinitions",
new XElement("RowDefinition", new XAttribute("Height", "50")),
new XElement("RowDefinition", new XAttribute("Height", "*"))),
new XElement("TextBlock",
new XAttribute("Grid.Row", "0"),
new XAttribute("FontSize", "16"),
new XAttribute("TextWrapping", "NoWrap"),
new XAttribute("Text", qbody)),
panel)));
foreach (var it in lst)
{
var node = it.Parent.XElement.NextNode as XText;
if (node != null)
{
string name = it.Parent % "name";
string value = it.Parent % "value";
string xname = name.Replace("[]","") + "-" + value;
_names[xname] = null;
var ee = new XElement("CheckBox",
new XAttribute("Content", node.Value),
new XAttribute("Name", xname));
panel.Add(ee);
}
}
return hub;
}
まあ、後から気づいたのですが、こんな風にちまちまと XAML を書くぐらいだったら適当なユーザーコントロールにしてしまったほうがいいんですよね。ListView を使って CheckBox を並べるようなユーザコントロールを作って、データバインドを使えば良いわけです。
XElement と XAttribute を使って書き下しているのですが、それぞれのコントロールを使っても同じことができます。実は、このコードは、いちど XAML で書いてから XElement に直しています。このあたり、Visual Basic の場合はヒアドキュメント風に XML を書くことができるのでメリットは大きいのですが…C# だといまいちですね。
XmlReader で文字列の XAML をロードさせる。
あらかじめデザイン上の HubSection のテンプレートを作っておいて、最初と最後のみ残して、その他はすり替えます。XamlReader.Load に渡す XML の xmlns はスキーマを設定しないといけないのですが、そうもそのままではうまくいかないので Replace で逃れています。これは、ルートの XDocument を作るときに指定すればよいでしょう。
// 最初と最後だけ残す
var sec0 = this.hub.Sections[0];
var sece = this.hub.Sections[this.hub.Sections.Count - 1];
this.hub.Sections.Clear();
this.hub.Sections.Add(sec0);
foreach (var it in hubs)
{
it.SetAttributeValue("ns", "http://schemas.microsoft.com/winfx/2006/xaml/presentation");
var xml = it.ToString(SaveOptions.None);
xml = xml.Replace("ns="http", "xmlns="http");
var sec = XamlReader.Load(xml) as HubSection;
this.hub.Sections.Add(sec);
}
this.hub.Sections.Add(sece);
コントロールの名前を検索する
実は XAML 文字列で渡すときには問題があって、内部のコントロールに触れないことです。それぞれのコントロールをクラスで組み合わせたときは適当なオブジェクトとして保存しておけばよいのですが、XAML 文字列の場合は、XamlReader.Load の後にオブジェクトができるので、Load した後にオブジェクトを見つける必要があります。
これをどうするのか?ってのが謎なのですが、実は Name プロパティが使えます。XAML で指定するときは x:Name でオブジェクトと結び付けをするのですが、実は FramworkElement に Name プロパティがあるのです。ここに名前を付けておいて、Load 後に検索をします。
// 画面を更新して _names を作る
await Task.Delay(1);
var names = new Dictionary<string, object>();
foreach (var key in _names.Keys)
{
var obj = FindByName(key, this);
names[key] = obj;
}
_names = names;
いちど画面に描画してあげてから、Name を検索します。Storyboard のように描画自体がないものはどうなるのかわかりません。アニメーションを使うときには必須なので、これも調べておかないといけませんね。
FindByName メソッドは自作したものです。x:Name を検索するときの FindName メソッドと同じですね。プレロードする XAML の場合は、構築しながら x:Name に対応するオブジェクトを保存していくので高速なのですが、こっちのはコントロールのツリーを探索していくのでちょっと遅いです。とはいえ、画面に表示されるコントロール程度ならば問題ないでしょう。
object FindByName(string name, DependencyObject el , int indent = 0)
{
string sp = "";
for ( int i=0; i<indent; i++ ) sp += " ";
// Debug.WriteLine(sp + el.GetType().Name);
var fw = el as FrameworkElement;
if (fw != null)
{
if (fw.Name == name)
{
return fw;
}
}
int cnt = VisualTreeHelper.GetChildrenCount( el );
for (int i = 0; i < cnt; i++)
{
var child = VisualTreeHelper.GetChild(el, i);
var obj = FindByName(name, child, indent + 1);
if (obj != null)
{
return obj;
}
}
return null;
}
そんな訳で、XamlReader.Load を使って動的に XAML をロードすることができます。このぐらいならばユーザコントロールを作ったほうがデザイン的にもよいのですが、実は RichTextBlock を作るときに有効に働きます。その話はまた後日。現在制作中の「ふぁぼ付箋」アプリで使っているテクニックです。
