[英]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>
[...]
我認為最好的方法是以某種方式添加一個事件觸發器,在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>
不幸的是,這不適用於ItemsSource
和CompositeCollection
將此重置項添加到任意列表。 原因是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中。 希望這可以幫助。 如果發現錯誤,請打開反饋。
結果視覺:
選擇項目:
行為代碼:
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.