Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WPF - Restore Previous SelectedItem when ComboBox ItemSource Changes

I'm implementing a ComboBox that can be refreshed by users using a button. I'm trying to make it so that the previously selected item is automatically reselected if still present inside the ComboBox after a refresh.

MainWindow.xaml:

<ComboBox Canvas.Left="10" Canvas.Top="10" DisplayMemberPath="Name" IsEnabled="{Binding Path=Enabled}" ItemsSource="{Binding Path=Items}" SelectedItem="{Binding Mode=TwoWay, Path=SelectedItem}" Width="379"/>
<Button Content="{x:Static p:Resources.TextRefresh}" Canvas.Right="10" Canvas.Top="10" Click="OnClickButtonRefresh" Width="75"/>

MainWindow.xaml.cs:

public MainWindow()
{
    InitializeComponent();
    DataContext = m_BrowserInstances = new BrowserInstancesViewModel();
}

private void OnClickButtonRefresh(Object sender, RoutedEventArgs e)
{
    m_BrowserInstances.Populate();
}

[EDITED TO CURRENT VERSION] BrowserInstancesViewModel.cs:

public sealed class BrowserInstancesViewModel : ViewModel
{
    private Boolean m_Enabled;
    public Boolean Enabled
    {
        get { return m_Enabled; }
    }

    private BrowserInstance m_SelectedItem;
    public BrowserInstance SelectedItem
    {
        get { return m_SelectedItem; }
        set
        {
            if (m_SelectedItem != value)
            {
                m_SelectedItem = value;
                NotifyPropertyChanged("SelectedItem");
            }
        }
    }

    private ObservableCollection<BrowserInstance> m_Items;
    public ObservableCollection<BrowserInstance> Items
    {
        get { return m_Items; }
    }

    public BrowserInstancesViewModel()
    {
        Populate();
    }

    private static Func<BrowserInstance, Boolean> Recover(BrowserInstance selectedItem)
    {
        return x =>
        {
            Process currentProcess = x.Process;
            Process selectedProcess = selectedItem.Process;

            if (currentProcess.Id != selectedProcess.Id)
                return false;

            if (currentProcess.MainModule.BaseAddress != selectedProcess.MainModule.BaseAddress)
                return false;

            if (currentProcess.MainWindowTitle != selectedProcess.MainWindowTitle)
                return false;

            return true;
        };
    }

    public void Populate()
    {
        BrowserInstance item = m_SelectedItem;
        List<BrowserInstance> items = new List<BrowserInstance>();

        foreach (Process process in Process.GetProcessesByName("chrome"))
            items.Add(new BrowserInstance(process));

        if (items.Count > 0)
        {
            m_Enabled = true;

            m_Items = new ObservableCollection<BrowserInstance>(items.OrderBy(x => x.Process.Id));

            if (item != null)
                m_SelectedItem = m_Items.SingleOrDefault(Recover(item));

            if (m_SelectedItem == null)
                m_SelectedItem = m_Items[0];
        }
        else
        {
            m_Enabled = false;

            m_Items = new ObservableCollection<BrowserInstance>();
            m_Items.Add(new BrowserInstance());

            m_SelectedItem = m_Items[0];
        }

        NotifyPropertyChanged("Enabled");
        NotifyPropertyChanged("Items");
        NotifyPropertyChanged("SelectedItem");
    }
}

I can get back the previously selected item, but only sometimes. Looks like the code is not working properly when I need to select a default value (Index 0) if the previous selected item cannot be recovered.

like image 584
Tommaso Belluzzo Avatar asked Apr 07 '26 16:04

Tommaso Belluzzo


1 Answers

You need to set m_SelectedItem to the item found by SingleOrDefault(Recover(...)).

Currently, you are setting it to the old instance. That instance no longer exists in the list and apparently your BrowserInstance class doesn't implement any equality members.

Correct code based on your current code:

if(selectedItem != null)
    m_SelectedItem = m_Items.SingleOrDefault(Recover(selectedItem));
