Lesson 02.06

Add SearchBox for ListBox

Let's improve the user experience inside our form by adding search box functionality for our ListBox. This will really make the difference and allow users to easier find the right views. Here is how to do that…

Lesson 02.06

Add SearchBox for ListBox

Let's improve the user experience inside our form by adding search box functionality for our ListBox. This will really make the difference and allow users to easier find the right views. Here is how to do that…

Summary

Add Search Functionality

Lastly let's make our ListBox work with the Search box that we made earlier. This will allow users to quickly filter the right elements in the list to select the right elements.

There are many different ways to achieve this result, and I've tried a lot of them. And in this lesson I will share the best one I've found and it's also the easiest to explain.

So let's dive in and add the search box.

Modifying the XAML Code

We're going to open the XAML code and make a few small changes here. Firstly, we need to introduce an event to our search <TextBox>.

So let's add something like this:

<TextBox x:Name="UI_search" TextChanged="UIe_search_text_changed"/>

This will trigger an event handler that will change displayed items in your listboxes. This way, we'll be able to filter and display only the views that match the written keywords.

Now let's copy the whole XAML code and bring it to our pushbutton in pyRevit. Here is my full XAML code.

<Window Title="EF-First WPF Form"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Height="Auto" Width="300" MinHeight="150"
        WindowStartupLocation="CenterScreen" ResizeMode="NoResize"
        SizeToContent="Height">

    <!--Stack Panel for all elements-->
    <StackPanel Margin="10" >

        <!--First Label and TextBox-->
        <DockPanel Margin="5">
            <TextBlock x:Name="UI_textblock1" Text="Input A" Margin="0,0,10,0" FontWeight="Bold" />
            <TextBox   x:Name="UI_textbox1"   Text="Default Value..."  Foreground="Gray"/>
        </DockPanel>

        <!--Second Label and TextBox-->
        <DockPanel Margin="5">
            <TextBlock Text="Input B" Margin="0,0,10,0" FontWeight="Bold"/>
            <TextBox x:Name="UI_textbox2"
                Text="Default Value..." Foreground="Gray" />
        </DockPanel>

        <!--Third Label with ComboBox-->
        <DockPanel Margin="5">
            <TextBlock Text="Input C" Margin="0,0,10,0" FontWeight="Bold"/>
            <ComboBox x:Name="UI_combobox">
                <ComboBoxItem Content="Walls"/>
                <ComboBoxItem Content="Floors"/>
                <ComboBoxItem Content="Roofs"/>
                <ComboBoxItem Content="Windows" IsSelected="True"/>
                <ComboBoxItem Content="Doors"/>
            </ComboBox>
        </DockPanel>

        <!--Checkboxes-->
        <DockPanel HorizontalAlignment="Center" Margin="5">
            <CheckBox x:Name="UI_check1" Content="Check 1" Margin="0,0,10,0" />
            <CheckBox x:Name="UI_check2" Content="Check 2" Margin="0,0,10,0" />
            <CheckBox x:Name="UI_check3" Content="Check 3" />

        </DockPanel>

        
        <!--ListBox of Random Views-->
        <StackPanel Margin="5">
            <!--ListBox Label-->
            <TextBlock Text="Select Views:" FontWeight="Bold" Margin="0,0,0,5"/>

            <!--Search Box-->
            <DockPanel Margin="5">
                <TextBlock Text="🔎" Margin="0,0,5,0" />
                <TextBox x:Name="UI_search" TextChanged="UIe_search_text_changed"/>
            </DockPanel>

            <ListBox x:Name="UI_listbox" Height="150" >
                <!--ListBox Item with a CheckBox-->
                <ListBoxItem>
                    <CheckBox Content="View - A"/>
                </ListBoxItem>

                <ListBoxItem>
                    <CheckBox Content="View - B"/>
                </ListBoxItem>

                <ListBoxItem>
                    <CheckBox Content="View - C"/>
                </ListBoxItem>

            </ListBox>

            <DockPanel HorizontalAlignment="Center" Margin="0,10,0,0">
                <Button Content="Select All" Width="100"  Click="UIe_btn_select_all" Margin="0,0,10,0"/>
                <Button Content="Select None" Width="100" Click="UIe_btn_select_none"/>
            </DockPanel>
        </StackPanel>

        <!--Separator-->
        <Separator Margin="5,5,5,12"/>

        <!--Submit Button-->
        <Button Content="Submit!" 
                Width="100" 
                Click="UIe_btn_run" />

Python Logic for SearchBox

Now let's look at the python code. Obviously we need to create a method to handle the Event we've created.

I've mentioned mentioned that there are many different ways to achieve the same search functionality, and the easiest way is to play with the visibility of your ListBoxItems.

Visibility has 3 options:

  • Visible

  • Hidden (Still takes space in form)

  • Collapsed (Doesn't take space in form)

You could also recreate your lists, but then it becomes a hassle to remember what was selected before and after… So it's better to just Hide and Show based on the search keyword.

💡Make sure you import Visibility from .NET library, because Revit API also has another Visibility Class!


So, whenever this event is triggered we are going to do the following:

  • Get Search Input (+make it lowercase)

  • If SearchBox Empty:

    • Iterate Through all Items

    • Make all items Visible

  • If SearchBox has value:

    • Get <CheckBox> and <TextBlock> from <ListBoxItem>

    • Get view name (+make it lowercase)

    • Check if search value in view name

    • Adjust Visibility Property

  • Lastly, we will also make sure Wildcard search works.

Here is the code for these steps:

    def UIe_search_text_changed(self, sender, e):
        search_text = self.search.lower()
        
        if search_text:
            for listbox_item in self.UI_listbox.Items:
                checkbox = listbox_item.Content
                textblock = checkbox.Content
                view_name = textblock.Text.lower()
        
                if search_text in view_name:
                    listbox_item.Visibility = Visibility.Visible
                else:
                    listbox_item.Visibility = Visibility.Collapsed
   
        if not search_text:
            for listbox_item in self.UI_listbox.Items:
                listbox_item.Visibility = Visibility.Visible

Testing the Application

Now let's test it in Revit and see if your SearchBox filters elements in your <ListBox>

Alright, it works, but right now it only takes exact matches.

Instead, I want to use wildcard search. This will allow me to write multiple words and filter views that contain all written words. This is much more powerful search.


Implementing Wildcard Search

To implement Wildcard search we just need to make a minor adjustment.

We will need to:

  • Split search value into words

  • Create a check if ALL words are in the view name

  • That's it…

Here is how to do it with the code:

    def UIe_search_text_changed(self, sender, e):
        search_text = self.search.lower()

        if search_text:
            search_words = search_text.split()

            for listbox_item in self.UI_listbox.Items:
                checkbox  = listbox_item.Content
                textblock = checkbox.Content
                view_name = textblock.Text.lower()

                if all(word in view_name for word in search_words):
                    listbox_item.Visibility = Visibility.Visible
                else:
                    listbox_item.Visibility = Visibility.Collapsed

        if not search_text:
            for listbox_item in self.UI_listbox.Items:
                listbox_item.Visibility = Visibility.Visible

And now you should have a wildcard search that will make it so much more versatile to look for the right elements in your <ListBox>.

Fix Select All / None Buttons

Lastly, we need to adjust the logic in our Select All/None buttons. Because at the moment they will select or deselect all visible and hidden elements. And we want to make sure that they only affect the visible elements.

And it's very simple, we just need to add an if statement in our listbox_select_all method to check if ListBoxItem is Visible or not.

Here is how to do that:

    def listbox_select_all(self, toggle):
        """ Set all ListBoxItem to be Selected or Not
        :param toggle: True/False"""
        for listbox_item in self.UI_listbox.Items:
            if listbox_item.Visibility == Visibility.Visible:
                checkbox = listbox_item.Content
                checkbox.IsChecked = toggle

And now your Select All/None buttons will work as expected and won't disrupt your workflow.

Final Code:

Here is the code from the lesson to avoid ant confusion:

#-*- coding: utf-8 -*-
__title__   = "02.06 - ListBox Search"
__doc__     = """Version = 1.0
Date    = 15.09.2024
________________________________________________________________
Description:
Learn how to add SearchBox functionality to ListBox using Events.
________________________________________________________________
Author: Erik Frits"""


# ╦╔╦╗╔═╗╔═╗╦═╗╔╦╗╔═╗
# ║║║║╠═╝║ ║╠╦╝ ║ ╚═╗
# ╩╩ ╩╩  ╚═╝╩╚═ ╩ ╚═╝ IMPORTS
#====================================================================================================
from Autodesk.Revit.DB import *
from pyrevit import forms   # By importing forms you also get references to WPF package! IT'S Very IMPORTANT !!!
import wpf, os, clr         # wpf can be imported only after pyrevit.forms!
from pyrevit.forms import select_views

# .NET Imports
clr.AddReference("System")
from System.Collections.Generic import List
from System.Windows import Application, Window, Visibility
from System.Windows.Controls import CheckBox, Button, TextBox, ListBoxItem, TextBlock
from System import Uri

# ╦  ╦╔═╗╦═╗╦╔═╗╔╗ ╦  ╔═╗╔═╗
# ╚╗╔╝╠═╣╠╦╝║╠═╣╠╩╗║  ║╣ ╚═╗
#  ╚╝ ╩ ╩╩╚═╩╩ ╩╚═╝╩═╝╚═╝╚═╝ VARIABLES
#====================================================================================================
PATH_SCRIPT = os.path.dirname(__file__)
doc     = __revit__.ActiveUIDocument.Document #type: Document
uidoc   = __revit__.ActiveUIDocument
app     = __revit__.Application

# Global Variables
all_views  = FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_Views).WhereElementIsNotElementType().ToElements()
dict_views = {'[{}]_{}'.format(view.ViewType,view.Name) : view for view in all_views}

# ╔╦╗╔═╗╦╔╗╔  ╔═╗╔═╗╦═╗╔╦╗
# ║║║╠═╣║║║║  ╠╣ ║ ║╠╦╝║║║
# ╩ ╩╩ ╩╩╝╚╝  ╚  ╚═╝╩╚═╩ ╩ MAIN FORM
#====================================================================================================
# Inherit .NET Window for your UI Form Class
class FirstButton(Window):
    def __init__(self):
        # Connect to .xaml File (in the same folder!)
        path_xaml_file = os.path.join(PATH_SCRIPT, 'ListBoxSearch.xaml')
        wpf.LoadComponent(self, path_xaml_file)

        # Populate Listbox with Views
        self.populate_listbox_with_views()

        # Show Form
        self.ShowDialog()


    # ╔╦╗╔═╗╔╦╗╦ ╦╔═╗╔╦╗╔═╗
    # ║║║║╣  ║ ╠═╣║ ║ ║║╚═╗
    # ╩ ╩╚═╝ ╩ ╩ ╩╚═╝═╩╝╚═╝ METHODS
    def populate_listbox_with_views(self):

        # Clear ListBox
        self.UI_listbox.Items.Clear()

        # Add ViewNames as CheckBoxes to ListBox
        for view_name, view in dict_views.items():
            # checkbox.Content = view_name # This will have issues with displaying _

            # Create TextBlock to avoid error with '_'
            textblock      = TextBlock()
            textblock.Text = view_name

            # Create CheckBox And add textblock to it
            checkbox         = CheckBox()
            checkbox.Content = textblock
            checkbox.Tag     = view

            # Add Checkbox to ListBoxItem
            listbox_item = ListBoxItem()
            listbox_item.Content = checkbox

            # Add ListBoxItem to ListBox
            self.UI_listbox.Items.Add(listbox_item)

    def listbox_select_all(self, toggle):
        """ Set all ListBoxItem to be Selected or Not
        :param toggle: True/False"""
        for listbox_item in self.UI_listbox.Items:
            if listbox_item.Visibility == Visibility.Visible:
                checkbox = listbox_item.Content
                checkbox.IsChecked = toggle

    # ╔═╗╦═╗╔═╗╔═╗╔═╗╦═╗╔╦╗╦╔═╗╔═╗
    # ╠═╝╠╦╝║ ║╠═╝║╣ ╠╦╝ ║ ║║╣ ╚═╗
    # ╩  ╩╚═╚═╝╩  ╚═╝╩╚═ ╩ ╩╚═╝╚═╝

    # @property.setter
    # def textbox1(self, value):
    #     if type(value) == str:
    #         self.UI_textbox1.Text = value

    @property
    def textbox1(self):
        return self.UI_textbox1.Text

    @property
    def textbox2(self):
        return self.UI_textbox2.Text

    @property
    def search(self):
        return self.UI_search.Text

    @property
    def combobox_item(self):
        return self.UI_combobox.SelectedItem

    @property
    def check1(self):
        return self.UI_check1.IsChecked

    @property
    def check2(self):
        return self.UI_check2.IsChecked

    @property
    def check3(self):
        return self.UI_check3.IsChecked

    @property
    def selected_listbox_items(self):
        # Get Selected ListBox Item.
        selected_views = []
        for listbox_item in self.UI_listbox.Items:
            checkbox = listbox_item.Content
            if checkbox.IsChecked:
                selected_views.append(checkbox.Tag)

        return selected_views



    # ╔╗ ╦ ╦╔╦╗╔╦╗╔═╗╔╗╔  ╔═╗╦  ╦╔═╗╔╗╔╔╦╗╔═╗
    # ╠╩╗║ ║ ║  ║ ║ ║║║║  ║╣ ╚╗╔╝║╣ ║║║ ║ ╚═╗
    # ╚═╝╚═╝ ╩  ╩ ╚═╝╝╚╝  ╚═╝ ╚╝ ╚═╝╝╚╝ ╩ ╚═╝
    def UIe_btn_run(self, sender, e):
        print('Form Submitted!')
        self.Close()

    def UIe_btn_select_all(self, sender, e):
        self.listbox_select_all(True)

    def UIe_btn_select_none(self, sender, e):
        self.listbox_select_all(False)


    def UIe_search_text_changed(self, sender, e):
        search_text = self.search.lower()

        if search_text:
            search_words = search_text.split()

            for listbox_item in self.UI_listbox.Items:
                checkbox  = listbox_item.Content
                textblock = checkbox.Content
                view_name = textblock.Text.lower()

                if all(word in view_name for word in search_words):
                    listbox_item.Visibility = Visibility.Visible
                else:
                    listbox_item.Visibility = Visibility.Collapsed

        if not search_text:
            for listbox_item in self.UI_listbox.Items:
                listbox_item.Visibility = Visibility.Visible



    def UIe_search_text_changed(self, sender, e):
        search_text = self.search.lower()

        if search_text:
            search_words = search_text.split()

            for listbox_item in self.UI_listbox.Items:
                checkbox  = listbox_item.Content
                textblock = checkbox.Content
                view_name = textblock.Text.lower()

                if all(word in view_name for word in search_words):
                    listbox_item.Visibility = Visibility.Visible
                else:
                    listbox_item.Visibility = Visibility.Collapsed

        if not search_text:
            for listbox_item in self.UI_listbox.Items:
                listbox_item.Visibility = Visibility.Visible





