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

Donnerstag, 12. April 2012

Binding multiple ComboBoxes to the same collection and using Filters

When multiple ComboBoxes bind to the same collection and the collection uses a Filtering, the items of all ComboBoxes get filtered. Not as expected only the one using the filter. If a ComboBox already has a value selected and another ComboBox uses a filter on the collection, the first ComboBox could lose its value because the SelectedItem is no longer contained in the displayed list.

This hapens because in WPF all Collections contain a default ICollectionView. This view can be retrieved with:

var view = CollectionViewSource.GetDefaultView(collection);


This can easily be handled if each ComboBox binds to a unique ICollectionView. It doesn't matter if the underlying Collection is the same as long as the ICollectionViews differ.


IList<ComboBoxExtDataObject> _dataObjects;
public IList<ComboBoxExtDataObject> DataObjects
{
    get
    {
        if (_dataObjects == null)
        {
            _dataObjects = new ObservableCollection<ComboBoxExtDataObject>();

            for (int i = 1; i <= 50; i++)
                _dataObjects.Add(new ComboBoxExtDataObject { Id = i, Name = "Name" + i });
        }
        return _dataObjects;
    }
}

ICollectionView _sources1;
public ICollectionView Sources1
{
    get
    {
        if (_sources1 == null)
        {
            var lst = new CollectionViewSource();
            lst.Source = _dataObjects;
            _sources1 = lst.View;
        }
        return _sources1;
    }
}

ICollectionView _sources2;
public ICollectionView Sources2
{
    get
    {
        if (_sources2 == null)
        {
            var lst = new CollectionViewSource();
            lst.Source = _dataObjects;
            _sources2 = lst.View;
        }
        return _sources2;
    }
}

In this case I bind the ComboBox1 to Sources1 and the ComboBox2 to Sources2. Now I can easily use the Filtering that I added to my ComboBox without changing the CollectionView of the other ComboBox.

Retrieving the value of a property from a DataBound object

In WPF it's easy to bind a object to a DependencyProperty and can be accessed just like any normal Property. You can even access the Properties of that object and get the values of these if you know what type they are.
But what is if the Binding is of a random type and you would like to access the value of a property on that unknown?

Let's say for example you want to add a Filtering to the ComboBox.
ComboBoxes can bind to any type as long as it implements IEnumerable. But for Filtering you need to know the value of a Property inside the objects in the IEnumerable.

To access the value of a Property on the Bound objects, you need to create a new Binding on the desired Property for each of the objects in the IEnumerable. The thing you need to tell the ComboBox is the name of the Property of which you would like to get the value of. In the example I simply created a DependencyProperty containing the name(path). You could also use the DisplayMemberPath or analyse the Template of the Bound objects.

public bool Filter(object obj)
{
    // get the value from the property out of the bound object
    var value = GetValueFromBindingPath(obj, FilterMemberPath);
    if(value != null)
    {
        if(value.ToString().Contains(FilterString))
           return true;
    }
    return false;
}

/// <summary>
/// the name of the property that the filtering should be made on in the bound objects
/// </summary>
public string FilterMemberPath
{
    get { return (string)GetValue(FilterMemberPathProperty); }
    set { SetValue(FilterMemberPathProperty, value); }
}

public static readonly DependencyProperty FilterMemberPathProperty =
    DependencyProperty.Register("FilterMemberPath", typeof(string), typeof(ComboBoxExt), 
                                new UIPropertyMetadata(null));

#region Value from Binding / Path

// 
// we create a virtual binding and bind to the property from the
// path that was set in the FiltermemberPath property
// with the binding we can easily retreave the object that is
// contained behind the property
//
// tests with over 5000 items have shown 
// that this has no impact on performance
//

/// <summary>
/// gets the object/value from a property from the object according to the path
/// </summary>
/// <param name="obj"></param>
/// <param name="propertyPath"></param>
/// <returns></returns>
public static object GetValueFromBindingPath(object obj, string propertyPath)
{
    Binding binding = new Binding(propertyPath);
    binding.Mode = BindingMode.OneTime;
    binding.Source = obj;
    BindingOperations.SetBinding(_dummy, Dummy.ValueProperty, binding);
    return _dummy.GetValue(Dummy.ValueProperty);
}

// dummy object for getting the value from a path on a object
private static readonly Dummy _dummy = new Dummy();

private class Dummy : DependencyObject
{
    public static readonly DependencyProperty ValueProperty =
        DependencyProperty.Register("Value", typeof(object), typeof(Dummy), 
                                    new UIPropertyMetadata(null));
}

#endregion


This looks like a hack and maybe (most probably) it is a big one.
But it works quite well and its quite fast.

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