Posts mit dem Label SelectedItems werden angezeigt. Alle Posts anzeigen
Posts mit dem Label SelectedItems werden angezeigt. Alle Posts anzeigen

Montag, 21. November 2011

Binding to the SelectedItems of ListBox using attached properties

In the last post I wrote about how to extend the ListBox to enable a DataBinding to the SelectedItems property.
It is not always possible to extend a control, so I took another look at the post by Marlon Grech where he uses a attached property to do the trick. I merged the two examples and resulted with a attached property that can handle a two way binding.

The only thing with this approach is that the collection, that the SelectedItems bind to, has to be created on initialization of the ViewModel

Here is the code to the attached property:
 public class ListBoxHelper  
 {  
    #region SelectedItems  
   
    ///  
    /// SelectedItems Attached Dependency Property  
    ///  
    public static readonly DependencyProperty SelectedItemsProperty =  
       DependencyProperty.RegisterAttached("SelectedItems", typeof (IList), 
                          typeof (ListBoxHelper),  
                          new FrameworkPropertyMetadata((IList) null,  
                                       new PropertyChangedCallback(OnSelectedItemsChanged)));  
   
    /// <summary>  
    /// Gets the SelectedItems property.  
    /// </summary>  
    /// <param name="d"></param>  
    /// <returns></returns>  
    public static IList GetSelectedItems(DependencyObject d)  
    {  
       return (IList) d.GetValue(SelectedItemsProperty);  
    }  
   
    /// <summary>  
    /// Sets the SelectedItems property.  
    /// </summary>  
    /// <param name="d"></param>  
    /// <param name="value"></param>  
    public static void SetSelectedItems(DependencyObject d, IList value)  
    {  
       d.SetValue(SelectedItemsProperty, value);  
    }  
   
    /// <summary>  
    /// Called when SelectedItems is set  
    /// </summary>  
    /// <param name="d"></param>  
    /// <param name="e"></param>  
    private static void OnSelectedItemsChanged(DependencyObject d, 
                                             DependencyPropertyChangedEventArgs e)  
    {  
       var listBox = (ListBox) d;  
       SetInternalSelectedItemsToPublic(listBox);  
       IList selectedItems = GetSelectedItems(listBox);  
   
       if (selectedItems is ObservableCollection<object>)  
       {  
          // if the list is a observable collection binde to the changed event 
          // to update the internal collection
          (selectedItems as ObservableCollection<object>).CollectionChanged += delegate  
                             {  
                                        SetSelectedDataItemsToInternal(listBox);  
                             };  
       }  
   
       listBox.SelectionChanged += delegate  
                      {  
                               SetInternalSelectedItemsToPublic(listBox);  
                      };  
    }  
   
   
    #region Implementation  
   
    static bool _isChanging;  
   
    private static void SetSelectedDataItemsToInternal(ListBox listBox)  
    {  
       if (_isChanging)  
          return;  
   
       _isChanging = true;  
   
       var selectedDataItems = GetSelectedItems(listBox);  
   
       if (listBox.SelectedItems != null)  
       {  
          listBox.SelectedItems.Clear();  
   
          if (selectedDataItems == null)  
          {  
             _isChanging = false;  
             return;  
          }  
   
          foreach (var item in selectedDataItems)  
             listBox.SelectedItems.Add(item);  
       }  
   
       _isChanging = false;  
    }  
   
    private static void SetInternalSelectedItemsToPublic(ListBox listBox)  
    {  
       if (_isChanging)  
          return;  
   
       _isChanging = true;  
   
       var selectedDataItems = GetSelectedItems(listBox);  
   
       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 (listBox.SelectedItems != null)  
       {  
          foreach (var item in listBox.SelectedItems)  
             selectedDataItems.Add(item);  
       }  
   
       _isChanging = false;  
    }  
         
    #endregion  
   
    #endregion  
 }  

And here is the XAML with the binding:
<ListBox ItemsSource="{Binding ListItems}" 
   c:ListBoxHelper.SelectedItems="{Binding SlectedListItems}" 
   SelectionMode="Multiple"/>

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"/>