我正在创建一个自定义WPF控件(CheckedListComboBox),该控件允许用户从列表中选择一个或多个选项。本质上,用户打开一个下拉式控件,然后选中要选择的项目。当用户选中或取消选中列表中的选项时,控件的主(非弹出)区域将更新以反映用户所做的选择。
以下是实际使用的控件的示例:
我对控件的状态非常满意,但是我想改进其中的一部分,我不确定如何进行。
生成反映用户选择的文本当前取决于在每个选定项目上调用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>