Utiliser un composant Blazor dans une application ASP.NET Core MVC

Blazor est un nouveau framework qui arrive avec ASP.NET Core 3, et qui permets de faire du développement web client en C# au lieu de Javascript. Il propose 2 modèles d’hébergement : un mode client, où les dlls sont envoyées au client avec un runtime .NET, et exécutées grâce à WebAssembly ; et un mode serveur, où le code est exécuté par le serveur, et le client reçoit les mises à jour par une connexion SignalR. Pour plus d’informations, je vous invite à aller lire la documentation de Blazor.

Lorsque vous démarrez un nouveau projet ASP.NET Core3, vous avez la possibilité de créer un projet pur Blazor, ou un projet MVC, mais il est tout à fait possible d’ajouter des composants Blazor à une ou plusieurs page d’un projet MVC.

Note : cet article utilise Blazor en mode server-side, et est basé sur la preview 7 - il peut y avoir des changements dans les prochaines versions.

Migration vers ASP.NET Core 3

Blazor est une nouveauté d’ASP.NET Core 3. Si vous démarrez un nouveau projet ASP.NET Core 3, vous pouvez passer au paragraphe suivant.

Si vous souhaitez utiliser Blazor sur un projet existant sur un framework précédent, il vous faudra d’abord le migrer vers la dernière version : comme d’habitude, lisez la documentation pour effectuer la migration, notamment le passage sur la migration du code de routage pour utiliser .UseEndpoints().

Configuration de Blazor dans le Startup.cs

Pour activer Blazor dans le projet, il y a 2 modifications à faire dans le Startup.cs :

  • Dans la méthode ConfigureServices, ajoutez services.AddServerSideBlazor(); : enregistrement du service Blazor
  • Dans la méthode Configure, allez dans le UseEndoints et ajoutez endpoints.MapBlazorHub(); : configuration du hub SignalR utilisé par Blazor

On se retrouve avec le code qui ressemble à ça :

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
    services.AddServerSideBlazor();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
    }

    app.UseStaticFiles();

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Home}/{action=Index}/{id?}");
        endpoints.MapBlazorHub();
    });
}

Ajout d’un composant Blazor dans une page

Pour mon exemple, j’ai repris le composant Counter du template Blazor, sans l’attribut @page

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    int currentCount = 0;

    void IncrementCount()
    {
        currentCount++;
    }
}

Puis on peux référencer le composant depuis notre vue MVC :

@(await Html.RenderComponentAsync<BlazorInMvc.Components.Counter>(RenderMode.Server))

Vous pouvez lancer la page : vous verrez le composant s’afficher avec le “Current count” à 0, et le bouton. Mais si vous cliquez dessus, rien ne se passe. Il faut ajouter un fichier javascript pour établir la connexion SignalR :

@section Scripts{
    <script src="~/_framework/blazor.server.js"></script>
}

Si l’url de votre page n’est pas à la racine du site, la connexion ne pourra pas s’effectuer correctement. Dans ce cas, il faut rajouter un élément dans l’en-tête de votre page pour préciser le dossier où elle se trouve. Si l’url de ma page est /Home/Counter, le dossier est /Home. J’ajoute donc à ma page :

@section Header{
    <base href="~/Home" />
}

Maintenant vous pouvez retester, et le composant fonctionne : les clics sur le bouton mettent bien à jour le compteur.

Utilisation de la navigation Blazor

À priori, l’utilisation de la navigation Blazor avec le routing MVC risque de poser soucis, mais en étant rigoureux dans la définition de ses routes, on peut faire fonctionner les 2 ensembles.

J’ai 2 pages MVC : /Home et /Home/Counter (même si l’une contient un composant Blazor, c’est toujours une page MVC), et je veux créer 2 pages Blazor : /Nav/Page1 et /Nav/Page2, qui fonctionneront en mode SPA lorsqu’on navigue de l’une à l’autre (et seulement entre ces 2).

Pour faire ça, on va créer un contrôleur qui recevra les requêtes vers les pages Blazor, et on configure le routing pour que toutes les pages sous l’url /Nav pointent vers la même Action.

