简体   繁体   English

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

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

I want to achieve the following: I want to have a ComboBox which displays the available COM ports.我想实现以下目标:我想要一个显示可用 COM 端口的 ComboBox。 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 端口并将选择设置为最后选择的值(从应用程序设置)。

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".尽管 ComboBox 不显示任何内容(知道新的 SelectedItem 不在 ItemsSource 中已经“足够聪明”),但 ViewModel 已更新为“无效值”。 I actually expected that the Binding has the same value which the ComboBox displays.我实际上希望 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?还有其他我没有看到的解决方案吗?

In short, you can't set SelectedItem to the value, that is not in ItemsSource . 简而言之,您不能将SelectedItem设置为ItemsSource没有的值。 AFAIK, this is default behavior of all Selector descendants, which is rather obvious: settings SelectedItem isn'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 ). AFAIK,这是所有Selector子孙的默认行为,这非常明显:设置SelectedItem不仅是数据更改,还应该导致一些视觉上的后果,例如生成项目容器和重新绘制项目(所有这些操作操纵ItemsSource )。 The best you can do here is code like this: 您在这里可以做的最好的事情是这样的代码:

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

Another option is to allow user to enter arbitrary values in ComboBox by making it editable. 另一种选择是让用户在输入任意值ComboBox通过使编辑。

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 SelectedItem changes to a value not in the ComboBox , it will be added to the ComboBox (but not the ItemsSource). 如果SelectedItem更改为不在ComboBox的值,它将被添加到ComboBox (而不是ItemsSource)。 The next time SelectedItem is changed via Binding , any items not in ItemsSource will be removed from the ComboBox . 下次通过Binding更改SelectedItem ,不在ItemsSource所有项目将从ComboBox删除。

Also, the ComboBoxAdaptor allows you to insert a Null item into the ComboBox . 另外, ComboBoxAdaptor允许您将Null项插入ComboBox This is an optional feature that you can turn off by setting AllowNull="False" in the xaml. 这是一项可选功能,您可以通过在xaml中设置AllowNull="False"来关闭它。

You can achieve something similar by creating a single-cell Grid, and then putting your ComboBox in the grid, and putting a TextBlock on top of the ComboBox.您可以通过创建一个单单元格的 Grid,然后将 ComboBox 放在网格中,并将 TextBlock 放在 ComboBox 的顶部来实现类似的效果。 The TextBlock's visibility just needs to be controlled by a binding to the ComboBox's Text.IsEmpty property. TextBlock 的可见性只需要通过绑定到 ComboBox 的 Text.IsEmpty 属性来控制。

You might have to adjust the margins, alignment, size, and other properties of the textbox to get it to look nice.您可能需要调整文本框的边距、对齐方式、大小和其他属性以使其看起来不错。

<Grid>            
    <ComboBox Name="MyComboBox"
              ItemsSource="{Binding Options}"
              SelectedIndex="{Binding SelectedIndex}" />

    <TextBlock Text="{Binding EmptySelectionPromptText}"
               Margin="4 3 0 0"
               Visibility="{Binding ElementName=MyComboBox, Path=Text.IsEmpty, Converter={StaticResource BoolToVis}}">
    </TextBlock>
</Grid>

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM