从 ListBox 中删除已选择项时,如何取消选择?

7
我有一个ListBox,它的ItemsSource绑定到一个自定义类上。该类已经(正确地)实现了INotifyCollectionChanged,并且SelectedItem绑定到ViewModel中的一个字段。
问题在于,当我从ItemsSource集合中删除一个当前SelectedItem时,它会立即更改选择为相邻项。我非常希望它只是取消选择。
导致问题的原因是,ItemsSource类包含来自其他集合的元素,这些元素要么满足某个(运行时常数)谓词,要么是Active。Active与SelectedItem“同步”(有理由)。因此,一个项目只有在被选中时才可以允许出现在ListBox中,这意味着当用户选择其他项目时,它可能应该消失。
我的函数(在“模型”深处)在SelectedItem更改时被调用:
//Gets old Active item
var oldActiveSchema = Schemas.FirstOrDefault(sch => sch.IsActive);

//Makes the new Item active (which triggers adding it into `ItemsSource` in case it didn't satisfy the Predicate)
((PowerSchema)newActiveSchema).IsActive = true;
//Triggers PropertyChanged on ViewModel with the new Active item
CurrentSchema = newActiveSchema;
RaisePropertyChangedEvent(nameof(CurrentSchema)); (#1)

//Changes the old item so it stops being Active -> gets removed from `ItemsSource` (#2)
if (oldActiveSchema != null) { ((PowerSchema)oldActiveSchema).IsActive = false; }

问题在于,由于(#1)触发的SelectedItem更改而更新 ListBox 的原因被推迟了(更新 ListBox 的消息可能会进入WPF消息循环中并等待当前计算完成 )。

另一方面,从ItemsSource中删除oldActiveSchema是立即的,并且立即触发SelectedItem的更改为接替旧选项的下一个选项(当您删除选定的项目时,会选择相邻的项目)。并且由于SelectedItem的更改会触发我的函数,该函数将CurrentSchema设置为错误的(相邻的)项目,因此它重写了用户选择的CurrentSchema(#1),当由于PropertyChanged更新 ListBox 的消息运行时,它只是使用相邻的一个更新它。
非常感谢任何帮助。
如果有人想深入了解实际代码:

删除选择只需要将您的ViewModel中的SelectedItem属性设置为null。由于问题是模式更改,只需将所选项存储在本地变量中,将所选项设置为null,然后再从集合中删除所选项即可。 - Brandon Kramer
如果我在聊天室里不活跃,请加入 #WPF 并@我。我已经查看了你的代码,但无法立即找出如何触发问题。如果我不在的话,那里还有很多其他乐于助人的居民可以帮忙。 - Maverik
1
事实上,Brandon关于选择更改的想法也是我考虑的。如果将选择器(Selector).IsSelected绑定到IsActive,您的选择就可以自动跟随IsActive标志,而无需处理当前项。 - Maverik
1
在查看您的代码后,发现如果值为null,则会特别忽略设置尝试,但是唯一删除选择的方法是将选定值设置为null。既然如此,您希望发生什么?CurrentSchema为空会导致错误吗? - Brandon Kramer
坦白地说,模型没有选择任何项目是没有多大意义的,虽然我可以解决这个问题。话虽如此,我的直觉告诉我删除当前选定元素将强制视图删除选择,将 null 作为选定项(由于忽略而不会传播到模型),然后当 #1 调用的消息被处理时,视图将加载正确的当前选定项(由上面的行设置)并且一切都将正常工作。 - Petrroll
我真的不明白存储selecteditem,将其设置为null,然后删除它会有什么帮助。因为在这种情况下,通过将其设置为null强制更新View的选择仍然只会在删除已完成之后进行处理。就像现在通过第1行和上面的那个强制更新发生的方式一样。或者我错过了什么?我会研究通过IsActive绑定,这可能完全消除问题。 - Petrroll
1个回答

3

诊断

你的问题关键在于在ListBox上设置了IsSynchronizedWithCurrentItem="True"。它的作用是保持ListBox.SelectedItemListBox.Items.CurrentItem同步。此外,ListBox.Items.CurrentItem与源集合的默认集合视图的ICollectionView.CurrentItem属性同步(在你的情况下,这个视图由CollectionViewSource.GetDefaultView(Schemas)返回)。现在,当你从Schemas集合中删除一个项目,这个项目也恰好是相应集合视图的CurrentItem时,默认情况下视图会将其CurrentItem更新为下一个项目(如果删除的项目是最后一个,则更新为前一个项目,如果删除的项目是集合中唯一的项目,则更新为null)。

问题的第二部分是当 ListBox.SelectedItem 被改变导致你的视图模型属性更新时,你的 RaisePropertyChangedEvent(nameof(ActiveSchema)) 会在更新过程完成之后被处理,特别是在从 ActiveSchema setter 返回控制之后。你可以观察到 getter 不会立即被调用,而是在 setter 完成之后才会被调用。重要的是,Schemas 视图的 CurrentItem 也不会立即更新以反映新选择的项目。另一方面,当你在先前选定的项目上设置 IsActive = false 时,它会立即将该项目从 Schemas 集合中 "移除",这反过来会导致集合视图的 CurrentItem 更新,并且链立即继续更新 ListBox.SelectedItem。此时你可以观察到 ActiveSchema setter 将再次被调用。因此,即使在处理用户选择的先前更改(到下一个已选项目)之前,你的 ActiveSchema 也会再次更改。
有几种方法可以解决这个问题: #1 在您的 ListBox 上设置 IsSynchronizedWithCurrentItem="False"(或不进行任何更改)。这将使您的问题消失,无需任何努力。但如果出于某种原因需要,请使用其他任何解决方案。 #2 通过使用守卫标志来防止重新进入设置 ActiveSchema 的尝试:
bool ignoreActiveSchemaChanges = false;
public IPowerSchema ActiveSchema
{
    get { return pwrManager.CurrentSchema; }
    set
    {
        if (ignoreActiveSchemaChanges) return;
        if (value != null && !value.IsActive)
        {
            ignoreActiveSchemaChanges = true;
            pwrManager.SetPowerSchema(value);
            ignoreActiveSchemaChanges = false;
        }
    }
}

这将导致您的视图模型忽略集合视图的“CurrentItem”的自动更新,最终“ActiveSchema”将保持预期值。
#3
在“删除”先前选择的项之前,手动更新集合视图的“CurrentItem”为新选择的项。 您需要引用“MainWindowViewModel.Schemas”集合,因此可以将其作为参数传递给您的“setNewCurrSchema”方法,也可以将代码封装在委托中并将其作为参数传递。 我只会展示第二个选项:
在“PowerManager”类中:
//we pass the action as an optional parameter so that we don't need to update
//other code that uses this method
private void setNewCurrSchema(IPowerSchema newActiveSchema, Action action = null)
{
    var oldActiveSchema = Schemas.FirstOrDefault(sch => sch.IsActive);

    ((PowerSchema)newActiveSchema).IsActive = true;
    CurrentSchema = newActiveSchema;
    RaisePropertyChangedEvent(nameof(CurrentSchema));

    action?.Invoke();

    if (oldActiveSchema != null)
    {
        ((PowerSchema)oldActiveSchema).IsActive = false;
    }
}

MainWindowViewModel类中:
public IPowerSchema ActiveSchema
{
    get { return pwrManager.CurrentSchema; }
    set
    {
        if (value != null && !value.IsActive)
        {
            var action = new Action(() =>
            {
                //this will cause a reentrant attempt to set the ActiveSchema,
                //but it will be ignored because at this point value.IsActive == true
                CollectionViewSource.GetDefaultView(Schemas).MoveCurrentTo(value);
            });
            pwrManager.SetPowerSchema(value, action);
        }
    }
}

请注意,这需要引用PresentationFramework程序集。如果您不想在视图模型程序集中具有此依赖项,则可以创建一个事件,由视图订阅,并由视图运行所需的代码(已经依赖于PresentationFramework程序集)。这种方法通常称为交互请求模式(请参见Prism 5.0指南中的用户交互模式部分,在MSDN上)。

#4

将“删除”先前选择的项推迟到绑定更新完成之后再执行。这可以通过使用Dispatcher将要执行的代码排队来实现:

private void setNewCurrSchema(IPowerSchema newActiveSchema)
{
    var oldActiveSchema = Schemas.FirstOrDefault(sch => sch.IsActive);

    ((PowerSchema)newActiveSchema).IsActive = true;
    CurrentSchema = newActiveSchema;
    RaisePropertyChangedEvent(nameof(CurrentSchema));

    if (oldActiveSchema != null)
    {
        //queue the code for execution
        //in case this code is called due to binding update the current dispatcher will be
        //the one associated with UI thread so everything should work as expected
        Dispatcher.CurrentDispatcher.InvokeAsync(() =>
        {
            ((PowerSchema)oldActiveSchema).IsActive = false;
        });
    }
}

这需要引用WindowsBase程序集,但可以通过使用解决方案#3中描述的方法,在视图模型程序集中避免引用它。

个人建议采用解决方案#1或#2,因为它可以保持您的PowerManager类的清晰,并且#3和#4似乎容易出现意外行为。


谢谢,我有类似于#3和#4的想法,但这两种解决方案对我来说都有点“不干净”。而且我真的不想用UI相关的技巧来污染PowerManager。#2是个好主意。尽管它有点显而易见,但我没有想到。不过我会选择#1。我有点误解了IsSynchronizedWithCurrentItem=的作用,并认为它是我的用例所必需的。结果证明并非如此。 - Petrroll

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