Ignacio Soler Garcia
Ignacio Soler Garcia

Reputation: 21855

Filter items of a ListBox based on the text of a TextBox using only XAML in WPF

I currently have a ListBox binded to a collection of items. As the collection is big we want to filter the items being shown based on the text entered on a TextBox.

What I'm asking is if this is possible to implement using only XAML, I don't want to modify the collection of items, I would like to modify the Visibility of each of the items based on the filter.

Hope its clear,

thanks!

Upvotes: 11

Views: 22209

Answers (6)

CodeNaked
CodeNaked

Reputation: 41393

You can use the CollectionViewSource to apply filtering, another example can be found here and here.

Upvotes: 6

Karl_Schuhmann
Karl_Schuhmann

Reputation: 1332

Like CodeNaked and devdigital told you CollectionViewSource/CollectionView/ICollectionView are the keys to your goal

It's a MVVM patter but this is a View only related problem so I don't want this code at the ViewModel.

thats not the right way because the View only shows what she get´s but shouldn´t modifi so it should/must be your ViewModel who handel changes

so now some code snips:

    public class myVM
    {
        public CollectionViewSource CollViewSource { get; set; }
        public string SearchFilter
        {
            get;
            set
            {
              if(!string.IsNullOrEmpty(SearchFilter))
                 AddFilter();

                CollViewSource.View.Refresh(); // important to refresh your View
            }
        }
        public myVM(YourCollection)
        {
            CollViewSource = new CollectionViewSource();//onload of your VM class
            CollViewSource.Source = YourCollection;//after ini YourCollection
        }
    }

Xaml Snip:

    <StackPanel>
        <TextBox Height="23" HorizontalAlignment="Left"  Name="tB" VerticalAlignment="Top" 
                 Width="120" Text="{Binding SearchFilter,UpdateSourceTrigger=PropertyChanged}" />
        <DataGrid Name="testgrid" ItemsSource="{Binding CollViewSource.View}"/>
    </StackPanel>

Edit i forgot the Filter

private void AddFilter()
{
    CollViewSource.Filter -= new FilterEventHandler(Filter);
    CollViewSource.Filter += new FilterEventHandler(Filter);  

}

private void Filter(object sender, FilterEventArgs e)
{
    // see Notes on Filter Methods:
    var src = e.Item as YourCollectionItemTyp;
    if (src == null)
        e.Accepted = false;
    else if ( src.FirstName !=null && !src.FirstName.Contains(SearchFilter))// here is FirstName a Property in my YourCollectionItem
        e.Accepted = false;
}

Upvotes: 8

brunnerh
brunnerh

Reputation: 184506

The only thing XAML really does is encapsulating logic in a declarative fashion. Using markup extensions you can do quite a lot, here's an example:

<StackPanel>
    <StackPanel.Resources>
        <CollectionViewSource x:Key="items" Source="{Binding Data}">
            <CollectionViewSource.Filter>
                <me:Filter>
                    <me:PropertyFilter PropertyName="Name"
                            RegexPattern="{Binding Text, Source={x:Reference filterbox}}" />
                </me:Filter>
            </CollectionViewSource.Filter>
        </CollectionViewSource>
    </StackPanel.Resources>
    <TextBox Name="filterbox" Text="Skeet">
        <TextBox.TextChanged>
            <me:ExecuteActionsHandler ThrowOnException="false">
                <me:CallMethodAction>
                    <me:CallMethodActionSettings MethodName="Refresh"
                            TargetObject="{Binding Source={StaticResource items}}" />
                </me:CallMethodAction>
            </me:ExecuteActionsHandler>
        </TextBox.TextChanged>
    </TextBox>
    <!-- ListView here -->
</StackPanel>

(Note that this works but it will trip every GUI designer, also there is no IntelliSense for the events as they usually are not set via element syntax.)

There are several markup extensions here of which two create handlers and one creates an action:

  • FilterExtension
  • ExecuteActionsHandlerExtension
  • CallMethodActionExtension

The extensions look like this:

[ContentProperty("Filters")]
class FilterExtension : MarkupExtension
{
    private readonly Collection<IFilter> _filters = new Collection<IFilter>();
    public ICollection<IFilter> Filters { get { return _filters; } }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return new FilterEventHandler((s, e) =>
            {
                foreach (var filter in Filters)
                {
                    var res = filter.Filter(e.Item);
                    if (!res)
                    {
                        e.Accepted = false;
                        return;
                    }
                }
                e.Accepted = true;
            });
    }
}

