Donnerstag, 12. April 2012

Filering in ComboBoxes

To achieve filtering in a ComboBox I simply extended the ComboBox.

If I want to Filter the items in the DropDown list I simply bind to the FilterString property.
This example can even handle wildcards.

public class ComboBoxExt : ComboBox
{

#region Filter

#region Implementation

IList<object> _displayedItems;

/// <summary>
/// refreshes the filtering of the combobox according to the string in
/// <see cref="FilterString"/>
/// </summary>
private void RefreshFilter()
{
    var source = ItemsSource as ICollectionView;
    if (source == null)
        source = Items as ICollectionView;
            
    if (source != null)
    {
        if (_displayedItems == null)
            _displayedItems = new ObservableCollection<object>();
        _displayedItems.Clear();

        source.Filter = new Predicate<object>(Filter);

        if (_displayedItems.Count > 0)
        {
            var focusedElement = Keyboard.FocusedElement;

            SelectedItem = _displayedItems[0];

            if (focusedElement != null)
                focusedElement.Focus();
        }
    }
}

/// <summary>
/// method that gets called by the predicate in the ICollectionView.Filter that
/// filters the entries in the dropdown
/// according to the string in <see cref="FilterString"/>
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
private bool Filter(object obj)
{
    // no filter is set
    if (string.IsNullOrEmpty(FilterString))
        return true;

    // no filterpath is set
    if (string.IsNullOrEmpty(FilterMemberPath))
        return true;

    // get the value from the property out of the bound object
    var value = GetValueFromBindingPath(obj, FilterMemberPath);
    if (value == null)
        return false;

    //
    // matching filterstring and value using regex found on:
    // http://www.codeproject.com/Articles/11556/Converting-Wildcards-to-Regexes
    //

    // accept sql and dos Wildcards
    // convert all sql wildcards to dos wildcards
    var filter = FilterString.Replace("%", "*").Replace("_", "?");

    // convert all wildcards to regex wildcards and add a * to search all prepending chars
    filter = "^" + Regex.Escape(filter).Replace("\\*", ".*").Replace("\\?", ".") + ".*$";
            
    // match the value with the regex filter
    bool isFiltered = Regex.IsMatch(value.ToString(), filter, RegexOptions.IgnoreCase);
            
    if (isFiltered)
        _displayedItems.Add(obj);

    return isFiltered;
}

#endregion

#region DependcyProperties

#region FilterMemberPath

//
// the filtermemeberpath is used to get the property that the filter should be used on
// 

/// <summary>
/// gets or set the object path to the property that is filtered on the bound object
/// </summary>
public string FilterMemberPath
{
    get
    {
        return (string)GetValue(FilterMemberPathProperty);
    }
    set
    {
        SetValue(FilterMemberPathProperty, value);
    }
}

/// <summary>
/// the dependencyproperty for the <see cref="FilterMemberPath"/> property
/// </summary>
public static readonly DependencyProperty FilterMemberPathProperty =
    DependencyProperty.Register("FilterMemberPath", typeof(string), typeof(ComboBoxExt), 
      new FrameworkPropertyMetadata(null, 
           new PropertyChangedCallback(OnFilterMemberPathPropertyChanged)));

private static void OnFilterMemberPathPropertyChanged(DependencyObject sender, 
                                        DependencyPropertyChangedEventArgs args)
{
}

#endregion

#region FilterString

/// <summary>
/// gets or sets the string that is used for filtering the combobox
/// </summary>
public string FilterString
{
    get
    {
        return (string)GetValue(FilterStringProperty);
    }
    set
    {
        SetValue(FilterStringProperty, value);
    }
}
        
/// <summary>
/// dependencyproperty for the <see cref="FilterString"/> property
/// </summary>
public static readonly DependencyProperty FilterStringProperty =
    DependencyProperty.Register(
    "FilterString", 
    typeof(string), 
    typeof(ComboBoxExt), 
    new FrameworkPropertyMetadata(
        null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault | 
              FrameworkPropertyMetadataOptions.AffectsRender | 
              FrameworkPropertyMetadataOptions.AffectsParentMeasure,
        new PropertyChangedCallback(OnFilterStringPropertyChanged)));


private static void OnFilterStringPropertyChanged(DependencyObject sender, 
                                    DependencyPropertyChangedEventArgs args)
{
    var dt = sender as ComboBoxExt;
    if (dt == null)
        return;
            
    dt.RefreshFilter();
}

#endregion
        
#endregion

#endregion

#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
}

Keine Kommentare: