diff --git a/src/cascadia/TerminalControl/Resources/en-US/Resources.resw b/src/cascadia/TerminalControl/Resources/en-US/Resources.resw index e94aa14bd92..434edb36e9b 100644 --- a/src/cascadia/TerminalControl/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalControl/Resources/en-US/Resources.resw @@ -178,6 +178,14 @@ Ctrl+Click to follow link + + No results + Will be presented near the search box when Find operation returned no results. + + + Searching... + Will be presented near the search box when Find operation is running. + Unable to find the selected font "{0}". diff --git a/src/cascadia/TerminalControl/SearchBoxControl.cpp b/src/cascadia/TerminalControl/SearchBoxControl.cpp index 57026c40321..432da162c5e 100644 --- a/src/cascadia/TerminalControl/SearchBoxControl.cpp +++ b/src/cascadia/TerminalControl/SearchBoxControl.cpp @@ -4,6 +4,7 @@ #include "pch.h" #include "SearchBoxControl.h" #include "SearchBoxControl.g.cpp" +#include using namespace winrt; using namespace winrt::Windows::UI::Xaml; @@ -18,12 +19,26 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation this->CharacterReceived({ this, &SearchBoxControl::_CharacterHandler }); this->KeyDown({ this, &SearchBoxControl::_KeyDownHandler }); + this->RegisterPropertyChangedCallback(UIElement::VisibilityProperty(), [this](auto&&, auto&&) { + // Once the control is visible again we trigger SearchChanged event. + // We do this since we probably have a value from the previous search, + // and in such case logically the search changes from "nothing" to this value. + // A good example for SearchChanged event consumer is Terminal Control. + // Once the Search Box is open we want the Terminal Control + // to immediately perform the search with the value appearing in the box. + if (Visibility() == Visibility::Visible) + { + _SearchChangedHandlers(TextBox().Text(), _GoForward(), _CaseSensitive()); + } + }); _focusableElements.insert(TextBox()); _focusableElements.insert(CloseButton()); _focusableElements.insert(CaseSensitivityButton()); _focusableElements.insert(GoForwardButton()); _focusableElements.insert(GoBackwardButton()); + + StatusBox().Width(_GetStatusMaxWidth()); } // Method Description: @@ -209,4 +224,143 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation { e.Handled(true); } + + // Method Description: + // - Handler for changing the text. Triggers SearchChanged event + // Arguments: + // - sender: not used + // - e: event data + // Return Value: + // - + void SearchBoxControl::TextBoxTextChanged(winrt::Windows::Foundation::IInspectable const& /*sender*/, winrt::Windows::UI::Xaml::RoutedEventArgs const& /*e*/) + { + _SearchChangedHandlers(TextBox().Text(), _GoForward(), _CaseSensitive()); + } + + // Method Description: + // - Handler for clicking the case sensitivity toggle. Triggers SearchChanged event + // Arguments: + // - sender: not used + // - e: not used + // Return Value: + // - + void SearchBoxControl::CaseSensitivityButtonClicked(winrt::Windows::Foundation::IInspectable const& /*sender*/, winrt::Windows::UI::Xaml::RoutedEventArgs const& /*e*/) + { + _SearchChangedHandlers(TextBox().Text(), _GoForward(), _CaseSensitive()); + } + + // Method Description: + // - Formats a status message representing the search state: + // * "Searching" - if totalMatches is negative + // * "No results" - if totalMatches is 0 + // * "?/n" - if totalMatches=n matches and we didn't start the iteration over matches + // (usually we will get this after buffer update) + // * "m/n" - if we are currently at match m out of n. + // * "m/max+" - if n > max results to show + // * "?/max+" - if m > max results to show + // Arguments: + // - totalMatches - total number of matches (search results) + // - currentMatch - the index of the current match (0-based) + // Return Value: + // - status message + winrt::hstring SearchBoxControl::_FormatStatus(int32_t totalMatches, int32_t currentMatch) + { + if (totalMatches < 0) + { + return RS_(L"TermControl_Searching"); + } + + if (totalMatches == 0) + { + return RS_(L"TermControl_NoMatch"); + } + std::wstringstream ss; + + if (currentMatch < 0 || currentMatch > (MaximumTotalResultsToShowInStatus - 1)) + { + ss << CurrentIndexTooHighStatus; + } + else + { + ss << currentMatch + 1; + } + + ss << StatusDelimiter; + + if (totalMatches > MaximumTotalResultsToShowInStatus) + { + ss << TotalResultsTooHighStatus; + } + else + { + ss << totalMatches; + } + + return ss.str().data(); + } + + // Method Description: + // - Helper method to measure the width of the text block given the text and the font size + // Arguments: + // - text: the text to measure + // - fontSize: the size of the font to measure + // Return Value: + // - the size in pixels + double SearchBoxControl::_TextWidth(winrt::hstring text, double fontSize) + { + winrt::Windows::UI::Xaml::Controls::TextBlock t; + t.FontSize(fontSize); + t.Text(text); + t.Measure({ FLT_MAX, FLT_MAX }); + return t.ActualWidth(); + } + + // Method Description: + // - This method tries to predict the maximal size of the status box + // by measuring different possible statuses + // Return Value: + // - the size in pixels + double SearchBoxControl::_GetStatusMaxWidth() + { + const auto fontSize = StatusBox().FontSize(); + const auto maxLength = std::max({ _TextWidth(_FormatStatus(-1, -1), fontSize), + _TextWidth(_FormatStatus(0, -1), fontSize), + _TextWidth(_FormatStatus(MaximumTotalResultsToShowInStatus, MaximumTotalResultsToShowInStatus - 1), fontSize), + _TextWidth(_FormatStatus(MaximumTotalResultsToShowInStatus + 1, MaximumTotalResultsToShowInStatus - 1), fontSize), + _TextWidth(_FormatStatus(MaximumTotalResultsToShowInStatus + 1, MaximumTotalResultsToShowInStatus), fontSize) }); + + return maxLength; + } + + // Method Description: + // - Formats and sets the status message in the status box. + // Increases the size of the box if required. + // Arguments: + // - totalMatches - total number of matches (search results) + // - currentMatch - the index of the current match (0-based) + // Return Value: + // - + void SearchBoxControl::SetStatus(int32_t totalMatches, int32_t currentMatch) + { + const auto status = _FormatStatus(totalMatches, currentMatch); + const auto requiredWidth = _TextWidth(status, StatusBox().FontSize()); + if (requiredWidth > StatusBox().Width()) + { + StatusBox().Width(requiredWidth); + } + + StatusBox().Text(status); + } + + // Method Description: + // - Enables / disables results navigation buttons + // Arguments: + // - enable: if true, the buttons should be enabled + // Return Value: + // - + void SearchBoxControl::SetNavigationEnabled(bool enabled) + { + GoBackwardButton().IsEnabled(enabled); + GoForwardButton().IsEnabled(enabled); + } } diff --git a/src/cascadia/TerminalControl/SearchBoxControl.h b/src/cascadia/TerminalControl/SearchBoxControl.h index cdf4ee63830..ab78166d63f 100644 --- a/src/cascadia/TerminalControl/SearchBoxControl.h +++ b/src/cascadia/TerminalControl/SearchBoxControl.h @@ -17,13 +17,17 @@ Author(s): #include "winrt/Windows.UI.Xaml.h" #include "winrt/Windows.UI.Xaml.Controls.h" #include "../../cascadia/inc/cppwinrt_utils.h" - #include "SearchBoxControl.g.h" namespace winrt::Microsoft::Terminal::TerminalControl::implementation { struct SearchBoxControl : SearchBoxControlT { + static constexpr int32_t MaximumTotalResultsToShowInStatus = 999; + static constexpr std::wstring_view TotalResultsTooHighStatus = L"999+"; + static constexpr std::wstring_view CurrentIndexTooHighStatus = L"?"; + static constexpr std::wstring_view StatusDelimiter = L"/"; + SearchBoxControl(); void TextBoxKeyDown(winrt::Windows::Foundation::IInspectable const& /*sender*/, winrt::Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e); @@ -31,17 +35,26 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation void SetFocusOnTextbox(); void PopulateTextbox(winrt::hstring const& text); bool ContainsFocus(); + void SetStatus(int32_t totalMatches, int32_t currentMatch); + void SetNavigationEnabled(bool enabled); void GoBackwardClicked(winrt::Windows::Foundation::IInspectable const& /*sender*/, winrt::Windows::UI::Xaml::RoutedEventArgs const& /*e*/); void GoForwardClicked(winrt::Windows::Foundation::IInspectable const& /*sender*/, winrt::Windows::UI::Xaml::RoutedEventArgs const& /*e*/); void CloseClick(winrt::Windows::Foundation::IInspectable const& /*sender*/, winrt::Windows::UI::Xaml::RoutedEventArgs const& e); + void TextBoxTextChanged(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::RoutedEventArgs const& e); + void CaseSensitivityButtonClicked(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::RoutedEventArgs const& e); WINRT_CALLBACK(Search, SearchHandler); + WINRT_CALLBACK(SearchChanged, SearchHandler); TYPED_EVENT(Closed, TerminalControl::SearchBoxControl, Windows::UI::Xaml::RoutedEventArgs); private: std::unordered_set _focusableElements; + static winrt::hstring _FormatStatus(int32_t totalMatches, int32_t currentMatch); + static double _TextWidth(winrt::hstring text, double fontSize); + double _GetStatusMaxWidth(); + bool _GoForward(); bool _CaseSensitive(); void _KeyDownHandler(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e); diff --git a/src/cascadia/TerminalControl/SearchBoxControl.idl b/src/cascadia/TerminalControl/SearchBoxControl.idl index 78011841c21..0de5f825ee7 100644 --- a/src/cascadia/TerminalControl/SearchBoxControl.idl +++ b/src/cascadia/TerminalControl/SearchBoxControl.idl @@ -11,8 +11,11 @@ namespace Microsoft.Terminal.TerminalControl void SetFocusOnTextbox(); void PopulateTextbox(String text); Boolean ContainsFocus(); + void SetStatus(Int32 totalMatches, Int32 currentMatch); + void SetNavigationEnabled(Boolean enabled); event SearchHandler Search; + event SearchHandler SearchChanged; event Windows.Foundation.TypedEventHandler Closed; } } diff --git a/src/cascadia/TerminalControl/SearchBoxControl.xaml b/src/cascadia/TerminalControl/SearchBoxControl.xaml index f64ae36ff66..bb87ab6efb3 100644 --- a/src/cascadia/TerminalControl/SearchBoxControl.xaml +++ b/src/cascadia/TerminalControl/SearchBoxControl.xaml @@ -53,7 +53,7 @@ - + @@ -161,8 +161,16 @@ KeyDown="TextBoxKeyDown" Margin="5" HorizontalAlignment="Left" - VerticalAlignment="Center"> + VerticalAlignment="Center" + TextChanged="TextBoxTextChanged"> + + + Style="{StaticResource ToggleButtonStyle}" + Click="CaseSensitivityButtonClicked"> diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index a1549b125ec..1cd6b99971c 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -38,6 +38,15 @@ constexpr const auto TsfRedrawInterval = std::chrono::milliseconds(100); // The minimum delay between updating the locations of regex patterns constexpr const auto UpdatePatternLocationsInterval = std::chrono::milliseconds(500); +// The minimum delay between triggering search upon output to terminal +constexpr const auto SearchUponOutputInterval = std::chrono::milliseconds(500); + +// The delay before performing the search after output to terminal +constexpr const auto SearchAfterOutputDelay = std::chrono::milliseconds(800); + +// The delay before performing the search after change of search criteria +constexpr const auto SearchAfterChangeDelay = std::chrono::milliseconds(200); + DEFINE_ENUM_FLAG_OPERATORS(winrt::Microsoft::Terminal::TerminalControl::CopyFormat); namespace winrt::Microsoft::Terminal::TerminalControl::implementation @@ -113,6 +122,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation auto onReceiveOutputFn = [this](const hstring str) { _terminal->Write(str); _updatePatternLocations->Run(); + _updateSearchStatus->Run(); }; _connectionOutputEventToken = _connection.TerminalOutput(onReceiveOutputFn); @@ -184,10 +194,26 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation ScrollBarUpdateInterval, Dispatcher()); + _updateSearchStatus = std::make_shared>( + [weakThis = get_weak()]() { + if (auto control{ weakThis.get() }) + { + // If in the middle of the search, recompute the matches. + // We avoid navigation to the first result to prevent auto-scrolling. + if (control->_searchState.has_value()) + { + const SearchState searchState{ control->_searchState->Text, control->_searchState->Sensitivity }; + control->_searchState.emplace(searchState); + control->_SearchAsync(std::nullopt, SearchAfterOutputDelay); + } + } + }, + SearchUponOutputInterval, + Dispatcher()); + static constexpr auto AutoScrollUpdateInterval = std::chrono::microseconds(static_cast(1.0 / 30.0 * 1000000)); _autoScrollTimer.Interval(AutoScrollUpdateInterval); _autoScrollTimer.Tick({ this, &TermControl::_UpdateAutoScroll }); - _ApplyUISettings(); } @@ -227,34 +253,177 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation // Method Description: // - Search text in text buffer. This is triggered if the user click // search button or press enter. + // In the live search mode it will be also triggered once every time search criteria changes // Arguments: - // - text: the text to search + // - text: not used // - goForward: boolean that represents if the current search direction is forward - // - caseSensitive: boolean that represents if the current search is case sensitive + // - caseSensitive: not used // Return Value: // - - void TermControl::_Search(const winrt::hstring& text, const bool goForward, const bool caseSensitive) + void TermControl::_Search(const winrt::hstring& /*text*/, const bool goForward, const bool /*caseSensitive*/) { - if (text.size() == 0 || _closing) + _SelectSearchResult(goForward); + } + + // Method Description: + // - Search text in text buffer. + // This is triggered when the user starts typing, clicks on navigation or + // when the search is active and the terminal text is changing + // Arguments: + // - goForward: optional boolean that represents if the current search direction is forward + // - delay: time in milliseconds to wait before performing the search + // (grace time to allow next search to start) + // Return Value: + // - + fire_and_forget TermControl::_SearchAsync(std::optional goForward, Windows::Foundation::TimeSpan const& delay) + { + // Run only if the search state was initialized + if (_closing || !_searchState.has_value()) { return; } - const Search::Direction direction = goForward ? - Search::Direction::Forward : - Search::Direction::Backward; + const auto originalSearchId = _searchState->SearchId; + auto weakThis{ this->get_weak() }; - const Search::Sensitivity sensitivity = caseSensitive ? - Search::Sensitivity::CaseSensitive : - Search::Sensitivity::CaseInsensitive; + // If no matches were computed it means we need to perform the search + if (!_searchState->Matches.has_value()) + { + // Before we search, let's wait a bit: + // probably the search criteria or the data are still modified. + co_await winrt::resume_after(delay); - Search search(*GetUiaData(), text.c_str(), direction, sensitivity); - auto lock = _terminal->LockForWriting(); - if (search.FindNext()) + // Switch back to Dispatcher so we can set the Searching status + co_await winrt::resume_foreground(Dispatcher()); + if (auto control{ weakThis.get() }) + { + // If search box was collapsed or the new one search was triggered - let's cancel this one + if (!_searchState.has_value() || _searchState->SearchId != originalSearchId) + { + co_return; + } + + // Let's mark the start of searching + if (_searchBox) + { + _searchBox->SetStatus(-1, -1); + _searchBox->SetNavigationEnabled(false); + } + + std::vector> matches; + if (!_searchState->Text.empty()) + { + // We perform explicit search forward, so the first result will also be the earliest buffer location + // We will use goForward later to decide if we need to select 1 of n or n of n. + Search search(*GetUiaData(), _searchState->Text.c_str(), Search::Direction::Forward, _searchState->Sensitivity); + while (co_await _SearchOne(search)) + { + // if search box was collapsed or the new one search was triggered - let's cancel this one + if (!_searchState.has_value() || _searchState->SearchId != originalSearchId) + { + co_return; + } + + matches.push_back(search.GetFoundLocation()); + } + + // if search box was collapsed or the new one search was triggered - let's cancel this one + if (!_searchState.has_value() || _searchState->SearchId != originalSearchId) + { + co_return; + } + } + _searchState->Matches.emplace(std::move(matches)); + } + } + + if (auto control{ weakThis.get() }) { - _terminal->SetBlockSelection(false); - search.Select(); + _SelectSearchResult(goForward); + } + } + + // Method Description: + // - Selects one of precomputed search results in the terminal (if exist). + // - Updates the search box control accordingly. + // - The selection might be preceded by going to next / previous result + // - goForward: if true, select next result; if false, select previous result; + // if not set, remain at the current result. + // Return Value: + // - + void TermControl::_SelectSearchResult(std::optional goForward) + { + if (_searchState.has_value() && _searchState->Matches.has_value()) + { + auto& state = _searchState.value(); + auto& matches = state.Matches.value(); + + if (goForward.has_value()) + { + state.UpdateIndex(goForward.value()); + + const auto currentMatch = state.GetCurrentMatch(); + if (currentMatch.has_value()) + { + auto lock = _terminal->LockForWriting(); + _terminal->SetBlockSelection(false); + _terminal->SelectNewRegion(currentMatch->first, currentMatch->second); + _renderer->TriggerSelection(); + } + } + + if (_searchBox) + { + _searchBox->SetStatus(gsl::narrow(matches.size()), state.CurrentMatchIndex); + _searchBox->SetNavigationEnabled(!_searchState->Matches->empty()); + } + } + } + + // Method Description: + // - Search for a single value in a background + // Arguments: + // - search: search object to use to find the next match + // Return Value: + // - + winrt::Windows::Foundation::IAsyncOperation TermControl::_SearchOne(Search& search) + { + bool found{ false }; + auto weakThis{ this->get_weak() }; + + co_await winrt::resume_background(); + if (auto control{ weakThis.get() }) + { + // We don't lock the terminal for the duration of the entire search, + // since if the terminal was modified the search ID will be updated. + auto lock = _terminal->LockForWriting(); + found = search.FindNext(); + } + + co_await winrt::resume_foreground(Dispatcher()); + co_return found; + } + + // Method Description: + // - The handler for the "search criteria changed" event. Clears selection and initiates a new search. + // Arguments: + // - text: the text to search + // - goForward: indicates whether the search should be performed forward (if set to true) or backward + // - caseSensitive: boolean that represents if the current search is case sensitive + // Return Value: + // - + void TermControl::_SearchChanged(const winrt::hstring& text, const bool goForward, const bool caseSensitive) + { + if (_searchBox && _searchBox->Visibility() == Visibility::Visible) + { + // Clear the selection reset the anchor + _terminal->ClearSelection(); _renderer->TriggerSelection(); + + const auto sensitivity = caseSensitive ? Search::Sensitivity::CaseSensitive : Search::Sensitivity::CaseInsensitive; + const SearchState searchState{ text, sensitivity }; + _searchState.emplace(searchState); + _SearchAsync(goForward, SearchAfterChangeDelay); } } @@ -269,6 +438,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation void TermControl::_CloseSearchBoxControl(const winrt::Windows::Foundation::IInspectable& /*sender*/, RoutedEventArgs const& /*args*/) { _searchBox->Visibility(Visibility::Collapsed); + _searchState.reset(); // Set focus back to terminal control this->Focus(FocusState::Programmatic); @@ -3267,6 +3437,53 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation return _terminal->GetTaskbarProgress(); } + std::atomic SearchState::_searchIdGenerator{ 0 }; + + // Method Description: + // - Updates the index of the current match according to the direction. + // The index will remain unchanged (usually -1) if the number of matches is 0 + // Arguments: + // - goForward: if true, move to the next match, else go to previous + // Return Value: + // - + void SearchState::UpdateIndex(bool goForward) + { + if (Matches.has_value()) + { + const int numMatches = ::base::saturated_cast(Matches->size()); + if (numMatches > 0) + { + if (CurrentMatchIndex == -1) + { + CurrentMatchIndex = goForward ? 0 : numMatches - 1; + } + else + { + CurrentMatchIndex = (numMatches + CurrentMatchIndex + (goForward ? 1 : -1)) % numMatches; + } + } + } + } + + // Method Description: + // - Retrieves the current match + // Arguments: + // - + // Return Value: + // - current match, null-opt if current match is invalid + // (e.g., when the index is -1 or there are no matches) + std::optional> SearchState::GetCurrentMatch() + { + if (Matches.has_value() && CurrentMatchIndex > -1 && CurrentMatchIndex < Matches->size()) + { + return til::at(Matches.value(), CurrentMatchIndex); + } + else + { + return std::nullopt; + } + } + // -------------------------------- WinRT Events --------------------------------- // Winrt events need a method for adding a callback to the event and removing the callback. // These macros will define them both for you. diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 288dea233dc..3f83ddbc2ce 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -98,6 +98,28 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation const hstring _message; }; + struct SearchState + { + public: + static std::atomic _searchIdGenerator; + + SearchState(const winrt::hstring& text, const Search::Sensitivity sensitivity) : + Text(text), + Sensitivity(sensitivity), + SearchId(_searchIdGenerator.fetch_add(1)) + { + } + + const winrt::hstring Text; + const Search::Sensitivity Sensitivity; + const size_t SearchId; + std::optional>> Matches; + int32_t CurrentMatchIndex{ -1 }; + + void UpdateIndex(bool goForward); + std::optional> GetCurrentMatch(); + }; + struct TermControl : TermControlT { TermControl(IControlSettings settings, TerminalConnection::ITerminalConnection connection); @@ -209,6 +231,8 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation std::shared_ptr> _updatePatternLocations; + std::shared_ptr> _updateSearchStatus; + struct ScrollBarUpdate { std::optional newValue; @@ -261,6 +285,8 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation winrt::Windows::UI::Xaml::Controls::SwapChainPanel::LayoutUpdated_revoker _layoutUpdatedRevoker; + std::optional _searchState; + void _ApplyUISettings(); void _UpdateSystemParameterSettings() noexcept; void _InitializeBackgroundBrush(); @@ -326,8 +352,12 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation const unsigned int _NumberOfClicks(winrt::Windows::Foundation::Point clickPos, Timestamp clickTime); double _GetAutoScrollSpeed(double cursorDistanceFromBorder) const; + winrt::Windows::Foundation::IAsyncOperation _SearchOne(Search& search); void _Search(const winrt::hstring& text, const bool goForward, const bool caseSensitive); + void _SearchChanged(const winrt::hstring& text, const bool goForward, const bool caseSensitive); void _CloseSearchBoxControl(const winrt::Windows::Foundation::IInspectable& sender, Windows::UI::Xaml::RoutedEventArgs const& args); + fire_and_forget _SearchAsync(std::optional goForward, Windows::Foundation::TimeSpan const& delay); + void _SelectSearchResult(std::optional goForward); // TSFInputControl Handlers void _CompositionCompleted(winrt::hstring text); diff --git a/src/cascadia/TerminalControl/TermControl.xaml b/src/cascadia/TerminalControl/TermControl.xaml index 8ef1d8f835a..51dde3521d6 100644 --- a/src/cascadia/TerminalControl/TermControl.xaml +++ b/src/cascadia/TerminalControl/TermControl.xaml @@ -76,6 +76,7 @@ HorizontalAlignment="Right" VerticalAlignment="Top" Search="_Search" + SearchChanged="_SearchChanged" Closed="_CloseSearchBoxControl" />