WPF ComboBox:将 SelectedItem 设置为不在 ItemsSource 中的项目 -> 绑定奇怪

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/26376772/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-13 12:36:58  来源:igfitidea点击:

WPF ComboBox: Set SelectedItem to item not in ItemsSource -> Binding oddity

c#wpfbindingcombobox

提问by Andreas Duering

I want to achieve the following: I want to have a ComboBox which displays the available COM ports. On Startup (and clicking a "refresh" button) I want to get the available COM ports and set the selection to the last selected value (from the application settings).

我想实现以下目标:我想要一个显示可用 COM 端口的 ComboBox。在启动时(并单击“刷新”按钮),我想获取可用的 COM 端口并将选择设置为最后选择的值(来自应用程序设置)。

If the value from the settings (last com port) is not in the list of values (available COM ports) following happens:

如果设置中的值(最后一个 com 端口)不在值列表(可用的 COM 端口)中,则会发生以下情况:

Although the ComboBox doesn't display anything (it's "clever enough" to know that the new SelectedItem is not in ItemsSource), the ViewModel is updated with the "invalid value". I actually expected that the Binding has the same value which the ComboBox displays.

尽管 ComboBox 不显示任何内容(知道新的 SelectedItem 不在 ItemsSource 中已经“足够聪明”),但 ViewModel 已更新为“无效值”。我实际上希望 Binding 具有与 ComboBox 显示相同的值。

Code for demonstration purposes:

用于演示目的的代码:

MainWindow.xaml:

主窗口.xaml:

    <Window x:Class="DemoComboBinding.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="MainWindow" Height="350" Width="525"
            xmlns:local="clr-namespace:DemoComboBinding">
        <Window.Resources>
            <local:DemoViewModel x:Key="vm" />
        </Window.Resources>
        <StackPanel Orientation="Vertical">
            <ComboBox SelectedItem="{Binding Source={StaticResource vm}, Path=Selected}" x:Name="combo"
            ItemsSource="{Binding Source={StaticResource vm}, Path=Source}"/>
            <Button Click="Button_Click">Set different</Button> <!-- would be refresh button -->
            <Label Content="{Binding Source={StaticResource vm}, Path=Selected}"/> <!-- shows the value from the view model -->
        </StackPanel>
    </Window>

MainWindow.xaml.cs:

主窗口.xaml.cs:

    // usings removed
    namespace DemoComboBinding
    {
        public partial class MainWindow : Window
        {
            //...
            private void Button_Click(object sender, RoutedEventArgs e)
            {
                combo.SelectedItem = "COM4"; // would be setting from Properties
            }
        }
    }

ViewModel:

视图模型:

    namespace DemoComboBinding
    {
        class DemoViewModel : INotifyPropertyChanged
        {
            string selected;

            string[] source = { "COM1", "COM2", "COM3" };

            public string[] Source
            {
                get { return source; }
                set { source = value; }
            }

            public string Selected
            {
                get { return selected; }
                set { 
                    if(selected != value)
                    {
                        selected = value;
                        OnpropertyChanged("Selected");
                    }
                }
            }

            #region INotifyPropertyChanged Members

            public event PropertyChangedEventHandler PropertyChanged;

            void OnpropertyChanged(string propertyname)
            {
                var handler = PropertyChanged;
                if(handler != null)
                {
                    handler(this, new PropertyChangedEventArgs(propertyname));
                }
            }

            #endregion
        }
    }

A solution I initially came up with would be to check inside the Selected setter if the value to set is inside the list of available COM ports (if not, set to empty string and send OPC).

我最初想出的一个解决方案是检查 Selected setter 中要设置的值是否在可用 COM 端口列表中(如果不是,则设置为空字符串并发送 OPC)。

What I wonder: Why does that happen? Is there another solution I didn't see?

我想知道:为什么会这样?还有其他我没有看到的解决方案吗?

回答by Dennis

In short, you can't set SelectedItemto the value, that is not in ItemsSource. AFAIK, this is default behavior of all Selectordescendants, which is rather obvious: settings SelectedItemisn't only a data changing, this also should lead to some visual consequences like generating an item container and re-drawing item (all those things manipulate ItemsSource). The best you can do here is code like this:

