dotnet / wpf

WPF is a .NET Core UI framework for building Windows desktop applications.
MIT License
7.03k stars 1.16k forks source link

WPF ComboBox SelectedItedm NULL #9478

Open vsfeedback opened 1 month ago

vsfeedback commented 1 month ago

This issue has been moved from a ticket on Developer Community.


[severity:It bothers me. A fix would be nice] MVVM binding cannot be performed correctly when ComboBox Items contain prefix entries. For example, Items={1,10,100}, SelectValue and SelectedItem equals NULL when Text=10 is assigned, please check this BUG and give a fix, thank you


Original Comments

Feedback Bot on 7/15/2024, 06:33 PM:

(private comment, text removed)

Ann Yang [MSFT] on 7/16/2024, 00:13 AM:

(private comment, text removed)

U_1351008 on 7/16/2024, 10:21 PM:

(private comment, text removed)

U_1351008 on 7/23/2024, 10:44 PM:

(private comment, text removed)

Feedback Bot on 7/23/2024, 10:27 PM:

(private comment, text removed)

Feedback Bot on 7/24/2024, 00:49 AM:

(private comment, text removed)


Original Solutions

(no solutions)

xlsupport commented 1 month ago

Just a visitor here... The debug.7z from the original post is corrupt. I can run the exe, but the source folder is not readable. Can OP verify and upload it here, please?

From a first glance I doubt it's a bug, but rather OP's misinterpretation of the Text property... The Text property is used for searching, when the cb is editable. For binding you use SelectedIndex,SelectedItem or SelectedValue (where SelectedValuePath decides which property from the SelectedItem is used in the SelectedValue property)

I could be wrong though.. looking forward to that source code.

miloush commented 1 month ago

As far as I can see the SourceCode is a text file that contains the sources, not a folder.

xlsupport commented 1 month ago

aha! It proves my point though: OP needs to learn to bind to the the properties meant for binding. I also noted that he raises the PropertyChanged event in his viewmodel BEFORE setting the underlying property. Not good practice. The eventhandler will not know the new value.

Tomisacats commented 1 month ago

// This is the source code please view, thank you

<Window x:Class="ComboBoxBugShow.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:ComboBoxBugShow"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <WrapPanel>
            <StackPanel Margin="10">
                <Label Content="Normal"/>
                <ComboBox x:Name="cmb"
                          ItemsSource="{Binding CmbSources}"
                          DisplayMemberPath="Desc"
                          Text="{Binding CmbText}" 
                          SelectionChanged="cmb_SelectionChanged"/>
                <StackPanel Orientation="Horizontal">
                    <Label Content="CmbText:" Width="150"/>
                    <TextBox x:Name="sel" Width="80"/>
                </StackPanel>
                <StackPanel Orientation="Horizontal">
                    <Label Content="BindingCmbText:" Width="150"/>
                    <TextBox x:Name="selbind" Width="80" Text="{Binding CmbText}"/>
                </StackPanel>
            </StackPanel>

            <StackPanel Margin="10">
                <Label Content="Bug"/>
                <ComboBox x:Name="cmbBug" 
                          ItemsSource="{Binding CmbBugSources}"
                          DisplayMemberPath="Desc"
                          Text="{Binding CmbBugText}"
                          SelectionChanged="cmbBug_SelectionChanged"/>
                <StackPanel Orientation="Horizontal">
                    <Label Content="CmbBugText:" Width="150"/>
                    <TextBox x:Name="selBug" Width="80"/>
                </StackPanel>
                <StackPanel Orientation="Horizontal">
                    <Label Content="BindingCmbBugText:" Width="150"/>
                    <TextBox x:Name="selBugbind" Width="80"
                             Text="{Binding CmbBugText}"/>
                </StackPanel>
            </StackPanel>

            <StackPanel Margin="10">
                <Label Content="ComBug"/>
                <ComboBox x:Name="cmbComBug" 
                          ItemsSource="{Binding CmbComBugSources}"
                          DisplayMemberPath="Desc"
                          Text="{Binding CmbComBugText}"
                          SelectionChanged="cmbComBug_SelectionChanged"/>
                <StackPanel Orientation="Horizontal">
                    <Label Content="CmbComBugText:" Width="150"/>
                    <TextBox x:Name="selComBug" Width="80"/>
                </StackPanel>
                <StackPanel Orientation="Horizontal">
                    <Label Content="BindingCmbComBugText:" Width="150"/>
                    <TextBox x:Name="selComBugbind" Width="80"
                             Text="{Binding CmbComBugText}"/>
                </StackPanel>
            </StackPanel>

            <Button Content="Bug" Click="Button_Click"/>
        </WrapPanel>
    </Grid>
