🌐
🔍 100%
👁️

🎙️ Sélectionner une voix

🎙️ Select a voice

📊 TP 10 : Recherche & Filtrage Multi-Critères (Temps Réel)

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 :

  • Avoir terminé le TP 9 (Le graphique filtrant le tableau par "Lieu" doit être fonctionnel).

Activité 1 : La Barre de Recherche Textuelle (Temps Réel) (25 min)

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.

  1. Ouvrez Components/Pages/MyDashboard.razor.
  2. Dans le bloc @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>

Activité 2 : Le Filtre Multi-Critères (Mémoire) (20 min)

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.

  1. Toujours dans 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 !


Activité 3 : Optimisation Data - Le filtrage Côté Serveur (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.

  1. Ouvrez 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 !


🚀 Exercices d'application (En autonomie)

Pour un vrai Dashboard Data, les utilisateurs aiment pouvoir trier les colonnes (ex: voir les températures les plus hautes en premier).

Exercice 1 : Ajout d'un filtre "Uniquement les valeurs critiques"

  1. Dans MyDashboard.razor, ajoutez une case à cocher (Checkbox) Bootstrap à côté de la barre de recherche :
    <InputCheckbox @bind-Value="ShowCriticalOnly" /> Afficher alertes (> 30.0)
  2. Interceptez le changement de cette case pour rappeler ExecuteSearch().
  3. Mettez à jour le service (SearchSensorsAsync) pour accepter ce 3ème paramètre booléen et ajouter le .Where(s => s.Value > 30.0) dynamiquement.

Exercice 2 : Tri dynamique (Sorting)

  1. Modifiez l'en-tête du tableau dans le composant SensorTable.razor pour que le mot "Valeur" soit cliquable (un bouton ou un lien).
  2. Au clic, déclenchez un EventCallback nommé OnSortRequested.
  3. Dans MyDashboard.razor, captez cet événement et modifiez votre liste FilteredSensors en utilisant LINQ (.OrderBy ou .OrderByDescending).

🏁 Synthèse du TP 10

Ce TP marque la transition entre un développeur "Junior" et un Ingénieur Data averti.

📊 LAB 10: Multi-Criteria Search & Filtering (Real-Time)

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:

  • Have completed LAB 9 (The chart filtering the table by "Location" must be functional).

Activity 1: The Text Search Bar (Real-Time) (25 min)

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.

  1. Open Components/Pages/MyDashboard.razor.
  2. In the @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>

Activity 2: The Multi-Criteria Filter (In-Memory) (20 min)

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.

  1. Still in 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!


Activity 3: Data Optimization - Server-Side Filtering (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.

  1. Open 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!


🚀 Practice Exercises (On your own)

For a real Data Dashboard, users like to be able to sort columns (e.g., see the highest temperatures first).

Exercise 1: Adding a "Critical values only" filter

  1. In MyDashboard.razor, add a Bootstrap Checkbox next to the search bar:
    <InputCheckbox @bind-Value="ShowCriticalOnly" /> Show alerts (> 30.0)
  2. Intercept the change of this checkbox to recall ExecuteSearch().
  3. Update the service (SearchSensorsAsync) to accept this 3rd boolean parameter and dynamically add .Where(s => s.Value > 30.0).

Exercise 2: Dynamic Sorting

  1. Modify the table header in the SensorTable.razor component so that the word "Value" is clickable (a button or a link).
  2. On click, trigger an EventCallback named OnSortRequested.
  3. In MyDashboard.razor, catch this event and modify your FilteredSensors list using LINQ (.OrderBy or .OrderByDescending).

🏁 LAB 10 Summary

This LAB marks the transition from a "Junior" developer to a knowledgeable Data Engineer.