[WPF]TreeViewにXElementを属性付きBinding


C#初学者用記事をいろいろ書き込んでる最中ですが、自分の為の備忘録。

WPFのTreeViewにXMLデータをそのまま流し込む方法として、XDocumentを適用する方法はいくつか見つかったものの、XElementを元データとしてBindingする方法がなかなか見つからなかったのでメモ
\# そんなニッチなことするやつあんまいないのか

* XDocumentを元データする参考記事
    * [[WPF][C#]XMLデータをTreeViewに表示 その１ by 一矢報いる](http://challengeandresponse.blogspot.com/2014/04/wpfcxmltreeview.html)
    * [TreeViewのHierarchicalDataTemplate(XML表示) by mikeo_410](http://mikeo410.minim.ne.jp/cms/~programmingbindingtreeviewhierarchical)

上記参考記事と同様に、気象庁のサンプルデータを使わせてもらうことにする
[気象庁防災情報XMLフォーマット　技術資料](http://xml.kishou.go.jp/tec_material.html)

## Viewの実装

基本的には上記参考ページと同じ内容となるので、諸々割愛しつつ、ソースコードを貼る

xamlは以下の通り。
※注意 デフォルトの"MainWindow"という名前から"MainView"という名前に変えている

```XML:MainView.xaml
<Window x:Class="WpfTestView.MainView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfTestView"
        xmlns:vm="clr-namespace:WpfTestViewModel;assembly=WpfTestViewModel"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <vm:MainViewModel/>
    </Window.DataContext>
    <Window.Resources>
        <local:XAttributeConverter x:Key="XAttributeConverter" />
    </Window.Resources>
    <Grid>
        <Border Margin="10" BorderBrush="Black" BorderThickness="1">
            <!-- TreeViewのItemsSourceはIEnumerableでないといけないので、IEnumerable<XElement>-->
            <TreeView ItemsSource="{Binding XTreeRoot, Mode=OneWay}">
                <TreeView.ItemTemplate>
                    <HierarchicalDataTemplate ItemsSource="{Binding Elements}">
                        <StackPanel Orientation="Horizontal">
                            <TextBlock x:Name="TagName" Text="{Binding Name.LocalName}" />
                            <TextBlock x:Name="AttrStart" Text="(" />
                            <!-- XAttributeはBindingサポートしてないのでConverterを使う -->
                            <ItemsControl ItemsSource="{Binding Converter={StaticResource XAttributeConverter}}">
                                <ItemsControl.ItemTemplate>
                                    <DataTemplate>
                                        <StackPanel Orientation="Horizontal" Margin="2,0">
                                            <TextBlock Text="{Binding Name.LocalName}" />
                                            <TextBlock Text="=&quot;" />
                                            <TextBlock Text="{Binding Value}" />
                                            <TextBlock Text="&quot;" />
                                        </StackPanel>
                                    </DataTemplate>
                                </ItemsControl.ItemTemplate>
                                <ItemsControl.ItemsPanel>
                                    <ItemsPanelTemplate>
                                        <StackPanel Orientation="Horizontal" />
                                    </ItemsPanelTemplate>
                                </ItemsControl.ItemsPanel>
                            </ItemsControl>
                            <TextBlock x:Name="AttrEnd" Text=")" />
                            <TextBlock x:Name="Separater" Text=" : " />
                            <TextBlock x:Name="TagValue" Text="{Binding Value}" />
                        </StackPanel>
                        <!-- ノードによって表示形式を切替え -->
                        <HierarchicalDataTemplate.Triggers>
                            <DataTrigger Binding="{Binding NodeType}" Value="Text">
                                <Setter TargetName="TagName" Property="Text" Value="Value" />
                                <Setter TargetName="AttrStart" Property="Text" Value="" />
                                <Setter TargetName="AttrEnd" Property="Text" Value="" />
                                <Setter TargetName="Separater" Property="Text" Value="" />
                                <Setter TargetName="TagValue" Property="Text" Value="" />
                            </DataTrigger>
                            <DataTrigger Binding="{Binding HasAttributes}" Value="False">
                                <Setter TargetName="AttrStart" Property="Text" Value="" />
                                <Setter TargetName="AttrEnd" Property="Text" Value="" />
                            </DataTrigger>
                            <DataTrigger Binding="{Binding HasElements}" Value="True">
                                <Setter TargetName="Separater" Property="Text" Value="" />
                                <Setter TargetName="TagValue" Property="Text" Value="" />
                            </DataTrigger>
                        </HierarchicalDataTemplate.Triggers>
                    </HierarchicalDataTemplate>
                </TreeView.ItemTemplate>
            </TreeView>
        </Border>
    </Grid>
</Window>
```

主に説明しておくべきところとしてはコメントにも書いてるが、XAMLに記載している"XTreeRoot"というプロパティのみがViewModel側とバインドされるもので、それより下階層のBinding要素はXElementオブジェクトにバインドされている
HierarchicalDataTemplate下のStackPanelやTriggersに関しては、完全に個人的な趣向で書いているので自由にカスタマイズ可能

ViewModelの前にXAttributeをバインドする為のConverterを書いておく

```C#:XAttributeConverter.cs
public class XAttributeConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (!(value is XElement x)) return Enumerable.Empty<XAttribute>();
        return x.Attributes();
    }
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}
```

ConvertではXElementオブジェクトのIEnumerable<XAttribute>を返すように書いている。

今回の例ではXMLの内容は更新しないのでTreeViewはOneWay（Source→Viewの方向のみ）としている
なのでConvertBackは使用しないのでNotImplementedExceptionとしておく。

## ViewModelの実装

```C#:MainViewModel.cs
public class MainViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = "")
    {
        if (PropertyChanged == null) return;
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }

    public MainViewModel()
    {
        XDocument xDoc = XDocument.Load(@".\15_12_01_161130_VPWW54.xml");
        XTreeRoot = new List<XElement>() { xDoc.Root };
    }

    private IEnumerable<XElement> _xTreeRoot = null;
    public IEnumerable<XElement> XTreeRoot
    {
        get => _xTreeRoot;
        set
        {
            _xTreeRoot = value;
            OnPropertyChanged();
        }
    }
}
```

これで次のような画面が表示される
![1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/782968/eeae0955-d8c0-63ff-e49f-bc90721ca908.png)

デフォルトではツリーは全て閉じた状態で始まる為、もし最初から開いた状態にしておきたい場合、
最初に挙げた参考記事を参照方

読み込んでいるファイルは適当に選んでいる（大きすぎず、小さすぎないくらいのファイル）
XAMLのコメントにも書いたが、以下2点が肝要。（自分が詰まったので）
* バインドするのはIEnumerable<XElement>
* XAttributeはバインドサポート外なのでconverter使う