Sonntag, 20. November 2011

Binding to the SelectedItems of ListBox by extending the ListBox

A short while ago I needed to get access to the SelectedItems with DataBinding in my MVVM implementation. The SelectedItems property in the ListBox is read only and so there is no way to have a Binding to it. While searching for a solution I stumbled upon this post by Marlon Grech.
He explains how to create a bindable property for the SelectedItems using attached properties.

The problem with his example is that the binding only works one way when the user selects items in the ListBox. I needed to have a way where I could select items from the ViewModel as well.
The other thing was that I already had an extension of the ListBox.

So instead of creating an attached property I simply took the example and extended the ListBox with the SelectedDataItems property.

Here is the code to the extended ListBox
 public class ListBoxExt : ListBox  
 {  
    static ListBoxExt()  
    {  
       DefaultStyleKeyProperty.OverrideMetadata(typeof (ListBoxExt), 
           new FrameworkPropertyMetadata(typeof (ListBoxExt)));  
    }  
   
    protected override void OnSelectionChanged(SelectionChangedEventArgs e)  
    {  
       SetInternalSelectedItemsToPublic();  
   
       base.OnSelectionChanged(e);  
    }  
   
    #region SelectedDataItems  
   
    public static readonly DependencyProperty SelectedDataItemsProperty = 
       DependencyProperty.Register(  
       "SelectedDataItems",   
       typeof(IList),   
       typeof(ListBoxExt),  
       new FrameworkPropertyMetadata(  
          (IList) null,   
          FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,  
          new PropertyChangedCallback(OnSelectedDataItemsPropertyChangedCallback)));  
   
    /// <summary>  
    /// Gets an IList containing all selected items  
    /// </summary>  
    public IList SelectedDataItems  
    {  
       get  
       {  
          return (IList) GetValue(SelectedDataItemsProperty);  
       }  
       set  
       {  
          SetValue(SelectedDataItemsProperty, value);  
       }  
    }  
   
   
    /// <summary>  
    /// Handles changes to the SelectedDataItems property.  
    /// </summary>  
    private static void OnSelectedDataItemsPropertyChangedCallback(
            DependencyObject d, 
            DependencyPropertyChangedEventArgs e)  
    {  
       var listBox = d as ListBoxExt;  
       if (listBox == null)  
          return;  
   
       listBox.SetSelectedDataItemsCollection(e.NewValue as IList);  
    }  
   
    private void SetSelectedDataItemsCollection(IList selection)  
    {  
       if (_isChanging)  
          return;  
   
       SetSelectedDataItemsToInternal(selection);  
   
       if (selection != null && selection is ObservableCollection<object>)  
       {  
          // add a eventlistner to keep a two way binding  
          (selection as ObservableCollection<object>).CollectionChanged += 
                new System.Collections.Specialized.NotifyCollectionChangedEventHandler(
                                                BoundSelectedDataItemsCollectionChanged);  
       }  
    }  
   
    #region Implementation  
   
    bool _isChanging;  
   
    private void SetSelectedDataItemsToInternal(IList selectedDataItems)  
    {  
       if (_isChanging)  
          return;  
   
       _isChanging = true;  
   
       if (SelectedItems != null)  
       {  
          SelectedItems.Clear();  
   
          if (selectedDataItems == null)  
          {  
             _isChanging = false;  
             return;  
          }  
   
          foreach (var item in selectedDataItems)  
             SelectedItems.Add(item);  
       }  
   
       _isChanging = false;  
    }  
   
    private void SetInternalSelectedItemsToPublic()  
    {  
       if (_isChanging)  
          return;  
   
       _isChanging = true;  
   
       if (SelectedDataItems == null)  
       {  
          var selection = new ObservableCollection<object>();  
          selection.CollectionChanged += 
               new System.Collections.Specialized.NotifyCollectionChangedEventHandler(
                                              BoundSelectedDataItemsCollectionChanged);  
          SelectedDataItems = selection;  
       }  
   
       if (SelectedDataItems == null)  
       {  
          // the bound data type is not of typ IList / IList<object> / 
          // IEnumerable / Ienumerable<object>  
          // in this case the bound property has to be initialized first by the caller  
          _isChanging = false;  
          return;  
       }  
   
       SelectedDataItems.Clear();  
   
       if (base.SelectedItems != null)  
       {  
          foreach (var item in base.SelectedItems)  
             SelectedDataItems.Add(item);  
       }  
   
       _isChanging = false;  
    }  
   
    /// <summary>  
    /// gets executed when the bound collection is of type ObservableCollection 
    /// and the collection gets changed  
    /// </summary>  
    /// <param name="sender"></param>  
    /// <param name="e"></param>  
    void BoundSelectedDataItemsCollectionChanged(object sender, 
                        System.Collections.Specialized.NotifyCollectionChangedEventArgs e)  
    {  
       if (_isChanging)  
          return;  
   
       if (SelectedDataItems == null)  
          return;  
       SetSelectedDataItemsToInternal(SelectedDataItems);  
    }  
   
    #endregion  
   
    #endregion  
 }  


To be able to create a two way binding, the property for the SelectedDataItems has to be IList, IList<object>, IEnumerable or IEnumerable<object>. If a concrete type is used in the generic enumerations the binding will only work one way to the list.
Here is the XAML with the bindings:
<c:ListBoxExt ItemsSource="{Binding ListItems}" 
    SelectedDataItems="{Binding SlectedListItems}" 
    SelectionMode="Multiple"/>

Keine Kommentare: