I have a ListBox
that has its ItemsSource
bound to a custom class that (properly) implements an INotifyCollectionChanged
and a SelectedItem
bound to a field in a ViewModel.
The problem is that when I remove a currently SelectedItem
from the ItemsSource
collection it immediately changes the selection to an neighboring item. I would very much prefer if it just removed selection.
The reason why it's such a problem for me is following. The ItemsSource
class contains elements from some other collection that either satisfy some (during runtime constant) Predicate or are Active
. Being Active
is "synchronized" with being SelectedItem
(there're reasons for that). So it's very much possible for an item be allowed in the ListBox
only if it's selected which means it might be supposed to vanish when user selects some other one.
My function (deep in "model") that gets called when SelectedItem
gets changed:
//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; }
The issue is that for some reason an update of ListBox
due to the change of SelectedItem
that's supposed to get triggered by (#1) gets postponed (the message to update the ListBox
probably ends up in a WPF message loop and waits there until current computation finishes).
The removal of oldActiveSchema
from the ItemsSource
, on the other hand, is immediate and also instantly triggers a change of SelectedItem
to a one that's next to the old one (when you remove the selected item, a neighboring gets selected instead). And because the change of SelectedItem
triggers my function that sets CurrentSchema
to the wrong (neighboring) item it rewrites the user-selected CurrentSchema
(#1) and by the time the message to update ListBox
due to PropertyChanged
gets run it just updates it with the neighboring one.
Any help is greatly appreciated.
Actual code if anyone would like to dig deeper:
SelectedItem
instead of the one user chose
SelectedItem
chosen by user enters the method as one that's supposed to get activeSelectedItem
stops being active -> gets removed from collection (44-41)MoveCurrencyOffDeletedElement
moves SelectedItem
SelectedItem
gets changed to a neighboring oneListBox. ObjectCollection class, and how to clear all item selection using the ClearSelected method. The code first moves the currently selected item in the ListBox to the top of the list. The code then removes all items before the currently selected item and clears all selections in the ListBox.
The key to your problems is that you set IsSynchronizedWithCurrentItem="True"
on your ListBox
. What it does is it keeps the ListBox.SelectedItem
and ListBox.Items.CurrentItem
in sync. Also, ListBox.Items.CurrentItem
is synchronized with the ICollectionView.CurrentItem
property of the default collection view for the source collection (this view is returned by CollectionViewSource.GetDefaultView(Schemas)
in your case). Now when you remove an item from the Schemas
collection which also happens to be the CurrentItem
of the corresponding collection view, the view by default updates its CurrentItem
to the next item (or the previous one if the removed item was the last one, or to null
if the removed item was the only item in the collection).
The second part of the problem is that when the ListBox.SelectedItem
is changed causing an update to your view-model property, your RaisePropertyChangedEvent(nameof(ActiveSchema))
is processed after the update process is finished, in particular after the control is returned from the ActiveSchema
setter. You can observe that the getter is not hit immediately, but only after the setter is done. What's important, the CurrentItem
of the Schemas
view is also not updated immediately to reflect the newly selected item. On the other hand, when you set IsActive = false
on the previously selected item, it causes immediate "removal" of this item from the Schemas
collection, which in turn causes an update of the CurrentItem
of the collection view, and the chain immediately continues to update the ListBox.SelectedItem
. You can observe that at this point the ActiveSchema
setter will be hit again. So your ActiveSchema
will be changed again (to the item next to the previously selected one) even before you've finished processing the previous change (to the item selected by the user).
There are several ways to address this issue:
#1
Set the IsSynchronizedWithCurrentItem="False"
on your ListBox
(or leave it untouched). This will make your problem go away with no effort. If however for some reason it is required, use any of the other solutions.
#2
Prevent reentrant attempts to set the ActiveSchema
by using a guard flag:
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;
}
}
}
This will cause automatic updates to the collection view's CurrentItem
to be ignored by your view-model, and ultimately the ActiveSchema
will maintain the expected value.
#3
Manually update the collection view's CurrentItem
to the newly selected item before you "remove" the previously selected one. You will need a reference to the MainWindowViewModel.Schemas
collection, so you can either pass it as a parameter to your setNewCurrSchema
method or encapsulate the code in a delegate and pass that as the parameter. I'll only show the second option:
In the PowerManager
class:
//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;
}
}
In the MainWindowViewModel
class:
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);
}
}
}
Note though that this requires a reference to the PresentationFramework
assembly. If you don't want that dependency in your view-model assembly, you could create an event which would be subscribed to by the view and the required code would be run by the view (which already depends on the PresentationFramework
assembly). This method is often referred to as interaction request pattern (see User Interaction Patterns) section in the Prism 5.0 guide on MSDN).
#4
Defer the "removal" of the previously selected item until the binding update is finished. This can be achieved by queueing the code to be executed using a 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;
});
}
}
This requires reference to the WindowsBase
assembly, which again can be avoided in the view-model assembly by utilizing the method described for solution #3.
Personally I'd go with solution #1 or #2 because it keeps your PowerManager
class clean, and #3 and #4 seem prone to unexpected behavior.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With