Thursday, November 13, 2014

Null (blank row) in Item Controls

Note: This is not my original work (copied and tailored in fact). Have blogged it here just for my future reference and of course if someone wants to use it.


Many times there comes situation when while working on list box or combo box (drop down) we want to show an empty line apart from the items available in drop down. Here we will be creating a custom control as we will generally use such drop downs across the application.

Code

    /// <summary>
    /// Adapts a <see cref="Selector"/> control to include an item representing null.
    /// This element is a <see cref="ContentControl"/> whose <see cref="ContentControl.Content"/>
    /// should be a Selector, such as a <see cref="ComboBox"/>, <see cref="ListBox"/>,
    /// or <see cref="ListView"/>.
    /// </summary>
    /// <remarks>
    /// In XAML, place this element immediately outside the target Selector, and set the
    /// <see cref="ItemsSource"/> property instead of the Selector's ItemsSource.
    /// </remarks>
    /// <example>
    /// <code>

    /// <local:NullItemSelectorItemsSource=&quot;{Binding CustomerList}&quot;>
    ///     <ComboBox .../>
    /// </local:NullItemSelector>
    /// </code>
    /// </example>

    [ContentProperty("Selector")]
    public class NullItemSelector : ContentControl
    {
        ICollectionView _collectionView;
        /// <summary>
        /// Gets or sets the collection view associated with the internal <see cref="CompositeCollection"/>
        /// that combines the null-representing item and the <see cref="ItemsSource"/>.
        /// </summary>

        protected ICollectionView CollectionView
        {
            get { return _collectionView; }
            set { _collectionView = value; }
        }

        /// <summary>
        /// Identifies the <see cref="Selector"/> property.
        /// </summary>

        public static readonly DependencyProperty SelectorProperty = DependencyProperty.Register(
            "Selector", typeof(Selector), typeof(NullItemSelector),
            new FrameworkPropertyMetadata(new PropertyChangedCallback(Selector_Changed)));

        static void Selector_Changed(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            NullItemSelector adapter = (NullItemSelector)sender;
            adapter.Content = e.NewValue;
            Selector selector = (Selector)e.OldValue;
            if (selector != null) selector.SelectionChanged -= adapter.Selector_SelectionChanged;
            selector = (Selector)e.NewValue;
            if (selector != null)
            {
                selector.IsSynchronizedWithCurrentItem = true;
                selector.SelectionChanged += adapter.Selector_SelectionChanged;
            }
            adapter.Adapt();
        }

        /// <summary>
        /// Gets or sets the Selector control.
        /// </summary>

        public Selector Selector
        {
            get { return (Selector)GetValue(SelectorProperty); }
            set { SetValue(SelectorProperty, value); }
        }

        /// <summary>
        /// Identifies the <see cref="ItemsSource"/> property.
        /// </summary>

       public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register(
            "ItemsSource", typeof(IEnumerable), typeof(NullItemSelector),
            new FrameworkPropertyMetadata(new PropertyChangedCallback(ItemsSource_Changed)));

        static void ItemsSource_Changed(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            NullItemSelector adapter = (NullItemSelector)sender;
            adapter.Adapt();
        }

        /// <summary>
        /// Gets or sets the data items.
        /// </summary>

        public IEnumerable ItemsSource
        {
            get { return (IEnumerable)GetValue(ItemsSourceProperty); }
            set { SetValue(ItemsSourceProperty, value); }
        }

        /// <summary>
        /// Identifies the <see cref="NullItem"/> property.
        /// </summary>

        public static readonly DependencyProperty NullItemProperty = DependencyProperty.Register(
            "NullItem", typeof(object), typeof(NullItemSelector), new PropertyMetadata("(None)"));

        /// <summary>
        /// Gets or sets the null-representing object to display in the Selector.
        /// (The default is the string &quot;(None)&quot;.)
        /// </summary>
        public object NullItem
        {
            get { return GetValue(NullItemProperty); }
            set { SetValue(NullItemProperty, value); }
        }

        /// <summary>
        /// Creates a new instance.
        /// </summary>

        public NullItemSelector()
        {
            IsTabStop = false;
        }

        /// <summary>
        /// Updates the Selector control's <see cref="ItemsControl.ItemsSource"/> to include the
        /// <see cref="NullItem"/> along with the objects in <see cref="ItemsSource"/>.
        /// </summary>

        protected void  Adapt()
        {
            if (CollectionView != null)
            {
                CollectionView.CurrentChanged -= CollectionView_CurrentChanged;
                CollectionView = null;
            }
            if (Selector != null && ItemsSource != null)
            {
                CompositeCollection comp = new CompositeCollection();
                comp.Add(NullItem);
                comp.Add(new CollectionContainer { Collection = ItemsSource } );

                CollectionView = CollectionViewSource.GetDefaultView(comp);
                if (CollectionView != null) CollectionView.CurrentChanged += CollectionView_CurrentChanged;

                Selector.ItemsSource = comp;
            }
        }

        bool _isChangingSelection;
        /// <summary>
        /// Triggers binding sources to be updated if the <see cref="NullItem"/> is selected.
        /// </summary>
        /// <param name="sender">sender</param>
        /// <param name="e">event data</param>

        protected void Selector_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (Selector.SelectedItem == NullItem)
            {
            
                if (!_isChangingSelection)
                {
                    _isChangingSelection = true;
                    try
                    {
                        // Selecting the null item doesn't trigger an update to sources bound to properties
                        // like SelectedItem, so move selection away and then back to force this.
                        int selectedIndex = Selector.SelectedIndex;
                        Selector.SelectedIndex = -1;
                        Selector.SelectedIndex = selectedIndex;
                    }
                    finally
                    {
                        _isChangingSelection = false;
                    }
                }
            }
        }

        /// <summary>
        /// Selects the <see cref="NullItem"/> if the source collection's current item moved to null.
        /// </summary>
        /// <param name="sender">sender</param>
        /// <param name="e">event data</param>

        void CollectionView_CurrentChanged(object sender, EventArgs e)
        {
            if (Selector != null && ((ICollectionView)sender).CurrentItem == null && Selector.Items.Count != 0)
            {
                Selector.SelectedIndex = 0;
            }
        }
    }
}

Explanation
  1. NullItem property - This is the actual null item which will be displayed as blank on UI. 
  2. Selector - This is the WPF item control which is being used (combo box, list box). It registers the selector as this new control and attaches the Selection changed property in callback method Selector_Changed.
  3. CollectionView - is the view of your collection.
  4. ItemsSource - the actual collection of data from source.The callback method calls another important method Adapt. Adapt method actually just creates a new Composite collection which has nullitem also apart from the actual itemssource.

To use this custom control is fairly easy, you just need to include in xaml like this:
<local:NullItemSelector  ItemsSource="{Binding myBinding}">
                <ComboBox  SelectedValuePath="Value" .../>

        </local:NullItemSelector>

No comments:

Post a Comment