泥庭

2015年3月26日

【WPF】TreeViewにStyleを適用しよう

Filed under: 未分類 — yone64 @ 7:29 PM
タイトルは嘘ではないですが、TreeViewにStyleをした場合に遭遇したバグ(?)と戦った記録です。
TreeView向けのStyleを作りたい方は、残念ながらご期待に添えないです…

なお、今回戦ったバグは、↓とほぼほぼ同じです。
EvaluateOldNewStates() throws exception when updating TreeViewItem.IsSelected
https://social.msdn.microsoft.com/Forums/vstudio/en-US/36f5037a-18ce-43e7-8784-644d60ee9812/evaluateoldnewstates-throws-exception-when-updating-treeviewitemisselected?forum=wpf

なお、今回の再現確認には、↓を利用しています。(MSDN Forumでも出てきていたのでw)
WPF Themes https://www.nuget.org/packages/Wpf.Themes/1.1.0
で、出来上がりのコードは↓にあるので、適宜参照してください。
https://github.com/yone64/TreeViewSample/tree/master/TreeViewSample


準備

まず、NugetからWPF Themesをインストールしましょう。
すると、Themeが適用されるようにApp.xamlが変更されてますが、今回は「ExpressionDark」を利用するので、
App.xamlを下記通り書き換えます。
<?xml version="1.0" encoding="utf-8"?>
<Application x:Class="TreeViewSample.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" StartupUri="MainWindow.xaml">
    <Application.Resources>
         
    <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Themes\ExpressionDark.xaml" />
                <!--
                <ResourceDictionary Source="Themes\BureauBlack.xaml" />
                <ResourceDictionary Source="Themes\BureauBlue.xaml" />
                <ResourceDictionary Source="Themes\ExpressionDark.xaml" />
                <ResourceDictionary Source="Themes\ExpressionLight.xaml" />
                <ResourceDictionary Source="Themes\ShinyBlue.xaml" />
                <ResourceDictionary Source="Themes\ShinyRed.xaml" />
                <ResourceDictionary Source="Themes\WhistlerBlue.xaml" />
                -->
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary></Application.Resources>
</Application>
これで、「ExpressionDark」のテーマが適用されているはずなので、
次にMainWindowにTreeViewを配置していきます。
<Window x:Class="TreeViewSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <TreeView>
            <TreeViewItem Header="Item1">
                <TreeViewItem Header="Item1-1" />
            </TreeViewItem>
            <TreeViewItem Header="Item2" />
        </TreeView>
    </Grid>
</Window>
この状態で実行してみると、下のようなWindowが表示されると思います。

キャプチャ

ここで重要なのは、アイテムにマウスをのせた時の挙動です。
画像だとわかりませんが、マウスをのせると若干しろっぽくなり、はずすとゆっくりと元の色に戻ります。
この動作を設定しているのは、ExpressionDark.xamlの下記部分です。xamlの意味は、まぁなんとなくわかりますよね。
<MultiTrigger>
    <MultiTrigger.ExitActions>
        <BeginStoryboard Storyboard="{StaticResource HoverOff}" x:Name="HoverOff_BeginStoryboard"/>
    </MultiTrigger.ExitActions>
    <MultiTrigger.EnterActions>
        <BeginStoryboard Storyboard="{StaticResource HoverOn}"/>
    </MultiTrigger.EnterActions>
    <MultiTrigger.Conditions>
        <Condition Property="IsMouseOver" SourceName="Selection_Border" Value="True" />
        <Condition Property="IsSelected" Value="False" />
    </MultiTrigger.Conditions>
</MultiTrigger>
これをベースに、TreeView側をBinding対応していきます。
まず、VM。必要最小限なので特になにもないですが、IsSelectedはTreeViewItemのIsSelectedとBinding予定です。
using System.Collections.Generic;

namespace TreeViewSample
{
    public class TreeViewItemViewModel
    {
        public string Header { get; set; }
        public List<TreeViewItemViewModel> Children { get; set; }
        public bool IsSelected { get; set; }
    }
}
これを使って、MainWindow.xamlおよびMainWindow.xaml.csを書き換えます
<Window x:Class="TreeViewSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:treeViewSample="clr-namespace:TreeViewSample"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <TreeView ItemsSource="{Binding}">
            <TreeView.ItemTemplate>
                <HierarchicalDataTemplate DataType="{x:Type treeViewSample:TreeViewItemViewModel}" ItemsSource="{Binding Children}">
                    <TextBlock Text="{Binding Header}" />
                </HierarchicalDataTemplate>
            </TreeView.ItemTemplate>
        </TreeView>
    </Grid>
</Window>
using System.Collections.Generic;
using System.Windows;

namespace TreeViewSample
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            this.DataContext = new List<TreeViewItemViewModel>
            {
                new TreeViewItemViewModel
                {
                    Header = "Item1",
                    Children = new List<TreeViewItemViewModel> { new TreeViewItemViewModel { Header = "Item1-1" } }
                },
                new TreeViewItemViewModel { Header = "Item2" }
            };
        }
    }
}
ここまでは何の問題もないですね。TreeViewの場合はTemplateにHierarchicalDataTemplateを使います。
実行しても同じ結果になります。

終わりの始まり

さて、TreeViewItemの選択状態をViewModelから取得変更したい場合は、VMのプロパティーとBindingするのが手っ取り早いです。
<TreeView ItemsSource="{Binding}">
    <TreeView.ItemTemplate>
        <HierarchicalDataTemplate DataType="{x:Type treeViewSample:TreeViewItemViewModel}" ItemsSource="{Binding Children}">
            <TextBlock Text="{Binding Header}" />
        </HierarchicalDataTemplate>
    </TreeView.ItemTemplate>
    <TreeView.ItemContainerStyle>
        <Style TargetType="{x:Type TreeViewItem}">
            <Setter Property="IsSelected" Value="{Binding IsSelected}" />
        </Style>
    </TreeView.ItemContainerStyle>
</TreeView>
でも、これをやってしまうと、デフォルトのStyleが当たらなくなるので、BaseOnを追加します。
(デフォルトのStyleは何もしなくても継承してほしいところですが、仕方ない。)
 <TreeView ItemsSource="{Binding}">
     <TreeView.ItemTemplate>
         <HierarchicalDataTemplate DataType="{x:Type treeViewSample:TreeViewItemViewModel}" ItemsSource="{Binding Children}">
             <TextBlock Text="{Binding Header}" />
         </HierarchicalDataTemplate>
     </TreeView.ItemTemplate>
     <TreeView.ItemContainerStyle>
         <Style TargetType="{x:Type TreeViewItem}" BasedOn="{StaticResource {x:Type TreeViewItem}}">
             <Setter Property="IsSelected" Value="{Binding IsSelected}" />
         </Style>
     </TreeView.ItemContainerStyle>
 </TreeView>
で、VM側からIsSelectedを操作するために、MainWindow.xaml.csを少し変更します。
using System.Collections.Generic;
using System.Windows;

