Search code examples
wpftreeviewtreeviewitemdropshadow

How to apply TreeView highlight color when TreeViewItem contains border with DropShadowEffect?


Situation:

I want a DropShadowEffect around my TreeViewItem, at the same time I want the TreeViewItem to change color when selected.

Problem:

For the DropShadowEffect not to be applied to the text in the TreeViewItem I need to set Background="White" on the border which contains the DropShadowEffect, this seems to override the highlighting of the TreeViewItem when selected.

I can either have the DropShadowEffect correctly applied like in this example:

enter image description here

Or I can have correct highlighting on selection, but dropshadow on the text, like in this example:

enter image description here

Question:

Is there a way to keep the highlighting possibilities of the TreeView and also keep the DropShadowEffect from beeing applied to the text in the TreeViewItem?

What I tried so far:

  1. I tried setting the Panel.ZIndex of the TreeViewItem in the style you see below like <Setter Property="Panel.ZIndex" Value="5"/>.
  2. I tried using two borders, which occupy the same space, like in this tutorial.

Additions:

I can't apply a white background to the TreeViewItem, because I have a border with rounded edges around it, which the perfectly square background of the TreeViewItem not respects.

I don't want the DropShadowEffect applied to the Expander button.

Minimal (Not-)Working Example:

TreeView:

<Window x:Class="TreeViewDropShadowExampl.MainWindow"
        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:TreeViewDropShadowExampl"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">

    <TreeView ItemsSource="{Binding Root}" MinWidth="100" MinHeight="100">
        <TreeView.Resources>

            <HierarchicalDataTemplate DataType="{x:Type local:Node}" ItemsSource="{Binding Children}">
                <Grid>
                    <Border BorderBrush="Gray">
                        <TextBlock Text="{Binding ID}"/>
                        <Border.Effect>
                            <DropShadowEffect Color="Gray" BlurRadius="2"/>
                        </Border.Effect>
                    </Border>
                </Grid>             
            </HierarchicalDataTemplate>

            <Style TargetType="{x:Type TreeViewItem}">
                <Setter Property="Margin" Value="2" />
            </Style>

        </TreeView.Resources>
    </TreeView>
</Window>

VM:

using System.Collections.ObjectModel;

namespace TreeViewDropShadowExampl
{
    class VM 
    {
        public VM()
        {
            var root = new Node("1");
            Root.Add(root);
            for (int i = 1; i < 4; i++)
            {
                Node newNode = new Node("1." + i.ToString());
                for (int j = 1; j < 4; j++)
                {
                    newNode.Children.Add(new Node("1." + i.ToString() + "." + j.ToString()));
                }
                root.Children.Add(newNode);
            }
        }

        public ObservableCollection<Node> Root { get; set; } = new ObservableCollection<Node>();
    }
}

Node:

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace TreeViewDropShadowExampl
{
    public class Node : INotifyPropertyChanged
    {
        #region WPF integration properties
        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
        #endregion WPF integration properties

        public Node(string id)
        {
            ID = id;
        }

        private string _id;
        public string ID
        {
            get { return _id; }
            set
            {
                _id = value;
                // Call OnPropertyChanged whenever the property is updated
                OnPropertyChanged();
            }
        }
        public ObservableCollection<Node> Children { get; set; } = new ObservableCollection<Node>();
    }
}

Solution

  • Instead of applying the effect to the Border inside of your data template, apply the effect to the whole item:

    <TreeView>
      <TreeView.ItemContainerStyle>
        <Style TargetType="TreeViewItem">
          <Style.Triggers>
            <Trigger Property="IsSelected" Value="False">
              <Setter Property="Background" Value="White"/>
            </Trigger>
          </Style.Triggers>
          <Setter Property="Effect">
            <Setter.Value>
              <DropShadowEffect Color="Gray" BlurRadius="2"/>
            </Setter.Value>
          </Setter>
        </Style>
      </TreeView.ItemContainerStyle>
    </TreeView>
    

    Don't forget to remove the effect from your data template's Border.
    Or even remove the Border completely, if you only needed it for the shadow effect.

    Update:

    You can apply the white background to the item too. Just make it in a trigger to ensure that it only occurs when the item is not selected.

    Update 2:

    Having all the new requirements, here is a complete solution for your problem.

    First of all, it is possible to have a rounded selection rectangle - you just need to change the selection border's style.

    <TreeView>
      <TreeView.ItemContainerStyle>
        <Style TargetType="TreeViewItem">
          <!-- This style changes the item's border appearance -->
          <Style.Resources>
            <Style TargetType="Border">
              <Setter Property="Effect">
                <Setter.Value>
                  <DropShadowEffect Color="Gray" BlurRadius="2"/>
                </Setter.Value>
              </Setter>
    
              <!-- Set the same corner radius here as in your data template -->
              <Setter Property="CornerRadius" Value="8"/>
            </Style>
          </Style.Resources>
          <Setter Property="Margin" Value="2"/>
        </Style>
      </TreeView.ItemContainerStyle>
      <TreeView.ItemTemplate>
        <HierarchicalDataTemplate>
          <Grid>
            <Border x:Name="RoundedBorder" CornerRadius="8">
              <!-- Your content here -->              
            </Border>            
          </Grid>             
          <HierarchicalDataTemplate.Triggers>
            <DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource FindAncestor, AncestorType=TreeViewItem}}" Value="False">
              <!-- Only setting the item's background when it's not selected -->
              <Setter TargetName="RoundedBorder" Property="Background" Value="White"/>
            </DataTrigger>
          </HierarchicalDataTemplate.Triggers>
        </HierarchicalDataTemplate>    
      </TreeView.ItemTemplate>
    </TreeView>