WPF - 当ComboBox的ItemSource发生变化时恢复先前选择的SelectedItem

3

我正在实现一个ComboBox,用户可以使用按钮进行刷新。我试图使之前选择的项目在刷新后如果仍然存在于ComboBox中,则自动重新选择。

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();
}

[已编辑至当前版本] 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");
    }
}

我可以获取之前选中的项,但有时候不行。看起来当需要选择默认值(索引0)时,代码不能正常工作,如果无法恢复先前选择的项目。

1个回答

3

你需要将m_SelectedItem设置为由SingleOrDefault(Recover(...))找到的项。

目前,你正在将其设置为旧实例。该实例在列表中不再存在,显然你的BrowserInstance类没有实现任何相等成员。

基于当前代码的正确代码:

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

更新:

您上传的代码有两个问题。

  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;
    }
    

如果您正确实现了BrowserInstance的相等性,您可以利用WPF功能来保留当前选定的项目。
Populate的代码可以简化为:

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];
}
BrowserInstance 的相等性实现如下:
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;
        }
    }
}

哇,谢谢!在自定义类上实现相等成员的最佳方法是什么?我的BrowserInstance类只有两个字段:System.Diagnostics.Process Process和System.String Name...我只需要比较进程,但我找不到一个正确检查两个进程是否相同的方法。 - Tommaso Belluzzo
@Zarathos:您需要重写 Object.EqualsObject.GetHashCode 方法。如何执行比较取决于您和您的程序如何定义 BrowserInstance 的相等性。例如,您可以使用 Recover 中的逻辑。 - Daniel Hilgarth
嗯...我正在测试我的代码,但仍然存在一些问题。如果我不断地按刷新按钮,由于某种未知的原因,ComboBox选择的项目会闪烁(有时为空,有时不为空)。我无法理解原因。我根据您的建议编辑了我的代码,并更新了问题。 - Tommaso Belluzzo
对不起,我现在已经正确修改了它。但是当我有一个选定的项目并且我很快点击刷新按钮时,仍然会出现一些问题。它应该始终保持当前的项目,但实际上有时 SelectedItem 为 null,并且没有从 ComboBox 中选择任何值。看起来在 SingleOrDefault 后发生了一些错误,我无法选择列表中的第一个项目或从中获取的选定项目是错误的。 - Tommaso Belluzzo
@Zarathos:原因很简单:你只比较了进程ID和基地址,但没有比较名称。你也需要加上名称。 - Daniel Hilgarth
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接