if(m_SelectedItem == null)
    m_SelectedItem = m_Items[0];

Update:

The code you uploaded has two problems.

  1. The value of the Process property of the default BrowserInstance object that you add if there is no process is null. This leads to a NullReferenceException in the comparison code used by SingleOrDefault.
    Fix it by changing the preceding if to

    if(selectedItem != null && selectedItem.Process != null)
    
  2. At the end of the Populate method you raise the PropertyChanged event for Items - to update the values in the combobox - and for SelectedItem - to set the selected item to the one the user had previously selected.
    The problem here is that WPF will update SelectedItem with null when PropertyChanged is raised for Items as it doesn't find the previously selected item in the new item list. This effectively overwrites the new selected item you computed in the Populate method.
    Fix it by not assigning the new selected item to m_SelectedItem but to selectedItem and assign that value to SelectedItem after the PropertyChanged event for Items was raised:

    public void Populate()
    {
        BrowserInstance selectedItem = m_SelectedItem;
        List<BrowserInstance> items = new List<BrowserInstance>();
    
        foreach (Process process in Process.GetProcessesByName("chrome"))
            items.Add(new BrowserInstance(process));
    
        if (items.Count > 0)
        {
            m_Enabled = true;
    
            m_Items = new ObservableCollection<BrowserInstance>(items.OrderBy(x => x.Process.Id));
    
            if (selectedItem != null && selectedItem.Process != null)
                selectedItem = m_Items.SingleOrDefault(x => (x.Process.Id == selectedItem.Process.Id) && (x.Process.MainModule.BaseAddress == selectedItem.Process.MainModule.BaseAddress));
    
            if (selectedItem == null)
                selectedItem = m_Items[0];
        }
        else
        {
            m_Enabled = false;
    
            m_Items = new ObservableCollection<BrowserInstance>();
            m_Items.Add(new BrowserInstance());
    
            selectedItem = m_Items[0];
        }
    
        NotifyPropertyChanged("Enabled");
        NotifyPropertyChanged("Items");
        SelectedItem = selectedItem;
    }
    

If you would properly implement equality for BrowserInstance you could make use of the WPF feature that keeps the currently selected item.
The code of Populate can be simplified like this:

public void Populate()
{
    BrowserInstance selectedItem = m_SelectedItem;
    List<BrowserInstance> items = new List<BrowserInstance>();

    foreach (Process process in Process.GetProcessesByName("chrome"))
        items.Add(new BrowserInstance(process));

    m_Enabled = items.Any();
    m_Items = new ObservableCollection<BrowserInstance>(items.OrderBy(x => x.Process.Id));
    if(!m_Enabled)
        m_Items.Add(new BrowserInstance());

    NotifyPropertyChanged("Enabled");
    NotifyPropertyChanged("Items");
    if (SelectedItem == null)
        SelectedItem = m_Items[0];
}

The equality implementation of BrowserInstance looks like this:

public sealed class BrowserInstance : IEquatable<BrowserInstance>
{

    // ...

    public bool Equals(BrowserInstance other)
    {
        if (ReferenceEquals(null, other))
            return false;
        if (ReferenceEquals(this, other))
            return true;
        if (m_Process == null)
        {
            if (other.m_Process == null)
                return true;
            return false;
        }

        if (other.m_Process == null)
            return false;

        return m_Process.Id == other.m_Process.Id && m_Process.MainModule.BaseAddress == other.m_Process.MainModule.BaseAddress;
    }

    public override bool Equals(object obj)
    {
        return Equals(obj as BrowserInstance);
    }

    public override int GetHashCode()
    {
        unchecked
        {
            return m_Process != null ? ((m_Process.Id.GetHashCode() * 397) ^ m_Process.MainModule.BaseAddress.GetHashCode()) : 0;
        }
    }
}
like image 119
Daniel Hilgarth Avatar answered Apr 10 '26 05:04

Daniel Hilgarth



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!