2

For my WPF TreeView user control, I would like to display a progress ring around a ToggleButton to indicate that the node's children are populating, like so:

Image

(Full disclosure, I saw this type of behavior in some open source software, I just can't remember where). I still want the toggle button to be interactive (i.e., get mouse events, like hover and click) so the user can collapse the node if they no longer wish to wait for the update to complete.

My first thought was to add the progress ring as an adorner layer to the ToggleButton. However, since the WPF adorner layer lies on top of the UI element in Z-order, I cannot seem to interact with the toggle button below the progress ring. Is there a way to draw the toggle button on top of the adorner layer? Or does an adorner layer not seem like the right approach in this case? Would it be better to just trigger off a property to control the template of the toggle button and draw both the progress ring and the toggle button when updating the children? Thanks for any thoughts!

Relevant tutorials I am leveraging off:

Working with Checkboxes in the WPF TreeView

WPF Loading Wait Adorner (which has links to progress ring and adorner sources)

miesch1
  • 165
  • 2
  • 8

2 Answers2

2

You could use a grid to have the animation behind the expander (toggle button). The Grid will stack elements in the same cell in z-order. The animation could always exist and just be hidden, made visible by some trigger defined by you.

blueprint
  • 198
  • 8
  • Thanks for the excellent tip. This approach is how @netaholic posted below. I chose his answer simply because it included source. – miesch1 Sep 02 '15 at 00:02
1

I believe you don't need adorners to acomplish what you desire. I've edited tree view template via Blend, adding user control with busy animation to expander button in treeview item style. There is a lot to be done, but it can demonstrate you the approach. Also there is probably a better way to change visibility of indicator, rather then searching for it in a visual tree.

The way I see it is to create custom controls for treeview and treeviewitem. Override it's style and add a "IsLoading" property for treeviewitem. Switching this property should change visibility of usercontrol.

enter image description here

MainWindow.xaml

<Window x:Class="WpfApplication17.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" xmlns:local="clr-namespace:WpfApplication17">
    <Window.Resources>

        <Style x:Key="TreeViewItemFocusVisual">
            <Setter Property="Control.Template">
                <Setter.Value>
                    <ControlTemplate>
                        <Rectangle/>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
        <SolidColorBrush x:Key="TreeViewItem.TreeArrow.Static.Checked.Fill" Color="#FF595959"/>
        <SolidColorBrush x:Key="TreeViewItem.TreeArrow.Static.Checked.Stroke" Color="#FF262626"/>
        <SolidColorBrush x:Key="TreeViewItem.TreeArrow.MouseOver.Stroke" Color="#FF1BBBFA"/>
        <SolidColorBrush x:Key="TreeViewItem.TreeArrow.MouseOver.Fill" Color="Transparent"/>
        <SolidColorBrush x:Key="TreeViewItem.TreeArrow.MouseOver.Checked.Stroke" Color="#FF262626"/>
        <SolidColorBrush x:Key="TreeViewItem.TreeArrow.MouseOver.Checked.Fill" Color="#FF595959"/>
        <PathGeometry x:Key="TreeArrow" Figures="M0,0 L0,6 L6,0 z"/>
        <SolidColorBrush x:Key="TreeViewItem.TreeArrow.Static.Fill" Color="Transparent"/>
        <SolidColorBrush x:Key="TreeViewItem.TreeArrow.Static.Stroke" Color="#FF989898"/>
        <Style x:Key="ExpandCollapseToggleStyle" TargetType="{x:Type ToggleButton}">
            <Setter Property="Focusable" Value="False"/>
            <Setter Property="Width" Value="16"/>
            <Setter Property="Height" Value="16"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ToggleButton}">
                        <Grid HorizontalAlignment="Left" Height="16" VerticalAlignment="Top" Width="16">
                            <Border Background="Transparent" Height="16" Padding="5,5,5,5" Width="16">
                                <Path x:Name="ExpandPath" Data="{StaticResource TreeArrow}" Fill="{StaticResource TreeViewItem.TreeArrow.Static.Fill}" Stroke="{StaticResource TreeViewItem.TreeArrow.Static.Stroke}">
                                    <Path.RenderTransform>
                                        <RotateTransform Angle="135" CenterY="3" CenterX="3"/>
                                    </Path.RenderTransform>
                                </Path>
                            </Border>
                            <local:UserControl1 Name="busyInd"/>
                        </Grid>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsChecked" Value="True">
                                <Setter Property="RenderTransform" TargetName="ExpandPath">
                                    <Setter.Value>
                                        <RotateTransform Angle="180" CenterY="3" CenterX="3"/>
                                    </Setter.Value>
                                </Setter>
                                <Setter Property="Fill" TargetName="ExpandPath" Value="{StaticResource TreeViewItem.TreeArrow.Static.Checked.Fill}"/>
                                <Setter Property="Stroke" TargetName="ExpandPath" Value="{StaticResource TreeViewItem.TreeArrow.Static.Checked.Stroke}"/>
                            </Trigger>
                            <Trigger Property="IsMouseOver" Value="True">
                                <Setter Property="Stroke" TargetName="ExpandPath" Value="{StaticResource TreeViewItem.TreeArrow.MouseOver.Stroke}"/>
                                <Setter Property="Fill" TargetName="ExpandPath" Value="{StaticResource TreeViewItem.TreeArrow.MouseOver.Fill}"/>
                            </Trigger>
                            <MultiTrigger>
                                <MultiTrigger.Conditions>
                                    <Condition Property="IsMouseOver" Value="True"/>
                                    <Condition Property="IsChecked" Value="True"/>
                                </MultiTrigger.Conditions>
                                <Setter Property="Stroke" TargetName="ExpandPath" Value="{StaticResource TreeViewItem.TreeArrow.MouseOver.Checked.Stroke}"/>
                                <Setter Property="Fill" TargetName="ExpandPath" Value="{StaticResource TreeViewItem.TreeArrow.MouseOver.Checked.Fill}"/>
                            </MultiTrigger>
                        </ControlTemplate.Triggers>

                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
        <Style x:Key="TreeViewItemStyle1" TargetType="{x:Type TreeViewItem}">
            <Setter Property="Background" Value="Transparent"/>
            <Setter Property="HorizontalContentAlignment" Value="{Binding HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
            <Setter Property="VerticalContentAlignment" Value="{Binding VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
            <Setter Property="Padding" Value="1,0,0,0"/>
            <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
            <Setter Property="FocusVisualStyle" Value="{StaticResource TreeViewItemFocusVisual}"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type TreeViewItem}">
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition MinWidth="19" Width="Auto"/>
                                <ColumnDefinition Width="Auto"/>
                                <ColumnDefinition Width="*"/>
                            </Grid.ColumnDefinitions>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="Auto"/>
                                <RowDefinition/>
                            </Grid.RowDefinitions>
                            <ToggleButton x:Name="Expander" ClickMode="Press" IsChecked="{Binding IsExpanded, RelativeSource={RelativeSource TemplatedParent}}" Style="{StaticResource ExpandCollapseToggleStyle}"/>
                            <Border x:Name="Bd" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Grid.Column="1" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="true">
                                <ContentPresenter x:Name="PART_Header" ContentSource="Header" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                            </Border>
                            <ItemsPresenter x:Name="ItemsHost" Grid.ColumnSpan="2" Grid.Column="1" Grid.Row="1"/>
                        </Grid>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsExpanded" Value="false">
                                <Setter Property="Visibility" TargetName="ItemsHost" Value="Collapsed"/>
                            </Trigger>
                            <Trigger Property="HasItems" Value="false">
                                <Setter Property="Visibility" TargetName="Expander" Value="Hidden"/>
                            </Trigger>
                            <Trigger Property="IsSelected" Value="true">
                                <Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
                                <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}"/>
                            </Trigger>
                            <MultiTrigger>
                                <MultiTrigger.Conditions>
                                    <Condition Property="IsSelected" Value="true"/>
                                    <Condition Property="IsSelectionActive" Value="false"/>
                                </MultiTrigger.Conditions>
                                <Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.InactiveSelectionHighlightBrushKey}}"/>
                                <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.InactiveSelectionHighlightTextBrushKey}}"/>
                            </MultiTrigger>
                            <Trigger Property="IsEnabled" Value="false">
                                <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <Style.Triggers>
                <Trigger Property="VirtualizingPanel.IsVirtualizing" Value="true">
                    <Setter Property="ItemsPanel">
                        <Setter.Value>
                            <ItemsPanelTemplate>
                                <VirtualizingStackPanel/>
                            </ItemsPanelTemplate>
                        </Setter.Value>
                    </Setter>
                </Trigger>
            </Style.Triggers>
        </Style>
        <Style x:Key="BusyInd" TargetType="{x:Type Grid}"/>
    </Window.Resources>

    <Grid>
        <TreeView Name="treeView" SelectedItemChanged="treeView_SelectedItemChanged" HorizontalAlignment="Left" Height="300" Margin="10,10,0,0" VerticalAlignment="Top" Width="497" ItemContainerStyle="{DynamicResource TreeViewItemStyle1}">

            <TreeViewItem Header="TreeViewItem" IsExpanded="True">
                <TreeViewItem Header="TreeViewItem" HorizontalAlignment="Left" Width="474"/>
                <TreeViewItem Header="TreeViewItem" HorizontalAlignment="Left" Width="474"/>
            </TreeViewItem>
            <TreeViewItem Header="TreeViewItem" IsExpanded="True">
                <TreeViewItem Header="TreeViewItem" HorizontalAlignment="Left" Width="474"/>
                <TreeViewItem Header="TreeViewItem" HorizontalAlignment="Left" Width="474"/>
            </TreeViewItem>
            <TreeViewItem Header="TreeViewItem" IsExpanded="True">
                <TreeViewItem Header="TreeViewItem" HorizontalAlignment="Left" Width="474"/>
            </TreeViewItem>
        </TreeView>

    </Grid>
</Window>

MainWindow.cs

public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

        }

        private void treeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
        {
            TreeViewItem item_container = (TreeViewItem)treeView.ItemContainerGenerator.ContainerFromItem(treeView.SelectedItem);
            var indicator = FindChild<UserControl1>(item_container, "busyInd");
            indicator.Visibility = Visibility.Visible;
            Task.Factory.StartNew(() =>
            {
                Thread.Sleep(3000);
                Dispatcher.Invoke(() => { indicator.Visibility = Visibility.Collapsed; });
            });
        }

        public static T FindChild<T>(DependencyObject parent, string childName)
           where T : DependencyObject
        {
            if (parent == null) return null;

            T foundChild = null;

            int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
            for (int i = 0; i < childrenCount; i++)
            {
                var child = VisualTreeHelper.GetChild(parent, i);
                // If the child is not of the request child type child
                T childType = child as T;
                if (childType == null)
                {
                    // recursively drill down the tree
                    foundChild = FindChild<T>(child, childName);

                    // If the child is found, break so we do not overwrite the found child. 
                    if (foundChild != null) break;
                }
                else if (!string.IsNullOrEmpty(childName))
                {
                    var frameworkElement = child as FrameworkElement;
                    // If the child's name is set for search
                    if (frameworkElement != null && frameworkElement.Name == childName)
                    {
                        // if the child's name is of the request name
                        foundChild = (T)child;
                        break;
                    }
                }
                else
                {
                    // child element found.
                    foundChild = (T)child;
                    break;
                }
            }

            return foundChild;
        }
    }

