Collection element behavior state is not shown after the list binding source changes - wpf

I'm quite new at WPF and recently encountered a problem. I got an ObservableCollection< TankCar> TankCars in my ViewModel which is the Binding Source for a ListBox in the view. The ListBox has ItemTemplate that defines two behaviors:
<i:Interaction.Behaviors>
<ei:DataStateBehavior x:Name="Type62DataStateBehavior" Binding="{Binding Type}" Value="62" TrueState="IsType62"/>
<ei:DataStateBehavior x:Name="Type66DataStateBehavior" Binding="{Binding Type}" Value="66" TrueState="IsType66"/>
</i:Interaction.Behaviors>
When I add items to the collection, I can see them appear in the list. But the state is not shown until I call TankCars[i].RaisePropertyChanged("Type")
Moreover, when I need to switch to another collection, I call the code:
TankCars = new ObservableCollection<TankCar> (GetTankCars());
RaisePropertyChanged("TankCars"); //to notify the ListBox
foreach (var car in TankCars) {car.RaisePropertyChanged("Type");} //make states change (not working)
and it appears that after I change ItemSource binding through raising TankCars property change event, items states are not shown (TankCar PropertyChangedEvent is not bound to anything at the moment). If I place a button on a form, that launches the command that calls car.RaisePropertyChanged("Type") for the items, it refreshes the items states.
So the question is: how to make things right to make the behavior of the items trigger after adding new items to a collection and after replacing it by another one? And why the items states are not refreshed, when I raise PropertyChanged just after the changing ListBox item source?
Update: solved with the code below (also helped this answer). And no more manual raising PropertyChanged for collection items, I'm happy :)
public class SmartDataStateBehavior : DataStateBehavior
{
protected override void OnAttached()
{
base.OnAttached();
if (AssociatedObject != null)
{
AssociatedObject.Loaded += AssociatedObjectLoaded;
}
}
protected override void OnDetaching()
{
base.OnDetaching();
if (AssociatedObject != null)
{
AssociatedObject.Loaded -= AssociatedObjectLoaded;
}
}
private void AssociatedObjectLoaded(object sender, RoutedEventArgs e)
{
if (Binding == null || Value == null) return;
if (Binding.ToString() == Value.ToString())
{
if (TrueState != null)
VisualStateManager.GoToElementState(AssociatedObject, TrueState, true);
}
else
{
if (FalseState != null)
VisualStateManager.GoToElementState(AssociatedObject, FalseState, true);
}
}
}

When you have just added the items the respective containers with their behaviours have not yet been created and hence the type is not bound yet, making the notification meaningless.
You normally should not need to raise notifications outside the setter of a property, so i would try to avoid doing all that manually.
I do not know what your behavior does but if it affects the type somehow you should put that notification logic either into the setter of type if it has one or into the OnAttached override method of the bahaviour.

Related

ObservableCollection doesn't sort newly added items

I have the following ObservableCollection that's bound to a DataGrid:
public ObservableCollection<Message> Messages = new ObservableCollection<Message>;
XAML:
<DataGrid ItemsSource="{Binding Path=Messages}">
I sort it on startup, using default view:
ICollectionView view = CollectionViewSource.GetDefaultView(Messages);
view.SortDescriptions.Add(new SortDescription("TimeSent", ListSortDirection.Descending));
It all works fine, but the problem is that whenever I add a new message to Messages collection, it simply gets appended to the bottom of the list, and not sorted automatically.
Messages.Add(message);
Am I doing something wrong? I'm sure I could work around the problem by refreshing the view each time I add an item, but that just seems like the wrong way of doing it (not to mention performance-wise).
So I did a bit more investigating, and it turns out my problem is due to limitation of WPF datagrid. It will not automatically re-sort the collection when underlying data changes. In other words, when you first add your item, it will be sorted and placed in the correct spot, but if you change a property of the item, it will not get re-sorted. INotifyPropertyChanged has no bearing on sorting updates. It only deals with updating displayed data, but doesn't trigger sorting it. It's the CollectionChanged event that forces re-sorting, but modifying an item that's already in the collection won't trigger this particular event, and hence no sorting will be performed.
Here's another similar issue:
C# WPF Datagrid doesn't dynamically sort on data update
That user's solution was to manually call OnCollectionChanged().
In the end, I combined the answers from these two threads:
ObservableCollection not noticing when Item in it changes (even with INotifyPropertyChanged)
ObservableCollection and Item PropertyChanged
I also added 'smart' sorting, that only Calls OnCollectionChanged() if the property changed is the value that's being currently used in SortDescription.
public class MessageCollection : ObservableCollection<Message>
{
ICollectionView _view;
public MessageCollection()
{
_view = CollectionViewSource.GetDefaultView(this);
}
public void Sort(string propertyName, ListSortDirection sortDirection)
{
_view.SortDescriptions.Clear();
_view.SortDescriptions.Add(new SortDescription(propertyName, sortDirection));
}
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
this.AddPropertyChanged(e.NewItems);
break;
case NotifyCollectionChangedAction.Remove:
this.RemovePropertyChanged(e.OldItems);
break;
case NotifyCollectionChangedAction.Replace:
case NotifyCollectionChangedAction.Reset:
this.RemovePropertyChanged(e.OldItems);
this.AddPropertyChanged(e.NewItems);
break;
}
base.OnCollectionChanged(e);
}
private void AddPropertyChanged(IEnumerable items)
{
if (items != null)
{
foreach (var obj in items.OfType<INotifyPropertyChanged>())
{
obj.PropertyChanged += OnItemPropertyChanged;
}
}
}
private void RemovePropertyChanged(IEnumerable items)
{
if (items != null)
{
foreach (var obj in items.OfType<INotifyPropertyChanged>())
{
obj.PropertyChanged -= OnItemPropertyChanged;
}
}
}
private void OnItemPropertyChanged(object sender, PropertyChangedEventArgs e)
{
bool sortedPropertyChanged = false;
foreach (SortDescription sortDescription in _view.SortDescriptions)
{
if (sortDescription.PropertyName == e.PropertyName)
sortedPropertyChanged = true;
}
if (sortedPropertyChanged)
{
NotifyCollectionChangedEventArgs arg = new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Replace, sender, sender, this.Items.IndexOf((Message)sender));
OnCollectionChanged(arg);
}
}
My whole answer below is gibberish. As pointed out in the comments, if you bind to the collection itself, then you are implicitly binding to the default collection view. (However, as a comment at the link notes, Silverlight is an exception -- there no default collection view is created implicitly, unless the collection implements ICollectionViewFactory.)
The CollectionViewSource doesn't modify the underlying collection. To get the sorting, you'll need to bind to the view itself, eg:
<DataGrid ItemsSource="{Binding Path=CollectionViewSource.View}">
Note that, while the original collection (Messages) is untouched, your sorted view will get updated via the notification event:
If the source collection implements the INotifyCollectionChanged interface, the changes raised by the CollectionChanged event are propagated to the views.
I just found the problem, after trying to sort on another property and noticing that it happens to work. Turns out when my messages were being added to the collection the TimeSent property was being initialized to MinDate, and only then updated to actual date. So it was properly being placed at the bottom of the list. The issue is that the position wasn't getting updated when the TimeSent property was modified. Looks like I have an issue with propagation of INotifyPropertyChanged events (TimeSent resides in another object inside Message object).

How to set scroll position from view model with caliburn.micro?

I have a ListBox in my view, bound to a collection that is dynamically growing. I would like the scroll position to follow the last added item (which is appended to the bottom of the list). How can I achieve this with Caliburn.Micro?
An alternative could be to use the event aggregator to publish a message to the view.
Something like:
Aggregator.Publish(ItemAddedMessage<SomeItemType>(itemThatWasJustAdded));
and in the view:
public class SomeView : IHandle<ItemAddedMessage<SomeItemType>>
{
public void Handle(ItemAddedMessage<SomeItemType> message)
{
// Implement view specific behaviour here
}
}
It depends on what your requirements are but at least then the view is responsible for display concerns and you can still test the VM
Also you could just implement the code solely in the view - since it appears to be a view concern (e.g. using the events that listbox provides)
A behaviour would also be useful but maybe one that's a little less coupled to your types - e.g. a generic behaviour SeekAddedItemBehaviour which hooks listbox events to find the last item. Not sure if the listbox exposes the required events, but worth a look
EDIT:
Ok this may work full stop - you should be able to just attach this behaviour to the listbox and it should take care of the rest:
public class ListBoxSeekLastItemBehaviour : System.Windows.Interactivity.Behavior<ListBox>
{
private static readonly DependencyProperty ItemsSourceWatcherProperty = DependencyProperty.Register("ItemsSourceWatcher", typeof(object), typeof(ListBoxSeekLastItemBehaviour), new PropertyMetadata(null, OnItemsSourceWatcherPropertyChanged));
private ListBox _listBox = null;
private static void OnItemsSourceWatcherPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ListBoxSeekLastItemBehaviour source = d as ListBoxSeekLastItemBehaviour;
if (source != null)
source.OnItemsSourceWatcherPropertyChanged();
}
private void OnItemsSourceWatcherPropertyChanged()
{
// The itemssource has changed, check if it raises collection changed notifications
if (_listBox.ItemsSource is INotifyCollectionChanged)
{
// if it does, hook the CollectionChanged event so we can respond to items being added
(_listBox.ItemsSource as INotifyCollectionChanged).CollectionChanged += new NotifyCollectionChangedEventHandler(ListBoxSeekLastItemBehaviour_CollectionChanged);
}
}
void ListBoxSeekLastItemBehaviour_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems.Count > 0)
{
// If an item was added seek it
ScrollIntoView(e.NewItems[0]);
}
}
protected override void OnAttached()
{
base.OnAttached();
// We've been attached - get the associated listbox
var box = this.AssociatedObject as ListBox;
if (box != null)
{
// Hold a ref
_listBox = box;
// Set a binding to watch for property changes
System.Windows.Data.Binding binding = new System.Windows.Data.Binding("ItemsSource") { Source = _listBox; }
// EDIT: Potential bugfix - you probably want to check the itemssource here just
// in case the behaviour is applied after the original ItemsSource binding has been evaluated - otherwise you might miss the change
OnItemsSourceWatcherPropertyChanged();
}
}
private void ScrollIntoView(object target)
{
// Set selected item and try and scroll it into view
_listBox.SelectedItem = target;
_listBox.ScrollIntoView(target);
}
}
You probably want to tidy it up a bit and also make sure that the event handler for CollectionChanged is removed when the ItemsSource changes.
Also you might want to call it SeekLastAddedItemBehaviour or SeekLastAddedItemBehavior - I tend to keep the US spelling since it matches Microsoft's spelling. I think SeekLastItem sounds like it will scroll to the last item in the list rather than the last added item
You could reference the view in the view model using GetView(). That also couples the view and view model.
var myView = GetView() as MyView;
myView.MyListBox.DoStuff
Another option is to create a behavior. This is an example of how to use a behavior to expand a TreeView from the view model. The same could be applied to a ListBox.
Actually, there is an easier way to achieve this, without any of the above.
Just extend your Listbox with the following:
namespace Extensions.Examples {
public class ScrollingListBox : ListBox
{
protected override void OnItemsChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
int newItemCount = e.NewItems.Count;
if (newItemCount > 0)
this.ScrollIntoView(e.NewItems[newItemCount - 1]);
base.OnItemsChanged(e);
}
}
}
}
Then in Xaml, Declare the Location of your extension class as so:
xmlns:Extensions="clr-namespace:Extensions.Examples"
And when you create your listbox, instead of using
<Listbox></Listbox>
Just use your extended class
<Extensions:ScrollingListBox></Extensions:ScrollingListBox>

ItemsSource and collections in which only element properties change

I'm having grief with a ComboBox not reflecting changes in the properties of the collection to which its ItemsSource is bound.
There is a tree made up of a Categories observable collection of Category objects containing Setting objects. Some settings define the presentation names for the domain of values permitted for other settings. These are spread over several categories, but a little magic with LINQ produces an ObservableCollection in which ChannelCategory exposes properties ChannelNumber and ChannelTitle.
ChannelCategory.ChannelTitle is a StringSetting, ChannelNumber is an int, an immutable identity value for which ChannelTitle provides a display string. Omitting abundant but irrelevant other nodes, we have the following:
Channels
ChannelCategory
ChannelNumber = 1
ChannelTitle = "caption for channel one"
ChannelCategory
ChannelNumber = 2
ChannelTitle = "caption for channel two"
ChannelCategory
ChannelNumber = 3
ChannelTitle = "caption for channel three"
...
This Channels collection is prepared and exposed by a property on a class instantiated and added to the window resource dictionary in XAML (accessible as a StaticResource). This arrangement allows me declaratively bind a combobox to Channels
<ComboBox VerticalAlignment="Center" Grid.Column="2"
ItemsSource="{Binding Source={StaticResource cats}, Path=Channels}"
DisplayMemberPath="ChannelTitle.Editor"
SelectedValuePath="ChannelNumber"
SelectedValue="{Binding Editor}"
/>
This all works, but edits elsewhere to the ChannelTitle value are not reflected in the values shown in the combobox list.
Various debugging trickery with breakpoints and the DropDownOpened event allowed me to ascertain that the updates are available from the collection referenced by ItemsSource.
And finally we reach the mystery. Why doesn't the combobox detect changes to the elements of the collection? The collection itself is an ObservableCollection so it should notify property changes.
The elements of the collection are all ChannelCategory which is a DependencyObject, and ChannelCategory.ChannelTitle is a DependencyProperty.
I think the problem is that I'm neither adding nor removing items from the collection, so as far as the collection is concerned it has the same elements and therefore hasn't changed.
Can anyone suggest a strategy for causing changes to ChannelTitle to cause Channels collection to notify change so the combobox updates?
Rachel's suggestion ended up as shown below. In the context of my application there was considerably more complexity because each ChannelCategory owns a collection of settings objects, so the changing value is a property of a an object in a collection that is a property of an object in the collection to which ItemsSource is bound. But the essence of Rachel's suggestion simply needed application at two levels.
public class ObservableChannelCollection : ObservableCollection<ChannelCategory>
{
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
foreach (ChannelCategory channel in e.NewItems)
channel.PropertyChanged += channel_PropertyChanged;
if (e.OldItems != null)
foreach (ChannelCategory channel in e.OldItems)
channel.PropertyChanged -= channel_PropertyChanged;
base.OnCollectionChanged(e);
}
void channel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
OnPropertyChanged(e);
}
}
ObservableCollection tracks changes to the Collection itself, such as Add, Remove, Reset, etc, but it does not track changes to individual items inside the collection. So if you update the property on an item in an ObservableCollection, the collection doesn't get notified that something has changed.
One alternative is to add an event to the ObservableCollection.CollectionChanged event, and when new items get added, hook up a property change on the new items that will raise the collection changed event.
void MyObservableCollection_CollectionChanged(object sender, CollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
foreach(MyItem item in e.NewItems)
{
MyItem.PropertyChanged += MyItem_PropertyChanged;
}
}
if (e.OldItems!= null)
{
foreach(MyItem item in e.OldItems)
{
MyItem.PropertyChanged -= MyItem_PropertyChanged;
}
}
}
void MyItem_PropertyChanged(object sender, PropertyChange e)
{
RaisePropertyChanged("MyObservableCollection");
}

WPF: Cancel a user selection in a databound ListBox?

How do I cancel a user selection in a databound WPF ListBox? The source property is set correctly, but the ListBox selection is out of sync.
I have an MVVM app that needs to cancel a user selection in a WPF ListBox if certain validation conditions fail. Validation is triggered by a selection in the ListBox, rather than by a Submit button.
The ListBox.SelectedItem property is bound to a ViewModel.CurrentDocument property. If validation fails, the setter for the view model property exits without changing the property. So, the property to which ListBox.SelectedItem is bound doesn't get changed.
If that happens, the view model property setter does raise the PropertyChanged event before it exits, which I had assumed would be enough to reset the ListBox back to the old selection. But that's not working--the ListBox still shows the new user selection. I need to override that selection and get it back in sync with the source property.
Just in case that's not clear, here is an example: The ListBox has two items, Document1 and Document2; Document1 is selected. The user selects Document2, but Document1 fails to validate. The ViewModel.CurrentDocument property is still set to Document1, but the ListBox shows that Document2 is selected. I need to get the ListBox selection back to Document1.
Here is my ListBox Binding:
<ListBox
ItemsSource="{Binding Path=SearchResults, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
SelectedItem="{Binding Path=CurrentDocument, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
I did try using a callback from the ViewModel (as an event) to the View (which subscribes to the event), to force the SelectedItem property back to the old selection. I pass the old Document with the event, and it is the correct one (the old selection), but the ListBox selection doesn't change back.
So, how do I get the ListBox selection back in sync with the view model property to which its SelectedItem property is bound? Thanks for your help.
For future stumblers on this question, this page is what ultimately worked for me:
http://blog.alner.net/archive/2010/04/25/cancelling-selection-change-in-a-bound-wpf-combo-box.aspx
It's for a combobox, but works for a listbox just fine, since in MVVM you don't really care what type of control is calling the setter. The glorious secret, as the author mentions, is to actually change the underlying value and then change it back. It was also important to run this “undo” on a separate dispatcher operation.
private Person _CurrentPersonCancellable;
public Person CurrentPersonCancellable
{
get
{
Debug.WriteLine("Getting CurrentPersonCancellable.");
return _CurrentPersonCancellable;
}
set
{
// Store the current value so that we can
// change it back if needed.
var origValue = _CurrentPersonCancellable;
// If the value hasn't changed, don't do anything.
if (value == _CurrentPersonCancellable)
return;
// Note that we actually change the value for now.
// This is necessary because WPF seems to query the
// value after the change. The combo box
// likes to know that the value did change.
_CurrentPersonCancellable = value;
if (
MessageBox.Show(
"Allow change of selected item?",
"Continue",
MessageBoxButton.YesNo
) != MessageBoxResult.Yes
)
{
Debug.WriteLine("Selection Cancelled.");
// change the value back, but do so after the
// UI has finished it's current context operation.
Application.Current.Dispatcher.BeginInvoke(
new Action(() =>
{
Debug.WriteLine(
"Dispatcher BeginInvoke " +
"Setting CurrentPersonCancellable."
);
// Do this against the underlying value so
// that we don't invoke the cancellation question again.
_CurrentPersonCancellable = origValue;
OnPropertyChanged("CurrentPersonCancellable");
}),
DispatcherPriority.ContextIdle,
null
);
// Exit early.
return;
}
// Normal path. Selection applied.
// Raise PropertyChanged on the field.
Debug.WriteLine("Selection applied.");
OnPropertyChanged("CurrentPersonCancellable");
}
}
Note: The author uses ContextIdle for the DispatcherPriority for the action to undo the change. While fine, this is a lower priority than Render, which means that the change will show in the UI as the selected item momentarily changing and changing back. Using a dispatcher priority of Normal or even Send (the highest priority) preempts the display of the change. This is what I ended up doing. See here for details about the DispatcherPriority enumeration.
In .NET 4.5 they added the Delay field to the Binding. If you set the delay it will automatically wait to update so there is no need for the Dispatcher in the ViewModel. This works for validation of all Selector elements like the ListBox's and ComboBox's SelectedItem properties. The Delay is in milliseconds.
<ListBox
ItemsSource="{Binding Path=SearchResults, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
SelectedItem="{Binding Path=CurrentDocument, Mode=TwoWay, Delay=10}" />
-snip-
Well forget what I wrote above.
I just did an experiment, and indeed SelectedItem goes out of sync whenever you do anything more fancy in the setter. I guess you need to wait for the setter to return, and then change the property back in your ViewModel asynchronously.
Quick and dirty working solution (tested in my simple project) using MVVM Light helpers:
In your setter, to revert to previous value of CurrentDocument
var dp = DispatcherHelper.UIDispatcher;
if (dp != null)
dp.BeginInvoke(
(new Action(() => {
currentDocument = previousDocument;
RaisePropertyChanged("CurrentDocument");
})), DispatcherPriority.ContextIdle);
it basically queues the property change on the UI thread, ContextIdle priority will ensure it will wait for UI to be in consistent state. it Appears you cannot freely change dependency properties while inside event handlers in WPF.
Unfortunately it creates coupling between your view model and your view and it's an ugly hack.
To make DispatcherHelper.UIDispatcher work you need to do DispatcherHelper.Initialize() first.
Got it! I am going to accept majocha's answer, because his comment underneath his answer led me to the solution.
Here is wnat I did: I created a SelectionChanged event handler for the ListBox in code-behind. Yes, it's ugly, but it works. The code-behind also contains a module-level variable, m_OldSelectedIndex, which is initialized to -1. The SelectionChanged handler calls the ViewModel's Validate() method and gets a boolean back indicating whether the Document is valid. If the Document is valid, the handler sets m_OldSelectedIndex to the current ListBox.SelectedIndex and exits. If the document is invalid, the handler resets ListBox.SelectedIndex to m_OldSelectedIndex. Here is the code for the event handler:
private void OnSearchResultsBoxSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var viewModel = (MainViewModel) this.DataContext;
if (viewModel.Validate() == null)
{
m_OldSelectedIndex = SearchResultsBox.SelectedIndex;
}
else
{
SearchResultsBox.SelectedIndex = m_OldSelectedIndex;
}
}
Note that there is a trick to this solution: You have to use the SelectedIndex property; it doesn't work with the SelectedItem property.
Thanks for your help majocha, and hopefully this will help somebody else down the road. Like me, six months from now, when I have forgotten this solution...
If you are serious about following MVVM and don't want any code behind, and also don't like the use of the Dispatcher, which frankly is not elegant either, the following solution works for me and is by far more elegant than most of the solutions provided here.
It is based on the notion that in code behind you are able to stop the selection using the SelectionChanged event. Well now, if this is the case, why not create a behavior for it, and associate a command with the SelectionChanged event. In the viewmodel you can then easily remember the previous selected index and the current selected index. The trick is to have binding to your viewmodel on SelectedIndex and just let that one change whenever the selection changes. But immediately after the selection really has changed, the SelectionChanged event fires which now is notified via the command to your viewmodel. Because you remember the previously selected index, you can validate it and if not correct, you move the selected index back to the original value.
The code for the behavior is as follows:
public class ListBoxSelectionChangedBehavior : Behavior<ListBox>
{
public static readonly DependencyProperty CommandProperty
= DependencyProperty.Register("Command",
typeof(ICommand),
typeof(ListBoxSelectionChangedBehavior),
new PropertyMetadata());
public static DependencyProperty CommandParameterProperty
= DependencyProperty.Register("CommandParameter",
typeof(object),
typeof(ListBoxSelectionChangedBehavior),
new PropertyMetadata(null));
public ICommand Command
{
get { return (ICommand)GetValue(CommandProperty); }
set { SetValue(CommandProperty, value); }
}
public object CommandParameter
{
get { return GetValue(CommandParameterProperty); }
set { SetValue(CommandParameterProperty, value); }
}
protected override void OnAttached()
{
AssociatedObject.SelectionChanged += ListBoxOnSelectionChanged;
}
protected override void OnDetaching()
{
AssociatedObject.SelectionChanged -= ListBoxOnSelectionChanged;
}
private void ListBoxOnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
Command.Execute(CommandParameter);
}
}
Using it in XAML:
<ListBox x:Name="ListBox"
Margin="2,0,2,2"
ItemsSource="{Binding Taken}"
ItemContainerStyle="{StaticResource ContainerStyle}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
HorizontalContentAlignment="Stretch"
SelectedIndex="{Binding SelectedTaskIndex, Mode=TwoWay}">
<i:Interaction.Behaviors>
<b:ListBoxSelectionChangedBehavior Command="{Binding SelectionChangedCommand}"/>
</i:Interaction.Behaviors>
</ListBox>
The code that is appropriate in the viewmodel is as follows:
public int SelectedTaskIndex
{
get { return _SelectedTaskIndex; }
set { SetProperty(ref _SelectedTaskIndex, value); }
}
private void SelectionChanged()
{
if (_OldSelectedTaskIndex >= 0 && _SelectedTaskIndex != _OldSelectedTaskIndex)
{
if (Taken[_OldSelectedTaskIndex].IsDirty)
{
SelectedTaskIndex = _OldSelectedTaskIndex;
}
}
else
{
_OldSelectedTaskIndex = _SelectedTaskIndex;
}
}
public RelayCommand SelectionChangedCommand { get; private set; }
In the constructor of the viewmodel:
SelectionChangedCommand = new RelayCommand(SelectionChanged);
RelayCommand is part of MVVM light. Google it if you don't know it.
You need to refer to
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
and hence you need to reference System.Windows.Interactivity.
I came up against this recently, and came up with a solution that works well with my MVVM, without the need for and code behind.
I created a SelectedIndex property in my model and bound the listbox SelectedIndex to it.
On the View CurrentChanging event, I do my validation, if it fails, I simply use the code
e.cancel = true;
//UserView is my ICollectionView that's bound to the listbox, that is currently changing
SelectedIndex = UserView.CurrentPosition;
//Use whatever similar notification method you use
NotifyPropertyChanged("SelectedIndex");
It seems to work perfectly ATM. There may be edge cases where it doesnt, but for now, it does exactly what I want.
I had a very similar problem, the difference being that I am using ListView bound to an ICollectionView and was using IsSynchronizedWithCurrentItem rather than binding the SelectedItem property of the ListView. This worked well for me until I wanted to cancel the CurrentItemChanged event of the underlying ICollectionView, which left the ListView.SelectedItem out of sync with the ICollectionView.CurrentItem.
The underlying problem here is keeping the view in sync with the view model. Obviously cancelling a selection change request in the view model is trivial. So we really just need a more responsive view as far as I'm concerned. I'd rather avoid putting kludges into my ViewModel to work around limitations of the ListView synchronization. On the other hand I'm more than happy to add some view-specific logic to my view code-behind.
So my solution was to wire my own synchronization for the ListView selection in the code-behind. Perfectly MVVM as far as I'm concerned and more robust than the default for ListView with IsSynchronizedWithCurrentItem.
Here is my code behind ... this allows changing the current item from the ViewModel as well. If the user clicks the list view and changes the selection, it will immediately change, then change back if something down-stream cancels the change (this is my desired behavior). Note I have IsSynchronizedWithCurrentItem set to false on the ListView. Also note that I am using async/await here which plays nicely, but requires a little double-checking that when the await returns, we are still in the same data context.
void DataContextChangedHandler(object sender, DependencyPropertyChangedEventArgs e)
{
vm = DataContext as ViewModel;
if (vm != null)
vm.Items.CurrentChanged += Items_CurrentChanged;
}
private async void myListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var vm = DataContext as ViewModel; //for closure before await
if (vm != null)
{
if (myListView.SelectedIndex != vm.Items.CurrentPosition)
{
var changed = await vm.TrySetCurrentItemAsync(myListView.SelectedIndex);
if (!changed && vm == DataContext)
{
myListView.SelectedIndex = vm.Items.CurrentPosition; //reset index
}
}
}
}
void Items_CurrentChanged(object sender, EventArgs e)
{
var vm = DataContext as ViewModel;
if (vm != null)
myListView.SelectedIndex = vm.Items.CurrentPosition;
}
Then in my ViewModel class I have ICollectionView named Items and this method (a simplified version is presented).
public async Task<bool> TrySetCurrentItemAsync(int newIndex)
{
DataModels.BatchItem newCurrentItem = null;
if (newIndex >= 0 && newIndex < Items.Count)
{
newCurrentItem = Items.GetItemAt(newIndex) as DataModels.BatchItem;
}
var closingItem = Items.CurrentItem as DataModels.BatchItem;
if (closingItem != null)
{
if (newCurrentItem != null && closingItem == newCurrentItem)
return true; //no-op change complete
var closed = await closingItem.TryCloseAsync();
if (!closed)
return false; //user said don't change
}
Items.MoveCurrentTo(newCurrentItem);
return true;
}
The implementation of TryCloseAsync could use some kind of dialog service to elicit a close confirmation from the user.
Bind ListBox's property: IsEnabled="{Binding Path=Valid, Mode=OneWay}" where Valid is the view-model property with the validation algoritm. Other solutions look too far-fetched in my eyes.
When the disabled appearance is not allowed, a style could help out, but probably the disabled style is ok because changing the selection is not allowed.
Maybe in .NET version 4.5 INotifyDataErrorInfo helps, I dont'know.