</Window>
using ComboBoxBugShow.ViewModel;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace ComboBoxBugShow
{
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>
    public partial class MainWindow : Window
    {
        private MainWindowViewModel mainWindowViewModel = null;
        public MainWindow()
        {
            InitializeComponent();

            mainWindowViewModel = new MainWindowViewModel();

            var cmbSources = new ObservableCollection<CmbViewModel>();

            cmbSources.Add(new CmbViewModel(1, "One"));
            cmbSources.Add(new CmbViewModel(2, "Two"));
            cmbSources.Add(new CmbViewModel(3, "Three"));
            cmbSources.Add(new CmbViewModel(4, "Four"));
            cmbSources.Add(new CmbViewModel(5, "Five"));
            cmbSources.Add(new CmbViewModel(6, "Six"));

            mainWindowViewModel.CmbSources = cmbSources;

            var cmbBugSources = new ObservableCollection<CmbViewModel>();

            cmbBugSources.Add(new CmbViewModel(6, "One111111"));
            cmbBugSources.Add(new CmbViewModel(5, "One11111"));
            cmbBugSources.Add(new CmbViewModel(4, "One1111"));
            cmbBugSources.Add(new CmbViewModel(3, "One111"));
            cmbBugSources.Add(new CmbViewModel(2, "One11"));
            cmbBugSources.Add(new CmbViewModel(1, "One1"));

            mainWindowViewModel.CmbBugSources = cmbBugSources;

            var cmbComBugSources = new ObservableCollection<CmbViewModel>();

            cmbComBugSources.Add(new CmbViewModel(1, "One1"));
            cmbComBugSources.Add(new CmbViewModel(2, "One11"));
            cmbComBugSources.Add(new CmbViewModel(3, "One111"));
            cmbComBugSources.Add(new CmbViewModel(4, "One1111"));
            cmbComBugSources.Add(new CmbViewModel(5, "One11111"));
            cmbComBugSources.Add(new CmbViewModel(6, "One111111"));

            mainWindowViewModel.CmbComBugSources = cmbComBugSources;
            this.DataContext = mainWindowViewModel;
        }

        private void cmb_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            var cmbox = sender as ComboBox;

            var cvm = cmbox.SelectedItem as CmbViewModel;

            if (cvm == null)
            {
                sel.Text = "Select is Null";
            }
            else
            {
                sel.Text = cvm.Value.ToString();
            }
        }

        private void cmbBug_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            var cmbox = sender as ComboBox;

            var cvm = cmbox.SelectedItem as CmbViewModel;

            if (cvm == null)
            {
                selBug.Text = "Select is Null";
            }
            else
            {
                selBug.Text = cvm.Value.ToString();
            }
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            mainWindowViewModel.CmbText = "Two";
            mainWindowViewModel.CmbBugText = "One11";
            mainWindowViewModel.CmbComBugText = "One11";
        }

        private void cmbComBug_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            var cmbox = sender as ComboBox;

            var cvm = cmbox.SelectedItem as CmbViewModel;

            if (cvm == null)
            {
                selComBug.Text = "Select is Null";
            }
            else
            {
                selComBug.Text = cvm.Value.ToString();
            }

        }
    }
}
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ComboBoxBugShow.ViewModel
{
    public class CmbViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public CmbViewModel(int val, string desc)
        {
            _value = val;
            _desc = desc;
        }
        private int _value;
        public int Value
        {
            get { return _value; }
            set
            {
                if (_value != value)
                {
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Value"));
                    _value = value;
                }
            }
        }
        private string _desc;
        public string Desc
        {
            get { return _desc; }
            set
            {
                if (_desc != value)
                {
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Desc"));
                    _desc = value;
                }
            }
        }
    }
}
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ComboBoxBugShow.ViewModel
{
    public class MainWindowViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private string cmbText;
        public string CmbText
        {
            get { return cmbText; }
            set
            {
                if (cmbText != value)
                {
                    cmbText = value;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CmbText)));
                }
            }
        }

        private string cmbBugText;

        public string CmbBugText
        {
            get { return cmbBugText; }
            set
            {
                if (cmbBugText != value)
                {
                    cmbBugText = value;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CmbBugText)));
                }
            }
        }

        private string cmbComBugText;

        public string CmbComBugText
        {
            get { return cmbComBugText; }
            set
            {
                if (cmbComBugText != value)
                {
                    cmbComBugText = value;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CmbComBugText)));
                }
            }
        }

        private ObservableCollection<CmbViewModel> cmbSources;

        public ObservableCollection<CmbViewModel> CmbSources
        {
            get { return cmbSources; }
            set
            {
                if (cmbSources != value)
                {
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CmbSources)));
                    cmbSources = value;
                }
            }
        }

        private ObservableCollection<CmbViewModel> cmbBugSources;

        public ObservableCollection<CmbViewModel> CmbBugSources
        {
            get { return cmbBugSources; }
            set
            {
                if (cmbBugSources != value)
                {
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CmbBugSources)));
                    cmbBugSources = value;
                }
            }

        }

        private ObservableCollection<CmbViewModel> cmbComBugSources;

        public ObservableCollection<CmbViewModel> CmbComBugSources
        {
            get { return cmbComBugSources; }
            set
            {
                if (cmbComBugSources != value)
                {
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CmbComBugSources)));
                    cmbComBugSources = value;
                }
            }

        }

    }
}
xlsupport commented 1 month ago

@Tomisacats : As I originally guessed & later confirmed: you use the TEXT property for binding. You shouldn't. It's purpose is searching or editing items. And when It can't find the desired item, it can't select it... resulting in the SelectedIndex=-1 / SelectedItem = null. Also have a look at the documentation for SelectedValue (with SelectedValuePath) & DisplayMemberPath.
This is not a bug, it's your lack of understanding.

xlsupport commented 1 month ago

@Tomisacats I was bored & actually took the time to run your code.

Repeat following for your three combos and be amazed ;-)

<ComboBox x:Name="cmbBug"
          DisplayMemberPath="Desc"
          ItemsSource="{Binding CmbBugSources}"
          SelectedValue="{Binding CmbBugText}"
          SelectedValuePath="Desc"
          SelectionChanged="cmbBug_SelectionChanged" />

I'd also move the code that fills the itemssources to your ViewModel's constructor and add d:DataContext="{d:DesignInstance Type=viewmodel:MainWindowViewModel, IsDesignTimeCreatable=True}" to your MainWindow xaml.

That way you can see some data in your designer. HTH, Jurgen

Tomisacats commented 1 month ago

hello,I think you missed my point;

Snipaste_2024-07-31_15-18-51
xlsupport commented 1 month ago

duh... Text is for searching and editing! Let's make this more fun. Make cmbBug IsEditable=True, IsTextSearchEnabled=True. Now enter "One11" manually... "One11111", "One1111","One111","One11" eligible for Selection. But which one to pick?

In the ascending sort... he happens to pick the exact match...

Again: If you're not editing... use SelectedValue. PS: In your vm you raise the change event before change the value... NOT good. Makes no difference here, but it's a sign you're not very experienced.

miloush commented 1 month ago

@xlsupport please keep it civil and welcoming here and in your other issues

@Tomisacats please next time include the whole project structure with individual files in your repro (others can see Repro9478.zip), instructions what to do with it, and what you expect the behavior to be


First of all, the ComboBox.Text documentation says:

When IsEditable is false, setting this value has no effect.

That is clearly incorrect and should be fixed in the documentation.

When IsTextSearchEnabled is true, which is the default for ComboBox, setting the Text property invokes the TextSearch.

Now the TextSearch.Text says:

As soon as the user types enough of the value to distinguish the item from other items in the selection, the item will be selected.

Let's make it clear that "One11" does not meet this condition (it is not enough to distinguish it from other values) and no item being selected is expected as documented. On the contrary, one could argue that the 3rd combo should have not selected an item based on this criteria, which, however, would be an undesirable user experience.

Now the intent of setting the Text property on ComboBox seems to be to select it if it is a full match to an item:

https://github.com/dotnet/wpf/blob/43dd3c9a6144b57511c1bdf28297bd9b43bbac71/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Controls/ComboBox.cs#L755-L764

Unfortunately, it only checks the first item that has a prefix match. I could see a fix where other items would be considered.

Tomisacats commented 1 month ago

Yes, that's it. Thanks for understanding

xlsupport commented 1 month ago

@miloush, Thanks for your elaborate explanation. About being civil/welcoming: Point taken. It probably doesn't help that I'm Dutch. We tend to be considered blunt/too direct for some ;-) In 'my other issue' my patience is being severely tested and I took it out on OP.