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


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

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

上記参考記事と同様に、気象庁のサンプルデータを使わせてもらうことにする
気象庁防災情報XMLフォーマット 技術資料

Viewの実装

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

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

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を書いておく

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を返すように書いている。

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

ViewModelの実装

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();
        }
    }
}

これで次のような画面が表示される

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

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