【问题标题】:WPF: ComboBox with reset itemWPF:带有重置项的组合框
【发布时间】:2013-04-07 05:45:16
【问题描述】:

我想在 WPF 中创建一个 ComboBox,顶部有一个 null 项目,当它被选中时,SelectedItem 应该设置为 null(重置为默认状态)。我一直在搜索,但没有找到令人满意的解决方案。

如果可能的话,我希望它只使用 XAML 代码或附加行为,因为我真的不喜欢在 ViewModel 中更改视图的内容,或覆盖标准控件。

这是我到目前为止想出的(缩短的代码):

[...]
<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,但这是我遇到的问题。

任何想法如何做到这一点?还是更好的方法,比如附加行为?

【问题讨论】:

  • 我假设您没有使用 MVVM,是吗?
  • 使用内置功能是不可能的。你为什么要那个?在您的逻辑中,您可以处理“占位符项目”的具体 SelectedIndex。
  • 假设我有一个外键可以为空的数据库,我希望用户从该引用表中选择一个元素,但他也应该能够重置它(如果它是错误什么的),因为它可以为空。
  • 那么激活控件或取消激活的复选框解决方案似乎是最好的。或者当您将数据保存到数据库时,检查 selectedindex 是否等于您的“NONE”项目索引。如果是,则将null写入数据库,否则写入selectedindex
  • 我正在使用 MVVM,但我认为这不是更改 ViewModel 或 Model 的最佳解决方案,因为 View 的弱点,因为如果您要更改 View 以在另一个您还需要更改 ViewModel 的方式

标签: c# .net wpf xaml combobox


【解决方案1】:

考虑为“无”组合框项目实现Null Object Pattern,并将此项目添加到您的项目列表中。然后实现自定义逻辑以在该类中保存空对象,或者只检查所选项目是否为 NullItem 类型。

【讨论】:

  • 我遇到了类似的问题,我实现了空对象模式来解决它。
【解决方案2】:

对于类似的问题,我使用了以下解决方案。它利用绑定的 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();
}

【讨论】:

  • 您可以使用 ConverterParameter 来指示您希望与 null 结果关联的值。
【解决方案3】:

如果您选择一个项目,则可以重置选择。

<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

【讨论】:

  • 感谢您的回答,至少这是一个开始,也许我会找到一种在模板中使用它的方法
【解决方案4】:

这是这个问题的终极超级简单解决方案:

不要在 ItemsSource 中使用值为 null 的项目,而是使用 DbNull.Value 作为项目或项目的 value 属性。

就是这样。你完成了。没有值转换器、没有代码隐藏、没有 xaml 触发器、没有包装器、没有控制后代......

它很简单!

以下是绑定枚举值的简短示例,包括“空项”:

像这样创建您的 ItemsSource:

   var enumValues = new ArrayList(Enum.GetValues(typeof(MyEnum)));

   enumValues.Insert(0, DBNull.Value);

   return enumValues;

将此绑定到 ComboBox 的 ItemsSource。

将 ComboBox 的 SelectedValue 绑定到具有 MyEnum 类型的任何属性? (即 Nullable)。

完成!

背景:这种方法之所以有效,是因为 DbNull.Value 与 C# null 值不同,而另一方面,该框架包含许多强制方法来在这两者之间进行转换。 最终,这类似于前面提到的“Null 对象模式”,但不需要创建单独的 Null 对象,也不需要任何值转换器。

【讨论】:

  • 如果我没听错的话,我不喜欢这个解决方案有两个原因,首先你不能使用类型化的列表(它本身有很多问题)。其次,如果在 MVVM 方法中这样做,您的 View 应该处理有关如何表示数据而不是 ViewModel 的所有事情,您不应该混合层的职责。
  • 我不确定是否每个人都同意这里。它是“模型”,不应因演示要求而“妥协”,但 ViewModel 的真正目的是转换“模型”数据以进行演示。但是,当您想要一种仅查看的方法时,您可以尝试使用 CompositeCollection 将具有 DbNull.Value 的项目添加到集合中,并且仍然会有一个比此处介绍的任何其他解决方案都更简单的解决方案。
【解决方案5】:

比这里的一些答案更详细一点,但不想在我的背后有任何代码或 ViewModel 更改。我将其写为 WPF 行为。当附加到 XAML 时,它将在视觉对象中注入一个按钮。它将默认值设置为 -1(或者您可以调整为其他默认值)。这是一个可重用的控件,很容易在整个项目中添加到 XAML 中。希望这可以帮助。如果您发现错误,请接受反馈。

  1. 没有外部引用,您可以将其与您的代码一起使用,而无需其他 DLL。 (嗯,它确实使用 System.Windows.Interactivity,但大多数都会在 WPF 应用程序中使用它)
  2. 它可在您的整个应用程序中重复使用
  3. 风格将符合您的主题。
  4. 你可以随心所欲地把它顶起来
  5. 我知道这是一个有将近 6 年历史的帖子(截至我 2019 年撰写时),但如果你喜欢它 - 让它成为答案,因为没有一个!

结果视觉:

项目选择:

行为代码:

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”)。否则..这是我的:

<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 年前使用附加行为的答案...stackoverflow.com/a/16031655/503059
【解决方案6】:

虽然我同意 WPF ComboBoxnull item 问题有很多解决方案,但Andrei Zubov's reference to Null Object Pattern 启发我尝试一种不那么矫枉过正的替代方案,它包括将每个在将整个包装的集合注入 ComboBox.ItemsSource 属性之前,源项允许使用 null 值(也已包装)。所选项目将在 SelectedWrappedItem 属性中可用。

所以,首先你定义你的通用包装器...

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

希望对别人有帮助

【讨论】:

    【解决方案7】:

    去掉下面一行,添加一个CheckBox,就可以进行自定义操作了。

        <ComboBoxItem>(None)</ComboBoxItem>
    

    【讨论】:

    • 使用ComboBoxItem 的关键在于它看起来仍然像普通的ComboBox
    【解决方案8】:

    对这个解决方案仍然不是 100% 满意,但到目前为止我发现的最好的事情是,您只需要覆盖 ComboBox 样式并应用 AttachedBehaviour

    <ComboBox ItemsSource="{Binding Names}"
              ext:ComboBoxHelper.IsNullable="True" />
    

    来源: http://xamlblog.com/PostPage.aspx?postId=16#/Posts/16

    编辑: 由于链接已断开,因此链接到 Internet 档案: https://web.archive.org/web/20160420174905/http://xamlblog.com/PostPage.aspx?postId=16

    【讨论】:

    【解决方案9】:

    请使用以下代码。

        <ComboBoxItem IsSelected="{Binding ClearSelectedItems}">(None)</ComboBoxItem>
    

    在视图模型中,捕获“ClearSelectedItems”更改通知并清除 ItemsControl 的 SelectedItems。

    【讨论】:

    • 这是不可能的,因为这个元素永远不会被选中。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-04-05
    • 2013-06-15
    • 1970-01-01
    • 2011-04-30
    • 1970-01-01
    • 2011-09-24
    • 1970-01-01
    相关资源
    最近更新 更多