NavController.cs :

public class NavController : Controller
{
    public IActionResult Index()
    {
        return View();
    }
}

Startup.cs :

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(
        name: "blazor",
        pattern: "Nav/{page?}/{id?}",
        defaults: new { controller = "Nav", action = "Index" }
    );

    endpoints.MapControllerRoute(
        name: "default",
        pattern: "{controller=Home}/{action=Index}/{id?}");

    endpoints.MapBlazorHub();
});

L’action Index va contenir un composant Blazor, qui servira de routeur pour les 2 pages :

Nav.razor :

<Router AppAssembly="typeof(Startup).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="routeData" />
    </Found>
    <NotFound>
        <h1>Page not found</h1>
    </NotFound>
</Router>

Puis les composants Blazor correspondant à nos pages :

@page "/Nav/Page1"
@using Microsoft.AspNetCore.Components.Routing

<h3>Page1</h3>

<NavLink href="/Nav/Page2">Aller à la page 2</NavLink>

et

@page "/Nav/Page2"
@using Microsoft.AspNetCore.Components.Routing

<h3>Page2</h3>

<NavLink href="/Nav/Page1">Aller à la page 1</NavLink>

Les composants commencent par l’attribut @page, qui permets de définir à quelle url le composant doit répondre.

Maintenant vous pouvez passer de la page1 à la page2 sans avoir un rechargement complet de la page, la navigation se fait par la connexion SignalR. Si vous naviguer vers ou depuis une autre page, la navigation est une navigation html classique.

Code source

Vous pouvez récupérer le code source de cet article sur ma page Github : https://github.com/glacasa/Blog-BlazorInMvc.

Pour le compiler, vous devez avoir la dernière version de Visual Studio 2019 preview, ainsi que le framework dotnet core 3 preview 9 minimum.

packages.config vs PackageReference - le fonctionnement de Nuget avec le nouveau format csproj

Si vous développez un projets sur le framework .NET “classique”, et que vous avez décidé de passer à .NET Standard pour les projets liés, vous avez peut-être eu droit à une erreur FileNotFoundException au chargement d’une dll (utilisée par votre projet .NET Standard) - vous obligeant à référencer le package Nuget en question directement depuis l’application appellante.

Depuis quelques temps, les développeurs .NET peuvent utiliser un nouveau format de fichier .csproj (et pour les développeurs Visual Basic, tout ce que j’explique ici à propos des csproj s’applique aussi aux vbproj), dans les projets .NET Core ou .NET Standard.
Voici par exemple les 2 formats de csproj, l’un étant un projet console en .NET classique, l’autre en .NET Core :

Comparaison des anciens et nouveaux formats csproj

Outre le gain évident en terme de lisibilité, le nouveau format apporte une nouvelle gestion des packages Nuget. Voyons comment tout ça fonctionne maintenant.

L’ancien fonctionnement : le fichier packages.config

Si vous êtes développez avec l’ancien format csproj, lorsque vous référencez un package Nuget, il est ajouté dans le fichier packages.config, qui se trouve à la racine du projet.

<?xml version="1.0" encoding="utf-8"?>
<packages>
    <package id="Newtonsoft.Json" version="12.0.1" targetFramework="net471" />
</packages>

Si le package en référence d’autres, toutes les dépendances seront ajoutées dans ce fichier. On peut donc très vite arriver à un assez grand nombre de packages référencés ici.

Le fonctionnement de Nuget est ensuite très simple : il va télécharger le package et le mettre dans un dossier packages, situé au même niveau que le sln. Il va ensuite modifier le csproj pour ajouter la référence vers la ou les dll. Ces dll seront ensuite copiées dans le répertoire de destination en même temps que le projet.

Le nouveau fonctionnement :

La refonte du format csproj a été l’occasion d’améliorer la gestion des packages, et donc de gérer Nuget nativement. Cette fois, plus de fichier packages.config, si vous référencez un package il est directement ajouté dans le csproj en tant que package :

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>netcoreapp2.1</TargetFramework>
    </PropertyGroup>
    <ItemGroup>
        <PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
    </ItemGroup>
</Project>

Contrairement à l’ancien csproj, où toutes les références étaient des liens vers des dll, ici on sait que la référence vient de Nuget quand on utilise la balise <PackageReference>.

Autre différence avec l’ancien format : nous n’avons plus le dossier packages. Maintenant, les dll sont téléchargées dans le profil utilisateur (allez jeter un œil dans %userprofile%\.nuget\packages sur votre disque dur pour voir la liste), et elles ne sont pas copiées dans l’output lors de la compilation.

Utiliser les deux à la fois

Si vous avez des projets .NET Standard référencés par un projet .NET classique, vous allez vous retrouvez avec les 2 systèmes à la fois, qui ne sont pas vraiment compatibles, et c’est ce qui va causer la FileNotFoundException.

Supposons que votre projet .NET Standard référence Newtonsoft.Json. Il utilise le nouveau Nuget, et ne va pas copier la dll dans l’output.
Votre projet .NET classique lui utilise toujours l’ancien système de références de projets : il va récupérer l’output de l’autre projet, mais ne récupèreras donc pas Newtonsoft.Json. Et donc, lorsque vous aurez besoin de sérialiser des données à l’exécution, ça plante.

System.IO.FileNotFoundException

Un moyen simple de réparer ce problème est de simplement ajouter le package Nuget dans le projet principal, pour être sûr qu’il le récupère. Mais il y a mieux.

Utiliser PackageReference dans l’ancien csproj.

En fait, il est possible d’utiliser les dans les anciens csproj, la documentation se trouve ici : [Migrate from packages.config to PackageReference](https://docs.microsoft.com/en-us/nuget/reference/migrate-packages-config-to-package-reference).

Attention, ça ne fonctionne que dans Visual Studio 2017 (v 15.7 minimum), et les projets C++ et ASP.NET ne sont pas encore supportés.
Pour utiliser PackageReference, il suffit d’ajouter <RestoreProjectStyle>PackageReference</RestoreProjectStyle> dans le csproj :

</PropertyGroup>
    <RestoreProjectStyle>PackageReference</RestoreProjectStyle>
</PropertyGroup>

Attention, si vous avez déjà un packages.config, il faut désinstaller tous les packages avant de les remettre. Visual Studio 2017, vous propose de le faire automatiquement, il suffit de faire un clic droit sur le fichier packages.config, et cliquer sur “Migrate packages.config to PackageReference”.

Migration de packages.config vers PackageReference

Si tout se passe bien, ça marchera du premier coup (sinon bon courage pour débuguer les conflits de version). Une fois sur le nouveau Nuget, vous ne devriez plus avoir de soucis de fichiers manquants.

Générer des documents xlsx avec le OpenXml SDK

Depuis que je fais des applications métier, il y a une tâche qu’on me demande régulièrement : exporter des données dans Excel. Pour faire ça, il y a plusieurs solutions, plus ou moins simples, chacune avec avantages ou inconvénients.
On peux par exemple : utiliser des outils externes (Telerik, SSRS), faire de l’interrop Excel, ou générer un simple fichier texte au format CSV qui sera compris sans trop de problème avec le logiciel.

J’ai décidé de mon coté de me lancer dans la solution qui n’est pas la plus simple, mais qui me semble la plus propre : générer directement le fichier xlsx à l’aide du Open XML SDK ; avec l’avantage de pouvoir obtenir directement un fichier natif sans s’encombrer de dépendances, mais un développement plus compliqué car on va devoir travailler directement sur la structure du fichier OpenXML. Le SDK est compatible avec le framework .NET depuis la version 3.5, et avec .NET Standard 1.3, donc il devrait fonctionner dans à peu près tous vos projets C#.

Ici on fait un simple export de données, ça reste relativement simple puisqu’il va nous suffir de remplir des cases ; ça sera autre chose si on veut utiliser des fonctionnalités plus avancées d’Excel. Pour mon code, je me suis basé sur ce post de blog, qui explique comment démarrer : Creating a simple XLSX from scratch using the Open XML SDK.

Génération du fichier Excel

On commence donc par installer le Open XML SDK, qui se trouve maintenant sur Nuget : DocumentFormat.OpenXml, puis on va créer notre fichier :

using (var package = SpreadsheetDocument.Create(@"C:\\Temp\\Export.xlsx", SpreadsheetDocumentType.Workbook))
{
    // Initialisation du document
    var workbookPart = package.AddWorkbookPart();

    var workbook = new Workbook();
    workbook.AddNamespaceDeclaration("r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships");
    workbookPart.Workbook = workbook;

    var sheets = new Sheets();
    workbook.Append(sheets);

    // Ajout du premier onglet
    var sheet = new Sheet() { Name = "Onglet1", SheetId = 1, Id = "rId1" };
    sheets.Append(sheet);
    var worksheetPart = workbookPart.AddNewPart<WorksheetPart>("rId1");
    var worksheet = new Worksheet();
    var sheetData = new SheetData();
    worksheet.Append(sheetData);
    worksheetPart.Worksheet = worksheet;
}

Ce code là est le stric minimum pour avoir un document qui s’ouvre dans Excel sans erreur : on initialise le document, on crée une liste d’onglets, on ajoute un premier onglet et on le prépare à recevoir des données.

Ensuite, on va pouvoir remplir les cases :

    // Première ligne
    var row = new Row();
    sheetData.Append(row);

    // Ajout d'une cellule à la ligne
    Cell cell = new Cell()
    {
        CellReference = "A1",
        DataType = CellValues.InlineString
    };
    InlineString inlineString = new InlineString();
    Text text = new Text();
    text.Text = "Bonjour Excel !";
    inlineString.Append(text);
    cell.Append(inlineString);
    row.Append(cell);

Et voilà, la première case est remplie. Il vous suffit de faire ça pour chaque case et c’est bon !
Le format fonctione par ligne (Row), donc tant que vous êtes sur la même ligne (A1, B1, C1) vous ajoutez dans le même row, et dès que vous allez à la ligne vous en créez un nouveau.

Mutualisation du code

Maintenant que ça c’est fait, je me suis dit que c’était pas la peine de le refaire à chaque nouveau projet. J’ai donc créé un package Nuget qui permets de faire ça plus facilement. Il suffit de lui envoyer une liste d’objets ou le résultat d’une requête SQL, et ça génère le fichier.

Le package Nuget est ici, et le code source est là. Ça permets d’exporter au format CSV ou XLSX. D’ailleurs au passage, j’en profite pour demander de l’aide, si quelqu’un sait comment générer des fichiers OpenDocument pour LibreOffice, ça m’intéresse !

Pour générer vos fichier, vous pouvez ajouter des attributs ExportColumn sur vos classes :

public class SampleData
{
    [ExportColumn(Title = "Number", Order = 1)]
    public int IntData { get; set; }

    [ExportColumn(Title = "Text", Order = 2)]
    public string TextData { get; set; }
}

Puis les envoyer dans le DataExporter :

var data = new List<SampleData>()
{
    new SampleData{ IntData=5, TextData="Hello"},
    new SampleData{ IntData=20, TextData="Yoo"},
    new SampleData{ IntData=10, TextData="This is some text"},
};

var xlsxExporter = new XlsxDataExporter();
var xlsxResult = xlsxExporter.Export(data);

Si vous voulez juste exporter le résultat d’une requête SQL sans le mapper, vous pouvez aussi :

using (SqlConnection connection = new SqlConnection(connectionString))
{
    SqlCommand command = new SqlCommand("SELECT OrderID, CustomerID FROM dbo.Orders", connection);
    connection.Open();
    SqlDataReader reader = command.ExecuteReader();
    var xlsxExporter = new XlsxDataExporter();
    var xlsxResult = xlsxExporter.Export(reader);
}

Le résultat est un MemoryStream, que vous pouvez enregistrer directement dans un fichier, ou retourner comme résultat d’une action dans un site MVC.

J’espère que tout ça vous sera utile. J’ai écrit l’API pour mon propre usage, n’hésitez pas à tester et me faire des retours car ça peut encore évoluer.