ListBox.SelectedItems convention

May 9, 2011 at 3:35 PM

I noticed that there is no convention for ListBox.SelectedItems, and I would like to propose a possible way to implement a two-way interaction between view and view-model.

The convention creation is the same as usual:

ConventionManager.AddElementConvention<ListBox>(ItemsControl.ItemsSourceProperty, "DataContext", "Loaded")
    .ApplyBinding = (viewModelType, path, property, element, convention) =>
    {
        if (!ConventionManager.SetBinding(viewModelType, path, property, element, convention))
            return false;

        if(((ListBox)element).SelectionMode == SelectionMode.Single)
            ConventionManager.ConfigureSelectedItem(element, Selector.SelectedItemProperty, viewModelType, path);
        else
            ConfigureSelectedItems(element, viewModelType, path);

        ConventionManager.ApplyItemTemplate((ItemsControl)element, property);

        return true;
    };

As you can see, the above convention keeps the same functionalities offered by the standard ItemsControl, but sets a different convention for selected items, depending on the selection mode.

Since ListBox.SelectedItems cannot be set directly as a property (it is read-only), I used an EventTrigger to achieve the desired functionality (i.e. simulate a binding between SelectedItems and a view-model collections):

/// <summary>
///   Configures the selected item convention.
/// </summary>
/// <param name = "element">The element having the <see cref = "ListBox.SelectedItems" /> property.</param>
/// <param name = "viewModelType">The view model type.</param>
/// <param name = "path">The property path.</param>
public static void ConfigureSelectedItems(FrameworkElement element, Type viewModelType, string path)
{
    var index = path.LastIndexOf('.');
    index = index == -1 ? 0 : index + 1;
    var baseName = path.Substring(index);

    foreach (var potentialName in DerivePotentialSelectionNames(baseName))
    {
        var propertyInfo = viewModelType.GetPropertyCaseInsensitive(potentialName);
        if (propertyInfo != null)
        {
            var propertyType = propertyInfo.PropertyType;
            if (UpdateSelection.IsSupported(propertyType))
            {
                var selectionPath = path.Replace(baseName, potentialName);

                var triggers = Interaction.GetTriggers(element);
                var trigger = new EventTrigger("SelectionChanged");
                trigger.Actions.Add(new UpdateSelection(selectionPath, propertyType));

                triggers.Add(trigger);
                IoC.Get<ILog>().Info("SelectedItems convention applied to {0}.", element.Name);
                return;
            }
        }
    }
}

/// <summary>
///   Derives the SelectedItem property name.
/// </summary>
private static IEnumerable<string> DerivePotentialSelectionNames(string name)
{
    return new[] { "Active" + name, "Selected" + name, "Current"+ name };
}

Note the UpdateSelection object: it is a custom TriggerAction, and it is the core of the functionality:

