Suite et fin d'une série d'articles sur Blazor.
Si vous avez manqué les deux premiers :
Il s'agit ici de développer un logiciel musical.
Vous trouverez le code de la solution ici.
Par défaut, les projets utilisent le répertoire Ma musique(My music) configurable par l'utilisateur.
Vous pouvez faire pointer ce répertoire spécial sur le répertoire de votre choix.
Si vous préférez cela, remplacez dans la solution "Environment.GetFolderPath(Environment.SpecialFolder.MyMusic)" par le chemin de votre répertoire de musique.
Une fois le répertoire de musique créé, Il faut exécuter le projet de type console "Blazor.Song.Indexer". Ce dernier va générer un ficher tracks.json dans le répertoire du projet Blazor.Song.Net.Server. L'exécution prendra plus ou moins de temps en fonction du nombre de fichiers musicaux que vous possédez.
Vous pouvez maintenant exécuter le projet serveur et écouter vos morceaux musicaux.
Blazor.Song.Net.Client est le projet qui contient notre site web côté client.
Et donc le code Blazor.
Il est à noter qu'il est possible de faire du code Blazor côté serveur mais ça n'est pas le cas ici.
Le code source n'a pas pour but d'être un exemple de bonnes pratiques, c'est avant tout une démonstration du fonctionnement de la technologie Blazor.
Dans le projet Blazor.Song.Net.Client, les différents répertoires qui nous intéressent sont :
C'est le répertoire classique pour les fichiers statiques, tels que les fichiers html, css et js.
Contient des fichiers razor qui correspondent aux pages du site.
On entend page au sens SPA (Single Page Application). C'est en fait une même seule page web qui est chargée et affichée dans le navigateur. Les pages sont ici des url différentes qui fonctionnent via un système de Router que nous étudierons plus tard.
Contient des fichiers razor qui sont utilisés par des pages ou servent à définir le layout principal du site.
On y trouve aussi des classes C# qui sont utilisés par des pages cshtml.
Blazor est un framework de type SPA.
Il est principalement architecturé autour d'un principe de composants
Exemple : ./Shared/Player.cshtml
@using Blazor.Song.Net.Client.Wrap;
@using Blazor.Song.Net.Shared;
@using System.Linq;
@inject HttpClient Http;
@inject Services.IDataManager Data;
<div name="player" class="content">
<div name="playerInfoPanel" class="frame">
<PlayerAudio ref="playerAudio" bind-IsPlaying="@IsPlaying" />
<PlayerInfo ref="playerInfo" PlayerAudio="@playerAudio" />
</div>
[...]
</div>
@functions {
PlayerAudio playerAudio;
PlayerInfo playerInfo;
[...]
public void SetCurrentTrackNext()
{
if (PlaylistTracks.Count <= 1)
return;
Data.CurrentTrack = PlaylistTracks[(PlaylistTracks.IndexOf(Data.CurrentTrack) + 1) % PlaylistTracks.Count];
}
}
Le composant est écrit en razor et séparé en 2 parties :
Le composant possède plusieurs méthodes spécifiques que l'on peut surcharger.
public override void SetParameters(ParameterCollection parameters)
{
Console.WriteLine("SetParameters");
base.SetParameters(parameters);
}
protected async override Task OnInitAsync()
{
Console.WriteLine("OnInitAsync");
await base.OnInitAsync();
}
protected async override Task OnParametersSetAsync()
{
Console.WriteLine("OnParametersSetAsync");
await base.OnParametersSetAsync();
}
protected override bool ShouldRender()
{
Console.WriteLine("ShouldRender");
return base.ShouldRender();
}
protected async override Task OnAfterRenderAsync()
{
Console.WriteLine("OnAfterRenderAsync");
await base.OnAfterRenderAsync();
}
Elles se déclenchent dans l'ordre suivant sur un premier chargement :
[Initialisation du composant]
[Chargement du composant]
Elles se déclenchent dans l'ordre suivant sur le rafraîchissement du composant :
[Initialisation du rafraichissement composant]
[Rafraîchissement du composant]
Appelée avant que les paramètres soient définis. Le code personnalisé peut redéfinir la valeur d'un ou de plusieurs paramètres.
Appelée en synchrone/asynchrone, méthode appelée après l'initialisation du composant.
Appelé après que les paramètres aient été définis.
Appelé lors d'un rafraîchissement du composant, notamment via StateHasChanged.
Retourne un booléen qui indique si le composant doit être rafraîchi.
Appelé en synchrone/asynchrone, après le chargement du composant et après chaque rafraîchissement du composant, notamment via StateHasChanged.
Comment ça marche
Dans ./Shared/Player.cshtml
<PlayerAudio ref="playerAudio" bind-IsPlaying="@IsPlaying" />
<PlayerInfo ref="playerInfo" PlayerAudio="@playerAudio" />
Dans ./Shared/PlayerAudio.cshtml
[Parameter]
bool IsPlaying
{
get;
set;
}
[Parameter]
private Action<bool> IsPlayingChanged { get; set; }
[CascadingParameter]
ObservableList<TrackInfo> PlaylistTracks { get; set; }
Dans ./Shared/PlayerInfo.cshtml
[Parameter]
PlayerAudio PlayerAudio { get; set; }
Les composants peuvent posséder des paramètres définis en attributs.
L'attribut C# "Parameter" permet de spécifier une propriété qui correspond à un attribut à instancier lors de l'utilisation du composant.
On peut le définir de manière classique ou via "bind-[Mon paramètre]" en y associant une action nommée [Mon paramètre]Changed.
RenderFragment
On peut également définir des morceaux de Razor en tant que "Parameter" grâce à l'objet "RenderFragment".
Dans ./Shared/SongList.cshtml
<div name="songList" class="table-container frame">
<div class="table-scroll">
<table class="table is-hoverable is-fullwidth is-narrow">
[...]
@if (Tracks != null)
{
foreach (TrackInfo track in Tracks)
{
@RowTemplate(track)
}
}
</tbody>
</table>
</div>
</div>
@functions {
[...]
[Parameter]
RenderFragment<TrackInfo> RowTemplate { get; set; }
}
Ici, l'élément RowTemplate peut être défini de façon différente par le parent du composant SongList.
Le mot clé spécifique "context" permet de spécifier un objet utilisé par le paramètre.
Cet objet "context" est du type générique définit par le paramètre, ici de type TrackInfo.
Ceci permet, pour notre playlist ou bibliothèque, d'avoir des lignes affichées différement tout en appelant le même composant SongList :
Dans ./Shared/Playlist.cshtml
<SongList Tracks="@PlaylistTracks.ToList()" CurrentTrack="@Data.CurrentTrack">
<RowTemplate>
@{
string current = "";
if (context.Id == Data.CurrentTrack?.Id)
{
current = "current";
}
}
<tr ondblclick="@(e => PlaylistRowDoubleClick(context.Id))" class="playlistRow @current">
<td class="info" onclick="@(e => PlaylistRowClick(context.Id))">@context.Title</td>
<td class="info">
<div class="columns">
<NavLink href="@("library/artist:\"%2F^" + context.Artist + "$%2F\"")" class="column is-narrow">
<i class="fa fa-search"></i>
</NavLink>
<div class="column auto" onclick="@(e => PlaylistRowClick(context.Id))">@context.Artist</div>
</div>
</td>
<td class="info" onclick="@(e => PlaylistRowClick(context.Id))">@context.Duration.ToString("mm\\:ss")</td>
<td><button class="column button is-info" onclick="@(e => PlaylistRowRemoveClick(context.Id))"><i class="fa fa-times"></i></button></td>
</tr>
</RowTemplate>
</SongList>
Dans ./Shared/Library.cshtml
<SongList Tracks="@TrackListFiltered" CurrentTrack="@CurrentTrack">
<RowTemplate>
<tr ondblclick="@(e => DoubleclickPlaylistRow(context))" class="libraryRow">
<td>@context.Title</td>
<td>@context.Artist</td>
<td>@context.Duration.ToString("mm\\:ss")</td>
</tr>
</RowTemplate>
</SongList>
Dans ./Shared/MainLayout.cshtml
<CascadingValue Value="@PlaylistTracks">
<Player />
[...]
</CascadingValue>
@functions {
ObservableList<TrackInfo> PlaylistTracks { get; set; } = new ObservableList<TrackInfo>();
[...]
}
Dans ./Shared/Player.cshtml
<div name="player" class="content">
<div name="playerInfoPanel" class="frame">
<PlayerAudio ref="playerAudio" bind-IsPlaying="@IsPlaying" />
<PlayerInfo ref="playerInfo" PlayerAudio="@playerAudio" />
</div>
</div>
@functions {
[...]
[CascadingParameter]
ObservableList<TrackInfo> PlaylistTracks { get; set; }
[...]
}
Dans ./Shared/PlayerAudio.cshtml
[CascadingParameter]
ObservableList<TrackInfo> PlaylistTracks { get; set; }
Comme on le voit, un élément razor qui contient "<CascadingValue Value="@PlaylistTracks">" avec une propriété PlaylistTracks correspondante pourra partager la référence à tous les composants enfants de l'élément CascadingValue. Dans notre cas, le composant "Player" mais également les composants appelés en cascade tels que PlayerAudio et PlayerInfo. Chaque composant concerné est libre d'implémenter ou non la CascadingValue PlaylistTracks.
Blazor permet d'utiliser les événements natifs des éléments HTML. On peut appeler une méthode C# via ces événements.
Dans ./Shared/PlayerInfo.cshtml, evenement onclick
<progress id="songProgress" name="songProgress" class="progress is-primary trackProgress" value="@(TimeStatus.ToString())" max="100" onclick="@ProgressClick"></progress>
[...]
async void ProgressClick(UIMouseEventArgs e)
{
if (CurrentTrack == null)
return;
Wrap.Element element = new Wrap.Element("songProgress");
int offsetWidth = await element.GetOffsetWidth();
long newTime = (int)e.ClientX * ((int)CurrentTrack.Duration.TotalSeconds) / offsetWidth;
await PlayerAudio.SetTime((int)newTime);
}
Il existe une multitude d'événements tels que onclick, ondbleclick, onabort, oncopy, ondragenter, onwheel,...
Dans ./Shared/MainLayout.cshtml
<div class="main hero is-fullheight">
<div>
<CascadingValue Value="@PlaylistTracks">
<Player />
<div class="columns">
<NavLink href="playlist" class="button column">
Playlist
</NavLink>
<NavLink href="library" class="button column" Match=NavLinkMatch.Prefix>
Bibliothèque
</NavLink>
<NavLink href="settings" class="button column">
Paramètres
</NavLink>
</div>
@Body
</CascadingValue>
</div>
</div>
Le routeur permet de définir des pages qui correspondent à des liens.
On établit le lien avec l'élément NavLink en spécifiant l'adresse dans l'attribut href. On peut définir des options de correspondance avec l'attribut Match.
Dans ./Shared/Library.cshtml
@page "/library"
@page "/library/{Filter}"
[...]
@functions {
[Parameter]
string Filter {get;set;}
}
Dans la page correspondante, on définit une propriété "@page". On peut définir un "Parameter" dans le patron de la page, ce qui permet dans notre cas de définir dans l'adresse le filtre de recherche de notre bibliothèque.
Dans ./Services/DataManager.cs
public class DataManager : IDataManager
{
[...]
public DataManager(HttpClient client)
{
_client = client;
}
[...]
}
On créé tout d'abord notre objet qui implémente une interface. Il doit posséder un constructeur avec HttpClient en paramètre.
Dans ./Startup.cs
services.AddSingleton<IDataManager, DataManager>();
On déclare dans la méthode ConfigureServices l'objet à instancier comme un singleton.
Dans ./Page/Playlist.cshtml (Et presque tous les fichiers cshtml du projet)
@inject Services.IDataManager Data;
Notre instance est utilisable à travers chaque fichier cshtml en utilisant une propriété "@inject".
Ceci vous donne un aperçu de ce qu'il est possible de faire aujourd'hui avec Blazor. On peut voir que tout ce qui est nécessaire pour créer une application SPA est disponible.
Bonne écoute.
Commentaires :
Aucun commentaires pour le moment
Laissez un commentaire :