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

And*_*ing 1 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>
Run Code Online (Sandbox Code Playgroud)

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
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

视图模型:

    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
        }
    }
Run Code Online (Sandbox Code Playgroud)

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

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

Nuc*_*mer 6

我意识到这对你来说有点晚了,但我希望它至少对某人有帮助。如果有一些拼写错误,我很抱歉,我必须在记事本中输入以下内容:

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
    }
}
Run Code Online (Sandbox Code Playgroud)

如何在 Xaml 中使用它

<adaptor:ComboBoxAdaptor 
         NullItem="Please Select an Item.."
         ItemsSource="{Binding MyItemsSource}"
         SelectedItem="{Binding MySelectedItem}">
      <ComboBox Width="100" />
</adaptor:ComboBoxAdaptor>
Run Code Online (Sandbox Code Playgroud)

如果您发现ComboBox没有显示...

然后请记住将样式链接ComboBox到内容ComboBoxAdaptor

<Style TargetType="Adaptors:ComboBoxAdaptor">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="Adaptors:ComboBoxAdaptor">
                    <ContentPresenter Content="{TemplateBinding ComboBox}"
                                      Margin="{TemplateBinding Padding}" />
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>
Run Code Online (Sandbox Code Playgroud)

一些注意事项

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

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


Den*_*nis 5

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

public DemoViewModel()
{
    selected = Source.FirstOrDefault(s => s == yourValueFromSettings);
}
Run Code Online (Sandbox Code Playgroud)

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