/// <summary>
///   Class used to define a trigger action used to update the view-model selection.
/// </summary>
private class UpdateSelection : TriggerAction<ListBox>, IWeakEventListener
{
    #region Static Fields
    /// <summary>
    ///   The Value dependency property.
    /// </summary>
    private static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(object), typeof(UpdateSelection), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnValuePropertyChanged)));
    #endregion

    #region Static Members
    /// <summary>
    ///   Called when <see cref = "ValueProperty" /> has changed.
    /// </summary>
    /// <param name = "obj">The dependency object.</param>
    /// <param name = "e">The <see cref = "System.Windows.DependencyPropertyChangedEventArgs" /> instance containing the event data.</param>
    private static void OnValuePropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
    {
        var oldValue = e.OldValue;
        if (oldValue is INotifyCollectionChanged)
            CollectionChangedEventManager.RemoveListener((INotifyCollectionChanged)oldValue, (UpdateSelection)obj);

        var newValue = e.NewValue;
        if (newValue is INotifyCollectionChanged)
            CollectionChangedEventManager.AddListener((INotifyCollectionChanged)newValue, (UpdateSelection)obj);
    }

    /// <summary>
    ///   The method used to add an item in the collection.
    /// </summary>
    /// <typeparam name = "TList">The type of the collection.</typeparam>
    /// <typeparam name = "TValue">The type of the item.</typeparam>
    /// <param name = "collection">The collection.</param>
    /// <param name = "item">The item.</param>
    private static void Add<TList, TValue>(object collection, object item) where TList : ICollection<TValue>
    {
        ((TList)collection).Add((TValue)item);
    }

    /// <summary>
    ///   The method used to remove an item from the collection.
    /// </summary>
    /// <typeparam name = "TList">The type of the collection.</typeparam>
    /// <typeparam name = "TValue">The type of the item.</typeparam>
    /// <param name = "collection">The collection.</param>
    /// <param name = "item">The item.</param>
    private static void Remove<TList, TValue>(object collection, object item) where TList : ICollection<TValue>
    {
        ((TList)collection).Remove((TValue)item);
    }

    /// <summary>
    ///   Determines whether the trigger action is supported by the specified collection type.
    /// </summary>
    /// <param name = "collectionType">The type of the collection.</param>
    /// <returns>
    ///   <c>True</c> if the specified collection type is supported by the trigger action; otherwise, <c>false</c>.
    /// </returns>
    public static bool IsSupported(Type collectionType)
    {
        return typeof(IList).IsAssignableFrom(collectionType) || collectionType.IsImplementationOf(typeof(ICollection<>));
    }
    #endregion

    #region Fields
    /// <summary>
    ///   The path used to identify the selected items colleciton.
    /// </summary>
    private readonly string m_Path;

    /// <summary>
    ///   The action used to insert new items.
    /// </summary>
    private Action<object, object> m_Add;

    /// <summary>
    ///   The action used to remove old items.
    /// </summary>
    private Action<object, object> m_Remove;

    /// <summary>
    /// Flag used to determine if the selection update started from the view.
    /// </summary>
    private bool m_IsUpdatingFromView;

    /// <summary>
    /// Flag used to determine if the selection update started from the view-model.
    /// </summary>
    private bool m_IsUpdatingFromViewModel;
    #endregion

    #region Properties
    /// <summary>
    ///   Gets or sets the watched value.
    /// </summary>
    /// <value>The watched value.</value>
    private object Value
    {
        get { return GetValue(ValueProperty); }
    }
    #endregion

    /// <summary>
    ///   Initializes a new instance of the <see cref = "UpdateSelection" /> class.
    /// </summary>
    /// <param name = "path">The path used to identify the view-model property used to store the selection.</param>
    /// <param name = "collectionType">The target collection type.</param>
    public UpdateSelection(string path, Type collectionType)
    {
        m_Path = path;

        CreateActions(collectionType);
    }

    #region IWeakEventListener Members
    /// <summary>
    ///   Receives events from the centralized event manager.
    /// </summary>
    /// <param name = "managerType">The type of the <see cref = "T:System.Windows.WeakEventManager" /> calling this method.</param>
    /// <param name = "sender">Object that originated the event.</param>
    /// <param name = "e">Event data.</param>
    /// <returns>
    ///   <c>True</c> if the listener handled the event. It is considered an error by the <see cref = "T:System.Windows.WeakEventManager" /> handling in WPF�to register a listener for an event that the listener does not handle. Regardless, the method should return false if it receives an event that it does not recognize or handle.
    /// </returns>
    bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
    {
        if (managerType == typeof(CollectionChangedEventManager))
        {
            if (!m_IsUpdatingFromView)
            {
                m_IsUpdatingFromViewModel = true;
                var args = (NotifyCollectionChangedEventArgs)e;
                var list = (IList)AssociatedObject.GetValue(ListBox.SelectedItemsProperty);
                if (args.Action == NotifyCollectionChangedAction.Reset)
                    list.Clear();
                else
                {
                    if (args.OldItems != null)
                    {
                        foreach (var item in args.OldItems)
                            list.Remove(item);
                    }
                    if (args.NewItems != null)
                    {
                        foreach (var item in args.NewItems)
                            list.Add(item);
                    }
                }
                m_IsUpdatingFromViewModel = false;
            }

            return true;
        }

        return false;
    }
    #endregion

    /// <summary>
    ///   Called after the action is attached to an AssociatedObject.
    /// </summary>
    protected override void OnAttached()
    {
        base.OnAttached();

        this.SetBinding(ValueProperty, AssociatedObject, new PropertyPath(string.Format("DataContext.{0}", m_Path)), mode: BindingMode.OneWay);
    }

    /// <summary>
    ///   Called when the action is being detached from its AssociatedObject, but before it has actually occurred.
    /// </summary>
    protected override void OnDetaching()
    {
        base.OnDetaching();

        BindingOperations.ClearBinding(this, ValueProperty);
    }

    /// <summary>
    ///   Invokes the action.
    /// </summary>
    /// <param name = "parameter">The parameter to the action. If the action does not require a parameter, the parameter may be set to a null reference.</param>
    protected override void Invoke(object parameter)
    {
        if (!m_IsUpdatingFromViewModel)
        {
            m_IsUpdatingFromView = true;
            var args = (SelectionChangedEventArgs)parameter;
            var value = Value;

            //Could not retrieve the target property...
            if (value == null || value == DependencyProperty.UnsetValue)
                return;

            if (args.RemovedItems != null)
            {
                foreach (var item in args.RemovedItems)
                    m_Remove(value, item);
            }

            if (args.AddedItems != null)
            {
                foreach (var item in args.AddedItems)
                    m_Add(value, item);
            }
            m_IsUpdatingFromView = false;
        }
    }

    /// <summary>
    ///   Ensures that actions have been created.
    /// </summary>
    /// <param name = "collectionType">The collection type.</param>
    private void CreateActions(Type collectionType)
    {
        if (collectionType.IsImplementationOf(typeof(ICollection<>)))
        {
            m_Add = (Action<object, object>)Delegate.CreateDelegate(typeof(Action<object, object>), GetType().GetMethod("Add", BindingFlags.NonPublic | BindingFlags.Static).MakeGenericMethod(collectionType, collectionType.GetGenericArguments()[0]));
            m_Remove = (Action<object, object>)Delegate.CreateDelegate(typeof(Action<object, object>), GetType().GetMethod("Remove", BindingFlags.NonPublic | BindingFlags.Static).MakeGenericMethod(collectionType, collectionType.GetGenericArguments()[0]));
        }
        else if (typeof(IList).IsAssignableFrom(collectionType))
        {
            m_Add = (list, item) => ((IList)list).Add(item);
            m_Remove = (list, item) => ((IList)list).Remove(item);
        }
        else
            throw new InvalidOperationException(string.Format("Unable to create Add/Remove actions for {0} (unsupported type)", collectionType));
    }
}

The idea behind this TriggerAction is quite simple: as soon as the SelectionChanged event is fired, the action will try to keep the view-model collection and the view collection synchronized. At the same time, as soon as the view-model collection used to store the selection changes, the modification should be propagated to the view.

The view -> view-model interaction is quite simple, since the SelectionChanged event provides a list of added and removed items, but I would like to point out a couple of nice tricks. First of all, the action gets a reference to the view-model collection through a Binding (we can avoid reflection, this way, and we get property change notification as a bonus), moreover we need to create proper delegates to be able to add/remove items to a generic collection. The CreateActions method is responsible for this, and is able to manage both IList and ICollection<T> (note that IList<T> is not an IList, hence the need for the ICollection<T> specific support). To support the generic interface, I am forced to use  a bit of reflection, but the above code should be the fastest possible implementation (compared to DynamicExpression and IL generation).

The view-model ->view interaction, in a similar way, is more or less straight-forward; the only interesting point is the use of a WeakEventManager, to avoid a strong reference between view and view-model (no leakage can happen, since the event is removed as soon as the DataContext of the view is changed, but I prefer to use weak events when interacting directly with the view-model from the view).

The above code uses a couple of nice extensions; the first one is used to set a Binding on a generic DependencyObject in a simple way:

