New to Telerik UI for Blazor? Download free 30-day trial

Filter or Search TreeView Items

Environment

Product TreeView for Blazor

Description

This KB article answers the following questions:

  • How to build manual filter function for the Telerik Blazor TreeView?
  • How to filter TreeView items?
  • How to allow users to search in the TreeView items?
  • How to put search box in the Tree View to filter tree records?
  • How to search the TreeView nodes, for example, folders and files?
  • How to highlight the search text in the found TreeView items?

Solution

There are different ways to implement TreeView filtering, depending on the used TreeView data binding.

The suggested approach below relies on flat TreeView data. See this TreeView demo for a hierarchy data filtering example.

In both scenarios, there should be no loading of TreeView items on demand, otherwise the filtering will not provide all possible results.

  1. Add a TextBox for the filter string.
  2. (optional) Add a DropDownList or another suitable component for the filter operator.
  3. Use a Button OnClick event or the TextBox ValueChanged event to trigger the TreeView item search process.
  4. Create a DataSourceRequest object and populate its Filters property with a single FilterDescriptor.
  5. Execute the ToDataSourceResult() extension method on the TreeView Data. You will need to import the Telerik.DataSource.Extensions namespace.
  6. (optional) Add any missing parent items to the filtered items collection.
  7. (optional) Use a TreeView ItemTemplate to highlight the search string inside the displayed TreeView items.

If the filtering operator is fixed (for example, Contains), you can replace steps 4 and 5 with a standard LINQ expression:

var filteredItems = FlatData.Where(x => x.Text.Contains(TreeViewFilterValue)).ToList();

Searching TreeView items with different filter operators

@using Telerik.DataSource
@using Telerik.DataSource.Extensions

<TelerikTextBox Value="@TreeViewFilterValue"
                ValueChanged="@TreeViewFilterValueChanged"
                DebounceDelay="500"
                Placeholder="Type number or letter"
                Width="180px" />

<TelerikDropDownList Data="@StringFilterOperators"
                     TItem="@FilterOperatorItem"
                     TValue="@FilterOperator"
                     TextField="@nameof(FilterOperatorItem.Text)"
                     ValueField="@nameof(FilterOperatorItem.Operator)"
                     Value="@TreeViewFilterOperator"
                     ValueChanged="@TreeViewFilterOperatorChanged"
                     Width="180px" />

<p>@FilterLog</p>

<TelerikTreeView Data="@FilteredData" @bind-ExpandedItems="@ExpandedItems">
    <TreeViewBindings>
        <TreeViewBinding>
            <ItemTemplate>
                @{
                    var item = (TreeItem)context;

                    if (string.IsNullOrEmpty(TreeViewFilterValue) ||
                        TreeViewFilterOperator == FilterOperator.DoesNotContain)
                    {
                        @item.Text
                    }
                    else if (TreeViewFilterOperator == FilterOperator.IsContainedIn &&
                        TreeViewFilterValue.Contains(item.Text))
                    {
                        <strong>@item.Text</strong>
                    }
                    else
                    {
                        @( new MarkupString(item.Text.Replace(
                                TreeViewFilterValue,
                                $"<strong>{TreeViewFilterValue.ToUpper()}</strong>",
                                StringComparison.InvariantCultureIgnoreCase)
                        ) )
                    }
                }
            </ItemTemplate>
        </TreeViewBinding>
    </TreeViewBindings>
</TelerikTreeView>

<style>
    .k-treeview strong {
        color: red;
        background: yellow;
    }
</style>

@code {
    private List<TreeItem> FlatData { get; set; } = new List<TreeItem>();
    private List<TreeItem> FilteredData { get; set; } = new List<TreeItem>();
    private IEnumerable<object> ExpandedItems { get; set; } = new List<TreeItem>();

    private string FilterLog { get; set; } = string.Empty;

    #region Filtering Logic

    private string TreeViewFilterValue { get; set; } = string.Empty;
    private FilterOperator TreeViewFilterOperator { get; set; } = FilterOperator.Contains;

    private List<FilterOperatorItem> StringFilterOperators { get; set; } = new List<FilterOperatorItem>() {
        new FilterOperatorItem() { Operator = FilterOperator.IsEqualTo, Text = "Is Equal To" },
        new FilterOperatorItem() { Operator = FilterOperator.StartsWith, Text = "Starts With" },
        new FilterOperatorItem() { Operator = FilterOperator.Contains, Text = "Contains" },
        new FilterOperatorItem() { Operator = FilterOperator.DoesNotContain, Text = "Does Not Contain" },
        new FilterOperatorItem() { Operator = FilterOperator.EndsWith, Text = "Ends With" },
        new FilterOperatorItem() { Operator = FilterOperator.IsContainedIn, Text = "Is Contained In" }
    };

    private void TreeViewFilterOperatorChanged(FilterOperator newValue)
    {
        TreeViewFilterOperator = newValue;

        if (!string.IsNullOrEmpty(TreeViewFilterValue))
        {
            FilterTreeView();
        }
    }

    private void TreeViewFilterValueChanged(string newValue)
    {
        TreeViewFilterValue = newValue;

        if (!string.IsNullOrEmpty(TreeViewFilterValue))
        {
            FilterTreeView();
        }
        else
        {
            FilterLog = $"Showing all {FlatData.Count} items.";

            FilteredData = FlatData;
        }
    }

    private void FilterTreeView()
    {
        var request = new DataSourceRequest()
        {
            Filters = new List<IFilterDescriptor>() {
                new FilterDescriptor()
                {
                    Member = nameof(TreeItem.Text),
                    MemberType = typeof(string),
                    Operator = TreeViewFilterOperator,
                    Value = TreeViewFilterValue
                }
            }
        };

        var result = FlatData.ToDataSourceResult(request);

        var filteredItems = result.Data.Cast<TreeItem>().ToList();
        var matchCount = filteredItems.Count;

        var addedParents = new List<TreeItem>();

        foreach (var item in filteredItems)
        {
            PopulateParent(item.Id, item.ParentId, filteredItems, addedParents);
        }

        filteredItems.AddRange(addedParents);

        FilterLog = $"Found {matchCount} matches. Showing {filteredItems.Count} out of {FlatData.Count} items.";

        FilteredData = filteredItems;
    }

    private void PopulateParent(int itemId, int? parentId, List<TreeItem> filteredItems, List<TreeItem> addedParents)
    {
        var parentItem = FlatData.FirstOrDefault(x => x.Id == parentId);

        if (parentItem != null)
        {
            if (filteredItems.FindIndex(x => x.Id == parentItem.Id) == -1 &&
                addedParents.FindIndex(x => x.Id == parentItem.Id) == -1)
            {
                addedParents.Add(parentItem);
            }

            if (parentItem.ParentId != null)
            {
                PopulateParent(parentItem.Id, parentItem.ParentId, filteredItems, addedParents);
            }
        }
    }

    #endregion Filtering Logic

    #region Data Generation and Classes

    private int TreeLevels { get; set; } = 3;
    private int RootItems { get; set; } = 3;
    private int ItemsPerLevel { get; set; } = 2;
    private int IdCounter { get; set; } = 1;
    private Random Rnd { get; set; } = new Random();

    protected override void OnInitialized()
    {
        FlatData = FilteredData = LoadFlat();

        ExpandedItems = FlatData.Where(x => x.HasChildren == true);
    }

    private List<TreeItem> LoadFlat()
    {
        List<TreeItem> items = new List<TreeItem>();

        PopulateChildren(items, null, 1);

        return items;
    }

    private void PopulateChildren(List<TreeItem> items, int? parentId, int level)
    {
        var itemCount = level == 1 ? RootItems : ItemsPerLevel;

        for (int i = 1; i <= itemCount; i++)
        {
            var itemId = IdCounter++;

            items.Add(new TreeItem()
            {
                Id = itemId,
                Text = $"{itemId} " +
                    $"{(char)Rnd.Next(65, 91)}{(char)Rnd.Next(65, 91)}{(char)Rnd.Next(65, 91)} " +
                    $"{Rnd.Next(0, 10)}{Rnd.Next(0, 10)}{Rnd.Next(0, 10)}",
                ParentId = parentId,
                HasChildren = level < TreeLevels
            });

            if (level < TreeLevels)
            {
                PopulateChildren(items, itemId, level + 1);
            }
        }
    }

    public class TreeItem
    {
        public int Id { get; set; }
        public string Text { get; set; } = string.Empty;
        public int? ParentId { get; set; }
        public bool HasChildren { get; set; }
    }

    public class FilterOperatorItem
    {
        public FilterOperator Operator { get; set; }
        public string Text { get; set; } = string.Empty;
    }

    #endregion Data Generation and Classes
}

See Also

In this article