How to update Avalonia UI ComboBox when ObservableCollection was updated

645 views Asked by At

I have a C# Avalonia project to communicate any Serial (RS232) devices, currently i am at the beginning of developing this app. I decided to use Avalonia to build cross-platform application (Windows, Linux). However i faced an issue when component (ComboBox) source IList / IObservableCollection is updated i can't update combobox content.

My Ports list looks like:

    <ComboBox Name="PortsListSelect" ItemsSource="{Binding Path=Ports, Mode=TwoWay}"
                                                   SelectedItem="{Binding SelectedPortNumber, Mode=TwoWay}"
                                                   DropDownOpened="OnPortNumberListOpened"
                                                   Classes="ConnOptCombo" Margin="5 0 0 0" Width="100"/>

On dropdown is opened i handle event in MainWindow:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        _context = new MainWindowViewModel();
        DataContext = _context;
    }

    private void OnPortNumberListOpened(object? sender, EventArgs e)
    {
        _context.ReEnumeratePorts();
    }
        
    private readonly MainWindowViewModel _context;
}

In the event handler i call ReEnumeratePorts fucntion to check what ports we have in the system. In the my MainWindowViewModel i was trying to use dfferent approaches but still can't get it work:

public class MainWindowViewModel : ViewModelBase
{
    public MainWindowViewModel()
    {
        SerialOptions = new SerialDefaultsModel();
        _ports = new ObservableCollection<string (Rs232PortsEnumerator.GetAvailablePorts()?.ToList() ?? new List<string>());
        SelectedPortNumber = Ports.Any() ? Ports.First() : null;
    }

    // 
    public void ReEnumeratePorts()
    {
        this.RaisePropertyChanging("Ports");
        Ports = new ObservableCollection<string>(Rs232PortsEnumerator.GetAvailablePorts());
        SelectedPortNumber = Ports.Any() ? Ports.First() : null; 
        this.RaisePropertyChanged("Ports");
        this.RaisePropertyChanged("SelectedPortNumber");
    }

    public ObservableCollection<string> Ports
    {
        get { return _ports; }
        set
        {

            this.RaiseAndSetIfChanged(ref _ports, value);
        }
    }
    // ... other methods && props

    private ObservableCollection<string> _ports;
}

My Question is: How to enforce ComboBox to update it content by updating Binded ItemsSource value.

2

There are 2 answers

0
Michael Ushakov On BEST ANSWER

Finally I was able to do this but not via ObservableCollection, AvaloniaList or other "observable collection" (tutorials aren't working) instead of this i just used old well known Windows Forms Event Handling

  1. I replaced ObservableCollection with classic IList:
public class MainWindowViewModel : ViewModelBase
{
    public MainWindowViewModel()
    {
        _ports = new List<string (Rs232PortsEnumerator.GetAvailablePorts().ToList());
        SelectedPortNumber = Ports.Any() ? Ports.First() : null;
        // other initialize
    }

    // ReEnumeratePorts returns List!
    public IList<string> ReEnumeratePorts()
    {
        Ports.Clear();
        IList<string> newPorts = Rs232PortsEnumerator.GetAvailablePorts();
        Ports = newPorts;
        return newPorts;
    }

    // other methods ...
    
    // Ports property
    public IList<string> Ports
    {
        get { return _ports; }
        set
        {
            _ports = value;
            this.RaisePropertyChanged();
        }
    }

    // other props ...
    private IList<string> _ports;
}

  1. After some practical survey i discovered that PointerEntered is a most suitable event to nadle, therefore my axaml part shown below:
<ComboBox Name="PortsListSelect" ItemsSource="{Binding Path=Ports, Mode=OneWay}" SelectedItem="{Binding SelectedPortNumber, Mode=TwoWay}"
                                      PointerEntered="OnPortsPointerControlMouseOver" Classes="ConnOptCombo" Margin="5 0 0 0" Width="100"/>
  1. And i added following OnPortsPointerControlMouseOver method to my window code:
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        _context = new MainWindowViewModel();
        DataContext = _context;
    }
        
    private void OnPortsPointerControlMouseOver(object? sender, PointerEventArgs e)
    {
        IList<string> ports = _context.ReEnumeratePorts();
        PortsListSelect.ItemsSource = ports as IEnumerable;
        PortsListSelect.SelectedItem = ports.Any() ? ports [0]: null;
    }
        
    private readonly MainWindowViewModel _context;
}

Unfortunately i was unable to do the same via MVVM with an observables and a property change event raise but my solution is working, a little bit late i'll edit answer and post a link to video with demo of how all this works.

Full example could be found in github repo.

P.S. @Tarazed, thanks for attempts to help me to solve this issue.

7
Tarazed On

You aren't utilizing the ObservableCollection correctly. The point of an observable collection is to allow it to notify when it's contents change, but you are replacing it with an entirely new collection every time you re-enumerate by changing the holding property itself. Consider clearing the list and re-adding. Using this approach I recommend removing the setter on your Ports property, make it read-only unless you have a good reason to change it.

Below is a brute force approach, with some creativity you could filter just the ones that have changed, I'll leave that to you.

public void ReEnumeratePorts()
{
    Ports.Clear();
    var newPorts = Rs232PortsEnumerator.GetAvailablePorts();
    foreach (string port in newPorts)
    {
        Ports.Add(port);
    }
    // Set selected item, no need to raise property changed events here.
}

Truthfully, I'm not seeing why the original method didn't work at a glacne. It should have raised INotifyPropertyChanged and updated anyway. But it's not an efficient design creating a new object every time and throwing stuff off to the garbage collector needlessly, so I would remove that design anyway.