/// <summary>
///   Attaches a binding to this element, based on the provided path and source.
/// </summary>
/// <param name = "target">The target.</param>
/// <param name = "property">The property.</param>
/// <param name = "source">The source of the binding.</param>
/// <param name = "path">The source property path.</param>
/// <param name = "converter">The converter.</param>
/// <param name = "converterParameter">The converter parameter.</param>
/// <param name = "mode">The binding mode.</param>
/// <returns>
///   Records the conditions of the binding. This return value can be useful for error checking.
/// </returns>
public static BindingExpressionBase SetBinding(this DependencyObject target, DependencyProperty property, object source, PropertyPath path, IValueConverter converter = null, object converterParameter = null, BindingMode mode = BindingMode.Default)
{
	var binding = new Binding
				  {
						  Path = path,
						  Source = source,
						  Converter = converter,
						  ConverterParameter = converterParameter,
						  Mode = mode
				  };

	return BindingOperations.SetBinding(target, property, binding);
}

The second one is used to determine whether a collection implements ICollection<T>:

/// <summary>
///   Checks if the current <see cref = "Type" /> is an implementation of the specified one.
/// </summary>
/// <param name = "current">The current type.</param>
/// <param name = "type">The <see cref = "Type" /> to check for implementation.</param>
/// <returns><c>True</c> if the current <see cref = "Type" /> is an implementation of <paramref name = "type" />; otherwise, <c>false</c>.</returns>
/// <remarks>
///   If <paramref name = "type" /> is a generic definition, the method will report <c>true</c> if the current type inherits from a specific implementation of the generic type.
/// </remarks>
public static bool IsImplementationOf(this Type current, Type type)
{
	//Trivial check...
	if (typeof(object).Equals(type))
		return true;

	if (type.IsInterface)
		return current.GetInterfaces().Where(i => i.IsGenericType).Any(t => t.GetGenericTypeDefinition().Equals(type));

	if (type.IsGenericTypeDefinition)
	{
		while (current != null && !typeof(object).Equals(current))
		{
			var typeToCheck = current.IsGenericType ? current.GetGenericTypeDefinition() : current;
			if (typeToCheck.Equals(type))
				return true;

			current = current.BaseType;
		}

		return false;
	}

	return type.IsAssignableFrom(current);
}

Note that the above approach can be used with any object using multiple selection.

A working sample, using the current CM code-base, can be downloaded here.

Please, let me know your thoughts about this.

Coordinator
May 9, 2011 at 5:23 PM

Pretty cool stuff. It's too large for me to add as an ootb convention, but I think it would be another great example of something to go in a conventions project along with Telerik, etc. conventions if anyone wants to start one...

May 18, 2011 at 1:14 AM

BladeWise,

Is this possible for Silverlight.  There is no ListBox.SelectedItemsProperty in Silverlight?

Matt

May 18, 2011 at 1:22 AM

As long as there is a way to retrieve the currently selected items, the approach should work.

For the Silverlight implementation I suppose that the only required modification is

bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
    {
        if (managerType == typeof(CollectionChangedEventManager))
        {
            if (!m_IsUpdatingFromView)
            {
                m_IsUpdatingFromViewModel = true;
                var args = (NotifyCollectionChangedEventArgs)e;
                var list = (IList)((ListBox)AssociatedObject).SelectedItems;
                if (args.Action == NotifyCollectionChangedAction.Reset)
                    list.Clear();
                else
                {
                    if (args.OldItems != null)
                    {
                        foreach (var item in args.OldItems)
                            list.Remove(item);
                    }
                    if (args.NewItems != null)
                    {
                        foreach (var item in args.NewItems)
                            list.Add(item);
                    }
                }
                m_IsUpdatingFromViewModel = false;
            }

            return true;
        }

        return false;
    }

In short, you just need to avoid to use the SelectedItemsProperty dependency property, and use instead the CLR property SelectedItems.

I have not tested it, but I suppose there is no reason for this approach to be inapplicable.

May 18, 2011 at 1:39 AM

BladeWise,

I will give it a shot and let you know.

Thanks,

Matt

May 18, 2011 at 1:57 AM

BladeWise,

Just finished testing and it works beautifully.  I had to make a few minor changes to compile it in Silverlight.

Thanks again,

Matt

May 18, 2011 at 2:01 AM

Nice to know, would you mind posting here the required changes, to benefit other people interested in this convention? :)

May 18, 2011 at 2:44 AM

No problem.  Please refer to the download that BladeWise provides in the first post.

1.  In the Extensions file, please correct the following line from:

public static BindingExpressionBase SetBinding(this DependencyObject target, DependencyProperty property, object source, PropertyPath path, IValueConverter converter = null, object converterParameter = null, BindingMode mode = BindingMode.Default)

To

public static BindingExpressionBase SetBinding(this DependencyObject target, DependencyProperty property, object source, PropertyPath path, IValueConverter converter = null, object converterParameter = null, BindingMode mode = BindingMode.TwoWay)

Silverlight does not have a Default BindingMode.

2.  In the Conventions file, please correct the following line in the UpdateSelection class form:

private static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(object), typeof(UpdateSelection), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnValuePropertyChanged)));

To

private static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(object), typeof(UpdateSelection), new PropertyMetadata(null, new PropertyChangedCallback(OnValuePropertyChanged)));

Silverlight does not have a FrameworkPropertyMetadata class.

3.  In the Conventions file, please correct the following line in the UpdateSelection class in the ReceiveWeakEvent from (BladeWise provides this above as well):

var list = (IList)AssociatedObject.GetValue(ListBox.SelectedItemsProperty);

To

var list = (IList)AssociatedObject.SelectedItems;

Silverlight does not have a SelectedItemsProperty object on the ListBox.

 

That's all there is to it!  Thanks again!

May 18, 2011 at 2:46 AM

Thanks to you for taking the time to post th required modifications for Silverlight! ;)

May 19, 2011 at 6:21 AM

Great work. Thanks a lot. This is very valuable for my project.

May 20, 2011 at 3:38 AM

BladeWise,

How about exposing this as a Trigger independent of the convention?  

I am trying but I am having a little trouble with passing in the type information from XAML.  

I have some DataTemplates that I would like to have the same behavior.

Let me know what you think,

Matt

May 20, 2011 at 11:45 PM

I decided to go ahead and create a new class for a stand-alone TriggerAction.  I am using this in DataTemplates in Silverlight

that are using the DataType feature in Silverlight 5 Beta.  The following is a sample of the DataTemplate:

<!-- DataTable DataTemplate -->
<DataTemplate DataType="data:DataTable">
    <Border BorderThickness="1" Background="SteelBlue"
            Canvas.Top="{Binding Top, Mode=TwoWay}"
            Canvas.Left="{Binding Left, Mode=TwoWay}"
        >
        <StackPanel>
            <TextBlock Foreground="White" FontWeight="Bold" Text="{Binding TableName}" Margin="6,2,2,2" />
            <ListBox x:Name="DataColumns" Background="White" 
                        ItemsSource="{Binding DataColumns}" 
                        SelectionMode="Multiple">
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="SelectionChanged">
                        <cc:UpdateSelectionAction Path="QueryTableItems" CollectionTypeName="System.Collections.ObjectModel.ObservableCollection'1[[Dashboard.Data.Models.DataTable, Dashboard.Data, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]], System.Windows, Version=5.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e" />
                    </i:EventTrigger>
                </i:Interaction.Triggers>
            </ListBox>
        </StackPanel>
    </Border>
</DataTemplate>

Here is the code (it is basically exactly what BladWise originally posted but with just a little change):

/// <summary>
///   Class used to define a trigger action used to update the view-model selection.
/// </summary>
public class UpdateSelectionAction : TriggerAction<ListBox>, IWeakEventListener
{
    #region Static Fields
    /// <summary>
    ///   The Value dependency property.
    /// </summary>
    private static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(object), typeof(UpdateSelectionAction), new PropertyMetadata(null, new PropertyChangedCallback(OnValuePropertyChanged)));
    #endregion

    #region Static Members
    /// <summary>
    ///   Called when <see cref = "ValueProperty" /> has changed.
    /// </summary>
    /// <param name = "obj">The dependency object.</param>
    /// <param name = "e">The <see cref = "System.Windows.DependencyPropertyChangedEventArgs" /> instance containing the event data.</param>
    private static void OnValuePropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
    {
        var oldValue = e.OldValue;
        if (oldValue is INotifyCollectionChanged)
            CollectionChangedEventManager.RemoveListener((INotifyCollectionChanged)oldValue, (UpdateSelectionAction)obj);

        var newValue = e.NewValue;
        if (newValue is INotifyCollectionChanged)
            CollectionChangedEventManager.AddListener((INotifyCollectionChanged)newValue, (UpdateSelectionAction)obj);
    }

    /// <summary>
    ///   The method used to add an item in the collection.
    /// </summary>
    /// <typeparam name = "TList">The type of the collection.</typeparam>
    /// <typeparam name = "TValue">The type of the item.</typeparam>
    /// <param name = "collection">The collection.</param>
    /// <param name = "item">The item.</param>
    // ReSharper disable UnusedMember.Local
    private static void Add<TList, TValue>(object collection, object item) where TList : ICollection<TValue> // ReSharper restore UnusedMember.Local
    {
        ((TList)collection).Add((TValue)item);
    }

    /// <summary>
    ///   The method used to remove an item from the collection.
    /// </summary>
    /// <typeparam name = "TList">The type of the collection.</typeparam>
    /// <typeparam name = "TValue">The type of the item.</typeparam>
    /// <param name = "collection">The collection.</param>
    /// <param name = "item">The item.</param>
    // ReSharper disable UnusedMember.Local
    private static void Remove<TList, TValue>(object collection, object item) where TList : ICollection<TValue> // ReSharper restore UnusedMember.Local
    {
        ((TList)collection).Remove((TValue)item);
    }

    /// <summary>
    ///   Determines whether the trigger action is supported by the specified collection type.
    /// </summary>
    /// <param name = "collectionType">The type of the collection.</param>
    /// <returns>
    ///   <c>True</c> if the specified collection type is supported by the trigger action; otherwise, <c>false</c>.
    /// </returns>
    public static bool IsSupported(Type collectionType)
    {
        return typeof(IList).IsAssignableFrom(collectionType) || collectionType.IsImplementationOf(typeof(ICollection<>));
    }
    #endregion

    #region Fields
    /// <summary>
    ///   The path used to identify the selected items colleciton.
    /// </summary>
    private string m_Path;

    /// <summary>
    /// The target collection type
    /// </summary>
    private string m_collectionTypeName;
    private Type m_collectionType;

    /// <summary>
    ///   The action used to insert new items.
    /// </summary>
    private Action<object, object> m_Add;

    /// <summary>
    ///   Flag used to determine if the selection update started from the view.
    /// </summary>
    private bool m_IsUpdatingFromView;

    /// <summary>
    ///   Flag used to determine if the selection update started from the view-model.
    /// </summary>
    private bool m_IsUpdatingFromViewModel;

    /// <summary>
    ///   The action used to remove old items.
    /// </summary>
    private Action<object, object> m_Remove;
    #endregion

    #region Properties
    public string Path
    {
        get { return m_Path; }
        set { m_Path = value; }
    }

    /// <summary>
    /// Gets or sets the type of collection to add notifications.
    /// </summary>
    public string CollectionTypeName
    {
        get { return m_collectionTypeName; }
        set
        {
            m_collectionTypeName = value;
            m_collectionType = Type.GetType(m_collectionTypeName);
        }
    }

    /// <summary>
    ///   Gets or sets the watched value.
    /// </summary>
    /// <value>The watched value.</value>
    private object Value
    {
        get { return GetValue(ValueProperty); }
    }
    #endregion


    ///// <summary>
    /////   Initializes a new instance of the <see cref = "UpdateSelection" /> class.
    ///// </summary>
    public UpdateSelectionAction() { }

    #region IWeakEventListener Members
    /// <summary>
    ///   Receives events from the centralized event manager.
    /// </summary>
    /// <param name = "managerType">The type of the <see cref = "T:System.Windows.WeakEventManager" /> calling this method.</param>
    /// <param name = "sender">Object that originated the event.</param>
    /// <param name = "e">Event data.</param>
    /// <returns>
    ///   <c>True</c> if the listener handled the event. It is considered an error by the <see cref = "T:System.Windows.WeakEventManager" /> handling in WPF�to register a listener for an event that the listener does not handle. Regardless, the method should return false if it receives an event that it does not recognize or handle.
    /// </returns>
    bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
    {
        if (managerType == typeof(CollectionChangedEventManager))
        {
            if (!m_IsUpdatingFromView)
            {
                m_IsUpdatingFromViewModel = true;
                var args = (NotifyCollectionChangedEventArgs)e;
                var list = (IList)AssociatedObject.SelectedItems;
                if (args.Action == NotifyCollectionChangedAction.Reset)
                    list.Clear();
                else
                {
                    if (args.OldItems != null)
                    {
                        foreach (var item in args.OldItems)
                            list.Remove(item);
                    }
                    if (args.NewItems != null)
                    {
                        foreach (var item in args.NewItems)
                            list.Add(item);
                    }
                }
                m_IsUpdatingFromViewModel = false;
            }

            return true;
        }

        return false;
    }
    #endregion

    /// <summary>
    ///   Called after the action is attached to an AssociatedObject.
    /// </summary>
    protected override void OnAttached()
    {
        base.OnAttached();

        CreateActions(m_collectionType);
        this.SetBinding(ValueProperty, AssociatedObject, new PropertyPath(string.Format("DataContext.{0}", m_Path)), mode: BindingMode.OneWay);
    }

    /// <summary>
    ///   Called when the action is being detached from its AssociatedObject, but before it has actually occurred.
    /// </summary>
    protected override void OnDetaching()
    {
        base.OnDetaching();
        //this.AssociatedObject.Loaded -= AssociatedObject_Loaded;

        this.ClearValue(ValueProperty);
        // The following is the WPF version.
        //BindingOperations.ClearBinding(this, ValueProperty);
    }

    /// <summary>
    ///   Invokes the action.
    /// </summary>
    /// <param name = "parameter">The parameter to the action. If the action does not require a parameter, the parameter may be set to a null reference.</param>
    protected override void Invoke(object parameter)
    {
        if (!m_IsUpdatingFromViewModel)
        {
            m_IsUpdatingFromView = true;
            var args = (SelectionChangedEventArgs)parameter;
            var value = Value;

            //Could not retrieve the target property...
            if (value == null || value == DependencyProperty.UnsetValue)
                return;

            if (args.RemovedItems != null)
            {
                foreach (var item in args.RemovedItems)
                    m_Remove(value, item);
            }

            if (args.AddedItems != null)
            {
                foreach (var item in args.AddedItems)
                    m_Add(value, item);
            }
            m_IsUpdatingFromView = false;
        }
    }

    /// <summary>
    ///   Ensures that actions have been created.
    /// </summary>
    /// <param name = "collectionType">The collection type.</param>
    private void CreateActions(Type collectionType)
    {
        if (typeof(IList).IsAssignableFrom(collectionType))
        {
            m_Add = (list, item) => ((IList)list).Add(item);
            m_Remove = (list, item) => ((IList)list).Remove(item);
        }
        else if (collectionType.IsImplementationOf(typeof(ICollection<>)))
        {
            m_Add = (Action<object, object>)Delegate.CreateDelegate(typeof(Action<object, object>), GetType().GetMethod("Add", BindingFlags.NonPublic | BindingFlags.Static).MakeGenericMethod(collectionType, collectionType.GetGenericArguments()[0]));
            m_Remove = (Action<object, object>)Delegate.CreateDelegate(typeof(Action<object, object>), GetType().GetMethod("Remove", BindingFlags.NonPublic | BindingFlags.Static).MakeGenericMethod(collectionType, collectionType.GetGenericArguments()[0]));
        }
        else
            throw new InvalidOperationException(string.Format("Unable to create Add/Remove actions for {0} (unsupported type)", collectionType));
    }
};

