Adding a Search Bar to Xamarin.Forms.NavigationPage

Adding a Search Bar to Xamarin.Forms.NavigationPage

31 Oct 2020 Update: Updated for AndroidX + Xamarin.Essentials

Let's look at how to add the native search bar control into the Xamarin.Forms.NavigationPage

Both Xamarin.iOS and Xamarin.Android offer the capability to natively add a search bar to the navigation bar using UISearchController and SearchView, respectively.

But, it's not simple in Xamarin.Forms as one might expect.

If you'd like to see a completed solution, check out this app: https://github.com/brminnick/GitTrends

Otherwise, let's jump to the steps & code following this lovely GIF:

iOS Android
iOS Gif Android Gif

Xamarin.Forms Project

In the Xamarin.Forms project, we'll want to do the following:

iOS Large Titles

In iOS 11, Apple introduced prefersLargeTitles and updated their Human Interface Guidelines, recommending that iOS apps use Large Titles.

This is easy to accomplish in Xamarin.Forms thanks to the platform-specific method Xamarin.Forms.PlatformConfiguration.iOSSpecific.NavigationPage.SetPreferesLargeTitles .

We can set this platform-specific in our App.cs class:

using Xamarin.Forms.PlatformConfiguration;
using Xamarin.Forms.PlatformConfiguration.iOSSpecific;

public class App : Xamarin.Forms.Application
{
    public App()
    {
        var navigationPage = new Xamarin.Forms.NavigationPage(new MyContentPage());

        navigationPage.On<iOS>().SetPrefersLargeTitles(true);

        MainPage = navigationPage;
    }
}

ISearchPage

Let's create an interface, ISearchPage, that can be used across the Xamarin.Forms, Xamarin.Android and Xamarin.iOS projects.

public interface ISearchPage
{
    void OnSearchBarTextChanged(string text);
    event EventHandler<string> SearchBarTextChanged;
}

Xamarin.Forms.ContentPage

Let's now implement ISearchPage in our Xamarin.Forms.ContentPage

public class MyContentPage : ContentPage, ISearchPage
{
    public MyContentPage()
    {
        SearchBarTextChanged += HandleSearchBarTextChanged;
    }

    public event EventHandler<string> SearchBarTextChanged;

    void ISearchPage.OnSearchBarTextChanged((string text) => SearchBarTextChanged?.Invoke(this, text);

    void HandleSearchBarTextChanged(object sender, string searchBarText)
    {
        //Logic to handle updated search bar text
    }     
}

Xamarin.iOS Project

In the Xamarin.iOS project, we'll be doing the following:

  • Create SearchPageRenderer

Our Xamarin.iOS Custom Renderer uses a PageRenderer that adds a UISearchController to the NavigationItem on its parent page which is a UINavigationController. (The parent page is a UINavigationController, because in App.cs, we used a Xamarin.Forms.NavigationPage.)

If you're new to Xamarin.iOS and/or Custom Renderers, here's a chart showing how the various UI controls relate to each other on iOS:

Xamarin.iOS Xamarin.Forms Renderer Xamarin.Forms
UINavigationController NavigationRenderer NavigationPage
UIViewController PageRenderer ContentPage
using System;
using UIKit;
using MyNamespace;
using MyNamespace.iOS;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;

[assembly: ExportRenderer(typeof(MyContentPage), typeof(SearchPageRenderer))]
namespace MyNamespace.iOS
{
    public class SearchPageRenderer : PageRenderer, IUISearchResultsUpdating
    {
		readonly UISearchController _searchController;

        public SearchPageRenderer()
        {
            _searchController = new UISearchController(searchResultsController: null)
            {
                SearchResultsUpdater = this,
                DimsBackgroundDuringPresentation = false,
                HidesNavigationBarDuringPresentation = false,
                HidesBottomBarWhenPushed = true
            };
            _searchController.SearchBar.Placeholder = string.Empty;
        }

        public override void ViewDidAppear(bool animated)
        {
            base.ViewDidAppear(animated);

            if (ParentViewController.NavigationItem.SearchController is null)
            {
                ParentViewController.NavigationItem.SearchController = _searchController;
                DefinesPresentationContext = true;

                //Work-around to ensure the SearchController appears when the page first appears https://stackoverflow.com/a/46313164/5953643
                ParentViewController.NavigationItem.SearchController.Active = true;
                ParentViewController.NavigationItem.SearchController.Active = false;
            }
        }

        public override void ViewWillDisappear(bool animated)
        {
            base.ViewWillDisappear(animated);

            ParentViewController.NavigationItem.SearchController = null;
        }

        public void UpdateSearchResultsForSearchController(UISearchController searchController)
        {
            if (Element is ISearchPage searchPage)
                searchPage.OnSearchBarTextChanged(searchController.SearchBar.Text);
        }
        
        protected override void OnElementChanged(VisualElementChangedEventArgs e)
        {
            base.OnElementChanged(e);

            if (e.NewElement != null)
                e.NewElement.SizeChanged += HandleSizeChanged;
        }
        
        //Work-around to accommodate UISearchController height, https://github.com/brminnick/GitTrends/issues/171
        void HandleSizeChanged(object sender, EventArgs e)
        {
            if (ParentViewController?.NavigationItem.SearchController != null
                && Element.Height > -1
                && Element is Page page)
            {
                Element.SizeChanged -= HandleSizeChanged;

                if (NavigationController.NavigationBar.PrefersLargeTitles is true)
                {
                    var statusBarSize = UIApplication.SharedApplication.StatusBarFrame.Size;
                    var statusBarHeight = Math.Min(statusBarSize.Height, statusBarSize.Width);

                    page.Padding = new Thickness(page.Padding.Left,
                                                    page.Padding.Top,
                                                    page.Padding.Right,
                                                    page.Padding.Bottom + statusBarHeight);
                }
            }
        }
    }
}

Note: If you'd like to add a search bar to multiple pages, you can add additional ExportRenderer assembly attributes like so:

[assembly: ExportRenderer(typeof(MyContentPage), typeof(SearchPageRenderer))]
[assembly: ExportRenderer(typeof(MyContentPage2), typeof(SearchPageRenderer))]

Xamarin.Android Project

In the Xamarin.Android project, we'll be doing the following:

