繁体   English   中英

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

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

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

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

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

用于演示目的的代码:

主窗口.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>

主窗口.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)。

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

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

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

另一种选择是让用户在输入任意值ComboBox通过使编辑。

我知道这对您有所帮助有点晚,但我希望至少对您有所帮助。 对不起,如果有错别字,我必须在记事本中输入:

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)。 下次通过Binding更改SelectedItem ,不在ItemsSource所有项目将从ComboBox删除。

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

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

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

<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