我有一个ListBox
,它的ItemsSource
绑定到(正确)实现INotifyCollectionChanged
的自定义类,而SelectedItem
绑定到ViewModel中的字段。
问题是,当我从SelectedItem
集合中删除当前的ItemsSource
时,它会立即将所选内容更改为相邻的项。我非常希望它只是删除了选择。
对我来说这是个问题的原因是这样的。ItemsSource
类包含来自其他集合的元素,这些元素要么满足某些(在运行时常量期间)谓词,要么满足Active
。Active
与is 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)触发的ListBox
更改而导致的SelectedItem
更新由于某种原因而被推迟(用于更新ListBox
的消息可能在WPF消息循环中结束,并等待当前计算完成)。
另一方面,从oldActiveSchema
中删除ItemsSource
是即时的,并且会立即触发SelectedItem
更改为旧项旁边的SelectedItem
(当您删除所选的项时,会选择相邻的项)。而且,由于SelectedItem
的更改触发了我的函数,将CurrentSchema
设置为错误(相邻)项,因此它重写用户选择的CurrentSchema
(#1),并且在运行要更新ListBox
的消息时,只需使用相邻的消息更新。
任何帮助都是非常感谢的。
如果有人想深入挖掘实际代码:
SelectedItem
而不是一个用户选择时SelectedItem
输入的方法应该是活动的SelectedItem
不再是活动的->被从集合中删除(44-41)MoveCurrencyOffDeletedElement
移动SelectedItem
SelectedItem
更改为相邻的
发布于 2017-03-19 11:49:11
诊断
解决问题的关键是在IsSynchronizedWithCurrentItem="True"
上设置ListBox
。它所做的是使ListBox.SelectedItem
和ListBox.Items.CurrentItem
保持同步。此外,ListBox.Items.CurrentItem
与源集合的默认集合视图的ICollectionView.CurrentItem
属性同步(在您的示例中,此视图由CollectionViewSource.GetDefaultView(Schemas)
返回)。现在,当您从Schemas
集合中删除一个项时,它也恰好是相应集合视图的CurrentItem
,视图默认将其CurrentItem
更新为下一项(如果删除的项是最后一项,则更新前一项;如果删除的项是集合中的唯一项,则更新到null
)。
问题的第二部分是,当更改ListBox.SelectedItem
导致对视图模型属性进行更新时,RaisePropertyChangedEvent(nameof(ActiveSchema))
在完成后被处理为,特别是在从ActiveSchema
设置器返回控件之后。您可以观察到,getter不是立即命中,而是在设置器完成之后才被击中。重要的是,也没有立即更新Schemas
视图的Schemas
以反映新选择的项。另一方面,当您在以前选择的项上设置IsActive = false
时,它会立即从Schemas
集合中“删除”该项,这反过来会导致更新集合视图的CurrentItem
,并且链将立即继续更新ListBox.SelectedItem
。您可以观察到,此时ActiveSchema
设置器将再次被击中。因此,您的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
程序集)。这种方法通常被称为交互请求模式(请参阅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似乎容易出现意外行为。
https://stackoverflow.com/questions/42703092
复制相似问题