WPF TreeView with checkboxes

After much searching, I did not find a solution for the following problem. I need a treeview control with "checkboxed" elements and a property CheckedItemsfor convenient data binding (for example, the structure of the tree structure of folders, when the user checks folders, the size of the checked folders is displayed in the text box).

By the way, I read the article “Working with flags in WPF TreeView” by Josh Smith , but the “IsChecked” approach is not suitable in my case, because I need to bind CheckedItemsas a collection.

I would be grateful for any help!

alt text

. , CheckedItems CheckTreeView. - , CheckTreeView CheckedItems?

+5
1


-, CheckBoxTreeView . CheckBoxTreeViewLibrary

  • CheckedItems
  • CheckedItems - ObservableCollection<T>, T ItemsSource
  • CheckedItems
  • CheckBoxTreeViewItem ( ), CheckedItems ,

TreeView. IsChecked, CheckBoxTreeViewItemStyle.xaml ResourceDictionary. .

<Window.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="/CheckBoxTreeViewLibrary;component/Themes/CheckBoxTreeViewItemStyle.xaml"/>
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Window.Resources>

ItemContainerStyle

<cbt:CheckBoxTreeView ...>
    <cbt:CheckBoxTreeView.ItemContainerStyle>
        <Style TargetType="{x:Type cbt:CheckBoxTreeViewItem}"
               BasedOn="{StaticResource {x:Type cbt:CheckBoxTreeViewItem}}">
            <Setter Property="IsChecked" Value="{Binding IsChecked}"/>
            <!-- additional Setters, Triggers etc. -->
        </Style>
    </cbt:CheckBoxTreeView.ItemContainerStyle>
</cbt:CheckBoxTreeView>

CheckBoxTreeView.cs

namespace CheckBoxTreeViewLibrary
{
    [StyleTypedProperty(Property = "ItemContainerStyle", StyleTargetType = typeof(CheckBoxTreeViewItem))]
    public class CheckBoxTreeView : TreeView
    {
        public static DependencyProperty CheckedItemsProperty =
            DependencyProperty.Register("CheckedItems",
                                        typeof(IList),
                                        typeof(CheckBoxTreeView));

        private RoutedEventHandler Checked_EventHandler;
        private RoutedEventHandler Unchecked_EventHandler;

        public CheckBoxTreeView()
            : base()
        {
            Checked_EventHandler = new RoutedEventHandler(checkBoxTreeViewItem_Checked);
            Unchecked_EventHandler = new RoutedEventHandler(checkBoxTreeViewItem_Unchecked);

            DependencyPropertyDescriptor dpd =
                DependencyPropertyDescriptor.FromProperty(CheckBoxTreeView.ItemsSourceProperty, typeof(CheckBoxTreeView));
            if (dpd != null)
            {
                dpd.AddValueChanged(this, ItemsSourceChanged);
            }
        }
        void ItemsSourceChanged(object sender, EventArgs e)
        {
            Type type = ItemsSource.GetType();
            if (ItemsSource is IList)
            {
                Type listType = typeof(ObservableCollection<>).MakeGenericType(type.GetGenericArguments()[0]);
                CheckedItems = (IList)Activator.CreateInstance(listType);
            }
        }

        internal void OnNewContainer(CheckBoxTreeViewItem newContainer)
        {
            newContainer.Checked -= Checked_EventHandler;
            newContainer.Unchecked -= Unchecked_EventHandler;
            newContainer.Checked += Checked_EventHandler;
            newContainer.Unchecked += Unchecked_EventHandler;
        }

        protected override DependencyObject GetContainerForItemOverride()
        {
            CheckBoxTreeViewItem checkBoxTreeViewItem = new CheckBoxTreeViewItem();
            OnNewContainer(checkBoxTreeViewItem);
            return checkBoxTreeViewItem;
        }

        void checkBoxTreeViewItem_Checked(object sender, RoutedEventArgs e)
        {
            CheckBoxTreeViewItem checkBoxTreeViewItem = sender as CheckBoxTreeViewItem;

            Action action = () =>
            {
                var checkedItem = checkBoxTreeViewItem.Header;
                CheckedItems.Add(checkedItem);
            };
            this.Dispatcher.BeginInvoke(action, DispatcherPriority.ContextIdle);
        }

        void checkBoxTreeViewItem_Unchecked(object sender, RoutedEventArgs e)
        {
            CheckBoxTreeViewItem checkBoxTreeViewItem = sender as CheckBoxTreeViewItem;
            Action action = () =>
            {
                var uncheckedItem = checkBoxTreeViewItem.Header;
                CheckedItems.Remove(uncheckedItem);
            };
            this.Dispatcher.BeginInvoke(action, DispatcherPriority.ContextIdle);
        }

        public IList CheckedItems
        {
            get { return (IList)base.GetValue(CheckedItemsProperty); }
            set { base.SetValue(CheckedItemsProperty, value); }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        private void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}

CheckBoxTreeViewItem.cs

namespace CheckBoxTreeViewLibrary
{
    [StyleTypedProperty(Property = "ItemContainerStyle", StyleTargetType = typeof(CheckBoxTreeViewItem))]
    public class CheckBoxTreeViewItem : TreeViewItem
    {
        public static readonly RoutedEvent CheckedEvent = EventManager.RegisterRoutedEvent("Checked",
            RoutingStrategy.Direct,
            typeof(RoutedEventHandler),
            typeof(CheckBoxTreeViewItem));