简而言之,您不能设置SelectedItem为值,即不在ItemsSource. AFAIK,这是所有Selector后代的默认行为,这是相当明显的:设置SelectedItem不仅是数据更改,这还应该导致一些视觉效果,例如生成项目容器和重新绘制项目(所有这些操作ItemsSource)。你能在这里做的最好的事情是这样的代码:

public DemoViewModel()
{
    selected = Source.FirstOrDefault(s => s == yourValueFromSettings);
}

Another option is to allow user to enter arbitrary values in ComboBoxby making it editable.

另一种选择是允许用户ComboBox通过使其可编辑来输入任意值。

回答by NuclearProgrammer

I realize this is a bit late to help you, but I hope that it helps someone at least. I'm sorry if there are some typos, I had to type this in notepad:

我意识到这对您有所帮助有点晚了,但我希望它至少可以帮助某人。如果有错别字,我很抱歉,我不得不在记事本中输入:

ComboBoxAdaptor.cs:

ComboBoxAdaptor.cs:

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Linq;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Markup;

    namespace Adaptors
{
    [ContentProperty("ComboBox")]
    public class ComboBoxAdaptor : ContentControl
    {
        #region Protected Properties
        protected bool IsChangingSelection
        { get; set; }

        protected ICollectionView CollectionView
        { get; set; }
        #endregion

        #region Dependency Properties
        public static readonly DependencyProperty ComboBoxProperty =
            DependencyProperty.Register("ComboBox", typeof(ComboBox), typeof(ComboBoxAdaptor),
            new FrameworkPropertyMetadata(new PropertyChangedCallback(ComboBox_Changed)));

        private static void ComboBox_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var theComboBoxAdaptor = (ComboBoxAdaptor)d;
            theComboBoxAdaptor.ComboBox.SelectionChanged += theComboBoxAdaptor.ComboBox_SelectionChanged;
        }

        public ComboBox ComboBox
        {
            get { return (ComboBox)GetValue(ComboBoxProperty); }
            set { SetValue(ComboBoxProperty, value); }
        }

        public static readonly DependencyProperty NullItemProperty =
            DependencyProperty.Register("NullItem", typeof(object), typeof(ComboBoxAdaptor),
            new PropertyMetadata("(None)"));
        public object NullItem
        {
            get { return GetValue(NullItemProperty); }
            set { SetValue(NullItemProperty, value); }
        }

        public static readonly DependencyProperty ItemsSourceProperty =
            DependencyProperty.Register("ItemsSource", typeof(IEnumerable), typeof(ComboBoxAdaptor),
            new FrameworkPropertyMetadata(new PropertyChangedCallback(ItemsSource_Changed)));
        public IEnumerable ItemsSource
        {
            get { return (IEnumerable)GetValue(ItemsSourceProperty); }
            set { SetValue(ItemsSourceProperty, value); }
        }