As you can see, I added a new member called, m_collectionTypeName.  I also have a property that wraps this variable and

when it is setting, it also tries to obtain the type using the Type.GetType(...) method.  The only other difference is I had 

to provide a default constructor due to the requirements of XAML.

Hope this helps anyone else!  Also, if there is a better way to do this in DataTemplates I would love to know!

May 21, 2011 at 3:26 PM

Glad that you found a solution yourself, and sorry for the late answer! :)

Anyway, if you want to expose the trigger publicly, I would make the following changes:

  1. Convert Path into a PropertyPath object: using a string can be quite limitative, and there is no real reason for doing so. In my code, the path is taken directly from a naming convention, so it was 'natural' to use a string, but when dealing with a public property, it is better to use a property path (to bind to a sub-property, for example).
  2. Convert all public properties to dependency properties: allowing binding is a nice idea in this case.
  3. Handle property changed events properly: if the Path is not constant in the lifetime of the trigger, you need to handle the change and modify the trigger logic accordingly.
  4. Avoid to pass the collection type: there is no need to specify the collection type directly. Again, I did so since the Type was known beforehand, but if the trigger has to be used outside the convention context, it is better to let it retrieve the collection type by itself. 

Below is my proposed implementation of the trigger; note that I changed the name from UpdateSelection to SynchronizeSelection (it seemed clearer to me).

 

namespace CustomConventions
{
    #region Namespaces
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Collections.Specialized;
    using System.Reflection;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Interactivity;

    #endregion