public interface IFilter
{
    bool Filter(object item);
}

Quite straightforward, just loops through filters and applies them. Same goes for the ExecuteActionsHandlerExtension:

[ContentProperty("Actions")]
public class ExecuteActionsHandlerExtension : MarkupExtension
{
    private readonly Collection<Action> _actions = new Collection<Action>();
    public Collection<Action> Actions { get { return _actions; } }

    public bool ThrowOnException { get; set; }

    public ExecuteActionsHandlerExtension()
    {
        ThrowOnException = true;
    }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return new RoutedEventHandler((s, e) =>
            {
                try
                {
                    foreach (var action in Actions)
                    {
                        action.Invoke();
                    }
                }
                catch (Exception)
                {
                    if (ThrowOnException) throw;
                }
            });
    }
}

Now the last extension is a bit more complicated as it actually needs to do something concrete:

[ContentProperty("Settings")]
public class CallMethodActionExtension : MarkupExtension
{
    //Needed to provide dependency properties as MarkupExtensions cannot have any
    public CallMethodActionSettings Settings { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return new Action(() =>
            {
                bool staticCall = Settings.TargetObject == null;
                var argsCast = Settings.MethodArguments.Cast<object>();
                var types = argsCast.Select(x => x.GetType()).ToArray();
                var args = argsCast.ToArray();
                MethodInfo method;
                if (staticCall)
                {
                    method = Settings.TargetType.GetMethod(Settings.MethodName, types);
                }
                else
                {
                    method = Settings.TargetObject.GetType().GetMethod(Settings.MethodName, types);
                }
                method.Invoke(Settings.TargetObject, args);
            });
    }
}

public class CallMethodActionSettings : DependencyObject
{
    public static readonly DependencyProperty MethodNameProperty =
        DependencyProperty.Register("MethodName", typeof(string), typeof(CallMethodActionSettings), new UIPropertyMetadata(null));
    public string MethodName
    {
        get { return (string)GetValue(MethodNameProperty); }
        set { SetValue(MethodNameProperty, value); }
    }

    public static readonly DependencyProperty TargetObjectProperty =
        DependencyProperty.Register("TargetObject", typeof(object), typeof(CallMethodActionSettings), new UIPropertyMetadata(null));
    public object TargetObject
    {
        get { return (object)GetValue(TargetObjectProperty); }
        set { SetValue(TargetObjectProperty, value); }
    }

    public static readonly DependencyProperty TargetTypeProperty =
        DependencyProperty.Register("TargetType", typeof(Type), typeof(CallMethodActionSettings), new UIPropertyMetadata(null));
    public Type TargetType
    {
        get { return (Type)GetValue(TargetTypeProperty); }
        set { SetValue(TargetTypeProperty, value); }
    }

    public static readonly DependencyProperty MethodArgumentsProperty =
        DependencyProperty.Register("MethodArguments", typeof(IList), typeof(CallMethodActionSettings), new UIPropertyMetadata(null));
    public IList MethodArguments
    {
        get { return (IList)GetValue(MethodArgumentsProperty); }
        set { SetValue(MethodArgumentsProperty, value); }
    }

    public CallMethodActionSettings()
    {
        MethodArguments = new List<object>();
    }
}

All of these snippets are just quick drafts to demonstrate how one could approach this. (A draft for the property filter implementation can be found in this answer.)

Upvotes: 2

Cake
Cake

Reputation: 1

Use a data trigger on some property of the item in the collectin and you can do it all in xaml.

Upvotes: 0

Yuriy Zanichkovskyy
Yuriy Zanichkovskyy

Reputation: 1699

There is no way to accomplish this in XAML only. But there are other two ways: 1) using converter

<TextBox x:Name="text"/>
<ListBox Tag="{Binding ElementName=text}">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Visibility" Value="{Binding RelativeSource={RelativeSource AncestorType=ListBox},Path=Tag, Converter={StaticResource filterLogicConverter}}"/>
</Style>
</ListBox.ItemContainerStyle>
<LixtBox/>

2) better and more natural way is to use CollectionView.Filter property. It doesn't modify an underlying collection.

var collectionView = CollectionViewSource.GetDefaultView(your_collection);
collectionView.Filter = filter_predicate

Upvotes: 2

devdigital
devdigital

Reputation: 34349

You can do this with a CollectionViewSource. You wouldn't want to do this completely in XAML, as it would be much easier to test this if the filtering code is in your view model (assuming an MVVM design pattern).

Upvotes: 2

Related Questions