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

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
}