        public static readonly DependencyProperty SelectedItemProperty =
            DependencyProperty.Register("SelectedItem", typeof(object), typeof(ComboBoxAdaptor),
            new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
            new PropertyChangedCallback(SelectedItem_Changed)));
        public object SelectedItem
        {
            get { return GetValue(SelectedItemProperty); }
            set { SetValue(SelectedItemProperty, value); }
        }

        public static readonly DependencyProperty AllowNullProperty =
            DependencyProperty.Register("AllowNull", typeof(bool), typeof(ComboBoxAdaptor),
            new PropertyMetadata(true, AllowNull_Changed));
        public bool AllowNull
        {
            get { return (bool)GetValue(AllowNullProperty); }
            set { SetValue(AllowNullProperty, value); }
        }
        #endregion

        #region static PropertyChangedCallbacks
        static void ItemsSource_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ComboBoxAdaptor adapter = (ComboBoxAdaptor)d;
            adapter.Adapt();
        }

        static void AllowNull_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ComboBoxAdaptor adapter = (ComboBoxAdaptor)d;
            adapter.Adapt();
        }

        static void SelectedItem_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ComboBoxAdaptor adapter = (ComboBoxAdaptor)d;
            if (adapter.ItemsSource != null)
            {
                //If SelectedItem is changing from the Source (which we can tell by checking if the
                //ComboBox.SelectedItem is already set to the new value), trigger Adapt() so that we
                //throw out any items that are not in ItemsSource.
                object adapterValue = (e.NewValue ?? adapter.NullItem);
                object comboboxValue = (adapter.ComboBox.SelectedItem ?? adapter.NullItem);
                if (!object.Equals(adapterValue, comboboxValue))
                {
                    adapter.Adapt();
                    adapter.ComboBox.SelectedItem = e.NewValue;
                }
                //If the NewValue is not in the CollectionView (and therefore not in the ComboBox)
                //trigger an Adapt so that it will be added.
                else if (e.NewValue != null && !adapter.CollectionView.Contains(e.NewValue))
                {
                    adapter.Adapt();
                }
            }
        }
        #endregion

        #region Misc Callbacks
        void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (ComboBox.SelectedItem == NullItem)
            {
                if (!IsChangingSelection)
                {
                    IsChangingSelection = true;
                    try
                    {
                        int selectedIndex = ComboBox.SelectedIndex;
                        ComboBox.SelectedItem = null;
                        ComboBox.SelectedIndex = -1;
                        ComboBox.SelectedIndex = selectedIndex;
                    }
                    finally
                    {
                        IsChangingSelection = false;
                    }
                }
            }
            object newVal = (ComboBox.SelectedItem == null ? null : ComboBox.SelectedItem);
            if (!object.Equals(SelectedItem, newVal))
            {
                SelectedItem = newVal;
            }
        }

        void CollectionView_CurrentChanged(object sender, EventArgs e)
        {
            if (AllowNull && (ComboBox != null) && (((ICollectionView)sender).CurrentItem == null) && (ComboBox.Items.Count > 0))
            {
                ComboBox.SelectedIndex = 0;
            }
        }
        #endregion

        #region Methods
        protected void Adapt()
        {
            if (CollectionView != null)
            {
                CollectionView.CurrentChanged -= CollectionView_CurrentChanged;
                CollectionView = null;
            }
            if (ComboBox != null && ItemsSource != null)
            {
                CompositeCollection comp = new CompositeCollection();
                //If AllowNull == true, add a "NullItem" as the first item in the ComboBox.
                if (AllowNull)
                {
                    comp.Add(NullItem);
                }
                //Now Add the ItemsSource.
                comp.Add(new CollectionContainer { Collection = ItemsSource });
                //Lastly, If Selected item is not null and does not already exist in the ItemsSource,
                //Add it as the last item in the ComboBox
                if (SelectedItem != null)
                {
                    List<object> items = ItemsSource.Cast<object>().ToList();
                    if (!items.Contains(SelectedItem))
                    {
                        comp.Add(SelectedItem);
                    }
                }
                CollectionView = CollectionViewSource.GetDefaultView(comp);
                if (CollectionView != null)
                {
                    CollectionView.CurrentChanged += CollectionView_CurrentChanged;
                }
                ComboBox.ItemsSource = comp;
            }
        }
        #endregion
    }
}

How To Use It In Xaml

如何在 Xaml 中使用它

<adaptor:ComboBoxAdaptor 
         NullItem="Please Select an Item.."
         ItemsSource="{Binding MyItemsSource}"
         SelectedItem="{Binding MySelectedItem}">
      <ComboBox Width="100" />
</adaptor:ComboBoxAdaptor>

Some Notes

一些注意事项

If SelectedItemchanges to a value not in the ComboBox, it will be added to the ComboBox(but not the ItemsSource). The next time SelectedItemis changed via Binding, any items not in ItemsSourcewill be removed from the ComboBox.

如果SelectedItem更改了不在 中的值ComboBox,它将被添加到ComboBox(但不是 ItemsSource)。下次SelectedItem更改通过 时Binding,任何不在 中的项目ItemsSource将从 中删除ComboBox

Also, the ComboBoxAdaptorallows you to insert a Null item into the ComboBox. This is an optional feature that you can turn off by setting AllowNull="False"in the xaml.

此外,ComboBoxAdaptor允许您将 Null 项插入到ComboBox. 这是一个可选功能,您可以通过AllowNull="False"在 xaml 中设置来关闭它。