WPF: IsSynchronizedWithCurrentItem and ICollectionView Cancel Bug


Problem

As some of you may have noticed while working with a Selector-derived control and an ICollectionView, there is a bug with the IsSynchronizedWithCurrentItem property of Selector.  IsSynchronizedWithCurrentItem is provided by Selector to support the ability to keep its SelectedItem in-sync with the CurrentItem of its ItemSource’s ICollectionView. IsSynchronizedWithCurrentItem works perfectly except for one specific case: when the CurrentChanging event of ICollectionView is handled and the item change is cancelled.

In this case the Selector does not respect the cancel and it gets out-of-sync with the ICollectionView. I’d imagine this bug exists because the Selector class was likely implemented before ICollectionView, and Selector was not updated to match the addition. Whatever happened though the bug does exist in WPF 4.0 and can be very problematic. Luckily, with attached properties at our disposable we can work up an easy fix.

Solution

Filename: SelectorAttachedProperties.cs

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;

namespace CodeRelief.Wpf.Controls
{
    public static class SelectorAttachedProperties
    {
        private static Type _ownerType = typeof(SelectorAttachedProperties);

        #region IsSynchronizedWithCurrentItemFixEnabled

        public static readonly DependencyProperty IsSynchronizedWithCurrentItemFixEnabledProperty =
            DependencyProperty.RegisterAttached("IsSynchronizedWithCurrentItemFixEnabled", typeof(bool), _ownerType,
            new PropertyMetadata(false, OnIsSynchronizedWithCurrentItemFixEnabledChanged));

        public static bool GetIsSynchronizedWithCurrentItemFixEnabled(DependencyObject obj)
        {
            return (bool)obj.GetValue(IsSynchronizedWithCurrentItemFixEnabledProperty);
        }

        public static void SetIsSynchronizedWithCurrentItemFixEnabled(DependencyObject obj, bool value)
        {
            obj.SetValue(IsSynchronizedWithCurrentItemFixEnabledProperty, value);
        }

        private static void OnIsSynchronizedWithCurrentItemFixEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            Selector selector = d as Selector;
            if (selector == null || !(e.OldValue is bool && e.NewValue is bool) || e.OldValue == e.NewValue)
                return;

            bool enforceCurrentItemSync = (bool)e.NewValue;
            ICollectionView collectionView = null;

            EventHandler itemsSourceChangedHandler = null;
            itemsSourceChangedHandler = delegate
            {
                collectionView = selector.ItemsSource as ICollectionView;
                if (collectionView == null)
                    collectionView = CollectionViewSource.GetDefaultView(selector);
            };

            SelectionChangedEventHandler selectionChangedHanlder = null;
            selectionChangedHanlder = delegate
            {
                if (collectionView == null)
                    return;

                if (selector.IsSynchronizedWithCurrentItem == true && selector.SelectedItem != collectionView.CurrentItem)
                {
                    selector.IsSynchronizedWithCurrentItem = false;
                    selector.SelectedItem = collectionView.CurrentItem;
                    selector.IsSynchronizedWithCurrentItem = true;
                }
            };

            if (enforceCurrentItemSync)
            {
                TypeDescriptor.GetProperties(selector)["ItemsSource"].AddValueChanged(selector, itemsSourceChangedHandler);
                selector.SelectionChanged += selectionChangedHanlder;
            }
            else
            {
                TypeDescriptor.GetProperties(selector)["ItemsSource"].RemoveValueChanged(selector, itemsSourceChangedHandler);
                selector.SelectionChanged -= selectionChangedHanlder;
            }
        }

        #endregion IsSynchronizedWithCurrentItemFixEnabled
    }
}

Essentially all this attached property does is when set to true on a Selector-derived control, it adds handlers SelectionChanged and ItemsSource, so it can manually make sure the Selector is in-sync with its ICollectionView. Now I haven’t used this implementation more than a week, so there could be bugs but it seems to solve the problem. In addition, I have not yet tested setting the attached property back to false after it has been set to true, but I can’t see why anyone would want to do that anyways. Either way though, it should work.

Filename: MainWindow.xaml

<Window x:Class="IsSynchronizedWithCurrentItemFixEnabledExample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:Controls="clr-namespace:CodeRelief.Controls.AttachedProperties">

    <Grid>
        <ListBox Name="listBox"
                 IsSynchronizedWithCurrentItem="True"
                 ItemsSource="{Binding SomeItemsCollectionView}"
                 Controls:SelectorAttachedProperties.IsSynchronizedWithCurrentItemFixEnabled="True" />
    </Grid>
</Window>

This view just has a ListBox control with the IsSynchronizedWithCurrentItem set to true and the custom attached property also set to true. Note that both should be set to true since the attached property only fixes the problem with IsSynchronizedWithCurrentItem, it doesn’t re-implement it.

Filename: MainWindow.xaml.cs

using System.Collections.Generic;
using System.Windows;
using System.Windows.Data;

namespace IsSynchronizedWithCurrentItemFixEnabledExample
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            Loaded += new RoutedEventHandler(MainWindow_Loaded);
        }

        private void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            DataContext = this;

            SomeItems = new List();
            SomeItems.Add("Item1");
            SomeItems.Add("Item2");
            SomeItems.Add("Can't touch this!");
            SomeItems.Add("Item4");
            SomeItems.Add("Item5");

            SomeItemsCollectionView = new ListCollectionView(SomeItems);
            SomeItemsCollectionView.CurrentChanging += new System.ComponentModel.CurrentChangingEventHandler(SomeItemsCollectionView_CurrentChanging);
        }

        private void SomeItemsCollectionView_CurrentChanging(object sender, System.ComponentModel.CurrentChangingEventArgs e)
        {
            if (e.IsCancelable && listBox.SelectedItem.ToString() == "Can't touch this!")
                e.Cancel = true;
        }

        public List<string> SomeItems { get; private set; }
        public ListCollectionView SomeItemsCollectionView { get; private set; }
    }
}

In the code-behind we define the SomeItemsCollectionView and its source, and initialize them both there. Then we hook up the CurrentChanging event of the collectionview to cancel the change only for the “Can’t touch this!” item, which is the reason why in the xaml the listbox is named, so we can access it in code-behind. I want to point out that this has nothing to do with the implementation of the attached property, so this is a MVVM-compliant solution.

Sample Project

The sample project I’ve been demonstrating in this post can be found at the link below. The project is compressed as a ZIP archive, be sure to rename the file extension from “zip_” to “zip”. I have to do this because of hosting reasons.

http://www.freewebs.com/thrash505/BlogProjects/IsSynchronizedWithCurrentItemFixEnabledExample.zip_

Conclusion

Now the real solution to this issue would be for Microsoft to get someone to update the Selector class! (not to mention the buggy ListViewCollection – don’t get me started.. I am going to write a post on that class though as I have a whole list of bugs and oversights…) Although getting Microsoft to do anything except for adding new functionality is pretty much impossible. So, we are stuck with the workaround that I just described to you. At the very least, it is easy to add and MVVM-compliant, and of course works (so far!).

If you have issues with the implementation or other suggestions on this topic, please post a comment below.

, , , ,

  1. #1 by Anonymous on July 18, 2016 - 2:55 am

    work!! Thanks!!

  2. #2 by Anonymous on September 28, 2015 - 6:22 pm

    This is wrong: CollectionViewSource.GetDefaultView(selector)
    should be CollectionViewSource.GetDefaultView(selector.ItemsSource)
    or CollectionViewSource.GetDefaultView(selector.ItemsSource)

  3. #3 by Mattias on September 7, 2012 - 4:43 am

    There are some problems when you add items to the List SomeItems at runtime and not directly to the SomeItemsCollectionView. In different cases the Event CurrentChanging is called or not.

  4. #4 by Christoph on June 7, 2012 - 5:11 am

    I mean “Memory leal”. However I solved this via subclassing Behavior, see http://wpftutorial.net/Behaviors.html

  5. #5 by Christoph Dreßler on June 6, 2012 - 3:41 pm

    Great, but there is a weak event:
    selector.SelectionChanged += selectionChangedHandler;

    How can I achieve the unsubscription of this event?

    • #6 by Tim Valentine on June 13, 2012 - 7:12 pm

      setting the attached property back to false should do the trick.
      basically: selector.SelectionChanged -= selectionChangedHandler;

      or you could implement a weak event pattern if you want to get fancy

  6. #7 by Mark on May 16, 2012 - 12:54 pm

    This doesn’t seem to work for a DataGrid – any ideas how to support that?

    • #8 by Mark on May 16, 2012 - 2:17 pm

      I found that if I run these three lines using Dispatcher.CurrentDispatcher.BeginInvoke(…) then it seems to work OK with a DataGrid:

      selector.IsSynchronizedWithCurrentItem = false;
      selector.SelectedItem = collectionView.CurrentItem;
      selector.IsSynchronizedWithCurrentItem = true;

      I also had to add null checks for selector and collectionView for when the grid was being destroyed.

      • #9 by Matt on November 14, 2012 - 12:18 pm

        where do you add that? At the bottom of the OnIsSynchronizedWithCurrentItemFixEnabledChanged method?

  7. #10 by Chris Wooldridge on April 10, 2012 - 10:51 am

    Good work, a nice clean solution.

  8. #11 by codecontemplator on February 11, 2012 - 1:06 pm

    Nice work. Thank you.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: