0

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

rmbq
  • 427
  • 4
  • 20
  • 1
    *"I already tried to use INotifyPropertyChanged."* -- you did it wrong. Show us how you did it and we'll help you fix it. – 15ee8f99-57ff-4f92-890c-b56153 Jun 21 '17 at 13:20
  • question edited – rmbq Jun 21 '17 at 13:24
  • Did you also write `public class CustomComboBoxViewModel : INotifyPropertyChanged`? – Clemens Jun 21 '17 at 13:24
  • yes sorry, forgot it – rmbq Jun 21 '17 at 13:25
  • 2
    The `OnPropertyChanged(nameof(SelectedItem))` call should be in the SelectedItem property setter, after `_selectedItem = value` – Clemens Jun 21 '17 at 13:26
  • 1
    Immediately *after* a property named `X` changes its value, you **must** call `OnPropertyChanged(nameof(X))`. You **must** do that in the `set` block for `X`, *after* you give the backing variable a new value. No exceptions. You must do this for every property, and you must do it in the `set` block so it always happens every time you set the property. That is what we mean by "implement `INotifyPropertyChanged`". – 15ee8f99-57ff-4f92-890c-b56153 Jun 21 '17 at 13:29
  • @Clemens Itried and it's not working. btw i can call OnPropertyChanged where i want, probably i should call it in the setter just for coding standard – rmbq Jun 21 '17 at 13:29
  • @mbq Please copy the new version that isn't working and paste it into your question. We'll show you how to fix it. – 15ee8f99-57ff-4f92-890c-b56153 Jun 21 '17 at 13:31
  • 1
    You also have `_selectedItem = item` in AddItem. Replace that by `SelectedItem = item`. – Clemens Jun 21 '17 at 13:32
  • 1
    As a note, `Mode=OneWay` on the ItemsSource Binding is already the default and hence redundant. – Clemens Jun 21 '17 at 13:36
  • if i replace `_selectedItem = item` with `SelectedItem = item` file browse window will open twice so I can't. also set _selectedItem in that bad way (i know it's bad) is the only working solution i found – rmbq Jun 21 '17 at 13:38
  • Well, that is not an option. You have to set the property, not the backing field. Get the invocation of that strange (and yet unexplained) SelectionChanged event out of the property setter. – Clemens Jun 21 '17 at 13:43
  • `SelectedItem = item` is not needed because `Items.Insert(0, item);` updates the DataContext so both properties ItemsSource and SelectedItem are updated – rmbq Jun 21 '17 at 13:47
  • 1
    *"if i replace _selectedItem = item with SelectedItem = item file browse window will open twice"* -- then get all that stuff out of your setter and put it someplace where it belongs. Don't do it that way. It's an anti-pattern. `SelectedItem = item` **is needed**. "Updates the DataContext" doesn't make any sense. You need to listen to what Clemens tells you, because he is an expert. What you're doing here is easy if you do it right. Let him tell you how to do it right. – 15ee8f99-57ff-4f92-890c-b56153 Jun 21 '17 at 14:01
  • just try the code you are telling me to use. i followed Clemens suggestions and it's not working. i use wpf since 5 years so I'm not a newbie – rmbq Jun 21 '17 at 14:22
  • @mbq Well, if you're that knowledgeable, you can resolve this incredibly trivial issue without any great effort. Glad to hear it. – 15ee8f99-57ff-4f92-890c-b56153 Jun 21 '17 at 14:53
  • @EdPlunkett i didn't mean you are wrong, i mean i already tried a lot of things. i added a barebone example – rmbq Jun 21 '17 at 15:20
  • @mbq I tried your example. I fixed the re-entrancy problem (double browse dialog) with a flag. The 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. – 15ee8f99-57ff-4f92-890c-b56153 Jun 21 '17 at 15:45
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/147291/discussion-between-rmbq-and-ed-plunkett). – rmbq Jun 21 '17 at 15:47

2 Answers2

1

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.

Jens
  • 2,592
  • 2
  • 21
  • 41
1

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