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

时间:2014-10-15 07:24:56

标签: c# wpf binding combobox

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

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

虽然ComboBox没有显示任何内容(它足够聪明"知道新的SelectedItem不在ItemsSource中),但ViewModel更新了"无效的价值"。我实际上期望Binding具有与ComboBox显示的值相同的值。

用于演示目的的代码:

MainWindow.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:

    // 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
            }
        }
    }

视图模型:

    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
        }
    }

我最初提出的解决方案是检查Selected setter内部是否设置的值在可用COM端口列表内(如果没有,设置为空字符串并发送OPC)。

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

3 个答案:

答案 0 :(得分:5)

简而言之,您无法将SelectedItem设置为不在ItemsSource中的值。 AFAIK,这是所有Selector后代的默认行为,这是相当明显的:设置SelectedItem不仅是数据更改,这也会导致一些视觉后果,如生成项目容器和重新绘图项(所有这些操作ItemsSource)。你在这里做的最好的是这样的代码:

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

另一个选项是允许用户通过使其可编辑而在ComboBox中输入任意值。

答案 1 :(得分:2)

我意识到这对你有所帮助有点晚了,但我希望它至少对某人有帮助。对不起,如果有拼写错误,我必须在记事本中输入:

<强> 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
    }
}

如何在Xaml中使用

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

一些笔记

如果SelectedItem更改为ComboBox以外的值,则会将其添加到ComboBox(但不会添加到ItemsSource)。下次SelectedItem通过Binding进行更改时,ItemsSource以外的所有项目都将从ComboBox中删除。

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

答案 2 :(得分:0)

您可以通过创建单单元格Grid,然后将ComboBox放入网格中,并将TextBlock放在ComboBox顶部上,以实现类似的目的。只需通过绑定到ComboBox的Text.IsEmpty属性来控制TextBlock的可见性。

您可能需要调整文本框的边距,对齐方式,大小和其他属性,以使其看起来不错。

<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>