UserControl1.xaml

<UserControl x:Class="WpfApplication17.UserControl1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Height="Auto" Width="Auto" Background="Transparent" Visibility="Collapsed">
    <Viewbox Width="{Binding Width, ElementName=BusyIndicator}" Height="{Binding Height, ElementName=BusyIndicator}"      
        HorizontalAlignment="Center" VerticalAlignment="Center">
        <Grid Background="Transparent" ToolTip="Searching...." HorizontalAlignment="Center" VerticalAlignment="Center">
            <Canvas Name="Canvas1"  
         RenderTransformOrigin="0.5,0.5"  
         HorizontalAlignment="Center"         
         VerticalAlignment="Center" Width="120" Height="120">
                <Canvas.RenderTransform>
                    <RotateTransform Angle="0" />
                </Canvas.RenderTransform>
                <Canvas.Style>
                    <Style TargetType="Canvas">
                        <Style.Triggers>
                            <Trigger Property="IsVisible" Value="True">
                                <Trigger.EnterActions>
                                    <BeginStoryboard Name="Storyboard_Rotate">
                                        <Storyboard RepeatBehavior="Forever">
                                            <DoubleAnimation Storyboard.TargetProperty="RenderTransform.Angle"   
                          From="0" To="360" Duration="0:0:2"/>
                                        </Storyboard>
                                    </BeginStoryboard>
                                </Trigger.EnterActions>
                                <Trigger.ExitActions>
                                    <StopStoryboard BeginStoryboardName="Storyboard_Rotate" />
                                </Trigger.ExitActions>
                            </Trigger>

                        </Style.Triggers>
                    </Style>
                </Canvas.Style>
                <Ellipse Width="20" Height="20" Stretch="Fill" Fill="Black" Opacity="1.0" Canvas.Left="50" Canvas.Top="0" />
                <Ellipse Width="20" Height="20" Stretch="Fill" Fill="Black" Opacity="0.9" Canvas.Left="20.6107373853764" Canvas.Top="9.54915028125262" />
                <Ellipse Width="20" Height="20" Stretch="Fill" Fill="Black" Opacity="0.8" Canvas.Left="2.44717418524233" Canvas.Top="34.5491502812526" />
                <Ellipse Width="20" Height="20" Stretch="Fill" Fill="Black" Opacity="0.7" Canvas.Left="2.44717418524232" Canvas.Top="65.4508497187474" />
                <Ellipse Width="20" Height="20" Stretch="Fill" Fill="Black" Opacity="0.6" Canvas.Left="20.6107373853763" Canvas.Top="90.4508497187474" />
                <Ellipse Width="20" Height="20" Stretch="Fill" Fill="Black" Opacity="0.5" Canvas.Left="50" Canvas.Top="100" />
                <Ellipse Width="20" Height="20" Stretch="Fill" Fill="Black" Opacity="0.4" Canvas.Left="79.3892626146236" Canvas.Top="90.4508497187474" />
                <Ellipse Width="20" Height="20" Stretch="Fill" Fill="Black" Opacity="0.3" Canvas.Left="97.5528258147577" Canvas.Top="65.4508497187474" />
                <Ellipse Width="20" Height="20" Stretch="Fill" Fill="Black" Opacity="0.2" Canvas.Left="97.5528258147577" Canvas.Top="34.5491502812526" />
            </Canvas>
        </Grid>
    </Viewbox>
</UserControl>
netaholic
  • 1,345
  • 10
  • 22