WPF:带重置项的ComboBox

时间:2013-04-15 07:16:07

标签: c# .net wpf xaml combobox

我想在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>
[...]

OpenCombo

我认为最好的方法是以某种方式添加一个事件触发器,在项目被选中时将SelectedIndex设置为-1,但这里是我被卡住的地方。

任何想法如何做到这一点?或者更好的方式,如附加行为?

9 个答案:

答案 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>

不幸的是,这不适用于ItemsSourceCompositeCollection将此重置项添加到任意列表。原因是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 属性检索未包装的所选项目?

您可以get a working sample

希望它可以帮助别人

答案 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中。希望这可以帮助。如果发现错误,请打开以获取反馈。

  1. 没有外部引用,您可以将其与代码一起使用,而不能与其他DLL一起使用。 (嗯,它确实使用System.Windows.Interactivity,但大多数将在WPF应用程序中使用它)
  2. 它可在您的整个应用程序中重复使用
  3. 样式将符合您的主题。
  4. 您可以随心所欲
  5. 我知道这是一个使用了将近6年的主题(截至2019年我写这篇文章),但是如果您喜欢它-因为没有主题,请把它作为答案!

视觉效果:

所选项目:

ComboBox Clear Example

行为代码:

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。