簡體   English   中英

WPF:帶重置項的ComboBox

[英]WPF: ComboBox with reset item

我想在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項目時將SelectedIndex設置為-1 ,但這里是我遇到的問題。

任何想法如何做到這一點? 或者更好的方式,如附加行為?

考慮為“無”組合框項目實現Null對象模式 ,並將此項目添加到項目列表中。 然后實現用於在該類中保存null對象的自定義邏輯,或者僅檢查所選項是否為NullItem類型。

我使用以下解決方案來解決類似的問題。 它利用綁定的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();
}

如果選擇項目,則可以重置選擇。

<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

雖然我同意WPF ComboBox空項目問題有很多解決方案,但Andrei Zubov對Null對象模式的引用激發了我嘗試一種不那么過度的替代方案,它包括在注入之前用值包裝每個源項目allow(也包裝)整個包裝集合到ComboBox.ItemsSource屬性中。 所選項目將可用於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));
            }
        }
    }
}

並且,finnaly你的視圖(好吧,它是一個窗口)

<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屬性檢索未包裝的選定項目?

在這里你可以得到一個工作樣本

希望它可以幫助別人

這是這個問題的終極超簡單解決方案:

您可以使用DbNull.Value作為項目或項目的value屬性,而不是在ItemsSource中使用值為null的項目。

就這樣。 你完成了。 沒有值轉換器,沒有代碼隱藏,沒有xaml觸發器,沒有包裝器,沒有控制后代......

它只是工作!

這是綁定枚舉值的簡短示例,包括“null item”:

像這樣創建ItemsSource:

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

   enumValues.Insert(0, DBNull.Value);

   return enumValues;

將它綁定到ComboBox的ItemsSource。

將ComboBox的SelectedValue綁定到任何具有MyEnum類型的屬性? (即Nullable <MyEnum>)。

完成!

背景:這種方法有效,因為DbNull.Value與C#空值不同,而另一方面,框架包含許多強制方法,可以在這兩種方法之間進行轉換。 最終,這類似於提到的“Null對象模式”,但不需要創建單獨的null對象,也不需要任何值轉換器。

比這里的一些答案更精細,但不希望有任何代碼或我的ViewModel變化。 我把它寫成WPF行為。 當連接到XAML時,它將在視覺中注入一個按鈕。 它將默認值設置為-1(或者您可以調整為其他默認值)。 這是一個可重復使用的控件,可以在整個項目中輕松添加到XAML中。 希望這可以幫助。 如果發現錯誤,請打開反饋。

  1. 沒有外部引用,您可以將它與您的代碼一起使用,而不是其他DLL。 (好吧,它確實使用System.Windows.Interactivity,但大多數都會在WPF應用程序中使用它)
  2. 它可以在整個應用程序中重復使用
  3. 風格將符合您的主題。
  4. 你可以隨心所欲地填滿這個
  5. 我知道這是一個差不多有6年歷史的話題(截至2019年我的寫作),但是如果你喜歡它,那就把它作為答案,因為沒有一個!

結果視覺:

選擇項目:

ComboBox清除示例

行為代碼:

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>

刪除以下行並添加CheckBox,然后您可以執行自定義操作。

    <ComboBoxItem>(None)</ComboBoxItem>

仍然不是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

請使用以下代碼。

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

在viewmodel中,捕獲“ClearSelectedItems”更改通知並清除ItemsControl的SelectedItems。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM