简体   繁体   中英

WPF Combobox SelectedItem binding doesn't update from code

I have this combobox:

<ComboBox Grid.Column="1" SelectedItem="{Binding SelectedItem}" ItemsSource="{Binding Items, Mode=OneWay}" HorizontalAlignment="Stretch" VerticalAlignment="Center"/>

and this is the code:

public class CustomComboBoxViewModel
    {
    private bool DiscardSelChanged { get; set; }
    public ObservableCollection<string> Items { get; set; }

    public string SelectedItem
    {
        get { return _selectedItem; }
        set
        {
            if (!DiscardSelChanged)
                _selectedItem = value;
            bool old = DiscardSelChanged;
            DiscardSelChanged = false;
            if (!old)
                SelectionChanged?.Invoke(_selectedItem);
        }
    }

    public event Action<string> SelectionChanged;

    public void AddItem(string item)
    {
        var v = Items.Where(x => x.Equals(item)).FirstOrDefault();
        if (v != default(string))
        {
            SelectedItem = v;
        }
        else
        {
            DiscardSelChanged = true;
            _selectedItem = item;
            Items.Insert(0, item);
        }
    }
}

At startup I have only one item: Browse... . selecting it i can browse for a file and add its path to the ComboBox. AddItem method is called
If the selected file path doesn't exists in Items i add and select it (this is working).
If the selected file path exists in Items i want to automatically select it without adding it to the list again. this doesn't work and Browse... is the visualized item.
I already tried to use INotifyPropertyChanged .
I'm using .NET 4.6.2. Any ideas to get it working?

EDIT 4:barebone example

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Windows;

namespace WpfApp2
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        public MainWindow()
        {
            InitializeComponent();
            DataContext = this;

            Items = new ObservableCollection<string>();
            Items.Add(ASD);
        }
        private string ASD = @"BROWSE";
        private string _selectedItem;

        public string SelectedItem
        {
            get { return _selectedItem; }
            set
            {
                _selectedItem = value;
                OnPropertyChanged(nameof(SelectedItem));
                UploadFileSelection_SelectionChanged();
            }
        }
        public ObservableCollection<string> Items { get; set; }

        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual void OnPropertyChanged(string propertyName) =>
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

        private void AddItem(string item)
        {
            var v = Items.Where(x => x.Equals(item)).FirstOrDefault();
            if (v != default(string))
                SelectedItem = v;
            else
            {
                Items.Add(item);
                SelectedItem = item;
            }
        }

        private void UploadFileSelection_SelectionChanged()
        {
            if (SelectedItem == ASD)
            {
                Microsoft.Win32.OpenFileDialog dlg = new Microsoft.Win32.OpenFileDialog()
                {
                    DefaultExt = ".*",
                    Filter = "* Files (*.*)|*.*"
                };
                bool? result = dlg.ShowDialog();

                if (result == true)
                    AddItem(dlg.FileName);
            }
        }

    }
}

comboBox:

<ComboBox SelectedItem="{Binding SelectedItem}" ItemsSource="{Binding Items}"/>

try to:
- select FILE_A.txt
- select FILE_B.txt
- select FILE_A.txt again

You are setting _ selectedItem without calling OnPropertyChanged() afterwards. That's why it is not working. If you want a clear code solution consider implementing the property with OnPropertyChanged() like this:

int _example;
public int Example
{
    get
    {
        return _example;
    }
    set
    {
        _example = value;
        OnPropertyChanged(nameof(Example);
    }
}

Your code will be less error prone.

Do it as easy as possible:

public class ViewModel : INotifyPropertyChanged
{
    public ObservableCollection<string> Strings { get; set; }

    public ICommand AddAnotherStringCommand { get; set; }

    string _selectedItem;
    public string SelectedItem
    {
        get
        {
            return _selectedItem;
        }

        set
        {
            _selectedItem = value;
            OnPropertyChanged(nameof(this.SelectedItem));
        }
    }

    public int counter { get; set; } = 1;

    public ViewModel()
    {
        // RelayCommand from: https://stackoverflow.com/questions/22285866/why-relaycommand
        this.AddAnotherStringCommand = new RelayCommand<object>(AddAnotherString);
        this.Strings = new ObservableCollection<string>();
        this.Strings.Add("First item");
    }

    private void AddAnotherString(object notUsed = null)
    {
        this.Strings.Add(counter.ToString());
        counter++;
        this.SelectedItem = counter.ToString();
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

Main Window:

<Window x:Class="Test.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:Test"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <local:ViewModel x:Name="ViewModel" />
    </Window.DataContext>
    <StackPanel>
        <ComboBox ItemsSource="{Binding Strings}" SelectedItem="{Binding SelectedItem}"/>
        <Button Content="Add another item" Command="{Binding AddAnotherStringCommand}" />
    </StackPanel>
</Window>

In my case the value is changed every time, but you should be able to modify the code to fit your needs.

Make sure that you have a clear code structure and do not overcomplicate things.

If you want a more specific answer you should consider to present you whole code.

I tried your example. I fixed the re-entrancy problem (double browse dialog) with a flag:

private bool _browsing = false;
private void UploadFileSelection_SelectionChanged()
{
    if (_browsing)
    {
        return;
    }

    if (SelectedItem == ASD)
    {
        try
        {
            _browsing = true;
            Microsoft.Win32.OpenFileDialog dlg = new Microsoft.Win32.OpenFileDialog()
            {
                DefaultExt = ".*",
                Filter = "* Files (*.*)|*.*"
            };
            bool? result = dlg.ShowDialog();

            if (result == true)
                AddItem(dlg.FileName);
        }
        finally
        {
            _browsing = false;
        }
    }
}

It's caveman stuff but it works.

The real problem you have is that UploadFileSelection_SelectionChanged() is called, and updates SelectedItem before you exit the SelectedItem setter from the call that sets it to ASD .

So SelectedItem = v; in AddItem() has no effect on the combobox, because the combobox isn't responding to PropertyChanged right then.

This will fix that:

private void AddItem(string item)
{
    var v = Items.FirstOrDefault(x => x.Equals(item));

    if (v != default(string))
    {
        //SelectedItem = v;
        Task.Run(() => SelectedItem = v);
    }
    else
    {
        Items.Add(item);
        SelectedItem = item;
    }
}

Now we're doing it later.

But note that the other branch does work, the one where item is newly added to the collection. You can also fake it out by removing item and adding it again:

private void AddItem(string item)
{
    //  Harmless, if it's not actually there. 
    Items.Remove(item);

    Items.Add(item);
    SelectedItem = item;
}

That looks weirder, but since it doesn't rely on thread timing, it's probably a better solution. On the other hand, this is "viewmodel" code whose details are driven by the peculiarities of the implementation of the ComboBox control. That's not a good idea.

This should be probably be done in the view (leaving aside that in this contrived example our view is our viewmodel).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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