    /// <summary>
    ///   Class used to define a trigger action used to synchronize the view-model selection.
    /// </summary>
    public class SynchronizeSelection : TriggerAction<ListBox>, IWeakEventListener
    {
        #region Static Fields
        /// <summary>
        ///   The Value dependency property.
        /// </summary>
        private static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(object), typeof(SynchronizeSelection), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnValuePropertyChanged)));

        /// <summary>
        ///   The Path dependency property.
        /// </summary>
        public static readonly DependencyProperty PathProperty = DependencyProperty.Register("Path", typeof(PropertyPath), typeof(SynchronizeSelection), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnPathChanged)));
        #endregion

        #region Static Members
        /// <summary>
        ///   Handles changes to the Path property.
        /// </summary>
        /// <param name = "obj">The dependency object.</param>
        /// <param name = "e">The <see cref = "System.Windows.DependencyPropertyChangedEventArgs" /> instance containing the event data.</param>
        private static void OnPathChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            ((SynchronizeSelection)obj).OnPathChanged(e);
        }

        /// <summary>
        ///   Called when <see cref = "ValueProperty" /> has changed.
        /// </summary>
        /// <param name = "obj">The dependency object.</param>
        /// <param name = "e">The <see cref = "System.Windows.DependencyPropertyChangedEventArgs" /> instance containing the event data.</param>
        private static void OnValuePropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            var oldValue = e.OldValue;
            if (oldValue is INotifyCollectionChanged)
                CollectionChangedEventManager.RemoveListener((INotifyCollectionChanged)oldValue, (SynchronizeSelection)obj);

            var newValue = e.NewValue;
            if (newValue is INotifyCollectionChanged)
                CollectionChangedEventManager.AddListener((INotifyCollectionChanged)newValue, (SynchronizeSelection)obj);

            //The collection type could be changed... update actions...
            ((SynchronizeSelection)obj).CreateActions();
        }

        /// <summary>
        ///   The method used to add an item in the collection.
        /// </summary>
        /// <typeparam name = "TList">The type of the collection.</typeparam>
        /// <typeparam name = "TValue">The type of the item.</typeparam>
        /// <param name = "collection">The collection.</param>
        /// <param name = "item">The item.</param>
        // ReSharper disable UnusedMember.Local
        private static void Add<TList, TValue>(object collection, object item) where TList : ICollection<TValue> // ReSharper restore UnusedMember.Local
        {
            ((TList)collection).Add((TValue)item);
        }

        /// <summary>
        ///   The method used to remove an item from the collection.
        /// </summary>
        /// <typeparam name = "TList">The type of the collection.</typeparam>
        /// <typeparam name = "TValue">The type of the item.</typeparam>
        /// <param name = "collection">The collection.</param>
        /// <param name = "item">The item.</param>
        // ReSharper disable UnusedMember.Local
        private static void Remove<TList, TValue>(object collection, object item) where TList : ICollection<TValue> // ReSharper restore UnusedMember.Local
        {
            ((TList)collection).Remove((TValue)item);
        }

        /// <summary>
        ///   Determines whether the trigger action is supported by the specified collection type.
        /// </summary>
        /// <param name = "collectionType">The type of the collection.</param>
        /// <returns>
        ///   <c>True</c> if the specified collection type is supported by the trigger action; otherwise, <c>false</c>.
        /// </returns>
        public static bool IsSupported(Type collectionType)
        {
            return typeof(IList).IsAssignableFrom(collectionType) || collectionType.IsImplementationOf(typeof(ICollection<>));
        }
        #endregion

        #region Fields
        /// <summary>
        ///   The action used to insert new items.
        /// </summary>
        private Action<object, object> m_Add;

        /// <summary>
        ///   Flag used to determine if the selection update started from the view.
        /// </summary>
        private bool m_IsUpdatingFromView;

        /// <summary>
        ///   Flag used to determine if the selection update started from the view-model.
        /// </summary>
        private bool m_IsUpdatingFromViewModel;

        /// <summary>
        ///   The action used to remove old items.
        /// </summary>
        private Action<object, object> m_Remove;
        #endregion

        #region Properties
        /// <summary>
        ///   Gets or sets the path used to identify the property to be used as a selection backend.
        /// </summary>
        /// <value>The path used to identify the property to be used as a selection backend.</value>
        public PropertyPath Path
        {
            get { return (PropertyPath)GetValue(PathProperty); }
            set { SetValue(PathProperty, value); }
        }

        /// <summary>
        ///   Gets or sets the watched value.
        /// </summary>
        /// <value>The watched value.</value>
        private object Value
        {
            get { return GetValue(ValueProperty); }
        }
        #endregion

        #region IWeakEventListener Members
        /// <summary>
        ///   Receives events from the centralized event manager.
        /// </summary>
        /// <param name = "managerType">The type of the <see cref = "T:System.Windows.WeakEventManager" /> calling this method.</param>
        /// <param name = "sender">Object that originated the event.</param>
        /// <param name = "e">Event data.</param>
        /// <returns>
        ///   <c>True</c> if the listener handled the event. It is considered an error by the <see cref = "T:System.Windows.WeakEventManager" /> handling in WPF�to register a listener for an event that the listener does not handle. Regardless, the method should return false if it receives an event that it does not recognize or handle.
        /// </returns>
        bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
        {
            if (managerType == typeof(CollectionChangedEventManager))
            {
                if (!m_IsUpdatingFromView)
                {
                    m_IsUpdatingFromViewModel = true;
                    var args = (NotifyCollectionChangedEventArgs)e;
                    var list = (IList)AssociatedObject.GetValue(ListBox.SelectedItemsProperty);
                    if (args.Action == NotifyCollectionChangedAction.Reset)
                        list.Clear();
                    else
                    {
                        if (args.OldItems != null)
                        {
                            foreach (var item in args.OldItems)
                                list.Remove(item);
                        }
                        if (args.NewItems != null)
                        {
                            foreach (var item in args.NewItems)
                                list.Add(item);
                        }
                    }
                    m_IsUpdatingFromViewModel = false;
                }

                return true;
            }

            return false;
        }
        #endregion

        /// <summary>
        ///   Provides derived classes an opportunity to handle changes to the Path property.
        /// </summary>
        /// <param name = "e">The <see cref = "System.Windows.DependencyPropertyChangedEventArgs" /> instance containing the event data.</param>
        protected virtual void OnPathChanged(DependencyPropertyChangedEventArgs e)
        {
            this.SetBinding(ValueProperty, AssociatedObject, Path, mode: BindingMode.OneWay);
        }

        /// <summary>
        ///   Called after the action is attached to an AssociatedObject.
        /// </summary>
        protected override void OnAttached()
        {
            base.OnAttached();

            this.SetBinding(ValueProperty, AssociatedObject, Path, mode: BindingMode.OneWay);
        }

        /// <summary>
        ///   Called when the action is being detached from its AssociatedObject, but before it has actually occurred.
        /// </summary>
        protected override void OnDetaching()
        {
            base.OnDetaching();
            
            BindingOperations.ClearBinding(this, ValueProperty);
        }

        /// <summary>
        ///   Invokes the action.
        /// </summary>
        /// <param name = "parameter">The parameter to the action. If the action does not require a parameter, the parameter may be set to a null reference.</param>
        protected override void Invoke(object parameter)
        {
            if (!m_IsUpdatingFromViewModel)
            {
                m_IsUpdatingFromView = true;
                var args = (SelectionChangedEventArgs)parameter;
                var value = Value;

                //Could not retrieve the target property...
                if (value == null || value == DependencyProperty.UnsetValue)
                    return;

                if (args.RemovedItems != null)
                {
                    foreach (var item in args.RemovedItems)
                        m_Remove(value, item);
                }

                if (args.AddedItems != null)
                {
                    foreach (var item in args.AddedItems)
                        m_Add(value, item);
                }
                m_IsUpdatingFromView = false;
            }
        }

        /// <summary>
        ///   Ensures that actions have been created.
        /// </summary>
        private void CreateActions()
        {
            var collectionType = Value != null ? Value.GetType() : null;
            if (collectionType == null)
            {
                m_Add = (list, item) => { };
                m_Remove = (list, item) => { };
            }
            else if (typeof(IList).IsAssignableFrom(collectionType))
            {
                m_Add = (list, item) => ((IList)list).Add(item);
                m_Remove = (list, item) => ((IList)list).Remove(item);
            }
            else if (collectionType.IsImplementationOf(typeof(ICollection<>)))
            {
                m_Add = (Action<object, object>)Delegate.CreateDelegate(typeof(Action<object, object>), GetType().GetMethod("Add", BindingFlags.NonPublic | BindingFlags.Static).MakeGenericMethod(collectionType, collectionType.GetGenericArguments()[0]));
                m_Remove = (Action<object, object>)Delegate.CreateDelegate(typeof(Action<object, object>), GetType().GetMethod("Remove", BindingFlags.NonPublic | BindingFlags.Static).MakeGenericMethod(collectionType, collectionType.GetGenericArguments()[0]));
            }
            else
                throw new InvalidOperationException(string.Format("Unable to create Add/Remove actions for {0} (unsupported type)", collectionType));
        }
    }
}

 

 

