0

I'm working on getting a UserControl in WPF working that has a MenuItem populated with an ItemsSource, which creates a menu that goes n levels deep (although I'm just looking at TopMenuItem\Branches\Leaves right now). The wrinkle I'm having trouble with is that I want to filter the leaves through a textbox embedded into the menu. If a branch has no leaves, it also gets filtered out. It looks like this at the moment :

It looks like this at the moment

I'm working with an ObservableCollection of IMenuTreeItem, which can contain branches (which in turn also has an ObservableCollection of IMenuTreeItem) or leaves.

public interface IMenuTreeItem
{
    string Name { get; set; }
}

public class MenuTreeLeaf : IMenuTreeItem
{
    public string Name { get; set; }
    public Guid UID { get; set; }
    public ObjectType Type { get; set; }
    public Requirement Requirement { get; set; }

    public MenuTreeLeaf(string name, ObjectType type, Guid uID)
    {
        Type = type;
        Name = name;
        UID = uID;
    }
    public MenuTreeLeaf(string name)
    {
        Name = name;
    }
}
public class MenuTreeBranch : IMenuTreeItem, INotifyPropertyChanged
{
    public string Name { get; set; }
    private ObservableCollection<IMenuTreeItem> _items;
    public ObservableCollection<IMenuTreeItem> Items
    {
        get
        {
            return _items;
        }
        set
        {
            _items = value; OnPropertyChanged();
        }
    }

    public MenuTreeBranch(string name)
    {
        Name = name;
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string name = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    }
}

This is how I'm filtering. It very much feels like there's a better way.

            ObservableCollection<IMenuTreeItem> result = new ObservableCollection<IMenuTreeItem>(ItemsSource);
            for (int i = 0; i < result.Count; i++)
            {
                if (result[i] is MenuTreeBranch currentBranch)
                {
                    if (currentBranch.Items != null)
                        currentBranch.Items = new ObservableCollection<IMenuTreeItem>(currentBranch.Items.Where(x => x.Name.ToLower().Contains(SearchField.ToLower())));
                }
            }
            result = new ObservableCollection<IMenuTreeItem>(result.Where(x => (x as MenuTreeBranch).Items.Count > 0));
            result.Insert(0, new MenuTreeLeaf("[Search]"));
            return result;

So my main problems are:

  • When I've filtered, I can no longer unfilter. ItemsSource gets changed too. Could it be because I'm filtering in the ItemsSourceFiltered getter? I tried to clone, but eh, didn't change anything
  • When I call OnPropertyChanged on ItemsSourceFiltered any time text changes in the textbox, the menu closes. The menu definitely shouldn't close while you're inputting text.

Any advice?

TontonVelu
  • 471
  • 2
  • 8
  • 21
  • You need something like a private list of allitems. You could filter using a collectionview. There's an observablecollection ctor takes a list. So you could tolist() and new up an observablecollection passing that list. – Andy Jul 27 '20 at 10:24

1 Answers1

0

You may have a menu item class that exposes a recursive Filter string and a collection property that returns the filtered child items:

public class FilteredMenuItem : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public string Name { get; set; }

    public ICommand Command { get; set; }

    private string filter;

    public string Filter
    {
        get { return filter; }
        set
        {
            filter = value;

            foreach (var childItem in ChildItems)
            {
                childItem.Filter = filter;
            }

            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Filter)));
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FilteredChildItems)));
        }
    }

    public List<FilteredMenuItem> ChildItems { get; set; } = new List<FilteredMenuItem>();

    public IEnumerable<FilteredMenuItem> FilteredChildItems
    {
        get { return string.IsNullOrEmpty(Filter)
                ? ChildItems
                : ChildItems.Where(childItem => (bool)childItem.Name?.Contains(Filter)); }
    }
}

With a RootItem property in the view model like

public FilteredMenuItem RootItem { get; }
    = new FilteredMenuItem { Name = "Items" };

you may bind to it in XAML like this:

<StackPanel DataContext="{Binding RootItem}">
    <TextBox Text="{Binding Filter, UpdateSourceTrigger=PropertyChanged}"/>
    <Menu>
        <Menu.Resources>
            <Style TargetType="MenuItem">
                <Setter Property="Header" Value="{Binding Name}"/>
                <Setter Property="Command" Value="{Binding Command}"/>
                <Setter Property="ItemsSource"
                        Value="{Binding FilteredChildItems}"/>
            </Style>
        </Menu.Resources>
        <MenuItem/>
    </Menu>
</StackPanel>

While you populate the ChildItems property of each FilteredMenuItem, the view only shows the FilteredChildItems collection.

You may also notice that the above doesn't use ObservableCollection at all, since no items are added to or removed from any collection at runtime. You just have to make sure the item tree is populated before the view is loaded.

Clemens
  • 123,504
  • 12
  • 155
  • 268
  • Thanks for the answer. I had to restructure some since I need the Textbox as the first item in the Menu (like in my image), and leaves have to be a different class than the branches. It fixes the problem that I couldn't unfilter, but it still closes the Menu whenever I type anything into the searchbox. – Joana Almeida Jul 28 '20 at 12:04
  • A menu isn't supposed to stay open when a different element gets input. Consider using a different control, perhaps a TreeView. – Clemens Jul 28 '20 at 12:07
  • Even if the element is a MenuItem in the same menu, like the textbox? That is frustrating. I did see it in other tools in the past, which is why I got the idea. If this isn't possible at all, I guess I really have to restructure my screen and possibly use a treeview. – Joana Almeida Jul 28 '20 at 12:30
  • Maybe something like this would work: https://stackoverflow.com/a/40711644/1136211 – Clemens Jul 28 '20 at 12:40
  • doesn't seem to apply here. After some tests, the reason the menu closes is ItemsSource getting updated. If I leave the propertychange invoke out, the menu no longer closes. The interesting thing is that the menu also doesn't close when I delete characters. So essentially: Menu closes when list becomes smaller. Menu doesn't close when list becomes bigger. – Joana Almeida Jul 28 '20 at 13:17