Cancel That Please

How to make the user do what you need him to do

I was working with the TabControl the other day and I wanted to prevent the user from abandoning the task he was currently performing by cancelling the selection of a new tab.

This was more difficult than expected because Selector does not implement a SelectionChanging event with which you can cancel the selection changes.

Enter the SelectorAttachedProperties.HasActivatableSupport dependency property. Together with an IActivatable interface that is implemented by the model for the tab page it will take care of the nasty details of cancelling a selection.

the Activate and Deactivate methods wil get called on the model when a tab is switched, and if Deactivate returns false the tab won’t switch.

See the full code here

 <TabControl utils:SelectorAttachedProperties.HasActivatableSupport="True">
  <!-- ... -->
</TabControl>
    /// <summary> IActivatable should be implemented by a ViewModel. </summary>
    public interface IActivatable
    {
        /// <summary> Called when the module is activated. </summary>
        void Activate();

        /// <summary> Return true to continue with deactivation, false to cancel deactivation. </summary>
        bool Deactivate();
    }

    public static class SelectorAttachedProperties
    {
        private static readonly Type _ownerType = typeof(SelectorAttachedProperties);

        private static readonly ConditionalWeakTable<Selector, ActivationHandler> _handlers = new ConditionalWeakTable<Selector, ActivationHandler>();


        public static readonly DependencyProperty HasActivatableSupportProperty =
            DependencyProperty.RegisterAttached("HasActivatableSupport", typeof(bool), _ownerType,
            new PropertyMetadata(false, HasActivatableSupportChanged));

        public static bool GetHasActivatableSupport(DependencyObject obj)
        {
            return (bool)obj.GetValue(HasActivatableSupportProperty);
        }

        public static void SetHasActivatableSupport(DependencyObject obj, bool value)
        {
            obj.SetValue(HasActivatableSupportProperty, value);
        }

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

            var enableActivatableSupport = (bool)e.NewValue;
            var handler = _handlers.GetOrCreateValue(selector);

            TypeDescriptor.GetProperties(selector)["ItemsSource"].RemoveValueChanged(selector, handler.OnItemsSourceChanged);
            selector.SelectionChanged -= handler.OnSelectionChanged;

            if (enableActivatableSupport)
            {
                selector.IsSynchronizedWithCurrentItem = true;
                TypeDescriptor.GetProperties(selector)["ItemsSource"].AddValueChanged(selector, handler.OnItemsSourceChanged);
                selector.SelectionChanged += handler.OnSelectionChanged;
            }
        }

        private class ActivationHandler
        {
            ICollectionView _collectionView;

            public void OnItemsSourceChanged(object sender, EventArgs e)
            {
                var selector = sender as Selector;
                if (selector != null)
                {
                    var itemsSource = selector.ItemsSource;
                    _collectionView = itemsSource as ICollectionView ?? CollectionViewSource.GetDefaultView(itemsSource);
                    _collectionView.CurrentChanging += OnCurrentChanging;
                    _collectionView.CurrentChanged += OnCurrentChanged;
                }
            }

            public void OnSelectionChanged(object sender, SelectionChangedEventArgs args)
            {
                var collectionView = _collectionView;
                if (collectionView == null) return;

                var selector = sender as Selector;
                if (selector == null) return;

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

            private void OnCurrentChanging(object sender, CurrentChangingEventArgs e)
            {
                var activatable = (sender as ICollectionView)?.CurrentItem as IActivatable;
                if (activatable?.Deactivate() == false)
                {
                    if (e.IsCancelable)
                        e.Cancel = true;
                }
            }

            private void OnCurrentChanged(object sender, EventArgs e)
            {
                var activatable = (sender as ICollectionView)?.CurrentItem as IActivatable;
                activatable?.Activate();
            }
        }
    }

References That Inspired This Post

  1. Stack Overflow
  2. CodeRelief.NET