Module : Programmation .Net C#
Durée : 2h
Objectif : Utiliser des requêtes d'agrégation (LINQ GroupBy) pour
préparer des données analytiques. Intégrer la librairie Radzen pour créer un tableau de bord
interactif (Graphiques, Jauges, et filtrage croisé).
Pré-requis :
GroupBy).Pour ne pas réinventer la roue, nous utilisons Radzen.Blazor, une librairie open-source très performante pour la visualisation de données. (Référence : Documentation officielle)
dotnet add package Radzen.Blazor
2. Importation globale :
Ouvrez le fichier Components/_Imports.razor et ajoutez :
@using Radzen
@using Radzen.Blazor
3. Configuration du thème :
Ouvrez le fichier Components/App.razor. Dans la balise <head>, ajoutez
le composant de thème Radzen :
<RadzenTheme Theme="material" />
4. Enregistrement des services :
Ouvrez le fichier Program.cs et ajoutez cette ligne :
builder.Services.AddRadzenComponents();
5. Inclusion du script JavaScript :
Toujours dans Components/App.razor, juste avant la fermeture de la balise
</body>, ajoutez :
<script src="_content/Radzen.Blazor/Radzen.Blazor.js?v=@(typeof(Radzen.Colors).Assembly.GetName().Version)"></script>
💡 Remarque : Le paramètre
?v=@(...)est un mécanisme de cache-busting qui force le navigateur à recharger le script après chaque mise à jour du package.
Un graphique a besoin de données agrégées. Nous allons créer une requête LINQ qui sera traduite en
GROUP BY SQL.
Models/SensorData.cs (tout en bas du fichier), ajoutez un petit objet pour
transporter nos statistiques :public class LocationStat
{
public string LocationName { get; set; }
public double AverageValue { get; set; }
}
2. Ouvrez Services/ISensorService.cs et ajoutez :
Task<List<LocationStat>> GetAverageValueByLocationAsync();
3. Implémentez la méthode dans Services/SensorService.cs :
public async Task<List<LocationStat>> GetAverageValueByLocationAsync()
{
// EF Core traduit ceci en : SELECT Location, AVG(Value) FROM Sensors GROUP BY Location
return await _context.Sensors
.Include(s => s.Location)
.GroupBy(s => s.Location.Name)
.Select(g => new LocationStat
{
LocationName = g.Key ?? "Inconnu",
AverageValue = g.Average(s => s.Value)
})
.ToListAsync();
}
Nous allons afficher ce résultat visuellement dans notre Dashboard.
Components/Pages/MyDashboard.razor.@code, ajoutez la variable et chargez les données :private List<LocationStat> LocationStats = new();
protected override async Task OnInitializedAsync()
{
await LoadAll();
LocationStats = await SensorService.GetAverageValueByLocationAsync();
}
3. Dans le HTML (au-dessus de votre SensorTable), ajoutez le graphique :
<div class="row mb-4">
<div class="col-md-8">
<div class="card shadow-sm h-100">
<div class="card-header bg-white">
<h5 class="mb-0"><i class="bi bi-bar-chart-fill text-primary"></i> Températures Moyennes</h5>
</div>
<div class="card-body">
<RadzenChart>
<RadzenColumnSeries Data="@LocationStats" CategoryProperty="LocationName" ValueProperty="AverageValue" Title="Moyenne" Fill="#0d6efd" />
<RadzenValueAxis Formatter="@((value) => Math.Round((double)value, 1) + " °C")" />
<RadzenLegend Visible="false" />
</RadzenChart>
</div>
</div>
</div>
<!-- L'emplacement pour la colonne de droite (Activité 5) -->
</div>
C'est ici que l'approche "Composants" prend tout son sens. Nous allons faire en sorte que cliquer sur une barre du graphique filtre le tableau situé en dessous.
Components/Pages/MyDashboard.razor (bloc @code), ajoutez une
méthode pour réagir au clic sur le graphique :private string? SelectedLocationFilter = null;
// Cette méthode sera appelée par Radzen
private void OnChartClick(SeriesClickEventArgs args)
{
// args.Category contient le nom du lieu cliqué (ex: "Labo")
string clickedLocation = args.Category.ToString();
// Bascule (Toggle) : si on reclique sur le même, on annule le filtre
if (SelectedLocationFilter == clickedLocation)
SelectedLocationFilter = null;
else
SelectedLocationFilter = clickedLocation;
}
// On crée une propriété calculée qui filtre la liste pour le tableau
private List<SensorData> FilteredSensors => SelectedLocationFilter == null
? Sensors
: Sensors.Where(s => s.Location?.Name == SelectedLocationFilter).ToList();
2. Dans le HTML de MyDashboard.razor, connectez l'événement au graphique :
<RadzenChart SeriesClick="@OnChartClick">
3. Mettez à jour votre tableau pour qu'il utilise la liste filtrée au lieu de la liste complète :
<!-- On passe 'FilteredSensors' au lieu de 'Sensors' -->
<SensorTable Sensors="FilteredSensors" OnDeleteClicked="DeleteSensor" />
Testez ! Cliquez sur la barre d'un lieu : le tableau se met à jour instantanément sans recharger la page.
Pour surveiller la valeur maximale critique du réseau, rien ne vaut une jauge. Nous allons l'ajouter à côté du diagramme en barres.
<div class="row mb-4"> de l'activité 3, ajoutez une nouvelle colonne :
<div class="col-md-4">
<div class="card shadow-sm h-100">
<div class="card-header bg-white">
<h5 class="mb-0"><i class="bi bi-speedometer2 text-danger"></i> Valeur Max Détectée</h5>
</div>
<div class="card-body text-center d-flex flex-column justify-content-center">
<RadzenRadialGauge Style="width: 100%; height: 250px;">
<RadzenRadialGaugeScale StartAngle="0" EndAngle="100" Step="20">
<RadzenRadialGaugeScalePointer Value="@Max" Length="0.6" ShowValue="true" />
<!-- Zone verte -->
<RadzenRadialGaugeScaleRange From="0" To="40" Fill="green" />
<!-- Zone orange -->
<RadzenRadialGaugeScaleRange From="40" To="70" Fill="orange" />
<!-- Zone rouge -->
<RadzenRadialGaugeScaleRange From="70" To="100" Fill="red" />
</RadzenRadialGaugeScale>
</RadzenRadialGauge>
</div>
</div>
</div>
Remarque : Nous réutilisons ici la variable @Max que nous avions calculée au TP 6.
En tant que Data Engineer, on vous demande d'ajouter un diagramme de répartition (Pie Chart / Donut).
LocationCountStat (avec LocationName et
Count).
SensorService, créez une méthode GetSensorCountByLocationAsync().
.GroupBy(...) puis
.Select(g => new { ... Count = g.Count() }).Components/Pages/MyDashboard.razor, récupérez ces nouvelles données via le
service.<RadzenDonutSeries /> à l'intérieur d'un <RadzenChart> pour
afficher la répartition du nombre de sondes.
Code squelette pour l'exercice 2 :
<RadzenChart>
<RadzenDonutSeries Data="@CountStats" CategoryProperty="LocationName" ValueProperty="Count">
<TitleTemplate>
<div class="rz-donut-content">
<div>Total</div>
<div>@TotalSondes</div>
</div>
</TitleTemplate>
</RadzenDonutSeries>
</RadzenChart>
Ce TP démontre la puissance de Blazor pour le développement d'outils analytiques internes (BI).
Entity Framework faire le GroupBy côté SQL garantit de très hautes
performances, même avec des millions de lignes.SeriesClick d'un
graphique à une propriété calculée C# (FilteredSensors) permet de créer un
"Cross-Filtering" très fluide, typique des logiciels professionnels comme PowerBI ou Tableau.Module: .Net C# Programming
Duration: 2h
Objective: Use aggregation queries (LINQ GroupBy) to prepare analytical
data. Integrate the Radzen library to create an interactive dashboard (Charts, Gauges, and
cross-filtering).
Prerequisites:
GroupBy).To avoid reinventing the wheel, we use Radzen.Blazor, a very powerful open-source library for data visualization. (Reference: Official documentation)
dotnet add package Radzen.Blazor
2. Global import:
Open the Components/_Imports.razor file and add:
@using Radzen
@using Radzen.Blazor
3. Theme configuration:
Open the Components/App.razor file. In the <head> tag, add the Radzen
theme component:
<RadzenTheme Theme="material" />
4. Service registration:
Open the Program.cs file and add this line:
builder.Services.AddRadzenComponents();
5. JavaScript inclusion:
Still in Components/App.razor, just before the closing </body> tag, add:
<script src="_content/Radzen.Blazor/Radzen.Blazor.js?v=@(typeof(Radzen.Colors).Assembly.GetName().Version)"></script>
💡 Note: The
?v=@(...)parameter is a cache-busting mechanism that forces the browser to reload the script after each package update.
A chart needs aggregated data. We will create a LINQ query that will be translated into a SQL
GROUP BY.
Models/SensorData.cs (at the very bottom of the file), add a small object to
transport our statistics:public class LocationStat
{
public string LocationName { get; set; }
public double AverageValue { get; set; }
}
2. Open Services/ISensorService.cs and add:
Task<List<LocationStat>> GetAverageValueByLocationAsync();
3. Implement the method in Services/SensorService.cs:
public async Task<List<LocationStat>> GetAverageValueByLocationAsync()
{
// EF Core translates this into: SELECT Location, AVG(Value) FROM Sensors GROUP BY Location
return await _context.Sensors
.Include(s => s.Location)
.GroupBy(s => s.Location.Name)
.Select(g => new LocationStat
{
LocationName = g.Key ?? "Unknown",
AverageValue = g.Average(s => s.Value)
})
.ToListAsync();
}
We are going to display this result visually in our Dashboard.
Components/Pages/MyDashboard.razor.@code block, add the variable and load the data:private List<LocationStat> LocationStats = new();
protected override async Task OnInitializedAsync()
{
await LoadAll();
LocationStats = await SensorService.GetAverageValueByLocationAsync();
}
3. In the HTML (above your SensorTable), add the chart:
<div class="row mb-4">
<div class="col-md-8">
<div class="card shadow-sm h-100">
<div class="card-header bg-white">
<h5 class="mb-0"><i class="bi bi-bar-chart-fill text-primary"></i> Average Temperatures</h5>
</div>
<div class="card-body">
<RadzenChart>
<RadzenColumnSeries Data="@LocationStats" CategoryProperty="LocationName" ValueProperty="AverageValue" Title="Average" Fill="#0d6efd" />
<RadzenValueAxis Formatter="@((value) => Math.Round((double)value, 1) + " °C")" />
<RadzenLegend Visible="false" />
</RadzenChart>
</div>
</div>
</div>
<!-- The placeholder for the right column (Activity 5) -->
</div>
This is where the "Components" approach makes total sense. We will make it so that clicking on a chart bar filters the table below it.
Components/Pages/MyDashboard.razor (@code block), add a method to
react to the chart click:private string? SelectedLocationFilter = null;
// This method will be called by Radzen
private void OnChartClick(SeriesClickEventArgs args)
{
// args.Category contains the clicked location name (e.g., "Lab")
string clickedLocation = args.Category.ToString();
// Toggle: if we click on the same one again, we clear the filter
if (SelectedLocationFilter == clickedLocation)
SelectedLocationFilter = null;
else
SelectedLocationFilter = clickedLocation;
}
// We create a computed property that filters the list for the table
private List<SensorData> FilteredSensors => SelectedLocationFilter == null
? Sensors
: Sensors.Where(s => s.Location?.Name == SelectedLocationFilter).ToList();
2. In the HTML of MyDashboard.razor, connect the event to the chart:
<RadzenChart SeriesClick="@OnChartClick">
3. Update your table so it uses the filtered list instead of the full list:
<!-- We pass 'FilteredSensors' instead of 'Sensors' -->
<SensorTable Sensors="FilteredSensors" OnDeleteClicked="DeleteSensor" />
Test it! Click on a location bar: the table updates instantly without reloading the page.
To monitor the critical maximum value of the network, nothing beats a gauge. We will add it next to the bar chart.
<div class="row mb-4"> from activity 3, add a new column:<div class="col-md-4">
<div class="card shadow-sm h-100">
<div class="card-header bg-white">
<h5 class="mb-0"><i class="bi bi-speedometer2 text-danger"></i> Max Detected Value</h5>
</div>
<div class="card-body text-center d-flex flex-column justify-content-center">
<RadzenRadialGauge Style="width: 100%; height: 250px;">
<RadzenRadialGaugeScale StartAngle="0" EndAngle="100" Step="20">
<RadzenRadialGaugeScalePointer Value="@Max" Length="0.6" ShowValue="true" />
<!-- Green zone -->
<RadzenRadialGaugeScaleRange From="0" To="40" Fill="green" />
<!-- Orange zone -->
<RadzenRadialGaugeScaleRange From="40" To="70" Fill="orange" />
<!-- Red zone -->
<RadzenRadialGaugeScaleRange From="70" To="100" Fill="red" />
</RadzenRadialGaugeScale>
</RadzenRadialGauge>
</div>
</div>
</div>
Note: Here we are reusing the @Max variable that we calculated in LAB 6.
As a Data Engineer, you are asked to add a distribution chart (Pie Chart / Donut).
LocationCountStat class (with LocationName and
Count).
SensorService, create a GetSensorCountByLocationAsync() method.
.GroupBy(...) then
.Select(g => new { ... Count = g.Count() }).Components/Pages/MyDashboard.razor, retrieve this new data via the service.<RadzenDonutSeries /> component inside a
<RadzenChart> to display the sensor count distribution.
Skeleton code for exercise 2:
<RadzenChart>
<RadzenDonutSeries Data="@CountStats" CategoryProperty="LocationName" ValueProperty="Count">
<TitleTemplate>
<div class="rz-donut-content">
<div>Total</div>
<div>@TotalSondes</div>
</div>
</TitleTemplate>
</RadzenDonutSeries>
</RadzenChart>
This lab demonstrates the power of Blazor for developing internal analytical tools (BI).
Entity Framework handle the GroupBy on the SQL side guarantees very high
performance, even with millions of rows.
SeriesClick event to a
computed C# property (FilteredSensors) allows you to create very fluid
"Cross-Filtering," typical of professional software like PowerBI or Tableau.