我想在WPF中创建一个顶部有一个null
项的ComboBox,当选中此项时,SelectedItem应设置为null(重置为默认状态)。我一直在寻找,但没有找到令人满意的解决方案。
如果可能的话,我希望它只使用XAML代码或附加行为来实现,因为我不喜欢在ViewModel中更改View或覆盖标准控件。
这是我到目前为止提出的(缩短代码):
[...]
<Popup x:Name="PART_Popup" [...]>
<Border x:Name="PopupBorder" [...]>
<ScrollViewer x:Name="DropDownScrollViewer" [...]>
<StackPanel [...]>
<ComboBoxItem>(None)</ComboBoxItem>
<ItemsPresenter x:Name="ItemsPresenter"/>
</StackPanel>
</ScrollViewer>
</Border>
</Popup>
[...]
我认为最好的方法是以某种方式添加一个事件触发器,在项目被选中时将SelectedIndex
设置为-1
,但这里是我被卡住的地方。
任何想法如何做到这一点?或者更好的方式,如附加行为?
答案 0 :(得分:7)
考虑为“无”组合框项目实施Null Object Pattern,并将此项目添加到项目列表中。然后实现自定义逻辑以保存该类中的null对象,或者只检查所选项是否为NullItem类型。
答案 1 :(得分:4)
我使用以下解决方案来解决类似的问题。它利用绑定的Converter属性在内部表示(null是一个合理的值)和我想要出现在ComboBox中之间来回传递。我喜欢没有必要在模型或视图模型中添加显式列表,但我不喜欢转换器中的字符串文字与ComboBox中的字符串文字之间的脆弱连接。
<ComboBox SelectedValue="{Binding MyProperty, Converter={x:Static Converters:MyPropertySelectionConverter.Instance}}" >
<ComboBox.ItemsSource>
<CompositeCollection>
<sys:String>(none)</sys:String>
<CollectionContainer Collection="{Binding Source={x:Static Somewhere}, Path=ListOfPossibleValuesForMyProperty}" />
</CompositeCollection>
</ComboBox.ItemsSource>
</ComboBox>
然后转换器看起来像:
public class MyPropertySelectionConverter : IValueConverter
{
public static MyPropertySelectionConverter Instance
{
get { return s_Instance; }
}
public const String NoneString = "(none)";
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
Object retval = value as MyPropertyType;
if (retval == null)
{
retval = NoneString;
}
return retval;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
Object retval = null;
if (value is MyPropertyType)
{
retval = value;
}
else if (String.Equals(NoneString, value as String, StringComparison.OrdinalIgnoreCase))
{
retval = null;
}
else
{
retval = DependencyProperty.UnsetValue;
}
return retval;
}
private static MyPropertySelectionConverter s_Instance = new MyPropertySelectionConverter();
}
答案 2 :(得分:2)
如果选择项目,则可以重置选择。
<ComboBox x:Name="cb">
<ComboBox.Items>
<ComboBoxItem Content="(None)">
<ComboBoxItem.Triggers>
<EventTrigger RoutedEvent="Selector.Selected">
<BeginStoryboard>
<Storyboard Storyboard.TargetName="cb" Storyboard.TargetProperty="SelectedItem">
<ObjectAnimationUsingKeyFrames Duration="0:0:0">
<DiscreteObjectKeyFrame Value="{x:Null}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</ComboBoxItem.Triggers>
</ComboBoxItem>
<ComboBoxItem>First Item</ComboBoxItem>
<ComboBoxItem>Second Item</ComboBoxItem>
</ComboBox.Items>
</ComboBox>
不幸的是,这不适用于ItemsSource
和CompositeCollection
将此重置项添加到任意列表。原因是WPF无法解析此范围内的Storyboard.TargetName
。
但也许这有助于您继续重新尝试ComboBox
。
答案 3 :(得分:1)
虽然我同意 WPF ComboBox 的 null item 问题有很多解决方案,Andrei Zubov's reference to Null Object Pattern启发我尝试一个不那么过度的替代方案,其中包括在将整个包装集合注入 ComboBox.ItemsSource 属性之前,将每个源项目包装为 null 值(也包装)。所选项目将可用于 SelectedWrappedItem 属性。
所以,首先你定义你的通用Wrapper ......
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ComboBoxWrapperSample
{
/// <summary>
/// Wrapper that adds supports to null values upon ComboBox.ItemsSource
/// </summary>
/// <typeparam name="T">Source combobox items collection datatype</typeparam>
public class ComboBoxNullableItemWrapper<T>
{
string _nullValueText;
private T _value;
public T Value
{
get { return _value; }
set { _value = value; }
}
/// <summary>
///
/// </summary>
/// <param name="Value">Source object</param>
/// <param name="NullValueText">Text to be presented whenever Value argument object is NULL</param>
public ComboBoxNullableItemWrapper(T Value, string NullValueText = "(none)")
{
this._value = Value;
this._nullValueText = NullValueText;
}
/// <summary>
/// Text that will be shown on combobox items
/// </summary>
/// <returns></returns>
public override string ToString()
{
string result;
if (this._value == null)
result = _nullValueText;
else
result = _value.ToString();
return result;
}
}
}
定义您的商品模型......
using System.ComponentModel;
namespace ComboBoxWrapperSample
{
public class Person : INotifyPropertyChanged
{
// Declare the event
public event PropertyChangedEventHandler PropertyChanged;
public Person()
{
}
// Name property
private string _name;
public string Name
{
get { return _name; }
set
{
_name = value;
OnPropertyChanged("Name");
}
}
// Age property
private int _age;
public int Age
{
get { return _age; }
set
{
_age = value;
OnPropertyChanged("Age");
}
}
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
// Don't forget this override, since it's what defines ao each combo item is shown
public override string ToString()
{
return string.Format("{0} (age {1})", Name, Age);
}
}
}
定义ViewModel ...
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
namespace ComboBoxWrapperSample
{
public partial class SampleViewModel : INotifyPropertyChanged
{
// SelectedWrappedItem- This property stores selected wrapped item
public ComboBoxNullableItemWrapper<Person> _SelectedWrappedItem { get; set; }
public ComboBoxNullableItemWrapper<Person> SelectedWrappedItem
{
get { return _SelectedWrappedItem; }
set
{
_SelectedWrappedItem = value;
OnPropertyChanged("SelectedWrappedItem");
}
}
// ListOfPersons - Collection to be injected into ComboBox.ItemsSource property
public ObservableCollection<ComboBoxNullableItemWrapper<Person>> ListOfPersons { get; set; }
public SampleViewModel()
{
// Setup a regular items collection
var person1 = new Person() { Name = "Foo", Age = 31 };
var person2 = new Person() { Name = "Bar", Age = 42 };
List<Person> RegularList = new List<Person>();
RegularList.Add(person1);
RegularList.Add(person2);
// Convert regular collection into a wrapped collection
ListOfPersons = new ObservableCollection<ComboBoxNullableItemWrapper<Person>>();
ListOfPersons.Add(new ComboBoxNullableItemWrapper<Person>(null));
RegularList.ForEach(x => ListOfPersons.Add(new ComboBoxNullableItemWrapper<Person>(x)));
// Set UserSelectedItem so it targes null item
this.SelectedWrappedItem = ListOfPersons.Single(x => x.Value ==null);
}
// INotifyPropertyChanged related stuff
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
}
并且,最终你的视图(好吧,它是一个窗口)
<Window x:Class="ComboBoxWrapperSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ComboBoxWrapperSample"
xmlns:vm="clr-namespace:ComboBoxWrapperSample"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ignore="http://www.ignore.com"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance {x:Type vm:SampleViewModel}, IsDesignTimeCreatable=False}"
Title="MainWindow" Height="200" Width="300">
<StackPanel Orientation="Vertical" Margin="10">
<TextBlock Margin="0,10,0,0">Favorite teacher</TextBlock>
<ComboBox ItemsSource="{Binding ListOfPersons}"
SelectedItem="{Binding SelectedWrappedItem, Mode=TwoWay}">
</ComboBox>
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
<TextBlock>Selected wrapped value:</TextBlock>
<TextBlock Text="{Binding SelectedWrappedItem }" Margin="5,0,0,0" FontWeight="Bold"/>
</StackPanel>
</StackPanel>
</Window>
达到这一点,我是否提到您可以通过 SelectedWrappedItem.Value 属性检索未包装的所选项目?
希望它可以帮助别人
答案 4 :(得分:1)
这是解决这个问题的最终超简单解决方案:
不要在ItemsSource中使用值为null的项目,而是使用DbNull.Value作为项目或项目的value属性。
这就是全部。你完成了。没有值转换器,没有代码隐藏,没有xaml触发器,没有包装器,没有控制后代......
它简直有效!
这是绑定枚举值的简短示例,包括&#34; null项&#34;:
像这样创建ItemsSource:
var enumValues = new ArrayList(Enum.GetValues(typeof(MyEnum)));
enumValues.Insert(0, DBNull.Value);
return enumValues;
将此绑定到ComboBox的ItemsSource。
将ComboBox的SelectedValue绑定到任何具有MyEnum类型的属性? (即Nullable&lt; MyEnum&gt;)。
完成!
背景:这种方法有效,因为DbNull.Value与C#空值不同,而另一方面,框架包含许多强制方法,可以在这两种方法之间进行转换。 最终,这类似于上面提到的&#34; Null对象模式&#34;,但不需要创建单独的null对象,也不需要任何值转换器。
答案 5 :(得分:1)
比这里的一些答案更为详尽,但又不想在我的背后添加任何代码或更改ViewModel。我将此写为WPF行为。附加到XAML后,它将在视觉效果中注入一个按钮。它将默认值设置为-1(或者您可以将其设置为其他默认值)。这是一个可重用的控件,可以轻松地在整个项目中添加到XAML中。希望这可以帮助。如果发现错误,请打开以获取反馈。
视觉效果:
所选项目:
行为代码:
public class ComboBoxClearBehavior : Behavior<ComboBox>
{
private Button _addedButton;
private ContentPresenter _presenter;
private Thickness _originalPresenterMargins;
protected override void OnAttached()
{
// Attach to the Loaded event. The visual tree at this point is not available until its loaded.
AssociatedObject.Loaded += AssociatedObject_Loaded;
// If the user or code changes the selection, re-evaluate if we should show the clear button
AssociatedObject.SelectionChanged += AssociatedObject_SelectionChanged;
base.OnAttached();
}
protected override void OnDetaching()
{
// Its likely that this is already de-referenced, but just in case the visual was never loaded, we will remove the handler anyways.
AssociatedObject.Loaded -= AssociatedObject_Loaded;
base.OnDetaching();
}
private void AssociatedObject_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
EvaluateDisplay();
}
/// <summary>
/// Checks to see if the UI should show a Clear button or not based on what is or isn't selected.
/// </summary>
private void EvaluateDisplay()
{
if (_addedButton == null) return;
_addedButton.Visibility = AssociatedObject.SelectedIndex == -1 ? Visibility.Collapsed : Visibility.Visible;
// To prevent the text or content from being overlapped by the button, adjust the margins if we have reference to the presenter.
if (_presenter != null)
{
_presenter.Margin = new Thickness(
_originalPresenterMargins.Left,
_originalPresenterMargins.Top,
_addedButton.Visibility == Visibility.Visible ? ClearButtonSize + 6 : _originalPresenterMargins.Right,
_originalPresenterMargins.Bottom);
}
}
private void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
{
// After we have loaded, we will have access to the Children objects. We don't want this running again.
AssociatedObject.Loaded -= AssociatedObject_Loaded;
// The ComboBox primary Grid is named MainGrid. We need this to inject the button control. If missing, you may be using a custom control.
if (!(AssociatedObject.FindChild("MainGrid") is Grid grid)) return;
// Find the content presenter. We need this to adjust the margins if the Clear icon is present.
_presenter = grid.FindChildren<ContentPresenter>().FirstOrDefault();
if (_presenter != null) _originalPresenterMargins = _presenter.Margin;
// Create the new button to put in the view
_addedButton = new Button
{
Height = ClearButtonSize,
Width = ClearButtonSize,
HorizontalAlignment = HorizontalAlignment.Right
};
// Find the resource for the button - In this case, our NoChromeButton Style has no button edges or chrome
if (Application.Current.TryFindResource("NoChromeButton") is Style style)
{
_addedButton.Style = style;
}
// Find the resource you want to put in the button content
if (Application.Current.TryFindResource("RemoveIcon") is FrameworkElement content)
{
_addedButton.Content = content;
}
// Hook into the Click Event to handle clearing
_addedButton.Click += ClearSelectionButtonClick;
// Evaluate if we should display. If there is nothing selected, don't show.
EvaluateDisplay();
// Add the button to the grid - First Column as it will be right justified.
grid.Children.Add(_addedButton);
}
private void ClearSelectionButtonClick(object sender, RoutedEventArgs e)
{
// Sets the selected index to -1 which will set the selected item to null.
AssociatedObject.SelectedIndex = -1;
}
/// <summary>
/// The Button Width and Height. This can be changed in the Xaml if a different size visual is desired.
/// </summary>
public int ClearButtonSize { get; set; } = 15;
}
用法:
<ComboBox
ItemsSource="{Binding SomeItemsSource, Mode=OneWay}"
SelectedValue="{Binding SomeId, Mode=TwoWay}"
SelectedValuePath="SomeId">
<i:Interaction.Behaviors>
<behaviors:ComboBoxClearBehavior />
</i:Interaction.Behaviors>
</ComboBox>
对于此行为,您将需要两件事-您可能已经拥有了,但是这里是:
1。)按钮模板-代码正在寻找样式。就我而言,它称为NoChromeButton-如果您正在寻找一个交钥匙解决方案,则可以将我的添加到资源文件中:
<Style x:Key="NoChromeButton"
TargetType="{x:Type Button}">
<Setter Property="Background"
Value="Transparent" />
<Setter Property="BorderThickness"
Value="1" />
<Setter Property="Foreground"
Value="{DynamicResource WindowText}" />
<Setter Property="HorizontalContentAlignment"
Value="Center" />
<Setter Property="VerticalContentAlignment"
Value="Center" />
<Setter Property="Cursor"
Value="Hand"/>
<Setter Property="Padding"
Value="1" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Grid x:Name="Chrome"
Background="{TemplateBinding Background}"
SnapsToDevicePixels="true">
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
Margin="{TemplateBinding Padding}"
RecognizesAccessKey="True"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled"
Value="false">
<Setter Property="Foreground"
Value="#ADADAD" />
<Setter Property="Opacity"
TargetName="Chrome"
Value="0.5" />
</Trigger>
<Trigger
Property="IsMouseOver"
Value="True">
<Setter
TargetName="Chrome"
Property="Background"
Value="{DynamicResource ButtonBackgroundHover}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
此外,您将需要使用图标进行清除。如果您有一个,只需更新代码即可使用该资源(名为“ RemoveIcon”)。 Otherwize ..这是我的:
<Viewbox x:Key="RemoveIcon"
x:Shared="False"
Stretch="Uniform">
<Canvas Width="58"
Height="58">
<Path Fill="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Control, Mode=FindAncestor}}">
<Path.Data>
<PathGeometry Figures="M 29 0 C 13 0 0 13 0 29 0 45 13 58 29 58 45 58 58 45 58 29 58 13 45 0 29 0 Z M 43.4 40.6 40.6 43.4 29 31.8 17.4 43.4 14.6 40.6 26.2 29 14.6 17.4 17.4 14.6 29 26.2 40.6 14.6 43.4 17.4 31.8 29 Z"
FillRule="NonZero" />
</Path.Data>
</Path>
</Canvas>
</Viewbox>
答案 6 :(得分:0)
删除以下行并添加CheckBox,然后即可执行自定义操作。
<ComboBoxItem>(None)</ComboBoxItem>
答案 7 :(得分:0)
对这个解决方案仍然不是100%满意,但是到目前为止我找到的最好的东西,你只需要覆盖ComboBox样式并应用AttachedBehaviour
。
<ComboBox ItemsSource="{Binding Names}"
ext:ComboBoxHelper.IsNullable="True" />
来源: http://xamlblog.com/PostPage.aspx?postId=16#/Posts/16
修改强> 链接断开后链接到Internet Archive: https://web.archive.org/web/20160420174905/http://xamlblog.com/PostPage.aspx?postId=16
答案 8 :(得分:-1)
请使用以下代码。
<ComboBoxItem IsSelected="{Binding ClearSelectedItems}">(None)</ComboBoxItem>
在viewmodel中,捕获“ClearSelectedItems”更改通知并清除ItemsControl的SelectedItems。