/************************************************************************************* Extended WPF Toolkit Copyright (C) 2007-2013 Xceed Software Inc. This program is provided to you under the terms of the Microsoft Public License (Ms-PL) as published at http://wpftoolkit.codeplex.com/license For more features, controls, and fast professional support, pick up the Plus Edition at http://xceed.com/wpf_toolkit Stay informed: follow @datagrid on Twitter or Like http://facebook.com/datagrids ***********************************************************************************/ using System; using System.Collections; using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Input; using System.Collections.Generic; using System.Collections.Specialized; using System.Reflection; using Xceed.Wpf.Toolkit.Core.Utilities; namespace Xceed.Wpf.Toolkit.Primitives { public class Selector : ItemsControl, IWeakEventListener //should probably make this control an ICommandSource { #region Members private bool _surpressItemSelectionChanged; private bool _ignoreSelectedItemChanged; private bool _ignoreSelectedValueChanged; private int _ignoreSelectedItemsCollectionChanged; private int _ignoreSelectedMemberPathValuesChanged; private IList _selectedItems; private IList _removedItems = new ObservableCollection(); private ValueChangeHelper _selectedMemberPathValuesHelper; private ValueChangeHelper _valueMemberPathValuesHelper; #endregion //Members #region Constructors public Selector() { this.SelectedItems = new ObservableCollection(); AddHandler( Selector.SelectedEvent, new RoutedEventHandler( ( s, args ) => this.OnItemSelectionChangedCore( args, false ) ) ); AddHandler( Selector.UnSelectedEvent, new RoutedEventHandler( ( s, args ) => this.OnItemSelectionChangedCore( args, true ) ) ); _selectedMemberPathValuesHelper = new ValueChangeHelper( this.OnSelectedMemberPathValuesChanged ); _valueMemberPathValuesHelper = new ValueChangeHelper( this.OnValueMemberPathValuesChanged ); } #endregion //Constructors #region Properties public static readonly DependencyProperty CommandProperty = DependencyProperty.Register( "Command", typeof( ICommand ), typeof( Selector ), new PropertyMetadata( ( ICommand )null ) ); [TypeConverter( typeof( CommandConverter ) )] public ICommand Command { get { return ( ICommand )GetValue( CommandProperty ); } set { SetValue( CommandProperty, value ); } } #region Delimiter public static readonly DependencyProperty DelimiterProperty = DependencyProperty.Register( "Delimiter", typeof( string ), typeof( Selector ), new UIPropertyMetadata( ",", OnDelimiterChanged ) ); public string Delimiter { get { return ( string )GetValue( DelimiterProperty ); } set { SetValue( DelimiterProperty, value ); } } private static void OnDelimiterChanged( DependencyObject o, DependencyPropertyChangedEventArgs e ) { ( (Selector)o ).OnSelectedItemChanged( (string)e.OldValue, (string)e.NewValue ); } protected virtual void OnSelectedItemChanged( string oldValue, string newValue ) { if( !this.IsInitialized ) return; this.UpdateSelectedValue(); } #endregion #region SelectedItem property public static readonly DependencyProperty SelectedItemProperty = DependencyProperty.Register( "SelectedItem", typeof( object ), typeof( Selector ), new UIPropertyMetadata( null, OnSelectedItemChanged ) ); public object SelectedItem { get { return ( object )GetValue( SelectedItemProperty ); } set { SetValue( SelectedItemProperty, value ); } } private static void OnSelectedItemChanged( DependencyObject sender, DependencyPropertyChangedEventArgs args ) { ( ( Selector )sender ).OnSelectedItemChanged( args.OldValue, args.NewValue ); } protected virtual void OnSelectedItemChanged( object oldValue, object newValue ) { if( !this.IsInitialized || _ignoreSelectedItemChanged ) return; _ignoreSelectedItemsCollectionChanged++; SelectedItems.Clear(); if( newValue != null ) { SelectedItems.Add( newValue ); } this.UpdateFromSelectedItems(); _ignoreSelectedItemsCollectionChanged--; } #endregion #region SelectedItems Property public IList SelectedItems { get { return _selectedItems; } private set { if( value == null ) throw new ArgumentNullException( "value" ); INotifyCollectionChanged oldCollection = _selectedItems as INotifyCollectionChanged; INotifyCollectionChanged newCollection = value as INotifyCollectionChanged; if( oldCollection != null ) { CollectionChangedEventManager.RemoveListener( oldCollection, this ); } if( newCollection != null ) { CollectionChangedEventManager.AddListener( newCollection, this ); } _selectedItems = value; } } #endregion SelectedItems #region SelectedItemsOverride property public static readonly DependencyProperty SelectedItemsOverrideProperty = DependencyProperty.Register( "SelectedItemsOverride", typeof( IList ), typeof( Selector ), new UIPropertyMetadata( null, SelectedItemsOverrideChanged ) ); public IList SelectedItemsOverride { get { return ( IList )GetValue( SelectedItemsOverrideProperty ); } set { SetValue( SelectedItemsOverrideProperty, value ); } } private static void SelectedItemsOverrideChanged( DependencyObject sender, DependencyPropertyChangedEventArgs args ) { ( ( Selector )sender ).OnSelectedItemsOverrideChanged( ( IList )args.OldValue, ( IList )args.NewValue ); } protected virtual void OnSelectedItemsOverrideChanged( IList oldValue, IList newValue ) { if( !this.IsInitialized ) return; this.SelectedItems = ( newValue != null ) ? newValue : new ObservableCollection(); this.UpdateFromSelectedItems(); } #endregion #region SelectedMemberPath Property public static readonly DependencyProperty SelectedMemberPathProperty = DependencyProperty.Register( "SelectedMemberPath", typeof( string ), typeof( Selector ), new UIPropertyMetadata( null, OnSelectedMemberPathChanged ) ); public string SelectedMemberPath { get { return ( string )GetValue( SelectedMemberPathProperty ); } set { SetValue( SelectedMemberPathProperty, value ); } } private static void OnSelectedMemberPathChanged( DependencyObject o, DependencyPropertyChangedEventArgs e ) { Selector sel = ( ( Selector )o ); sel.OnSelectedMemberPathChanged( (string)e.OldValue, (string)e.NewValue ); } protected virtual void OnSelectedMemberPathChanged( string oldValue, string newValue ) { if( !this.IsInitialized ) return; this.UpdateSelectedMemberPathValuesBindings(); } #endregion //SelectedMemberPath #region SelectedValue public static readonly DependencyProperty SelectedValueProperty = DependencyProperty.Register( "SelectedValue", typeof( string ), typeof( Selector ), new FrameworkPropertyMetadata( null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedValueChanged ) ); public string SelectedValue { get { return ( string )GetValue( SelectedValueProperty ); } set { SetValue( SelectedValueProperty, value ); } } private static void OnSelectedValueChanged( DependencyObject o, DependencyPropertyChangedEventArgs e ) { Selector selector = o as Selector; if( selector != null ) selector.OnSelectedValueChanged( ( string )e.OldValue, ( string )e.NewValue ); } protected virtual void OnSelectedValueChanged( string oldValue, string newValue ) { if( !this.IsInitialized || _ignoreSelectedValueChanged ) return; UpdateFromSelectedValue(); } #endregion //SelectedValue #region ValueMemberPath public static readonly DependencyProperty ValueMemberPathProperty = DependencyProperty.Register( "ValueMemberPath", typeof( string ), typeof( Selector ), new UIPropertyMetadata( OnValueMemberPathChanged ) ); public string ValueMemberPath { get { return ( string )GetValue( ValueMemberPathProperty ); } set { SetValue( ValueMemberPathProperty, value ); } } private static void OnValueMemberPathChanged( DependencyObject o, DependencyPropertyChangedEventArgs e ) { Selector sel = ( ( Selector )o ); sel.OnValueMemberPathChanged( ( string )e.OldValue, ( string )e.NewValue ); } protected virtual void OnValueMemberPathChanged( string oldValue, string newValue ) { if( !this.IsInitialized ) return; this.UpdateValueMemberPathValuesBindings(); } #endregion #region ItemsCollection Property protected IEnumerable ItemsCollection { get { return ItemsSource ?? ( ( IEnumerable )Items ?? ( IEnumerable )new object[ 0 ] ); } } #endregion #endregion //Properties #region Base Class Overrides protected override bool IsItemItsOwnContainerOverride( object item ) { return item is SelectorItem; } protected override DependencyObject GetContainerForItemOverride() { return new SelectorItem(); } protected override void PrepareContainerForItemOverride( DependencyObject element, object item ) { base.PrepareContainerForItemOverride( element, item ); _surpressItemSelectionChanged = true; var selectorItem = element as FrameworkElement; selectorItem.SetValue( SelectorItem.IsSelectedProperty, SelectedItems.Contains(item) ); _surpressItemSelectionChanged = false; } protected override void OnItemsSourceChanged( IEnumerable oldValue, IEnumerable newValue ) { base.OnItemsSourceChanged( oldValue, newValue ); var oldCollection = oldValue as INotifyCollectionChanged; var newCollection = newValue as INotifyCollectionChanged; if( oldCollection != null ) { CollectionChangedEventManager.RemoveListener( oldCollection, this ); } if( newCollection != null ) { CollectionChangedEventManager.AddListener( newCollection, this ); } if( !this.IsInitialized ) return; if( !VirtualizingStackPanel.GetIsVirtualizing( this ) || (VirtualizingStackPanel.GetIsVirtualizing( this ) && (newValue != null)) ) { this.RemoveUnavailableSelectedItems(); } this.UpdateSelectedMemberPathValuesBindings(); this.UpdateValueMemberPathValuesBindings(); } // When a DataTemplate includes a CheckComboBox, some bindings are // not working, like SelectedValue. // We use a priority system to select the good items after initialization. public override void EndInit() { base.EndInit(); if( this.SelectedItemsOverride != null ) { this.OnSelectedItemsOverrideChanged( null, this.SelectedItemsOverride ); } else if( this.SelectedMemberPath != null ) { this.OnSelectedMemberPathChanged( null, this.SelectedMemberPath ); } else if( this.SelectedValue != null ) { this.OnSelectedValueChanged( null, this.SelectedValue ); } else if( this.SelectedItem != null ) { this.OnSelectedItemChanged( null, this.SelectedItem ); } if( this.ValueMemberPath != null ) { this.OnValueMemberPathChanged( null, this.ValueMemberPath ); } } #endregion //Base Class Overrides #region Events public static readonly RoutedEvent SelectedEvent = EventManager.RegisterRoutedEvent( "SelectedEvent", RoutingStrategy.Bubble, typeof( RoutedEventHandler ), typeof( Selector ) ); public static readonly RoutedEvent UnSelectedEvent = EventManager.RegisterRoutedEvent( "UnSelectedEvent", RoutingStrategy.Bubble, typeof( RoutedEventHandler ), typeof( Selector ) ); public static readonly RoutedEvent ItemSelectionChangedEvent = EventManager.RegisterRoutedEvent( "ItemSelectionChanged", RoutingStrategy.Bubble, typeof( ItemSelectionChangedEventHandler ), typeof( Selector ) ); public event ItemSelectionChangedEventHandler ItemSelectionChanged { add { AddHandler( ItemSelectionChangedEvent, value ); } remove { RemoveHandler( ItemSelectionChangedEvent, value ); } } #endregion //Events #region Methods protected object GetPathValue( object item, string propertyPath ) { if( item == null ) throw new ArgumentNullException( "item" ); if( String.IsNullOrEmpty( propertyPath ) || propertyPath == "." ) return item; PropertyInfo prop = item.GetType().GetProperty( propertyPath ); return ( prop != null ) ? prop.GetValue( item, null ) : null; } protected object GetItemValue( object item ) { return ( item != null ) ? this.GetPathValue( item, this.ValueMemberPath ) : null; } protected object ResolveItemByValue( string value ) { if( !String.IsNullOrEmpty( ValueMemberPath ) ) { foreach( object item in ItemsCollection ) { var property = item.GetType().GetProperty( ValueMemberPath ); if( property != null ) { var propertyValue = property.GetValue( item, null ); if( value.Equals( propertyValue.ToString(), StringComparison.InvariantCultureIgnoreCase ) ) return item; } } } return value; } internal void UpdateFromList( List selectedValues, Func GetItemfunction ) { _ignoreSelectedItemsCollectionChanged++; // Just update the SelectedItems collection content // and let the synchronization be made from UpdateFromSelectedItems(); SelectedItems.Clear(); if( (selectedValues != null) && (selectedValues.Count > 0) ) { ValueEqualityComparer comparer = new ValueEqualityComparer(); foreach( object item in ItemsCollection ) { object itemValue = GetItemfunction( item ); bool isSelected = (itemValue != null) && selectedValues.Contains( itemValue.ToString(), comparer ); if( isSelected ) { SelectedItems.Add( item ); } } } _ignoreSelectedItemsCollectionChanged--; this.UpdateFromSelectedItems(); } private bool? GetSelectedMemberPathValue( object item ) { PropertyInfo prop = this.GetSelectedMemberPathProperty(item); return ( prop != null ) ? ( bool )prop.GetValue( item, null ) : ( bool? )null; } private void SetSelectedMemberPathValue( object item, bool value ) { PropertyInfo prop = this.GetSelectedMemberPathProperty(item); if( prop != null ) { prop.SetValue( item, value, null ); } } private PropertyInfo GetSelectedMemberPathProperty(object item) { PropertyInfo propertyInfo = null; if( !String.IsNullOrEmpty( SelectedMemberPath ) && ( item != null ) ) { var property = item.GetType().GetProperty( SelectedMemberPath ); if( property != null && property.PropertyType == typeof( bool ) ) { propertyInfo = property; } } return propertyInfo; } /// /// When SelectedItems collection implements INotifyPropertyChanged, this is the callback. /// /// /// protected virtual void OnSelectedItemsCollectionChanged( object sender, NotifyCollectionChangedEventArgs e ) { if( _ignoreSelectedItemsCollectionChanged > 0 ) return; // Keep it simple for now. Just update all this.UpdateFromSelectedItems(); } private void OnItemSelectionChangedCore( RoutedEventArgs args, bool unselected ) { object item = this.ItemContainerGenerator.ItemFromContainer( ( DependencyObject )args.OriginalSource ); // When the item is it's own container, "UnsetValue" will be returned. if( item == DependencyProperty.UnsetValue ) { item = args.OriginalSource; } if( unselected ) { while( SelectedItems.Contains( item ) ) SelectedItems.Remove( item ); } else { if( !SelectedItems.Contains( item ) ) SelectedItems.Add( item ); } OnItemSelectionChanged( new ItemSelectionChangedEventArgs( Selector.ItemSelectionChangedEvent, this, item, !unselected ) ); } /// /// When the ItemsSource implements INotifyPropertyChanged, this is the change callback. /// /// /// private void OnItemsSourceCollectionChanged( object sender, NotifyCollectionChangedEventArgs args ) { this.RemoveUnavailableSelectedItems(); this.AddAvailableRemovedItems(); this.UpdateSelectedMemberPathValuesBindings(); this.UpdateValueMemberPathValuesBindings(); } /// /// This is called when any value of any item referenced by SelectedMemberPath /// is modified. This may affect the SelectedItems collection. /// private void OnSelectedMemberPathValuesChanged() { if( _ignoreSelectedMemberPathValuesChanged > 0 ) return; this.UpdateFromSelectedMemberPathValues(); } /// /// This is called when any value of any item referenced by ValueMemberPath /// is modified. This will affect the SelectedValue property /// private void OnValueMemberPathValuesChanged() { this.UpdateSelectedValue(); } private void UpdateSelectedMemberPathValuesBindings() { _selectedMemberPathValuesHelper.UpdateValueSource( ItemsCollection, SelectedMemberPath ); } private void UpdateValueMemberPathValuesBindings() { _valueMemberPathValuesHelper.UpdateValueSource( ItemsCollection, ValueMemberPath ); } /// /// This method will be called when the "IsSelected" property of an SelectorItem /// has been modified. /// /// protected virtual void OnItemSelectionChanged( ItemSelectionChangedEventArgs args ) { if( _surpressItemSelectionChanged ) return; RaiseEvent( args ); if( Command != null ) Command.Execute( args.Item ); } /// /// Updates the SelectedValue property based on what is present in the SelectedItems property. /// private void UpdateSelectedValue() { #if VS2008 string newValue = String.Join( Delimiter, SelectedItems.Cast().Select( x => GetItemValue( x ).ToString() ).ToArray() ); #else string newValue = String.Join( Delimiter, SelectedItems.Cast().Select( x => GetItemValue( x ) ) ); #endif if( String.IsNullOrEmpty( SelectedValue ) || !SelectedValue.Equals( newValue ) ) { _ignoreSelectedValueChanged = true; SelectedValue = newValue; _ignoreSelectedValueChanged = false; } } /// /// Updates the SelectedItem property based on what is present in the SelectedItems property. /// private void UpdateSelectedItem() { if( !SelectedItems.Contains( SelectedItem ) ) { _ignoreSelectedItemChanged = true; SelectedItem = ( SelectedItems.Count > 0 ) ? SelectedItems[ 0 ] : null; _ignoreSelectedItemChanged = false; } } /// /// Update the SelectedItems collection based on the values /// refered to by the SelectedMemberPath property. /// private void UpdateFromSelectedMemberPathValues() { _ignoreSelectedItemsCollectionChanged++; foreach( var item in ItemsCollection ) { bool? isSelected = this.GetSelectedMemberPathValue( item ); if( isSelected != null ) { if( isSelected.Value ) { if( !SelectedItems.Contains( item ) ) { SelectedItems.Add( item ); } } else { if( SelectedItems.Contains( item ) ) { SelectedItems.Remove( item ); } } } } _ignoreSelectedItemsCollectionChanged--; this.UpdateFromSelectedItems(); } internal void UpdateSelectedItems( IList selectedItems ) { if( selectedItems == null ) throw new ArgumentNullException( "selectedItems" ); // Just check if the collection is the same.. if( selectedItems.Count == this.SelectedItems.Count && selectedItems.Cast().SequenceEqual( this.SelectedItems.Cast() ) ) return; _ignoreSelectedItemsCollectionChanged++; this.SelectedItems.Clear(); foreach( object newItem in selectedItems ) { this.SelectedItems.Add( newItem ); } _ignoreSelectedItemsCollectionChanged--; this.UpdateFromSelectedItems(); } /// /// Updates the following based on the content of SelectedItems: /// - All SelectorItems "IsSelected" properties /// - Values refered to by SelectedMemberPath /// - SelectedItem property /// - SelectedValue property /// Refered to by the SelectedMemberPath property. /// private void UpdateFromSelectedItems() { foreach( object o in ItemsCollection ) { bool isSelected = SelectedItems.Contains( o ); _ignoreSelectedMemberPathValuesChanged++; this.SetSelectedMemberPathValue(o, isSelected); _ignoreSelectedMemberPathValuesChanged--; var selectorItem = ItemContainerGenerator.ContainerFromItem( o ) as SelectorItem; if( selectorItem != null ) { selectorItem.IsSelected = isSelected; } } UpdateSelectedItem(); UpdateSelectedValue(); } /// /// Removes all items from SelectedItems that are no longer in ItemsSource. /// private void RemoveUnavailableSelectedItems() { _ignoreSelectedItemsCollectionChanged++; HashSet hash = new HashSet( ItemsCollection.Cast() ); for( int i = 0; i < SelectedItems.Count; i++ ) { if( !hash.Contains( SelectedItems[ i ] ) ) { _removedItems.Add( SelectedItems[ i ] ); SelectedItems.RemoveAt( i ); i--; } } _ignoreSelectedItemsCollectionChanged--; UpdateSelectedItem(); UpdateSelectedValue(); } private void AddAvailableRemovedItems() { HashSet hash = new HashSet( ItemsCollection.Cast() ); for( int i = 0; i < _removedItems.Count; i++ ) { if( hash.Contains( _removedItems[ i ] ) ) { SelectedItems.Add( _removedItems[ i ] ); _removedItems.RemoveAt( i ); i--; } } } /// /// Updates the SelectedItems collection based on the content of /// the SelectedValue property. /// private void UpdateFromSelectedValue() { List selectedValues = null; if( !String.IsNullOrEmpty( SelectedValue ) ) { selectedValues = SelectedValue.Split( new string[] { Delimiter }, StringSplitOptions.RemoveEmptyEntries ).ToList(); } this.UpdateFromList( selectedValues, this.GetItemValue ); } #endregion //Methods #region IWeakEventListener Members public bool ReceiveWeakEvent( Type managerType, object sender, EventArgs e ) { if( managerType == typeof( CollectionChangedEventManager ) ) { if( object.ReferenceEquals( _selectedItems, sender ) ) { this.OnSelectedItemsCollectionChanged( sender, ( NotifyCollectionChangedEventArgs )e ); return true; } else if( object.ReferenceEquals( ItemsCollection, sender ) ) { this.OnItemsSourceCollectionChanged( sender, ( NotifyCollectionChangedEventArgs )e ); return true; } } return false; } #endregion #region ValueEqualityComparer private class private class ValueEqualityComparer : IEqualityComparer { public bool Equals( string x, string y ) { return string.Equals( x, y, StringComparison.InvariantCultureIgnoreCase ); } public int GetHashCode( string obj ) { return 1; } } #endregion } public delegate void ItemSelectionChangedEventHandler( object sender, ItemSelectionChangedEventArgs e ); public class ItemSelectionChangedEventArgs : RoutedEventArgs { public bool IsSelected { get; private set; } public object Item { get; private set; } public ItemSelectionChangedEventArgs( RoutedEvent routedEvent, object source, object item, bool isSelected ) : base( routedEvent, source ) { Item = item; IsSelected = isSelected; } } }