在自定义控件中重新创建ComboBox的DisplayMemberPath

时间:2018-08-13 14:32:54

标签: c# wpf xaml

我正在创建一个自定义WPF控件(CheckedListComboBox),该控件允许用户从列表中选择一个或多个选项。本质上,用户打开一个下拉式控件,然后选中要选择的项目。当用户选中或取消选中列表中的选项时,控件的主(非弹出)区域将更新以反映用户所做的选择。

以下是实际使用的控件的示例:

enter image description here

我对控件的状态非常满意,但是我想改进其中的一部分,我不确定如何进行。

生成反映用户选择的文本当前取决于在每个选定项目上调用ToString。在上面的示例中,这很好,因为我传递给控件的所有对象都是字符串。但是,如果我传入了一个没有覆盖ToString的自定义对象,我只会得到该对象的完全合格的类(例如MyNamespace.MyObject)。

我想做的是实现类似于WPF ComboBox的DisplayMemberPath属性,在这里我可以简单地为每个选定的项目指定要显示在控件的TextBox区域中的属性。

我可能会很懒,可以通过Reflection来解决这个问题,但是我知道这可能不是最快的(在性能方面)。

这是我到目前为止考虑过的选项:

强制所有绑定的项实现特定的界面-如果传递给控件的所有项都实现了一个接口,例如ICheckableItem,则我可以安全地访问每个项的相同属性填充文本框。我回避这种方法,因为我希望控件可以公开接受什么类型的项目。

使用ItemsControl来显示文本,而不是在背后的代码中生成文本-理论上,我可以在控件内私下维护一个已检查项目的列表,将该列表绑定到控制自身,然后以某种方式绑定到DisplayMemberPath中确定的属性。我不知道这是否可能,因为我认为它必须做某种双重约束的魔术才能起作用,而且我认为这是不可能的。如果我遵循此路线,还会出现分隔符(在这种情况下为逗号)出现的问题。

我上面列出的选项似乎无效,并且我想不出任何其他可能的方法。谁能提供其他解决方案?

到目前为止,这是我的代码:

XAML

<UserControl
    x:Class="MyNamespace.CheckedListComboBox"
    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"
    d:DesignHeight="30"
    d:DesignWidth="300"
    mc:Ignorable="d">
    <Grid x:Name="MainGrid">
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>
        <Grid x:Name="TextBoxHolderGrid">
            <TextBox
                x:Name="PART_TextBox"
                MaxWidth="{Binding ActualWidth, ElementName=TextBoxHolderGrid}"
                Focusable="False"
                IsReadOnly="True"
                TextWrapping="NoWrap" />
        </Grid>
        <ToggleButton
            x:Name="PART_ToggleButton"
            Grid.Column="1"
            Margin="-1,0,0,0"
            HorizontalAlignment="Right"
            ClickMode="Press"
            Focusable="False"
            IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}"
            IsTabStop="False">
            <Path
                x:Name="CollapsedArrow"
                Margin="2"
                HorizontalAlignment="Center"
                VerticalAlignment="Center"
                Data="M 0 0 L 4 4 L 8 0 Z">
                <Path.Fill>
                    <SolidColorBrush Color="{x:Static SystemColors.ControlTextColor}" />
                </Path.Fill>
            </Path>
        </ToggleButton>
        <Popup
            x:Name="PART_Popup"
            Grid.ColumnSpan="2"
            MinWidth="{Binding ActualWidth, ElementName=MainGrid}"
            MaxHeight="{Binding MaxPopupHeight, Mode=TwoWay, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}"
            Margin="0,-1,0,0"
            IsOpen="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}"
            Placement="Bottom"
            StaysOpen="False">
            <Border BorderThickness="1">
                <Border.BorderBrush>
                    <SolidColorBrush Color="{x:Static SystemColors.ControlTextColor}" />
                </Border.BorderBrush>
                <ScrollViewer x:Name="PART_DropDownScrollViewer" BorderThickness="1">
                    <ItemsControl
                        ItemTemplate="{Binding ItemTemplate, Mode=TwoWay, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}"
                        ItemsSource="{Binding ItemsSource, Mode=TwoWay, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}"
                        KeyboardNavigation.DirectionalNavigation="Contained">
                        <ItemsControl.Background>
                            <SolidColorBrush Color="{x:Static SystemColors.ControlLightLightColor}" />
                        </ItemsControl.Background>
                    </ItemsControl>
                </ScrollViewer>
            </Border>
        </Popup>
    </Grid>
</UserControl>

背后的代码

public partial class CheckedListComboBox : UserControl
{
    private bool mouseIsOverPopup = false;