  • Create Resources  > menu > MainMenu.xml
  • Create SearchPageRenderer

In the Xamarin.Android project, we'll first need to add MainMenu.xml to Resources > menu.

1. In the Xamarin.Android project, in the Resources folder, create a new folder called menu (if one doesn't already exist)

  • Note: The folder name, menu, needs to be lowercase

2. In the Resources > menu folder, create a new file called MainMenu.xml

3. Open Resources > menu > MainMenu.xml

4. In MainMenu.xml, add the following code:

<?xml version="1.0" encoding="utf-8" ?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto">
  <item android:id="@+id/ActionSearch"
        android:title="Filter"
        android:icon="@android:drawable/ic_menu_search"
        app:showAsAction="always|collapseActionView"
        app:actionViewClass="androidx.appcompat.widget.SearchView"/>
</menu>

Xamarin.Android Custom Renderer

Our Xamarin.Android Custom Renderer uses a PageRenderer to add a SearchView to the Toolbar.

using System.Collections.Generic;
using System.Linq;
using Android.Content;
using Android.Runtime;
using Android.Text;
using Android.Views;
using Android.Views.InputMethods;
using AndroidX.AppCompat.Widget;
using MyNamespace;
using MyNamespace.Droid;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;

[assembly: ExportRenderer(typeof(MyContentPage), typeof(SearchPageRenderer))]
namespace MyNamespace.Droid
{
    public class SearchPageRenderer : PageRenderer
    {
        public SearchPageRenderer(Context context) : base(context)
        {

        }
        
        //Add the Searchbar once Xamarin.Forms creates the Page
        protected override void OnElementChanged(ElementChangedEventArgs<Page> e)
        {
            base.OnElementChanged(e);

            if (e.NewElement is ISearchPage && e.NewElement is Page page && page.Parent is NavigationPage navigationPage && navigationPage.CurrentPage is ISearchPage)
                AddSearchToToolbar(page.Title);
        }

        protected override void OnAttachedToWindow()
        {
            base.OnAttachedToWindow();

			if (Element is ISearchPage && Element is Page page && page.Parent is NavigationPage navigationPage)
            {
            	//Workaround to re-add the SearchView when navigating back to an ISearchPage, because Xamarin.Forms automatically removes it
                navigationPage.Popped += HandleNavigationPagePopped;
                navigationPage.PoppedToRoot += HandleNavigationPagePopped;
            }
        }
        
        //Adding the SearchBar in OnSizeChanged ensures the SearchBar is re-added after the device is rotated, because Xamarin.Forms automatically removes it
        protected override void OnSizeChanged(int w, int h, int oldw, int oldh)
        {
            base.OnSizeChanged(w, h, oldw, oldh);

            if (Element is ISearchPage && Element is Page page && page.Parent is NavigationPage navigationPage && navigationPage.CurrentPage is ISearchPage)
            {
                AddSearchToToolbar(page.Title);
            }
        }

        protected override void Dispose(bool disposing)
        {
            if (GetToolbar() is Toolbar toolBar)
                toolBar.Menu?.RemoveItem(Resource.Menu.MainMenu);

            base.Dispose(disposing);
        }
        
        static IEnumerable<Toolbar> GetToolbars(ViewGroup viewGroup)
        {
            for (int i = 0; i < viewGroup.ChildCount; i++)
            {
                if (viewGroup.GetChildAt(i) is Toolbar toolbar)
                {
                    yield return toolbar;
                }
                else if (viewGroup.GetChildAt(i) is ViewGroup childViewGroup)
                {
                    foreach (var childToolbar in GetToolbars(childViewGroup))
                        yield return childToolbar;
                }
            }
        }

        Toolbar? GetToolbar()
        {
            if (Xamarin.Essentials.Platform.CurrentActivity.Window?.DecorView.RootView is ViewGroup viewGroup)
            {
                var toolbars = GetToolbars(viewGroup);

                //Return top-most Toolbar
                return toolbars.LastOrDefault();
            }

            return null;
        }
        
        //Workaround to re-add the SearchView when navigating back to an ISearchPage, because Xamarin.Forms automatically removes it
        void HandleNavigationPagePopped(object sender, NavigationEventArgs e)
        {
            if (sender is NavigationPage navigationPage
                && navigationPage.CurrentPage is ISearchPage)
            {
                AddSearchToToolbar(navigationPage.CurrentPage.Title);
            }
        }

        void AddSearchToToolbar(string pageTitle)
        {
            if (GetToolbar() is Toolbar toolBar
                && toolBar.Menu?.FindItem(Resource.Id.ActionSearch)?.ActionView?.JavaCast<SearchView>()?.GetType() != typeof(SearchView))
            {
                toolBar.Title = pageTitle;
                toolBar.InflateMenu(Resource.Menu.MainMenu);

                if (toolBar.Menu?.FindItem(Resource.Id.ActionSearch)?.ActionView?.JavaCast<SearchView>() is SearchView searchView)
                {
                    searchView.QueryTextChange += HandleQueryTextChange;
                    searchView.ImeOptions = (int)ImeAction.Search;
                    searchView.InputType = (int)InputTypes.TextVariationFilter;
                    searchView.MaxWidth = int.MaxValue; //Set to full width - http://stackoverflow.com/questions/31456102/searchview-doesnt-expand-full-width
                }
            }
        }

        void HandleQueryTextChange(object sender, SearchView.QueryTextChangeEventArgs e)
        {
            if (Element is ISearchPage searchPage)
                searchPage.OnSearchBarTextChanged(e?.NewText ?? string.Empty);
        }
    }
}

Note: If you'd like to add a search bar to multiple pages, you can add additional ExportRenderer assembly attributes like so:

[assembly: ExportRenderer(typeof(MyContentPage), typeof(SearchPageRenderer))]
[assembly: ExportRenderer(typeof(MyContentPage2), typeof(SearchPageRenderer))]