Module : Programmation .Net C#
Durée : 2h
Objectif : Implémenter un moteur de recherche réactif en combinant plusieurs filtres (Texte, Graphique). Apprendre à basculer un filtrage mémoire vers un filtrage SQL dynamique (IQueryable) pour optimiser les performances ("Big Data").
Pré-requis :
En HTML classique, il faut taper un texte puis cliquer sur un bouton "Rechercher". En Blazor, on peut intercepter chaque frappe clavier pour filtrer le tableau instantanément.
Components/Pages/MyDashboard.razor.@code, ajoutez une variable pour stocker le texte recherché :private string SearchText = "";
3. Au-dessus de l'appel au composant <SensorTable />, ajoutez une barre de recherche avec une icône Bootstrap.
Notez que nous utilisons un <input> standard et non un <InputText> pour pouvoir forcer l'événement oninput.
<div class="row mb-3">
<div class="col-md-6">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-search"></i></span>
@*
@bind="SearchText" : Lie la valeur.
@bind:event="oninput" : Met à jour la variable à chaque touche tapée !
*@
<input type="text"
@bind="SearchText"
@bind:event="oninput"
class="form-control"
placeholder="Rechercher par nom de capteur..." />
</div>
</div>
</div>
Actuellement, notre propriété FilteredSensors (créée au TP 9) ne regarde que le SelectedLocationFilter. Nous allons la modifier pour qu'elle combine la recherche textuelle ET le filtre du graphique.
MyDashboard.razor, modifiez la propriété calculée :private List<SensorData> FilteredSensors
{
get
{
// On part de la liste complète
var query = Sensors.AsEnumerable();
// 1. Application du filtre "Lieu" (venant du graphique Radzen)
if (!string.IsNullOrEmpty(SelectedLocationFilter))
{
query = query.Where(s => s.Location?.Name == SelectedLocationFilter);
}
// 2. Application du filtre "Texte" (venant de la barre de recherche)
if (!string.IsNullOrWhiteSpace(SearchText))
{
// Contains = recherche "LIKE %texte%" (Ignorer la casse avec StringComparison)
query = query.Where(s => s.Name.Contains(SearchText, StringComparison.OrdinalIgnoreCase));
}
return query.ToList();
}
}
Testez : Cliquez sur un lieu dans le graphique, puis commencez à taper le nom d'un capteur dans la barre. Les deux filtres se cumulent instantanément !
IQueryable) (45 min)Le problème actuel : La méthode LoadAll() récupère TOUTE la table SQL en RAM (Sensors), puis Blazor la filtre dans le navigateur. C'est inacceptable si la table contient des millions de lignes.
La solution : Envoyer les filtres au SensorService pour que Entity Framework génère un WHERE en SQL. Seules les lignes utiles traverseront le réseau.
Services/ISensorService.cs et ajoutez cette méthode :Task<List<SensorData>> SearchSensorsAsync(string? locationName, string? searchText);
2. Implémentez-la dans Services/SensorService.cs. C'est l'occasion de découvrir IQueryable, qui permet de construire une requête SQL étape par étape avant de l'exécuter.
public async Task<List<SensorData>> SearchSensorsAsync(string? locationName, string? searchText)
{
// AsQueryable() prépare une requête sans l'exécuter
IQueryable<SensorData> query = _context.Sensors.Include(s => s.Location).AsQueryable();
// Si un lieu est fourni, on ajoute un WHERE au SQL
if (!string.IsNullOrEmpty(locationName))
{
query = query.Where(s => s.Location.Name == locationName);
}
// Si un texte est fourni, on ajoute un autre WHERE (LIKE) au SQL
if (!string.IsNullOrEmpty(searchText))
{
query = query.Where(s => s.Name.Contains(searchText));
}
// L'exécution SQL (SELECT ...) se fait uniquement ici, avec ToListAsync() !
return await query.ToListAsync();
}
3. Mise à jour de l'UI (MyDashboard.razor) :
Puisque la BDD fait le filtrage, nous n'avons plus besoin de la liste Sensors complète ni de la propriété calculée en mémoire.
Modifiez le code pour déclencher la requête SQL à chaque changement de filtre.
// Remplacer l'ancienne logique par ceci :
private List<SensorData> FilteredSensors = new();
private string _searchText = "";
private string? SelectedLocationFilter = null;
// On remplace le getter auto par une propriété avec setter pour intercepter la saisie
private string SearchText
{
get => _searchText;
set
{
_searchText = value;
_ = ExecuteSearch(); // On relance la recherche SQL
}
}
protected override async Task OnInitializedAsync()
{
await ExecuteSearch();
}
private async Task OnChartClick(SeriesClickEventArgs args)
{
string clickedLocation = args.Category.ToString();
SelectedLocationFilter = SelectedLocationFilter == clickedLocation ? null : clickedLocation;
await ExecuteSearch(); // On relance la recherche SQL
}
// La méthode centrale qui interroge la base de données
private async Task ExecuteSearch()
{
FilteredSensors = await SensorService.SearchSensorsAsync(SelectedLocationFilter, SearchText);
}
Testez ! L'interface est identique pour l'utilisateur, mais derrière, vous venez de diviser la charge mémoire par 100 !
Pour un vrai Dashboard Data, les utilisateurs aiment pouvoir trier les colonnes (ex: voir les températures les plus hautes en premier).
MyDashboard.razor, ajoutez une case à cocher (Checkbox) Bootstrap à côté de la barre de recherche :<InputCheckbox @bind-Value="ShowCriticalOnly" /> Afficher alertes (> 30.0)ExecuteSearch().SearchSensorsAsync) pour accepter ce 3ème paramètre booléen et ajouter le .Where(s => s.Value > 30.0) dynamiquement.SensorTable.razor pour que le mot "Valeur" soit cliquable (un bouton ou un lien).EventCallback nommé OnSortRequested.MyDashboard.razor, captez cet événement et modifiez votre liste FilteredSensors en utilisant LINQ (.OrderBy ou .OrderByDescending).Ce TP marque la transition entre un développeur "Junior" et un Ingénieur Data averti.
@bind:event="oninput") : Permet d'offrir une expérience utilisateur fluide digne des meilleures SPA modernes, la recherche s'affinant à chaque lettre tapée.IEnumerable) vs en Base de données (IQueryable) :
IQueryable) : Indispensable pour le "Big Data". L'ORM (Entity Framework) construit une requête SQL complexe sur mesure en fonction des filtres actifs, et ne rapatrie que le strict nécessaire via le réseau.Module: .Net C# Programming
Duration: 2h
Objective: Implement a responsive search engine by combining multiple filters (Text, Chart). Learn how to switch from in-memory filtering to dynamic SQL filtering (IQueryable) to optimize performance ("Big Data").
Prerequisites:
In classic HTML, you have to type text and then click a "Search" button. In Blazor, we can intercept every keystroke to filter the table instantly.
Components/Pages/MyDashboard.razor.@code block, add a variable to store the searched text:private string SearchText = "";
3. Above the <SensorTable /> component call, add a search bar with a Bootstrap icon.
Note that we use a standard <input> and not an <InputText> to be able to force the oninput event.
<div class="row mb-3">
<div class="col-md-6">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-search"></i></span>
@*
@bind="SearchText" : Binds the value.
@bind:event="oninput" : Updates the variable on every keystroke!
*@
<input type="text"
@bind="SearchText"
@bind:event="oninput"
class="form-control"
placeholder="Search by sensor name..." />
</div>
</div>
</div>
Currently, our FilteredSensors property (created in LAB 9) only looks at the SelectedLocationFilter. We are going to modify it so that it combines the text search AND the chart filter.
MyDashboard.razor, modify the computed property:private List<SensorData> FilteredSensors
{
get
{
// We start with the full list
var query = Sensors.AsEnumerable();
// 1. Apply the "Location" filter (coming from the Radzen chart)
if (!string.IsNullOrEmpty(SelectedLocationFilter))
{
query = query.Where(s => s.Location?.Name == SelectedLocationFilter);
}
// 2. Apply the "Text" filter (coming from the search bar)
if (!string.IsNullOrWhiteSpace(SearchText))
{
// Contains = search "LIKE %text%" (Ignore case with StringComparison)
query = query.Where(s => s.Name.Contains(SearchText, StringComparison.OrdinalIgnoreCase));
}
return query.ToList();
}
}
Test it: Click on a location in the chart, then start typing a sensor name in the bar. The two filters are combined instantly!
IQueryable) (45 min)The current problem: The LoadAll() method fetches the ENTIRE SQL table into RAM (Sensors), then Blazor filters it in the browser. This is unacceptable if the table contains millions of rows.
The solution: Send the filters to the SensorService so that Entity Framework generates a WHERE in SQL. Only the necessary rows will travel across the network.
Services/ISensorService.cs and add this method:Task<List<SensorData>> SearchSensorsAsync(string? locationName, string? searchText);
2. Implement it in Services/SensorService.cs. This is the perfect opportunity to discover IQueryable, which allows building a SQL query step by step before executing it.
public async Task<List<SensorData>> SearchSensorsAsync(string? locationName, string? searchText)
{
// AsQueryable() prepares a query without executing it
IQueryable<SensorData> query = _context.Sensors.Include(s => s.Location).AsQueryable();
// If a location is provided, we add a WHERE to the SQL
if (!string.IsNullOrEmpty(locationName))
{
query = query.Where(s => s.Location.Name == locationName);
}
// If text is provided, we add another WHERE (LIKE) to the SQL
if (!string.IsNullOrEmpty(searchText))
{
query = query.Where(s => s.Name.Contains(searchText));
}
// The SQL execution (SELECT ...) only happens here, with ToListAsync()!
return await query.ToListAsync();
}
3. Updating the UI (MyDashboard.razor):
Since the database does the filtering, we no longer need the full Sensors list or the computed property in memory.
Modify the code to trigger the SQL query on every filter change.
// Replace the old logic with this:
private List<SensorData> FilteredSensors = new();
private string _searchText = "";
private string? SelectedLocationFilter = null;
// We replace the auto getter with a property having a setter to intercept input
private string SearchText
{
get => _searchText;
set
{
_searchText = value;
_ = ExecuteSearch(); // We relaunch the SQL search
}
}
protected override async Task OnInitializedAsync()
{
await ExecuteSearch();
}
private async Task OnChartClick(SeriesClickEventArgs args)
{
string clickedLocation = args.Category.ToString();
SelectedLocationFilter = SelectedLocationFilter == clickedLocation ? null : clickedLocation;
await ExecuteSearch(); // We relaunch the SQL search
}
// The central method that queries the database
private async Task ExecuteSearch()
{
FilteredSensors = await SensorService.SearchSensorsAsync(SelectedLocationFilter, SearchText);
}
Test it! The interface is identical for the user, but behind the scenes, you have just divided the memory load by 100!
For a real Data Dashboard, users like to be able to sort columns (e.g., see the highest temperatures first).
MyDashboard.razor, add a Bootstrap Checkbox next to the search bar:<InputCheckbox @bind-Value="ShowCriticalOnly" /> Show alerts (> 30.0)ExecuteSearch().SearchSensorsAsync) to accept this 3rd boolean parameter and dynamically add .Where(s => s.Value > 30.0).SensorTable.razor component so that the word "Value" is clickable (a button or a link).EventCallback named OnSortRequested.MyDashboard.razor, catch this event and modify your FilteredSensors list using LINQ (.OrderBy or .OrderByDescending).This LAB marks the transition from a "Junior" developer to a knowledgeable Data Engineer.
@bind:event="oninput"): Allows providing a fluid user experience worthy of the best modern SPAs, refining the search with every letter typed.IEnumerable) vs Database Filtering (IQueryable):
IQueryable): Essential for "Big Data". The ORM (Entity Framework) builds a custom complex SQL query based on active filters, and only fetches strictly what is necessary over the network.