        public static readonly RoutedEvent UncheckedEvent = EventManager.RegisterRoutedEvent("Unchecked",
            RoutingStrategy.Direct,
            typeof(RoutedEventHandler),
            typeof(CheckBoxTreeViewItem));

        public static readonly DependencyProperty IsCheckedProperty =
            DependencyProperty.Register("IsChecked",
                                        typeof(bool),
                                        typeof(CheckBoxTreeViewItem),
                                        new FrameworkPropertyMetadata(false,
                                                                      FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                                                                      CheckedPropertyChanged));

        private static void CheckedPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
        {
            CheckBoxTreeViewItem checkBoxTreeViewItem = (CheckBoxTreeViewItem)source;
            if (checkBoxTreeViewItem.IsChecked == true)
            {
                checkBoxTreeViewItem.OnChecked(new RoutedEventArgs(CheckedEvent, checkBoxTreeViewItem));
            }
            else
            {
                checkBoxTreeViewItem.OnUnchecked(new RoutedEventArgs(UncheckedEvent, checkBoxTreeViewItem));
            }
        }

        public CheckBoxTreeViewItem()
            : base()
        {
        }

        protected override DependencyObject GetContainerForItemOverride()
        {
            PropertyInfo parentTreeViewPi = typeof(TreeViewItem).GetProperty("ParentTreeView", BindingFlags.Instance | BindingFlags.NonPublic);
            CheckBoxTreeView parentCheckBoxTreeView = parentTreeViewPi.GetValue(this, null) as CheckBoxTreeView;
            CheckBoxTreeViewItem checkBoxTreeViewItem = new CheckBoxTreeViewItem();
            parentCheckBoxTreeView.OnNewContainer(checkBoxTreeViewItem);
            return checkBoxTreeViewItem;
        }

        [Category("Behavior")]
        public event RoutedEventHandler Checked
        {
            add
            {
                AddHandler(CheckedEvent, value);
            }
            remove
            {
                RemoveHandler(CheckedEvent, value);
            }
        }
        [Category("Behavior")]
        public event RoutedEventHandler Unchecked
        {
            add
            {
                AddHandler(UncheckedEvent, value);
            }
            remove
            {
                RemoveHandler(UncheckedEvent, value);
            }
        }

        public bool IsChecked
        {
            get { return (bool)base.GetValue(IsCheckedProperty); }
            set { base.SetValue(IsCheckedProperty, value); }
        }

        protected virtual void OnChecked(RoutedEventArgs e)
        {
            base.RaiseEvent(e);
        }
        protected virtual void OnUnchecked(RoutedEventArgs e)
        {
            base.RaiseEvent(e);
        }
    }
}

CheckBoxTreeViewItemStyle.xaml

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:cti="clr-namespace:CheckBoxTreeViewLibrary">
    <Style x:Key="TreeViewItemFocusVisual">
        <Setter Property="Control.Template">
            <Setter.Value>
                <ControlTemplate>
                    <Rectangle/>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    <PathGeometry x:Key="TreeArrow" Figures="M0,0 L0,6 L6,0 z"/>
    <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}">
                    <Border Background="Transparent" Height="16" Padding="5,5,5,5" Width="16">
                        <Path x:Name="ExpandPath" Data="{StaticResource TreeArrow}" Fill="Transparent" Stroke="#FF989898">
                            <Path.RenderTransform>
                                <RotateTransform Angle="135" CenterY="3" CenterX="3"/>
                            </Path.RenderTransform>
                        </Path>
                    </Border>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsMouseOver" Value="True">
                            <Setter Property="Stroke" TargetName="ExpandPath" Value="#FF1BBBFA"/>
                            <Setter Property="Fill" TargetName="ExpandPath" Value="Transparent"/>
                        </Trigger>
                        <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="#FF595959"/>
                            <Setter Property="Stroke" TargetName="ExpandPath" Value="#FF262626"/>
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    <Style TargetType="{x:Type cti:CheckBoxTreeViewItem}">
        <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 cti:CheckBoxTreeViewItem}">
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition MinWidth="15" Width="Auto"/>
                            <!--<ColumnDefinition Width="Auto"/>-->
                            <ColumnDefinition Width="*"/>
                        </Grid.ColumnDefinitions>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto" MinHeight="15"/>
                            <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">
                            <StackPanel Orientation="Horizontal">
                                <CheckBox Margin="0,2,4,0" x:Name="PART_CheckedCheckBox" IsChecked="{Binding IsChecked, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" />
                                <ContentPresenter x:Name="PART_Header" ContentSource="Header" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                            </StackPanel>
                        </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.ControlBrushKey}}"/>
                            <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
                        </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="VirtualizingStackPanel.IsVirtualizing" Value="true">
                <Setter Property="ItemsPanel">
                    <Setter.Value>
                        <ItemsPanelTemplate>
                            <VirtualizingStackPanel/>
                        </ItemsPanelTemplate>
                    </Setter.Value>
                </Setter>
            </Trigger>
        </Style.Triggers>
    </Style>
</ResourceDictionary>
+4

All Articles