開発中のOSSツールXAMLensの開発で、XAML内の{Binding ...}を取得しようとした備忘録です。

XElementを使ってみる

XAMLは、<Button>などの要素で構成されたツリー構造をもっているので、文字列のままでは要素や属性を簡単に取得できません。そのため、扱うためにいわゆるパース(Parse)をしてみます。C#だとXElementというのを使うとパースができるようなので試してみます。

var root = XElement.Parse(xaml);

みたいな感じですね。 rootという変数に、パースしたXAMLをXElementとして保持します。これにより、文字列としてではなく、「要素」や「属性」といった構造として扱えるようになりました。

XAMLのツリー構造

XAMLは<Button><Grid>などの要素が親子関係を持つツリー構造になっています。 コードが

<UserControl>
  <StackPanel>
    <TextBlock />
    <Button />
  </StackPanel>
</UserControl>

であれば、

UserControl
└── StackPanel
    ├── TextBlock
    └── Button

のような形です。

要素(Element)とは?

そもそも要素というのは、<StackPanel><TextBlock>のようなタグを表しています。 なので、

element.Elements()

のようにすることで、今見ている要素(element)の、子要素(.Elements)を取得することができます。

属性(Attribute)とは?

例えば、

<TextBlock
    Text="{Binding Name}"
    FontSize="16"/>

なら、属性は

  • Text
  • FontSize

です。
先ほどの要素(Element)が、TextBlockなら、属性(Attribute)がその中身であるTextなどになりますね。

そこで

attribute.Name

のようにすると、Textが取得できますし、

attribute.Value

にすると、その中身である{Binding Name}が取得できます。これは中身がバインディングではなくてただのテキストであっても同じです。

XAMLの中にあるすべての要素を取得

さっきの

<TextBlock
    Text="{Binding Name}"
    FontSize="16"/>

ぐらいであれば、特別に何か処理をしなくてもそのまま取得できますが、実際に書くXAMLは

    <StackPanel Margin="10">
        <TextBlock Text="{Binding Name}" FontSize="16" Margin="0,0,0,10"/>
        <Button Content="Click Me" Width="100" Height="30"/>
    </StackPanel>

みたいなツリー構造になっていますよね。
そうすると、ただ一つ取得処理を書いただけでは今いる要素から見て直下の要素しか取得できず、複雑な入れ子構造になっているとすべて判定することができません。
そこで使うのが再帰です。
一度で探索処理が終わってしまうのが問題なので、すべての要素の判定が終わるまで繰り返す(先ほどの例のコードで言うと、StackPanelを判定したら、次はTextBlockへ、その次はButtonへ…のように、現在の要素を判定したら、その子要素に対しても同じ処理を繰り返す)ことですべて取得することができます。
こういった繰り返し処理にはforeachを使うのが一般的です。
例としてメソッドで探索したいxamlを受け取り、foreach文で先ほどのコードの{Binding Name}を取得する処理を書いてみます。

        private void FindBindings(XElement element)
        {
            foreach (var attribute in element.Attributes()) // XAMLの属性をチェック
            {
                if (attribute.Value.StartsWith("{Binding"))
                {
                    System.Diagnostics.Debug.WriteLine(attribute.Value); // Bindingの内容を出力
                }
            }
            foreach (var child in element.Elements())
            {
                FindBindings(child);
            }
        }

始めのforeach文で、現在見ている要素が持つ

  • Text
  • FontSize

などの属性を確認します。もし属性に{Bindingという、バインディング文に必須のキーワードが含まれていれば、その属性の内容(attribute.Value)を出力し、含まれていなければ、下のforeach (var child in element.Elements())に移ります。 この中で、子要素(child)が存在する場合は、その子要素に対して再びFindBindings(child);という、先ほど行ったような探索処理をもう一度実行します。こうすることで、子要素のさらに子要素、そのまた子要素…とすべての要素を探索できます。