In this part of the series, we are going to explain how to create one version of Blazor WebAssembly Searching functionality.
In this example, we are going to implement a search by product name, but later on, you can modify it to your needs. As we did in a previous part with Paging, we are going to implement the Web API side first and then continue with the Blazor WebAssembly client-side application.
We won’t dive deep into the searching logic because we already have a great article on that topic, so if you want to learn more, feel free to read it.
For the complete navigation for this series, you can visit the Blazor Series page.
Searching Functionality Implementation – Web API Part
The first thing, we are going to do is to extend the ProductParameters
class in the Entities
project:
public class ProductParameters { const int maxPageSize = 50; public int PageNumber { get; set; } = 1; private int _pageSize = 4; public int PageSize { get { return _pageSize; } set { _pageSize = (value > maxPageSize) ? maxPageSize : value; } } public string SearchTerm { get; set; } }
The next thing we need to do is to implement the searching functionality itself. To do that, inside the Repository
folder, we are going to create a new RepositoryExtensions
folder and inside it a new RepositoryProductExtensions
class:
public static class RepositoryProductExtensions { public static IQueryable<Product> Search(this IQueryable<Product> products, string searchTerm) { if (string.IsNullOrWhiteSpace(searchTerm)) return products; var lowerCaseSearchTerm = searchTerm.Trim().ToLower(); return products.Where(p => p.Name.ToLower().Contains(lowerCaseSearchTerm)); } }
So, it’s a static class with a single static method that extends the IQueryable<Product>
type and accepts a single string parameter. The logic inside this class is pretty simple.
Now, let’s call this method in the ProductRepository
class:
public async Task<PagedList<Product>> GetProducts(ProductParameters productParameters) { var products = await _context.Products .Search(productParameters.SearchTerm) .ToListAsync(); return PagedList<Product> .ToPagedList(products, productParameters.PageNumber, productParameters.PageSize); }
Excellent. All we have to do is to test this.
Testing Web API Searching Implementation
To execute the test, we are going to use Postman with a single Get request:
There we go. We can see it works as expected.
Once we implement the client-side, we are going to test it together with paging.
Blazor WebAssembly Searching Implementation
In this section, the first thing we are going to do is to create a new Search.razor
component and a partial class file in the Components
folder:
After this, let’s modify the Search
class file:
public partial class Search { public string SearchTerm { get; set; } [Parameter] public EventCallback<string> OnSearchChanged { get; set; } }
We are going to use the SearchTerm
property to bind the value from our input text field, and OnSearchChanged
event callback to run the method from the Products
parent component.
Now, let’s modify the razor file:
<section style="margin-bottom: 10px"> <input type="text" class="form-control" placeholder="Search by product name" @bind-value="@SearchTerm" @bind-value:event="oninput" @onkeyup="async () => await OnSearchChanged.InvokeAsync(SearchTerm)" /> </section>
In this file, we create a simple input text element with the bootstrap’s form-control
class and a placeholder. Now, we have to pay attention to the @bind-value
attribute. With this attribute, we provide a two-way binding with the SearchTerm
property. This means by populating the SearchTerm
property, our input field will be populated and vice versa. But, if we leave only the @bind-value
attribute, the SearchTerm
property will receive value only when the user navigates away from the input field (in other words, when input loses focus). But we don’t want that.
What we want is to populate the SearchTerm
property as soon as we type any character, and for that, we use the @bind-value:event
attribute set to oninput
value. With this in place, the SearchTerm
property will receive value as we type.
Lastly, we have the @onkeyup
event, where we use the lambda expression to execute the OnSearchChanged
event callback and to pass the SearchTerm
as a parameter to the method inside the Products
component.
Modifying the Products Component Files
Now, we have to modify the Products.razor.cs
file by adding a new SearchChanged
method:
private async Task SearchChanged(string searchTerm) { Console.WriteLine(searchTerm); _productParameters.PageNumber = 1; _productParameters.SearchTerm = searchTerm; await GetProducts(); }
This method will accept the searchTerm
parameter from the event callback from the Search
component, assign that value to the _productParameters
object, set the PageNumber
to 1, and call the GetProducts
method. We have to set the PageNumber
to 1 because we rerender the list of products with our searching functionality and the paging should start from the first page. Additionally, we just log the searchTerm
parameter to inspect its value – just for the testing’s sake.
Of course, we have to modify the Products.razor
file:
<div class="row"> <div class="col-md-5"> <Search OnSearchChanged="SearchChanged"/> </div> <div class="col-md-5"> @*Place for sort*@ </div> <div class="col-md-2"> <a href="/createProduct">Create Product</a> </div> </div> <div class="row"> <div class="col"> <ProductTable Products="ProductList" /> </div> </div> <div class="row"> <div class="col"> <Pagination MetaData="MetaData" Spread="2" SelectedPage="SelectedPage" /> </div> </div>
Finally, let’s modify the GetProducts
method in the ProductHttpRepository
file, to include this additional query string parameter:
public async Task<PagingResponse<Product>> GetProducts(ProductParameters productParameters) { var queryStringParam = new Dictionary<string, string> { ["pageNumber"] = productParameters.PageNumber.ToString(), ["searchTerm"] = productParameters.SearchTerm == null ? "" : productParameters.SearchTerm }; //the rest of the code is the same return pagingResponse; }
We have to do a null check for the productParameters.SearchTerm
property because the AddQueryString
method will complain if it is null.
Excellent.
Now we can test this.
Testing and Improving Search Functionality
Let’s start the server and the client applications, navigate to the Products
page and type the Wal
term in the search field:
We can see the Blazor WebAssembly Searching functionality is working and we have only one product on the list. But, aside from the working part, this functionality is not optimized in terms of sending the HTTP Get request to the server. If you look at the console logs, you can see the SearchedChanged
method is logging our search term three times, for each character we type in the search field. Additionally, this means that our server is called every time we enter a character in the search field, and that’s not the best way. What we want is to send only one request with the complete search term.
To add the optimization part, let’s modify the Search.razor.cs
file:
public partial class Search { private Timer _timer; public string SearchTerm { get; set; } [Parameter] public EventCallback<string> OnSearchChanged { get; set; } private void SearchChanged() { if (_timer != null) _timer.Dispose(); _timer = new Timer(OnTimerElapsed, null, 500, 0); } private void OnTimerElapsed(object sender) { OnSearchChanged.InvokeAsync(SearchTerm); _timer.Dispose(); } }
So now, we create a timer and the SearchChanged
method where we dispose of the _timer
object if it is already created and create a new timer object. This object will execute the OnTimerElapsed
method after 500 milliseconds, and in this method, we run the method from the Products
component.
By doing this, we prevent sending the GET request for each character in the input field but instead, the request will be sent after half a second after the user finishes typing.
We have to modify one more thing in the Search.razor
file:
<section style="margin-bottom: 10px"> <input type="text" class="form-control" placeholder="Search by product name" @bind-value="@SearchTerm" @bind-value:event="oninput" @onkeyup="SearchChanged" /> </section>
Now, let’s start the Blazor WebAssembly application and inspect the result:
As we can see, the user sees the same result, but its implementation is more optimized than before.
Conclusion
There we go. We have fully functional Blazor WebAssembly Searching Functionality implemented in our application.
In the next article, we are going to learn about Sorting in Blazor WebAssembly.
Good practice, thanks very much.
Thank you too for the comment and for reading the article.
Thank you so much for your excellent articles on pagination, sorting and searching, they’ve been a great deal of help to me. I was wondering if you might have any advice about storing the product parameters client side so that the end user can bookmark their page/search/ordering and return to it easily later?
Well, that is a custom solution for each project. It depends. I am not sure that I can give you some specific advice for that.
A typo: SearchTearm -> SearchTerm
I fixed that and a few other issues in your code repo, but my PR was rejected.
Just to know, It doesn’t necessary to merge all commits in the PR with the repo. You can merge only specific commits from a PR with git cherry-pick. Git is more than just commit, push, pull.
Anyway, good articles series.
Well done!
Thank you.
Hello Ayub. We couldn’t accept PR for a simple reason, this article is part of the series and we can’t have only this one updated and other ones stay unchanged. If we update the series to .NET 6, we will update all the articles in the series. Thank you for the suggestions but we really can’t do it that way, at least not for the article in the series.
Very good article.
1- The SearchTerm In the ProductParameters have to be a nullable string, otherwise, if the SearchTerm is empty, API returns 400 Bad Request with following message:
2- Whenever possible, use null-coalescing operator. It’s more concise:
3- Also whenever possible, write asynchronous methods and use Async version of methods:
Hello Ayub. First of all, I’ve fixed your comment from 404 to 400. We use the plugin for the comments and unfortunately, it doesn’t allow editing comments, but I modified it for you.
Now regarding your comment:
1) I’ve just tested the API again and that shouldn’t be the case. In the Search extension method, we have a check:
if (string.IsNullOrWhiteSpace(searchTearm))
return products;
This will return all the products if your searchTerm parameter is null or empty
2) I agree, but yet again, some people are more used to ternary operators – that’s just the style of writing your code, I wouldn’t say “whenever”. Yet again, I agree with your point.
3) I agree with you on this as well, except in the situation you’ve just shown. Using async with void methods is not recommended at all unless you have to work with the windows events (OnClick…). Only in those cases using async with the void is recommended. Other than that either use async Task if it can be done or just void. Using async with void can cause a lot of issues especially with the exception tracing. Additionally, you don’t get too much from your async code in this situation. So, this is why I left the synchronous code here.
Thank you very much for the comment and the suggestions. All the best.
Can you check what is wrong with my code? It forces me to make SearchTerm nullable to resolve the Bad Request response?
https://github.com/kokabi1365/BlazorProducts.Server
To be honest I really have no time now to do that. If you didn’t fork our solution, maybe it is best to download our solution and test it. If you still get the same error with our solution just write here the way you’ve tested it so I can try that as well. But if you have no issues with our solution then just compare ours and yours.
I finally figured out what happened.
The problem was with the .NET 6!
By default, .NET 6 add following line to .csproj:
I had to delete it or make SearchTerm nullable.
Thank you
You are most welcome.
Amazing article. Thanks for sharing!
Thank you very much Dipendra. It is always nice to hear something like that.