# Show form to the user
UI = FirstButton()
print(UI.selected_listbox_items)

# Print Selected Views
# for view in UI.selected_listbox_items:
#     print(view.Name, view)

<Window Title="EF-First WPF Form"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Height="Auto" Width="300" MinHeight="150"
        WindowStartupLocation="CenterScreen" ResizeMode="NoResize"
        SizeToContent="Height">

    <!--Stack Panel for all elements-->
    <StackPanel Margin="10" >

        <!--First Label and TextBox-->
        <DockPanel Margin="5">
            <TextBlock x:Name="UI_textblock1" Text="Input A" Margin="0,0,10,0" FontWeight="Bold" />
            <TextBox   x:Name="UI_textbox1"   Text="Default Value..."  Foreground="Gray"/>
        </DockPanel>

        <!--Second Label and TextBox-->
        <DockPanel Margin="5">
            <TextBlock Text="Input B" Margin="0,0,10,0" FontWeight="Bold"/>
            <TextBox x:Name="UI_textbox2"
                Text="Default Value..." Foreground="Gray" />
        </DockPanel>

        <!--Third Label with ComboBox-->
        <DockPanel Margin="5">
            <TextBlock Text="Input C" Margin="0,0,10,0" FontWeight="Bold"/>
            <ComboBox x:Name="UI_combobox">
                <ComboBoxItem Content="Walls"/>
                <ComboBoxItem Content="Floors"/>
                <ComboBoxItem Content="Roofs"/>
                <ComboBoxItem Content="Windows" IsSelected="True"/>
                <ComboBoxItem Content="Doors"/>
            </ComboBox>
        </DockPanel>

        <!--Checkboxes-->
        <DockPanel HorizontalAlignment="Center" Margin="5">
            <CheckBox x:Name="UI_check1" Content="Check 1" Margin="0,0,10,0" />
            <CheckBox x:Name="UI_check2" Content="Check 2" Margin="0,0,10,0" />
            <CheckBox x:Name="UI_check3" Content="Check 3" />

        </DockPanel>

        
        <!--ListBox of Random Views-->
        <StackPanel Margin="5">
            <!--ListBox Label-->
            <TextBlock Text="Select Views:" FontWeight="Bold" Margin="0,0,0,5"/>

            <!--Search Box-->
            <DockPanel Margin="5">
                <TextBlock Text="🔎" Margin="0,0,5,0" />
                <TextBox x:Name="UI_search" TextChanged="UIe_search_text_changed"/>
            </DockPanel>

            <ListBox x:Name="UI_listbox" Height="150" >
                <!--ListBox Item with a CheckBox-->
                <ListBoxItem>
                    <CheckBox Content="View - A"/>
                </ListBoxItem>

                <ListBoxItem>
                    <CheckBox Content="View - B"/>
                </ListBoxItem>

                <ListBoxItem>
                    <CheckBox Content="View - C"/>
                </ListBoxItem>

            </ListBox>

            <DockPanel HorizontalAlignment="Center" Margin="0,10,0,0">
                <Button Content="Select All" Width="100"  Click="UIe_btn_select_all" Margin="0,0,10,0"/>
                <Button Content="Select None" Width="100" Click="UIe_btn_select_none"/>
            </DockPanel>
        </StackPanel>

        <!--Separator-->
        <Separator Margin="5,5,5,12"/>

        <!--Submit Button-->
        <Button Content="Submit!" 
                Width="100" 
                Click="UIe_btn_run" />

    </StackPanel>

</Window>
Conclusion

Alright, and that's it for this lesson, and I hope that you followed me along and implemented this powerful search functionality inside your forms. It's the thing that is really going to improve your user experience.

Our form is getting more advanced and it already includes everything we might need in our WPF forms. But I also need to show you how to populate items inside your <ComboBox>, and that's what we will focus on in the next lesson.

I wish you Happy Coding and we will see each other soon.

🙋‍♂️ See you in the next lesson.
- EF

🙋‍♂️ See you in the next lesson.
- EF

Discuss the lesson:

P.S. Sometimes this chat might experience connection issues.
Please be patient or join via Discord app so you can get the most out of this community and get access to even more chats.

Discuss the lesson:

P.S. Sometimes this chat might experience connection issues.
Please be patient or join via Discord app so you can get the most out of this community and get access to even more chats.