How to customize searching in Umbraco list views
Tags: umbraco, examine, csharp, angular
Published: 2021-01-13 22:29:42

---
title: How to customize searching in Umbraco list views
published: true
description: Learn how to make the search function of Umbracos list views, able to search in other things than the node names.
tags: umbraco,examine,csharp,angularjs
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/nj1n6cm0ptq83pspm2gf.jpg
---

When dealing with large archives of content, like a blog section, product catalog or similar, using list views is a great way to declutter the content tree in Umbraco.

Simply put, enabling the list view on the archive document type, all the children of that doc type will be hidden from the tree, and in stead shown in a "content app", in form of a sortable list.

![Umbraco List view of products](https://dev-to-uploads.s3.amazonaws.com/i/uabmpiexdf67k09i9a9h.png)

This makes it a lot easier to work with large archives, like the before mentioned blog section, product catalog etc.

But they don't help when you need to find your needle in the haystack of productnodes. Out of the box, Umbracos list view comes with a search function, but it is unfortunately limited to only search in the node names.

For something like a product catalog with thousands of products, it would be nice to be able to search for product nodes, by their SKUs. But this would require you to put the SKU in the node name. I would typically put the product name as the node name, and have the SKU on a property by itself.

Below you can see an example of the product catalog in the default starter kit of Umbraco, where I have searched for a products SKU. Nothing is found.

![Umbraco List view where searching for a sku returns no product nodes](https://dev-to-uploads.s3.amazonaws.com/i/323mq1ueh4p69zkro69x.png)

Luckily you can swap out the default search with your own, quite easily.

Using the magic of $http interceptors in AngularJS, you simply listen for requests to the default API endpoint for searching child nodes, and swapping it out, with your own endpoint.

## Build your own search logic, by inheriting the default
For this, I have created a controller, inheriting from Umbracos own `ContentController`.

```cs
using System.Linq;
using Examine;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Configuration;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Persistence;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
using Umbraco.Web;
using Umbraco.Web.Editors;
using Umbraco.Web.Models.ContentEditing;

namespace skttl
{
	public class CustomListViewSearchController : ContentController
	{
		public CustomListViewSearchController(PropertyEditorCollection propertyEditors, IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper)
			: base(propertyEditors, globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper)
		{
		}

		public PagedResult<ContentItemBasic<ContentPropertyBasic>> GetChildrenCustom(int id, string includeProperties, int pageNumber = 0, int pageSize = 0, string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = "", string cultureName = "")
		{
			// get the parent node, and its doctype alias from the content cache
			var parentNode = Services.ContentService.GetById(id);
			var parentNodeDocTypeAlias = parentNode != null ? parentNode.ContentType.Alias : null;

			// if the parent node is not "products", redirect to the core GetChildren() method
			if (parentNode?.ContentType.Alias != "products")
			{
				return GetChildren(id, includeProperties, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter);
			}

            // if we can't get the InternalIndex, redirect to the core GetChildren() method, but log an error
            if (!ExamineManager.Instance.TryGetIndex("InternalIndex", out IIndex index))
            {
                Logger.Error<CustomListViewSearchController>("Couldn't get InternalIndex for searching products in list view");
				return GetChildren(id, includeProperties, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter);
            }

			// find children using Examine

			// create search criteria
			var searcher = index.GetSearcher();
			var searchCriteria = searcher.CreateQuery();
			var searchQuery = searchCriteria.Field("parentID", id);

			if (!filter.IsNullOrWhiteSpace())
			{
				searchQuery = searchQuery.And().GroupedOr(new [] { "nodeName", "sku" }, filter);
			}

			// do the search, but limit the results to the current page 👉 https://shazwazza.com/post/paging-with-examine/
			// pageNumber is not zero indexed in this, so just multiply pageSize by pageNumber
			var searchResults = searchQuery.Execute(pageSize * pageNumber);

			// get the results on the current page
			// pageNumber is not zero indexed in this, so subtract 1 from the pageNumber
			var totalChildren = searchResults.TotalItemCount;
			var pagedResultIds = searchResults.Skip((pageNumber > 0 ? pageNumber - 1 : 0) * pageSize).Select(x => x.Id).Select(x => int.Parse(x)).ToList();
			var children = Services.ContentService.GetByIds(pagedResultIds).ToList();

			if (totalChildren == 0)
			{
				return new PagedResult<ContentItemBasic<ContentPropertyBasic>>(0, 0, 0);
			}

			var pagedResult = new PagedResult<ContentItemBasic<ContentPropertyBasic>>(totalChildren, pageNumber, pageSize);
			pagedResult.Items = children.Select(content =>
				Mapper.Map<IContent, ContentItemBasic<ContentPropertyBasic>>(content))
				.ToList(); // evaluate now

			return pagedResult;

		}
	}
}

```

By inheriting from `ContentController`, I can easily restore the default functionality when I don't need anything custom.

I have added a replication of the default `GetChildren` method from the `ContentController`, called `GetChildrenCustom`. It takes the same parameters, which enables me to just swap the url, when Umbraco is calling the API. But more on that later.

```cs
// get the parent node, and its doctype alias from the content cache
var parentNode = Services.ContentService.GetById(id);
var parentNodeDocTypeAlias = parentNode != null ? parentNode.ContentType.Alias : null;

// if the parent node is not "products", redirect to the core GetChildren() method
if (parentNode?.ContentType.Alias != "products")
{
	return GetChildren(id, includeProperties, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter);
}
```

At first I get the parent node from the `ContentService`, and verifies that the parent node is the product catalog. If not, I simply return the `GetChildren` method from the `ContentController`, restoring the default functionality.

If I am in a context of a product catalog node, I can start doing my own logic.

```cs
// if we can't get the InternalIndex, redirect to the core GetChildren() method, but log an error
if (!ExamineManager.Instance.TryGetIndex("InternalIndex", out IIndex index))
{
    Logger.Error<CustomListViewSearchController>("Couldn't get InternalIndex for searching products in list view");
    return GetChildren(id, includeProperties, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter);
}
```

At first I check I get the InternalIndex from Examine - if this fails, I revert to the default GetChildren again.

```cs
// find children using Examine

// create search criteria
var searcher = index.GetSearcher();
var searchCriteria = searcher.CreateQuery();
var searchQuery = searchCriteria.Field("parentID", id);

if (!filter.IsNullOrWhiteSpace())
{
    searchQuery = searchQuery.And().GroupedOr(new [] { "nodeName", "sku" }, filter);
}
```

But in most cases, the `InternalIndex` works (I still have yet to see an Umbraco installation without the `InternalIndex`). I can then proceed with my search.

I'm using Examine here, because it's faster than going through the `ContentService`, when dealing with property values. In this example, query for nodes, where the `parentId` field matches my parent node id.

And if the `filter` parameter has a value (this is the search field in the interface), I add a search for that, looking in both the `nodeName`, and the `sku` fields.

```cs
// do the search, but limit the results to the current page 👉 https://shazwazza.com/post/paging-with-examine/
// pageNumber is not zero indexed in this, so just multiply pageSize by pageNumber
var searchResults = searchQuery.Execute(pageSize * pageNumber);

// get the results on the current page
// pageNumber is not zero indexed in this, so subtract 1 from the pageNumber
var totalChildren = searchResults.TotalItemCount;
var pagedResultIds = searchResults.Skip((pageNumber > 0 ? pageNumber - 1 : 0) * pageSize).Select(x => x.Id).Select(x => int.Parse(x)).ToList();
var children = Services.ContentService.GetByIds(pagedResultIds).ToList();

if (totalChildren == 0)
{
    return new PagedResult<ContentItemBasic<ContentPropertyBasic>>(0, 0, 0);
}

var pagedResult = new PagedResult<ContentItemBasic<ContentPropertyBasic>>(totalChildren, pageNumber, pageSize);
pagedResult.Items = children.Select(content =>
    Mapper.Map<IContent, ContentItemBasic<ContentPropertyBasic>>(content))
    .ToList(); // evaluate now

return pagedResult;
```

Then onto the search. I don't want to return more nodes, than configured in the list view, so I implement paging on the search, as advised by [Shannon in his blogpost](https://shazwazza.com/post/paging-with-examine/).

At last I replicate some of the code from the default `GetChildren` method, returning similar results, but based on my examine search.

## Getting the backoffice to use my search logic
As I mentioned earlier, AngularJS comes with a concept called $http interceptors. In this, you can listen and react to different things, when AngularJS handles http requests.

For this trick to work, I need to change requests for `/umbraco/backoffice/UmbracoApi/Content/GetChildren` (the default endpoint for child nodes), and change it to my newly created one, which is located at `/umbraco/backoffice/api/CustomListViewSearch/GetChildrenCustom`.

This is done easily by adding a js file containing an interceptor like this.

```js
angular.module('umbraco.services').config([
   '$httpProvider',
   function ($httpProvider) {

       $httpProvider.interceptors.push(function ($q) {
           return {
               'request': function (request) {

                   // Redirect any requests for the listview to our custom list view UI
				   if (request.url.indexOf("backoffice/UmbracoApi/Content/GetChildren?id=") > -1)
					   request.url = request.url.replace("backoffice/UmbracoApi/Content/GetChildren", "backoffice/api/CustomListViewSearch/GetChildrenCustom");

                   return request || $q.when(request);
               }
           };
       });

   }]);
```

Note how I left out `/umbraco` from the urls being searched for. Some people like to change the backoffice folder name from `umbraco` to something else - security by obscurity and the likes. By just looking at the latter part of the url, I can support both.

Lastly I have to make sure Umbraco finds and includes my interceptor. This is done with a `package.manifest` file in my `App_Plugins` folder.

```js
{
  "javascript": [
    "/App_Plugins/CustomListViewSearch/CustomListViewSearch.js"
  ]
}
```

Restart your site, go to your product catalog, and voila. You can now search for product nodes, by typing SKUs.

![Umbraco List View where searching for a SKU returns a product node](https://dev-to-uploads.s3.amazonaws.com/i/myvnanqabupmljd8rbtl.png)

For completeness, you could also implement an [ISearchableTree](https://our.umbraco.com/Documentation/Extending/Section-Trees/Searchable-Trees/). This powers the global backoffice search, and enables your editors to simply press Ctrl+Space on their keyboard, and start searching.

I hope you liked this article, and if there is something you would like to know more about, feel free to comment, or tweet me :)

---

<span>Photo by <a href="https://unsplash.com/@nepumuk?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Peter Kleinau</a> on <a href="https://unsplash.com/s/photos/haystack?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></span>