I have modified the previous example to use the SynchronizeSelection, moreover I added a sample where the Trigger is used directly.

You can download it here.

May 21, 2011 at 11:49 PM

BladeWise,

I like what you have there but unfortunately for what I need it won't work.  The main reason for wanting a separate

TriggerAction is to accomodate typed DataTemplates.  The issue I have is that when working with DataTemplates,

the OnAttached method fires before we have a DataContext or are part of the VisualTree.  Thus, we need to provide

type information.  With inline markup, this isn't a problem but for DataTemplates I haven't found a more elegant

solution.  I still prefer what you have here for the Convention with inline markup but I believe I am forced to 

provide type information with DataTemplates.  I also believe that this is true for both Silverlight and WPF.

Please correct me if I am wrong.  

Thanks again for what you provided as I will use it in my standard conventions and what I posted earlier

for DataTemplates.

Regards,

Matt

May 22, 2011 at 1:01 AM

Matt, do you think the provided solution would not work with DataTemplates? In the last sample I used the trigger explicitly, without convention, and it was working fine (the code is exaclt as yours, except for the missing collection type, but it is not used inside a datatemplate... even if it should not be an issue).

I simply modified the trigger to use dependency properties and I removed the collection type information, since it was retrieved every time the associated collection changed (I still use the type, simply there is no reason to hardcode it).

I have no real complaints regarding your implementation, but I still prefer to use DPs.

Regarding the fact that OnAttached is fired before the DataContext is attached, it is not an issue, as long as the AssociatedObject is available. Once the binding over ValueProperty is set, if the DataContext is changed, the actions are updated. Moreover, every time the Path itself is changed, the Binding is updated as needed.

Can you provide a scenario where the latest implementation is not working?

May 22, 2011 at 3:38 AM

Blade, the current scenario that I have is using typed DataTemplates in Silveright.  The latest sample you provided will not

work in Silverlight.  I need to test it also in WPF for typed DataTemplates.  The reason that I don't believe it will work is that 

in the tests that I did in Silverlight, the OnAttached method is called before the actual template is part of the VisualTree

and, as such, does not have a DataContext.  I will try and create you a sample for you to look at as well.  I am surprised

that WPF does work if this is the behavior of Silverlight but I did have to do some slight modifications in order for your 

code to work in Silverlight.  I will try and get you a sample tomorrow.

Matt

May 22, 2011 at 10:54 AM

I still think that it should work even if the DataContext is set after the OnAttached method is called.

Consider these scenarios:

  1. The DataContext is set before the OnAttached is called: as soon as the element is attached, the binding is set, a valid value is retrieved and OnValuePropertyChanged is called, causing the Add/Remove actions to be created and events to be linked.
  2. The DataContext is set after the OnAttached is called: as soon as the element is attached, the binding is set and evaluates to an invalid value (either null or DependencyProperty.UnsetValue); later on the DataContext is set and the binding over the ValueProperty kicks in, providing a valid value and sforcing Add/Remove actions to be created and events to be linked.

Now, the only real issue I can see with the current implementation, is that if a selection is still in place (either on the view or in the view-model) before attaching, it is not properly synchronized. I already have a couple of solutions for this, and just need to time to code it down.

Anyway, I would love an example from your side, so that I can experiment a bit with both WPF or Silverlight approaches.

Aug 29, 2011 at 1:47 PM

Could you reupload the example code once again? Posted link to the example is not valid.

Aug 29, 2011 at 2:14 PM

The link is working for me, are you sure it is not an issue related to a proxy or something like that?

Aug 29, 2011 at 2:39 PM