namespace TreeViewSample
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            this.DataContext = new List<TreeViewItemViewModel>
            {
                new TreeViewItemViewModel
                {
                    Header = "Item1",
                    Children = new List<TreeViewItemViewModel> { new TreeViewItemViewModel { Header = "Item1-1", IsSelected = true} }
                },
                new TreeViewItemViewModel { Header = "Item2" }
            };
        }
    }
}
では、この状態で実行してみましょう。
起動しますね。でも、Item1-1を表示しようとした瞬間。。。
System.NullReferenceException はハンドルされませんでした。
HResult=-2147467261
Message=オブジェクト参照がオブジェクト インスタンスに設定されていません。
Source=PresentationFramework
StackTrace:
場所 System.Windows.StyleHelper.EvaluateOldNewStates(MultiTrigger multiTrigger, DependencyObject triggerContainer, DependencyProperty changedProperty, DependencyPropertyChangedEventArgs changedArgs, Int32 sourceChildIndex, Style style, FrameworkTemplate frameworkTemplate, Boolean& oldState, Boolean& newState)
場所 System.Windows.StyleHelper.OnTriggerSourcePropertyInvalidated(Style ownerStyle, FrameworkTemplate frameworkTemplate, DependencyObject container, DependencyProperty dp, DependencyPropertyChangedEventArgs changedArgs, Boolean invalidateOnlyContainer, FrugalStructList`1& triggerSourceRecordFromChildIndex, FrugalMap& propertyTriggersWithActions, Int32 sourceChildIndex)
場所 System.Windows.FrameworkElement.OnPropertyChanged(DependencyPropertyChangedEventArgs e)
場所 System.Windows.DependencyObject.NotifyPropertyChange(DependencyPropertyChangedEventArgs args)
場所 System.Windows.DependencyObject.UpdateEffectiveValue(EntryIndex entryIndex, DependencyProperty dp, PropertyMetadata metadata, EffectiveValueEntry oldEntry, EffectiveValueEntry& newEntry, Boolean coerceWithDeferredReference, Boolean coerceWithCurrentValue, OperationType operationType)
場所 System.Windows.StyleHelper.ApplyStyleOrTemplateValue(FrameworkObject fo, DependencyProperty dp)
場所 System.Windows.StyleHelper.InvalidateContainerDependents(DependencyObject container, FrugalStructList`1& exclusionContainerDependents, FrugalStructList`1& oldContainerDependents, FrugalStructList`1& newContainerDependents)
場所 System.Windows.StyleHelper.DoStyleInvalidations(FrameworkElement fe, FrameworkContentElement fce, Style oldStyle, Style newStyle)
場所 System.Windows.StyleHelper.UpdateStyleCache(FrameworkElement fe, FrameworkContentElement fce, Style oldStyle, Style newStyle, Style& styleCache)
場所 System.Windows.FrameworkElement.OnStyleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
場所 System.Windows.DependencyObject.OnPropertyChanged(DependencyPropertyChangedEventArgs e)
場所 System.Windows.FrameworkElement.OnPropertyChanged(DependencyPropertyChangedEventArgs e)
場所 System.Windows.DependencyObject.NotifyPropertyChange(DependencyPropertyChangedEventArgs args)
場所 System.Windows.DependencyObject.UpdateEffectiveValue(EntryIndex entryIndex, DependencyProperty dp, PropertyMetadata metadata, EffectiveValueEntry oldEntry, EffectiveValueEntry& newEntry, Boolean coerceWithDeferredReference, Boolean coerceWithCurrentValue, OperationType operationType)
場所 System.Windows.DependencyObject.SetValueCommon(DependencyProperty dp, Object value, PropertyMetadata metadata, Boolean coerceWithDeferredReference, Boolean coerceWithCurrentValue, OperationType operationType, Boolean isInternal)
場所 System.Windows.DependencyObject.SetValue(DependencyProperty dp, Object value)
場所 System.Windows.Controls.ItemsControl.ApplyItemContainerStyle(DependencyObject container, Object item)
場所 System.Windows.Controls.ItemsControl.MS.Internal.Controls.IGeneratorHost.PrepareItemContainer(DependencyObject container, Object item)
場所 System.Windows.Controls.ItemContainerGenerator.System.Windows.Controls.Primitives.IItemContainerGenerator.PrepareItemContainer(DependencyObject container)
場所 System.Windows.Controls.Panel.GenerateChildren()
場所 System.Windows.Controls.Panel.EnsureGenerator()
場所 System.Windows.Controls.Panel.get_InternalChildren()
場所 System.Windows.Controls.StackPanel.System.Windows.Controls.IStackMeasure.get_InternalChildren()
場所 System.Windows.Controls.StackPanel.StackMeasureHelper(IStackMeasure measureElement, IStackMeasureScrollData scrollData, Size constraint)
場所 System.Windows.Controls.StackPanel.MeasureOverride(Size constraint)
場所 System.Windows.FrameworkElement.MeasureCore(Size availableSize)
場所 System.Windows.UIElement.Measure(Size availableSize)
場所 MS.Internal.Helper.MeasureElementWithSingleChild(UIElement element, Size constraint)
場所 System.Windows.Controls.ItemsPresenter.MeasureOverride(Size constraint)
場所 System.Windows.FrameworkElement.MeasureCore(Size availableSize)
場所 System.Windows.UIElement.Measure(Size availableSize)
場所 System.Windows.Controls.Grid.MeasureCell(Int32 cell, Boolean forceInfinityV)
場所 System.Windows.Controls.Grid.MeasureCellsGroup(Int32 cellsHead, Size referenceSize, Boolean ignoreDesiredSizeU, Boolean forceInfinityV, Boolean& hasDesiredSizeUChanged)
場所 System.Windows.Controls.Grid.MeasureOverride(Size constraint)
場所 System.Windows.FrameworkElement.MeasureCore(Size availableSize)
場所 System.Windows.UIElement.Measure(Size availableSize)
場所 System.Windows.Controls.Control.MeasureOverride(Size constraint)
場所 System.Windows.FrameworkElement.MeasureCore(Size availableSize)
場所 System.Windows.UIElement.Measure(Size availableSize)
場所 System.Windows.ContextLayoutManager.UpdateLayout()
場所 System.Windows.ContextLayoutManager.UpdateLayoutCallback(Object arg)
場所 System.Windows.Media.MediaContext.InvokeOnRenderCallback.DoWork()
場所 System.Windows.Media.MediaContext.FireInvokeOnRenderCallbacks()
場所 System.Windows.Media.MediaContext.RenderMessageHandlerCore(Object resizedCompositionTarget)
場所 System.Windows.Media.MediaContext.RenderMessageHandler(Object resizedCompositionTarget)
場所 System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
場所 MS.Internal.Threading.ExceptionFilterHelper.TryCatchWhen(Object source, Delegate method, Object args, Int32 numArgs, Delegate catchHandler)
場所 System.Windows.Threading.DispatcherOperation.InvokeImpl()
場所 System.Windows.Threading.DispatcherOperation.InvokeInSecurityContext(Object state)
場所 System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
場所 System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
場所 System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
場所 System.Windows.Threading.DispatcherOperation.Invoke()
場所 System.Windows.Threading.Dispatcher.ProcessQueue()
場所 System.Windows.Threading.Dispatcher.WndProcHook(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
場所 MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
場所 MS.Win32.HwndSubclass.DispatcherCallbackOperation(Object o)
場所 System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
場所 MS.Internal.Threading.ExceptionFilterHelper.TryCatchWhen(Object source, Delegate method, Object args, Int32 numArgs, Delegate catchHandler)
場所 System.Windows.Threading.Dispatcher.LegacyInvokeImpl(DispatcherPriority priority, TimeSpan timeout, Delegate method, Object args, Int32 numArgs)
場所 MS.Win32.HwndSubclass.SubclassWndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam)
場所 MS.Win32.UnsafeNativeMethods.DispatchMessage(MSG& msg)
場所 System.Windows.Threading.Dispatcher.PushFrameImpl(DispatcherFrame frame)
場所 System.Windows.Threading.Dispatcher.PushFrame(DispatcherFrame frame)
場所 System.Windows.Threading.Dispatcher.Run()
場所 System.Windows.Application.RunDispatcher(Object ignore)
場所 System.Windows.Application.RunInternal(Window window)
場所 System.Windows.Application.Run(Window window)
場所 System.Windows.Application.Run()
場所 TreeViewSample.App.Main() 場所 c:\Users\*****\Documents\Visual Studio 2013\Projects\TreeViewSample\TreeViewSample\obj\Debug\App.g.cs:行 0
場所 System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args)
場所 System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
場所 Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
場所 System.Threading.ThreadHelper.ThreadStart_Context(Object state)
場所 System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
場所 System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
場所 System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
場所 System.Threading.ThreadHelper.ThreadStart()
InnerException:
という、えげつない落ち方をします。はい。残念。

考察

落ちてる箇所は、上のほうで引用したMultiTriggerで間違いなくて、
  • TriggerがMultiTriggerである
  • EnterAction or ExitActionを指定してある
  • ConditionでSourceNameプロパティを利用している
  • Binding経由でTriggerが動く
ぐらいの条件が重なった時に発生するようです。
なので、
<!-- MultiTriggerを使わない -->
<MultiDataTrigger>
    <MultiDataTrigger.ExitActions>
        <BeginStoryboard Storyboard="{StaticResource HoverOff}" x:Name="HoverOff_BeginStoryboard"/>
    </MultiDataTrigger.ExitActions>
    <MultiDataTrigger.EnterActions>
        <BeginStoryboard Storyboard="{StaticResource HoverOn}"/>
    </MultiDataTrigger.EnterActions>
    <MultiDataTrigger.Conditions>
        <Condition Binding="{Binding ElementName=Selection_Border, Path=IsMouseOver}" Value="True" />
        <Condition Binding="{Binding RelativeSource={RelativeSource Self}, Path=IsSelected}" Value="False" />
    </MultiDataTrigger.Conditions>
</MultiDataTrigger>
か、
<!-- EnterAction/ExitActionを使わない -->
<MultiTrigger>
    <Setter Property="Opacity" Value="1" TargetName="HoverBorder" />
    <MultiTrigger.Conditions>
        <Condition Property="IsMouseOver" SourceName="Selection_Border" Value="True" />
        <Condition Property="IsSelected" Value="False" />
    </MultiTrigger.Conditions>
</MultiTrigger>
か、
<!-- ConditionにSourceNameを使わない -->
<MultiTrigger>
    <MultiTrigger.ExitActions>
        <BeginStoryboard Storyboard="{StaticResource HoverOff}" x:Name="HoverOff_BeginStoryboard"/>
    </MultiTrigger.ExitActions>
    <MultiTrigger.EnterActions>
        <BeginStoryboard Storyboard="{StaticResource HoverOn}"/>
    </MultiTrigger.EnterActions>
    <MultiTrigger.Conditions>
        <Condition Property="IsMouseOver" Value="True" />
        <Condition Property="IsSelected" Value="False" />
    </MultiTrigger.Conditions>
</MultiTrigger>
かの対応で、落ちなくなるので回避が可能です。
ただ、2番目と3番目は若干TreeViewItem自体の動作が変わるので、MultiTriggerを辞めるのがお勧めです。
広告

コメントする »

まだコメントはありません。

RSS feed for comments on this post. TrackBack URI

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中

WordPress.com Blog.

%d人のブロガーが「いいね」をつけました。