ListView Binding refresh suggestion in WPF

I have an ObservableCollection bound to a ListBox and have a highlight mechanism set up with DataTriggers, when I had a simple set of highlighters (debug, warning, etc) I could simply enumerate the style with several data-triggers bound to the view model that exposes those options.
I have now upgraded the system to support multiple userdefined highlighters which expose themselves with IsHighlighted(xxx) methods (not properties).
How can I make the the ListView aware that the visual state (style's datatrigger) has changed? Is there a "refreshed" event I can fire and catch in a DataTrigger?
Update:
I have a DataTrigger mapped to an exposed property Active which simply returns a value of true, but despite that there is no update:
<DataTrigger Binding="{Binding Highlight.Active}"
Value="true">
<Setter Property="Background"
Value="{Binding Type, Converter={StaticResource typeToBackgroundConverter}}" />
<Setter Property="Foreground"
Value="{Binding Type, Converter={StaticResource typeToForegroundConverter}}" />
</DataTrigger>
When the condition of a DataTrigger changes, this should automatically cause the parent UI element to refresh.
A couple of things to check:
1. The input data of the trigger is actually changing as you expect it to.
2. The input data of the trigger binds to a dependency property. Otherwise, you will never know when the value updates.
If you showed us the appropiate parts of your XAML, that would help a great deal.
If you just want to set the colour of the item somehow, you could write a converter that does what you want:
<Thing Background="{Binding Converter={StaticResource MyItemColorConverter}}" />
In this case, the converter could call your IsHighlighted(xxx) method and return the appropriate colour for the Thing.
If you want to set more than one property, you could use multiple converters, but the idea starts to fall apart at some point.
Alternatively, you could use a converter on your DataBinding to determine whether the item in question falls into a certain category and then apply setters. It depends upon what you need!
EDIT
I have just re-read your question and realised I'm off the mark. Whoops.
I believe you can just raise INotifyPropertyChanged.PropertyChanged with a PropertyChangedEventArgs that uses string.Empty, and that forces the WPF binding infrastructure to refresh all bindings. Have you tried that?
I'm going to answer my own question with an explanation of what I needed to do.
It's a long answer as it seems I kept hitting against areas where WPF thought it knew better and would cache. If DataTrigger had a unconditional change, I wouldn't need any of this!
Firstly, let me recap some of the problem again. I have a ListView that can highlight different rows with different styles. Initially, these styles were built-in types, such as Debug and Error. In these cases I could easily latch onto the ViewModel changes of them as DataTriggers in the row-style and make each update immediately.
Once I upgraded to allow user-defined highlighters, I no longer had a property to latch onto (even if I dynamically created them, the style wouldn't know about them).
To get around this, I have implemented a HighlightingService (this can be discovered at any point by using my ServiceLocator and asking for a IHightlightingServce supporting instance). This service implements a number of important properties and methods:
public ObservableCollection<IHighlighter> Highlighters { get; private set; }
public IHighlighterStyle IsHighlighted(ILogEntry logEntry)
{
foreach (IHighlighter highlighter in Highlighters)
{
if ( highlighter.IsMatch(logEntry) )
{
return highlighter.Style;
}
}
return null;
}
Because the Highlighters collection is publicly accessible, I decided to permit that users of that collection could add/remove entries, negating my need to implement Add/Remove methods. However, because I need to know if the internal IHighlighter records have changed, in the constructor of the service, I register an observer to its CollectionChanged property and react to the add/remove items by registering another callback, this allows me to fire a service specific INotifyCollectionChanged event.
[...]
// Register self as an observer of the collection.
Highlighters.CollectionChanged += HighlightersCollectionChanged;
}
private void HighlightersCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
foreach (var newItem in e.NewItems)
{
System.Diagnostics.Debug.Assert(newItem != null);
System.Diagnostics.Debug.Assert(newItem is IHighlighter);
if (e.NewItems != null
&& newItem is IHighlighter
&& newItem is INotifyPropertyChanged)
{
// Register on OnPropertyChanged.
IHighlighter highlighter = newItem as IHighlighter;
Trace.WriteLine(string.Format(
"FilterService detected {0} added to collection and binding to its PropertyChanged event",
highlighter.Name));
(newItem as INotifyPropertyChanged).PropertyChanged += CustomHighlighterPropertyChanged;
}
}
}
else if (e.Action == NotifyCollectionChangedAction.Remove)
{
foreach (var oldItem in e.OldItems)
{
System.Diagnostics.Debug.Assert(oldItem != null);
System.Diagnostics.Debug.Assert(oldItem is IHighlighter);
if (e.NewItems != null
&& oldItem is IHighlighter
&& oldItem is INotifyPropertyChanged)
{
// Unregister on OnPropertyChanged.
IHighlighter highlighter = oldItem as IHighlighter;
Trace.WriteLine(string.Format(
"HighlightingService detected {0} removed from collection and unbinding from its PropertyChanged event",
highlighter.Name));
(oldItem as INotifyPropertyChanged).PropertyChanged -= CustomHighlighterPropertyChanged;
}
}
}
}
private void CustomHighlighterPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if ( sender is IHighlighter )
{
IHighlighter filter = (sender as IHighlighter);
Trace.WriteLine(string.Format("FilterServer saw some activity on {0} (IsEnabled = {1})",
filter.Name, filter.Enabled));
}
OnPropertyChanged(string.Empty);
}
With all of that, I now know whenever a user has changed a registered highlighter, but it has not fixed the fact that I can't associate a trigger to anything, so I can reflect the changes in the displayed style.
I couldn't find a Xaml only way of sorting this, so I made a custom-control containing my ListView:
public partial class LogMessagesControl : UserControl
{
private IHighlightingService highlight { get; set; }
public LogMessagesControl()
{
InitializeComponent();
highlight = ServiceLocator.Instance.Get<IHighlightingService>();
if (highlight != null && highlight is INotifyPropertyChanged)
{
(highlight as INotifyPropertyChanged).PropertyChanged += (s, e) => UpdateStyles();
}
messages.ItemContainerStyleSelector = new HighlightingSelector();
}
private void UpdateStyles()
{
messages.ItemContainerStyleSelector = null;
messages.ItemContainerStyleSelector = new HighlightingSelector();
}
}
This does a couple of things:
It assigns a new HighlightingSelector to the ItemContainerStyleSelector (the ListView is called messages).
It also registers itself to the PropertyChanged event of the HighlighterService which is a ViewModel.
Upon detecting a change, it replaces the current instance of HighlightingSelector on the ItemContainerStyleSelector (note, it swaps to null first as there is a comment on the web attributed to Bea Costa that this is necessary).
So, now all I need is a HighlightingSelector which takes into account the current highlighting selections (I know that should they change, it will be rebuilt), so I don't need to worry about things too much). The HighlightingSelector iterates over the registered highlighters and (if they're enabled) registers a style. I cache this in a Dictionary as rebuilding these could be expensive and since they only get built at the point the user has made a manual interaction, the increased cost of doing this up front isn't noticeable.
The runtime will make a call to HighlightingSelector.SelectStyle passing in the record I care about, all I do is return the appropriate style (which was based upon the users original highlighting preferences).
public class HighlightingSelector : StyleSelector
{
private readonly Dictionary<IHighlighter, Style> styles = new Dictionary<IHighlighter, Style>();
public HighlightingSelector()
{
IHighlightingService highlightingService = ServiceLocator.Instance.Get<IHighlightingService>();
if (highlightingService == null) return;
foreach (IHighlighter highlighter in highlightingService.Highlighters)
{
if (highlighter is TypeHighlighter)
{
// No need to create a style if not enabled, should the status of a highlighter
// change, then this collection will be rebuilt.
if (highlighter.Enabled)
{
Style style = new Style(typeof (ListViewItem));
DataTrigger trigger = new DataTrigger();
trigger.Binding = new Binding("Type");
trigger.Value = (highlighter as TypeHighlighter).TypeMatch;
if (highlighter.Style != null)
{
if (highlighter.Style.Background != null)
{
trigger.Setters.Add(new Setter(Control.BackgroundProperty,
new SolidColorBrush((Color) highlighter.Style.Background)));
}
if (highlighter.Style.Foreground != null)
{
trigger.Setters.Add(new Setter(Control.ForegroundProperty,
new SolidColorBrush((Color) highlighter.Style.Foreground)));
}
}
style.Triggers.Add(trigger);
styles[highlighter] = style;
}
}
}
}
public override Style SelectStyle(object item, DependencyObject container)
{
ILogEntry entry = item as ILogEntry;
if (entry != null)
{
foreach (KeyValuePair<IHighlighter, Style> pair in styles)
{
if (pair.Key.IsMatch(entry) && pair.Key.Enabled)
{
return pair.Value;
}
}
}
return base.SelectStyle(item, container);
}
}

Resources