从ListBox中删除所选项目时删除选择

时间:2017-03-09 18:42:08

标签: wpf

我有一个ListBox,其ItemsSource绑定到一个自定义类(正确)实现INotifyCollectionChangedSelectedItem绑定到ViewModel中的字段。

问题在于,当我从SelectedItem集合中删除当前ItemsSource时,会立即将所选内容更改为相邻项目。如果它只是删除了选择,我非常希望。

对我来说这样的问题之所以如此。 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; }

问题在于,出于某种原因,ListBox由于SelectedItem的更改而导致的更新(#1}}被推迟(#1)被推迟(更新的消息) ListBox可能最终在WPF消息循环中等待,直到当前计算结束。)

另一方面,从oldActiveSchema移除ItemsSource是立竿见影的,也会立即触发SelectedItem更改为旧版{1}}一个(当您删除所选项目时,相反选择一个邻居)。并且因为SelectedItem的更改触发了将CurrentSchema设置为错误(相邻)项的函数,所以它会重写用户选择的CurrentSchema(#1)并在更新消息时由于ListBox运行PropertyChanged,它只会使用相邻的更新。

非常感谢任何帮助。

实际代码,如果有人想深入挖掘:

  • ListBox
  • ViewModel
  • The model's method
  • Callstack当相邻项目被选为SelectedItem而不是用户选择的那个时
    • 第46行:用户选择的SelectedItem将该方法输入为应该激活的方法
    • 第45行:旧SelectedItem停止有效 - >从集合中删除(44-41)
    • 第32行:MoveCurrencyOffDeletedElement移动SelectedItem
    • 第5行:SelectedItem更改为相邻的

1 个答案:

答案 0 :(得分:3)

诊断

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

问题的第二部分是,当ListBox.SelectedItem更改导致更新您的视图模型属性时,您的RaisePropertyChangedEvent(nameof(ActiveSchema))在更新过程后被处理完成后,特别是在从ActiveSchema setter返回控件之后。您可以观察到getter不会立即被击中,但只有在setter完成后才会被击中。重要的是,CurrentItem视图的Schemas也不会立即更新以反映新选择的项目。另一方面,当您在之前选择的项目上设置IsActive = false时,会立即从Schemas集合中“删除”此项目,从而导致更新CurrentItem集合视图,链条立即继续更新ListBox.SelectedItem。您可以观察到此时ActiveSchema setter将再次被击中。因此,即使在您完成上一次更改(处理用户选择的项目)之前,您的ActiveSchema也会再次更改(到之前所选项目旁边的项目)。

解决方案

有几种方法可以解决这个问题:

<强>#1

IsSynchronizedWithCurrentItem="False"上设置ListBox(或保持不变)。这会让你的问题毫不费力地消失。但是,如果由于某种原因需要,请使用任何其他解决方案。

<强>#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程序集)。此方法在 MSDN 上的 Prism 5.0 指南中通常称为交互请求模式(请参阅User Interaction Patterns)部分。 / p>

<强>#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似乎容易出现意外行为。