The link also worked for me as well.  You might want to try again in another browser as well.

Dec 13, 2011 at 6:37 PM

mkduffi -

I'm a bit lost as to how you were able to get this working in Silverlight in the manner you indicated.  It appears to me that IWeakEventListener and CollectionChangedEventManager aren't available in Silverlight.  What am I missing?

IWeakEventListener 
Oct 16, 2012 at 4:50 PM

I was unable to get this working in Silverlight 5.  I've been getting "Failed to create a 'System.Windows.PropertyPath'".  Does this have anything to do with the OnAttached issue written of earlier?  Has anybody been successful in implementing this in Silverlight?

Nov 4, 2012 at 3:42 PM

this is rude!

Mar 16, 2013 at 9:33 PM
BladeWise wrote:
Note that the above approach can be used with any object using multiple selection. A working sample, using the current CM code-base, can be downloaded here. Please, let me know your thoughts about this.
The link is now truly dead. Any chance you could reupload it?
Apr 29, 2013 at 12:40 PM
I have updated the sample to the latest NuGet package and re-uploded it.
It is possible to download it here.
Sep 3, 2013 at 1:43 AM
Edited Sep 3, 2013 at 1:45 AM
Bladewise,

I am attempting to use your SynchronizeSelection code but I am having a few problems. I have got the SynchronizeSelection.cs, Extensions.cs and Conventions.cs in my code and I have added the Conventions.Apply() to my bootstrapper Configure method as per your sample.

I have the following in the XAML
    <ListBox x:Name="IOSNames" Height="60" SelectionMode="Multiple"/>
    <ScrollViewer  ScrollViewer.VerticalScrollBarVisibility="Auto">
                <ItemsControl ItemsSource="{Binding Path=SelectedIOSNames}"/>
    </ScrollViewer>
and code in the VM
    private BindableCollection<string> selectedIOS;
    public BindableCollection<string> SelectedIOSNames
        {
            get { return selectedIOS; }
            set
            {
                selectedIOS = value;
                NotifyOfPropertyChange(() => SelectedIOSNames);
            }
        }
When I select one or more items, they appear in the scrollviewer itemscontrol correctly but a breakpoint on the SelectedIOSNames setter never gets hit. I tried using the EventTrigger method from your sample as well and the same happens.

I removed the SelectionMode and added a SelectedIOSName property instead to my code with similar setter/getter. A breakpoint on this property gets hits correctly when I select an item. I need the multiple selection to call the VM code so that I can dynamically fetch data for another ListBox which depends on the selected values.

Do you have any idea why the multiple selection does not work?

The logger shows:

SelectedItems convention applied to IOSNames.
Binding Convention Applied: Element IOSNames.

Regards
Alan
Sep 3, 2013 at 7:26 AM
The above code does not replace the collection, so a setter on SelectedIOSNames is not needed.
The convention will actually update the contents of the list, so you need to monitor CollectionChanges over SelectedIOSNames to monitor changes, and not the property changes.
Sep 3, 2013 at 8:51 PM
Hi Bladewise,

Thanks for the swift response.

I didn't realise that there was a CollectionChanged event that I needed to hook into - it wasn't used in the sample code and wasn't that obvious for someone learning WPF. I mistakenly thought that it extended the normal CM conventions and that a SelectedIOSNames setter would get automagically called when the collection changed (just like when using a single selection calls SelectedIOSName) allowing me to do useful stuff.

I have added a SelectedIOSNames.CollectionChanged event handler instead - I hope this is the right way to go.

Regards
Alan
Sep 3, 2013 at 11:12 PM
Bladewise,

I have one other question which has got me a little stumped. I am trying to get items selected when the View loads. So in your example code I added the following line to the ShellViewModel constructor
 SelectedItems.Add(Items.First());
When I run this code, I see that Arabic (which is the first item in the list) appears in the right hand top panel BUT the Arabic line on the left hand side is not highlighted as being selected. I am probably doing something stupid - could you please enlighten me once more.

Thanks
Alan
Sep 3, 2013 at 11:15 PM
Edited Sep 3, 2013 at 11:46 PM
I added an override for OnActivate and put the line in there. That seems to work in your example but unfortunately it doesn't work in my environment because I am within a document pane in Excel.

I am using the non-generic bootstrapper and could only get the Shell to show as per my other post.

My views use ImportingConstructor. For some reason OnActivate is not being called, so I'm a bit stuck now on how to get the item to be selected when the View loads.

Thanks
Alan
Feb 7, 2014 at 8:51 PM
Edited Feb 7, 2014 at 8:56 PM
Just as a note to anyone who might run into this issue. NotifyCollectionChangedAction.Reset is not a Clear so the following piece in UpdateSelection.ReceiveWeakEvent`:
if (args.Action == NotifyCollectionChangedAction.Reset)
{
    list.Clear();
}
should be changed to:
if (args.Action == NotifyCollectionChangedAction.Reset)
{
    list.Clear();
    if (sender != null)
        foreach (var item in (IList)sender)
            list.Add(item);
}
This is only really noticeable when doing an AddRange or RemoveRange in BindableCollection or any other observable collection that uses Reset for changes other than Clear.
The added items are not available on args.NewItems so that's not an option in this case.

edit: added null check even though it's arguable that an exception should be throw if sender is null, take your pick.
Mar 12, 2015 at 10:30 PM
I have been using pmacnaughton modifications in my application (which is a WPF pane inside an Excel document customization). My users are reporting that the whole screen flickers in Excel 2010 when I update a large collection. In Excel 2013, I can see the selected cell flickers whilst the selected items are updated.

My Items/SelectedItems are bound to a ListBox (with a Checkbox). When I populate my Items collection (BindableCollection), I set IsNotifying to false, then I perform an AddRange before setting IsNotifying to true and calling Refresh. After setting my Items, I populate the SelectedItems (BindableCollection) using a similar method. It is this second action which causes the flicker - if I comment out that line, then no flicker.

I have tracked this down to the 'list.Add(item);' line from the above modifications - if I don't do this then again no flicker.
Is there a way to stop the screen flicker when binding Items/SelectedItems to a ListBox.