    public static readonly DependencyProperty ItemTemplateProperty = DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(CheckedListComboBox), new PropertyMetadata(null));
    public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register("ItemsSource", typeof(IEnumerable<CheckedListObject>), typeof(CheckedListComboBox), new PropertyMetadata(null, ItemsSourcePropertyChanged));
    public static readonly DependencyProperty IsDropDownOpenProperty = DependencyProperty.Register("IsDropDownOpen", typeof(bool), typeof(CheckedListComboBox), new PropertyMetadata(false, IsDropDownOpenChanged));
    public static readonly DependencyProperty MaxPopupHeightProperty = DependencyProperty.Register("MaxPopupHeight", typeof(double), typeof(CheckedListComboBox), new PropertyMetadata((double)200));

    public DataTemplate ItemTemplate
    {
        get { return (DataTemplate)GetValue(ItemTemplateProperty); }
        set { SetValue(ItemTemplateProperty, value); }
    }

    public IEnumerable<CheckedListObject> ItemsSource
    {
        get { return (IEnumerable<CheckedListObject>)GetValue(ItemsSourceProperty); }
        set { SetValue(ItemsSourceProperty, value); }
    }

    public bool IsDropDownOpen
    {
        get { return (bool)GetValue(IsDropDownOpenProperty); }
        set { SetValue(IsDropDownOpenProperty, value); }
    }

    public double MaxPopupHeight
    {
        get { return (double)GetValue(MaxPopupHeightProperty); }
        set { SetValue(MaxPopupHeightProperty, value); }
    }

    public CheckedListComboBox()
    {
        InitializeComponent();
    }

    private static void ItemsSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is CheckedListComboBox checkedListComboBox)
        {
            if (e.OldValue != null && e.OldValue is IEnumerable<CheckedListObject> oldItems)
            {
                foreach (var item in oldItems)
                {
                    item.PropertyChanged -= checkedListComboBox.SubItemPropertyChanged;
                }
            }

            if (e.OldValue != null && e.OldValue is INotifyCollectionChanged oldCollection)
            {
                oldCollection.CollectionChanged -= checkedListComboBox.ItemsSourceCollectionChanged;
            }

            if (e.NewValue != null && e.NewValue is IEnumerable<CheckedListObject> newItems)
            {
                foreach (var item in newItems)
                {
                    item.PropertyChanged += checkedListComboBox.SubItemPropertyChanged;
                }
            }

            if (e.NewValue != null && e.NewValue is INotifyCollectionChanged newCollection)
            {
                newCollection.CollectionChanged += checkedListComboBox.ItemsSourceCollectionChanged;
            }

            checkedListComboBox.CalculateComboBoxText();
        }
    }

    private void SubItemPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName.Equals("IsChecked", StringComparison.OrdinalIgnoreCase))
        {
            CalculateComboBoxText();
        }
    }

    private void ItemsSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        // The bound ItemsSource collection has changed, so we want to unsubscribe any old items
        // from the PropertyChanged event, and subscribe any new ones to this event.

        if (e.OldItems != null)
        {
            foreach (var oldItem in e.OldItems)
            {
                if (oldItem is CheckedListObject item)
                {
                    item.PropertyChanged -= SubItemPropertyChanged;
                }
            }
        }

        if (e.NewItems != null)
        {
            foreach (var newItem in e.NewItems)
            {
                if (newItem is CheckedListObject item)
                {
                    item.PropertyChanged += SubItemPropertyChanged;
                }
            }
        }

        // We also want to re-calculate the text in the ComboBox, in case any checked items
        // have been added or removed.

        CalculateComboBoxText();
    }

    private void CalculateComboBoxText()
    {
        var checkedItems = ItemsSource?.Where(item => item.IsChecked);

        if (checkedItems?.Any() ?? false)
        {
            PART_TextBox.Text = string.Join(", ", checkedItems.Select(i => i.Item?.ToString()));
        }
        else
        {
            PART_TextBox.Text = "";
        }
    }

    private static void IsDropDownOpenChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is CheckedListComboBox box)
        {
            if (box.IsDropDownOpen)
            {
                Mouse.Capture(box, CaptureMode.SubTree);
            }
            else
            {
                if (Mouse.Captured?.Equals(box) ?? false)
                {
                    Mouse.Capture(null);
                }
            }
        }
    }

    protected override void OnMouseMove(MouseEventArgs e)
    {
        if (IsDropDownOpen)
        {
            var textBoxPoint = e.GetPosition(PART_TextBox);
            var popupPoint = e.GetPosition(PART_Popup.Child);

            var mouseIsOverTextBox = !(textBoxPoint.X < 0 || textBoxPoint.X > PART_TextBox.ActualWidth || textBoxPoint.Y < 0 || textBoxPoint.Y > PART_TextBox.ActualHeight);
            var mouseIsOverPopup = !(popupPoint.X < 0 || popupPoint.X > PART_Popup.Child.RenderSize.Width || popupPoint.Y < 0 || popupPoint.Y > PART_Popup.Child.RenderSize.Height);

            if (mouseIsOverPopup && !mouseIsOverTextBox)
            {
                mouseIsOverPopup = true;
            }
            else
            {
                mouseIsOverPopup = false;
            }
        }
        else
        {
            mouseIsOverPopup = false;
        }

        base.OnMouseMove(e);
    }

    protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
    {
        if (IsDropDownOpen && !mouseIsOverPopup)
        {
            IsDropDownOpen = false;
        }

        base.OnMouseLeftButtonDown(e);
    }
}

这是每个项目的包装器类。

public class CheckedListObject : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private bool isChecked;
    private object item;

    public CheckedListItem()
    { }

    public CheckedListItem(object item, bool isChecked = false)
    {
        this.item = item;
        this.isChecked = isChecked;
    }

    public object Item
    {
        get { return item; }
        set
        {
            item = value;
            if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("Item"));
        }
    }


    public bool IsChecked
    {
        get { return isChecked; }
        set
        {
            isChecked = value;
            if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("IsChecked"));
        }
    }
}

最后,这是我使用控件的方式:

<p:CheckedListComboBox Grid.Row="2" ItemsSource="{Binding Items}">
    <p:CheckedListComboBox.ItemTemplate>
        <DataTemplate>
            <Grid Margin="4">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto" />
                    <ColumnDefinition />
                </Grid.ColumnDefinitions>
                <CheckBox Margin="0,0,6,0" IsChecked="{Binding IsChecked}" />
                <TextBlock Grid.Column="1" Text="{Binding Item}" />
            </Grid>
        </DataTemplate>
    </p:CheckedListComboBox.ItemTemplate>
</p:CheckedListComboBox>

0 个答案:

没有答案