[WPF Custom Controls] create ListBoxItem contain the CheckBox

1 Introduction

Xceed wpftoolkit provides a CheckListBox , the following effects:

But it is not very good with it, so its not as good as the reference UWP ListView achieve, but also good to see the animation:

Its style is as follows:

<ListViewItemPresenter ContentTransitions="{TemplateBinding ContentTransitions}"
    x:Name="Root"
    Control.IsTemplateFocusTarget="True"
    FocusVisualMargin="{TemplateBinding FocusVisualMargin}"
    SelectionCheckMarkVisualEnabled="{ThemeResource ListViewItemSelectionCheckMarkVisualEnabled}"
    CheckBrush="{ThemeResource ListViewItemCheckBrush}"
    CheckBoxBrush="{ThemeResource ListViewItemCheckBoxBrush}"
    DragBackground="{ThemeResource ListViewItemDragBackground}"
    DragForeground="{ThemeResource ListViewItemDragForeground}"
    FocusBorderBrush="{ThemeResource ListViewItemFocusBorderBrush}"
    FocusSecondaryBorderBrush="{ThemeResource ListViewItemFocusSecondaryBorderBrush}"
    PlaceholderBackground="{ThemeResource ListViewItemPlaceholderBackground}"
    PointerOverBackground="{ThemeResource ListViewItemBackgroundPointerOver}"
    PointerOverForeground="{ThemeResource ListViewItemForegroundPointerOver}"
    SelectedBackground="{ThemeResource ListViewItemBackgroundSelected}"
    SelectedForeground="{ThemeResource ListViewItemForegroundSelected}"
    SelectedPointerOverBackground="{ThemeResource ListViewItemBackgroundSelectedPointerOver}"
    PressedBackground="{ThemeResource ListViewItemBackgroundPressed}"
    SelectedPressedBackground="{ThemeResource ListViewItemBackgroundSelectedPressed}"
    DisabledOpacity="{ThemeResource ListViewItemDisabledThemeOpacity}"
    DragOpacity="{ThemeResource ListViewItemDragThemeOpacity}"
    ReorderHintOffset="{ThemeResource ListViewItemReorderHintThemeOffset}"
    HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
    VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
    ContentMargin="{TemplateBinding Padding}"
    CheckMode="{ThemeResource ListViewItemCheckMode}"
    RevealBackground="{ThemeResource ListViewItemRevealBackground}"
    RevealBorderThickness="{ThemeResource ListViewItemRevealBorderThemeThickness}"
    RevealBorderBrush="{ThemeResource ListViewItemRevealBorderBrush}">

Property is a lot, but there is no custom methods CheckBox style, but it also can not refer to how to achieve animation. Fortunately UWP also it provides a ListViewItemExpanded style, which has a complete layout, VisualState, etc., but there is almost a total of 500 lines, which take only part of MultiSelectStates also nearly 100 lines, this is too complicated, and this is some trouble in WPF but much simpler to implement.

2. Implement

Microsoft's documentation explains how to have the Create A ListViewItems with the CheckBox , the principle is very simple:

<DataTemplate x:Key="FirstCell">
  <StackPanel Orientation="Horizontal">
    <CheckBox IsChecked="{Binding Path=IsSelected, 
      RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListViewItem}}}"/>
  </StackPanel>
</DataTemplate>

It is to add a CheckBox in this CheckBox control template and bind to the ListViewItem IsSelected Binding properties by way of FindAncestor. Although the method ListView, but it also applies to ListBox. So I'm using this approach encapsulates a ListBox control, the current basically no function, just add a CheckBox in front of each ListBoxItem. Previously introduced how to customize ItemsControl , you want to customize a ListBox control, also need three:

  1. Defined ListBox
  2. ListBoxItem association and ListBox
  3. ListBox achieve logic
public class ExtendedListBox : ListBox
{
    public static readonly DependencyProperty IsMultiSelectCheckBoxEnabledProperty =
        DependencyProperty.Register(nameof(IsMultiSelectCheckBoxEnabled), typeof(bool), typeof(ExtendedListBox), new PropertyMetadata(true));

    public bool IsMultiSelectCheckBoxEnabled
    {
        get { return (bool)GetValue(IsMultiSelectCheckBoxEnabledProperty); }
        set { SetValue(IsMultiSelectCheckBoxEnabledProperty, value); }
    }

    protected override DependencyObject GetContainerForItemOverride()
    {
        return new ExtendedListBoxItem();
    }
}


public class ExtendedListBoxItem : ListBoxItem
{
    public ExtendedListBoxItem()
    {
        DefaultStyleKey = typeof(ExtendedListBoxItem);
    }
}

The above is all the code. Defines ExtendedListBoxand ExtendedListBoxItemtwo classes, and then rewrite the GetContainerForItemOverrideassociation of these two classes, and finally ExtendedListBoxthe code to mimic the UWP ListView provides IsMultiSelectCheckBoxEnabledproperty, other major functions provided by the XAML:

<Grid.ColumnDefinitions>
    <ColumnDefinition Width="Auto"/>
    <ColumnDefinition/>
</Grid.ColumnDefinitions>
<Primitives:KinoResizer>
    <CheckBox Margin="{TemplateBinding Padding}"
              IsChecked="{Binding IsSelected, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
              VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
              IsTabStop="False"
              x:Name="SelectionCheckMark"/>
</Primitives:KinoResizer>
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" Grid.Column="1"
                  Margin="{TemplateBinding Padding}"/>

ControlTemplate use Resizer packaging CheckBox, this is the time for transition animation CheckBox hide or show. Then add two DataTrigger in ControlTemplate.Triggers in accordance belongs to the ListBox IsMultiSelectCheckBoxEnabledand SelectionModedisplay or hide SelectionCheckMark:

<DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=ListBox},Path=SelectionMode}"
             Value="Single">
    <Setter Property="Visibility"
            TargetName="SelectionCheckMark"
            Value="Collapsed" />
</DataTrigger>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=ListBox},Path=IsMultiSelectCheckBoxEnabled}"
             Value="False">
    <Setter Property="Visibility"
            TargetName="SelectionCheckMark"
            Value="Collapsed" />
</DataTrigger>

The final results are as follows:

3. Add VisualState

Button ControlTemplate of the WPF does not use VisualState, but Button support VisualState, users can customize VisualState use the ControlTemplate. ExtendedListBoxItem imitate UWP MultiSelectEnabled and provides the VisualState MultiSelectDisabled two, because the carrier needs to know its ListBoxItem the ListBox IsMultiSelectCheckBoxEnabled and the SelectionMode, it is necessary to add a ListBoxItem Owner property and override the ListBox PrepareContainerForItemOverride function, this function is in the ListBoxItem Owner assignment:

protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
{
    base.PrepareContainerForItemOverride(element, item);
    if (element is ExtendedListBoxItem listBoxItem)
        listBoxItem.Owner = this;
}

Owner ListBoxItem used to monitor and change the IsMultiSelectCheckBoxEnabled SelectionMode and update VisualState change when these two values:

protected virtual void OnOwnerChanged(ExtendedListBox oldValue, ExtendedListBox newValue)
{
    if (oldValue != null)
    {
        var descriptor = DependencyPropertyDescriptor.FromProperty(ListBox.SelectionModeProperty, typeof(ExtendedListBox));
        descriptor.RemoveValueChanged(newValue, OnSelectionModeChanged);

        descriptor = DependencyPropertyDescriptor.FromProperty(ExtendedListBox.IsMultiSelectCheckBoxEnabledProperty, typeof(ExtendedListBox));
        descriptor.RemoveValueChanged(newValue, OnIsMultiSelectCheckBoxEnabledChanged);
    }
    if (newValue != null)
    {
        var descriptor = DependencyPropertyDescriptor.FromProperty(ListBox.SelectionModeProperty, typeof(ExtendedListBox));
        descriptor.AddValueChanged(newValue, OnSelectionModeChanged);

        descriptor = DependencyPropertyDescriptor.FromProperty(ExtendedListBox.IsMultiSelectCheckBoxEnabledProperty, typeof(ExtendedListBox));
        descriptor.AddValueChanged(newValue, OnIsMultiSelectCheckBoxEnabledChanged);
    }
}

private void OnSelectionModeChanged(object sender, EventArgs args)
{
    UpdateVisualStates(true);
}

private void OnIsMultiSelectCheckBoxEnabledChanged(object sender, EventArgs args)
{
    UpdateVisualStates(true);
}

为了使用VisualState我在ControlTemplate多写了80行代码,因为没有用上VisualTransition所以这个ControlTemplate有一些Bug,反正只是用来验证添加的两个VisualState是否有效。在ListBoxItem里用Trigger比使用VisualState更简洁有效。

4. 使用同样的原理为DataGrid的行添加ChechBox

DataGrid也可以用同样的原理为每一行添加CheckBox,只不过DataGrid的Template会负责很多。

首先自定义一个DataGrid类:

public class ExtendedDataGrid : DataGrid, IMultiSelector
{
    // Using a DependencyProperty as the backing store for IsMultiSelectCheckBoxEnabled.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty IsMultiSelectCheckBoxEnabledProperty =
        DependencyProperty.Register(nameof(IsMultiSelectCheckBoxEnabled), typeof(bool), typeof(ExtendedDataGrid), new PropertyMetadata(true));

    public ExtendedDataGrid()
    {
        DefaultStyleKey = typeof(ExtendedDataGrid);
    }

    public bool IsMultiSelectCheckBoxEnabled
    {
        get { return (bool)GetValue(IsMultiSelectCheckBoxEnabledProperty); }
        set { SetValue(IsMultiSelectCheckBoxEnabledProperty, value); }
    }
}

然后定义一个RowHeaderTemplate

<DataTemplate x:Key="DataGridRowHeaderTemplate">
    <Grid>
        <CheckBox IsChecked="{Binding IsSelected, Mode=TwoWay, RelativeSource={RelativeSource AncestorType={x:Type DataGridRow}, Mode=FindAncestor}}"
                      x:Name="SelectionCheckBox"/>
    </Grid>
</DataTemplate>

在DataGrid的Style上应用这个RowHeaderTemplate。最后再DataGrid的Style的Triggers中添加两个DataTrigger:

<Trigger Property="SelectionMode" Value="Single">
    <Setter Property="HeadersVisibility"  Value="Column" />
</Trigger>
<Trigger Property="IsMultiSelectCheckBoxEnabled" Value="False">
    <Setter Property="HeadersVisibility"  Value="Column"/>
</Trigger>

HeadersVisibility是个DataGridHeadersVisibility的属性,它用于控制DataGrid行和列的Header是否显示,因为我在每一行的开头放了CheckBox(就是使用上面定义的RowHeaderTempalte),所以定一只只显示Column的Header的话相当于隐藏了这个CheckBox,运行效果如下:

5. 结语

ListBox和DataGrid的自定义是个很大的话题,这里只实现最简单的功能,通常会根据业务需求逐渐增加更多需求。如果有更复杂的需求,我建议买商业的控件,毕竟DataGrid的自定义可以很复杂,花时间不如花钱。

6. 参考

How to_ Create ListViewItems with a CheckBox - WPF _ Microsoft Docs

ListBox Class (System.Windows.Controls) _ Microsoft Docs

DataGrid Class (System.Windows.Controls) _ Microsoft Docs

7. 源码

Kino.Toolkit.Wpf_ExtendedListBox.cs at master

Kino.Toolkit.Wpf_ExtendedDataGrid.cs at master

Guess you like

Origin www.cnblogs.com/dino623/p/Create_ListBoxIte_with_a_CheckBox.html