Version débuggée fonctionnelle du projet jusqu'à la suppression d'un item

master
Johnny RATTON 1 year ago
parent d4421d6d7e
commit 5de6a1565f

@ -1,12 +1,14 @@
<Router AppAssembly="@typeof(App).Assembly"> <CascadingBlazoredModal>
<Found Context="routeData"> <Router AppAssembly="@typeof(App).Assembly">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /> <Found Context="routeData">
<FocusOnNavigate RouteData="@routeData" Selector="h1" /> <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found> <FocusOnNavigate RouteData="@routeData" Selector="h1" />
<NotFound> </Found>
<PageTitle>Not found</PageTitle> <NotFound>
<LayoutView Layout="@typeof(MainLayout)"> <PageTitle>Not found</PageTitle>
<p role="alert">Sorry, there's nothing at this address.</p> <LayoutView Layout="@typeof(MainLayout)">
</LayoutView> <p role="alert">Sorry, there's nothing at this address.</p>
</NotFound> </LayoutView>
</Router> </NotFound>
</Router>
</CascadingBlazoredModal>

@ -8,12 +8,14 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Blazored.LocalStorage" Version="4.4.0" /> <PackageReference Include="Blazored.LocalStorage" Version="4.4.0" />
<PackageReference Include="Blazored.Modal" Version="7.1.0" />
<PackageReference Include="Blazorise.Bootstrap" Version="1.3.3" /> <PackageReference Include="Blazorise.Bootstrap" Version="1.3.3" />
<PackageReference Include="Blazorise.DataGrid" Version="1.3.3" /> <PackageReference Include="Blazorise.DataGrid" Version="1.3.3" />
<PackageReference Include="Blazorise.Icons.FontAwesome" Version="1.3.3" /> <PackageReference Include="Blazorise.Icons.FontAwesome" Version="1.3.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.0" PrivateAssets="all" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

@ -0,0 +1,47 @@
using BlazorProject.Models;
namespace BlazorProject.Factories;
public static class ItemFactory
{
public static ItemModel ToModel(Item item, byte[] imageContent)
{
return new ItemModel
{
Id = item.Id,
DisplayName = item.DisplayName,
Name = item.Name,
RepairWith = item.RepairWith,
EnchantCategories = item.EnchantCategories,
MaxDurability = item.MaxDurability,
StackSize = item.StackSize,
ImageContent = imageContent
};
}
public static Item Create(ItemModel model)
{
return new Item
{
Id = model.Id,
DisplayName = model.DisplayName,
Name = model.Name,
RepairWith = model.RepairWith,
EnchantCategories = model.EnchantCategories,
MaxDurability = model.MaxDurability,
StackSize = model.StackSize,
CreatedDate = DateTime.Now
};
}
public static void Update(Item item, ItemModel model)
{
item.DisplayName = model.DisplayName;
item.Name = model.Name;
item.RepairWith = model.RepairWith;
item.EnchantCategories = model.EnchantCategories;
item.MaxDurability = model.MaxDurability;
item.StackSize = model.StackSize;
item.UpdatedDate = DateTime.Now;
}
}

@ -0,0 +1,10 @@
<div class="simple-form">
<p>
Are you sure you want to delete @item.DisplayName ?
</p>
<button @onclick="ConfirmDelete" class="btn btn-primary">Delete</button>
<button @onclick="Cancel" class="btn btn-secondary">Cancel</button>
</div>

@ -0,0 +1,37 @@
using Blazored.Modal;
using Blazored.Modal.Services;
using BlazorProject.Models;
using BlazorProject.Services;
using Microsoft.AspNetCore.Components;
namespace BlazorProject.Modals;
public partial class DeleteConfirmation
{
[CascadingParameter]
public BlazoredModalInstance ModalInstance { get; set; }
[Inject]
public IDataService DataService { get; set; }
[Parameter]
public int Id { get; set; }
private Item item = new Item();
protected override async Task OnInitializedAsync()
{
// Get the item
item = await DataService.GetById(Id);
}
void ConfirmDelete()
{
ModalInstance.CloseAsync(ModalResult.Ok(true));
}
void Cancel()
{
ModalInstance.CancelAsync();
}
}

@ -1,5 +1,6 @@
using Blazored.LocalStorage; using Blazored.LocalStorage;
using BlazorProject.Models; using BlazorProject.Models;
using BlazorProject.Services;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms; using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
@ -17,6 +18,8 @@ public partial class Add
[Inject] [Inject]
public IWebAssemblyHostEnvironment WebHostEnvironment { get; set; } public IWebAssemblyHostEnvironment WebHostEnvironment { get; set; }
[Inject]
public IDataService DataService { get; set; }
/// <summary> /// <summary>
/// The default enchant categories. /// The default enchant categories.
/// </summary> /// </summary>
@ -38,43 +41,9 @@ public partial class Add
private async void HandleValidSubmit() private async void HandleValidSubmit()
{ {
// Get the current data await DataService.Add(itemModel);
var currentData = await LocalStorage.GetItemAsync<List<Item>>("data");
// Simulate the Id
itemModel.Id = currentData.Max(s => s.Id) + 1;
// Add the item to the current data
currentData.Add(new Item
{
Id = itemModel.Id,
DisplayName = itemModel.DisplayName,
Name = itemModel.Name,
RepairWith = itemModel.RepairWith,
EnchantCategories = itemModel.EnchantCategories,
MaxDurability = itemModel.MaxDurability,
StackSize = itemModel.StackSize,
CreatedDate = DateTime.Now
});
// Save the image
var imagePathInfo = new DirectoryInfo($"{WebHostEnvironment.BaseAddress}/images");
// Check if the folder "images" exist
if (!imagePathInfo.Exists)
{
imagePathInfo.Create();
}
// Determine the image name
var fileName = new FileInfo($"{imagePathInfo}/{itemModel.Name}.png");
// Write the file content
await File.WriteAllBytesAsync(fileName.FullName, itemModel.ImageContent);
// Save the data NavigationManager.NavigateTo("list");
await LocalStorage.SetItemAsync("data", currentData);
NavigationManager.NavigateTo("List.razor");
} }
private async Task LoadImage(InputFileChangeEventArgs e) private async Task LoadImage(InputFileChangeEventArgs e)

@ -0,0 +1,82 @@
@page "/edit/{Id:int}"
<h3>Edit</h3>
<EditForm Model="@itemModel" OnValidSubmit="@HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<p>
<label for="display-name">
Display name:
<InputText id="display-name" @bind-Value="itemModel.DisplayName" />
</label>
</p>
<p>
<label for="name">
Name:
<InputText id="name" @bind-Value="itemModel.Name" />
</label>
</p>
<p>
<label for="stack-size">
Stack size:
<InputNumber id="stack-size" @bind-Value="itemModel.StackSize" />
</label>
</p>
<p>
<label for="max-durability">
Max durability:
<InputNumber id="max-durability" @bind-Value="itemModel.MaxDurability" />
</label>
</p>
<p>
Enchant categories:
<div>
@foreach (var item in enchantCategories)
{
<label>
<input type="checkbox" @onchange="@(e => OnEnchantCategoriesChange(item, e.Value))" checked="@(itemModel.EnchantCategories.Contains(item) ? "checked" : null)" />@item
</label>
}
</div>
</p>
<p>
Repair with:
<div>
@foreach (var item in repairWith)
{
<label>
<input type="checkbox" @onchange="@(e => OnRepairWithChange(item, e.Value))" checked="@(itemModel.RepairWith.Contains(item) ? "checked" : null)" />@item
</label>
}
</div>
</p>
<p>
<label>
Current Item image:
@if (File.Exists($"{WebHostEnvironment.BaseAddress}/images/{itemModel.Name}.png"))
{
<img src="images/@(itemModel.Name).png" class="img-thumbnail" title="@itemModel.DisplayName" alt="@itemModel.DisplayName" style="max-width: 150px"/>
}
else
{
<img src="images/default.png" class="img-thumbnail" title="@itemModel.DisplayName" alt="@itemModel.DisplayName" style="max-width: 150px"/>
}
</label>
</p>
<p>
<label>
Item image:
<InputFile OnChange="@LoadImage" accept=".png" />
</label>
</p>
<p>
<label>
Accept Condition:
<InputCheckbox @bind-Value="itemModel.AcceptCondition" />
</label>
</p>
<button type="submit">Submit</button>
</EditForm>

@ -0,0 +1,109 @@
using BlazorProject.Factories;
using BlazorProject.Models;
using BlazorProject.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
namespace BlazorProject.Pages;
public partial class Edit
{
[Parameter]
public int Id { get; set; }
/// <summary>
/// The default enchant categories.
/// </summary>
private List<string> enchantCategories = new List<string>() { "armor", "armor_head", "armor_chest", "weapon", "digger", "breakable", "vanishable" };
/// <summary>
/// The current item model
/// </summary>
private ItemModel itemModel = new()
{
EnchantCategories = new List<string>(),
RepairWith = new List<string>()
};
/// <summary>
/// The default repair with.
/// </summary>
private List<string> repairWith = new List<string>() { "oak_planks", "spruce_planks", "birch_planks", "jungle_planks", "acacia_planks", "dark_oak_planks", "crimson_planks", "warped_planks" };
[Inject]
public IDataService DataService { get; set; }
[Inject]
public NavigationManager NavigationManager { get; set; }
[Inject]
public IWebAssemblyHostEnvironment WebHostEnvironment { get; set; }
protected override async Task OnInitializedAsync()
{
var item = await DataService.GetById(Id);
var fileContent = await File.ReadAllBytesAsync($"{WebHostEnvironment.BaseAddress}/images/default.png");
if (File.Exists($"{WebHostEnvironment.BaseAddress}/images/{itemModel.Name}.png"))
{
fileContent = await File.ReadAllBytesAsync($"{WebHostEnvironment.BaseAddress}/images/{item.Name}.png");
}
// Set the model with the item
itemModel = ItemFactory.ToModel(item, fileContent);
}
private async void HandleValidSubmit()
{
await DataService.Update(Id, itemModel);
NavigationManager.NavigateTo("list");
}
private async Task LoadImage(InputFileChangeEventArgs e)
{
// Set the content of the image to the model
using (var memoryStream = new MemoryStream())
{
await e.File.OpenReadStream().CopyToAsync(memoryStream);
itemModel.ImageContent = memoryStream.ToArray();
}
}
private void OnEnchantCategoriesChange(string item, object checkedValue)
{
if ((bool)checkedValue)
{
if (!itemModel.EnchantCategories.Contains(item))
{
itemModel.EnchantCategories.Add(item);
}
return;
}
if (itemModel.EnchantCategories.Contains(item))
{
itemModel.EnchantCategories.Remove(item);
}
}
private void OnRepairWithChange(string item, object checkedValue)
{
if ((bool)checkedValue)
{
if (!itemModel.RepairWith.Contains(item))
{
itemModel.RepairWith.Add(item);
}
return;
}
if (itemModel.RepairWith.Contains(item))
{
itemModel.RepairWith.Remove(item);
}
}
}

@ -17,16 +17,16 @@
Responsive> Responsive>
<DataGridColumn TItem="Item" Field="@nameof(Item.Id)" Caption="#" /> <DataGridColumn TItem="Item" Field="@nameof(Item.Id)" Caption="#" />
<DataGridColumn TItem="Item" Field="@nameof(Item.Id)" Caption="Image"> <DataGridColumn TItem="Item" Field="@nameof(Item.Id)" Caption="Image">
<DisplayTemplate> <DisplayTemplate>
@if (File.Exists($"{WebHostEnvironment.BaseAddress}/images/{context.Name}.png")) @if (File.Exists($"{WebHostEnvironment.BaseAddress}/images/{context.Name}.png"))
{ {
<img src="images/@(context.Name).png" class="img-thumbnail" title="@context.DisplayName" alt="@context.DisplayName" style="max-width: 150px"/> <img src="images/@(context.Name).png" class="img-thumbnail" title="@context.DisplayName" alt="@context.DisplayName" style="max-width: 150px"/>
} }
else else
{ {
<img src="images/default.png" class="img-thumbnail" title="@context.DisplayName" alt="@context.DisplayName" style="max-width: 150px"/> <img src="images/default.png" class="img-thumbnail" title="@context.DisplayName" alt="@context.DisplayName" style="max-width: 150px"/>
} }
</DisplayTemplate> </DisplayTemplate>
</DataGridColumn> </DataGridColumn>
<DataGridColumn TItem="Item" Field="@nameof(Item.DisplayName)" Caption="Display name" /> <DataGridColumn TItem="Item" Field="@nameof(Item.DisplayName)" Caption="Display name" />
<DataGridColumn TItem="Item" Field="@nameof(Item.StackSize)" Caption="Stack size" /> <DataGridColumn TItem="Item" Field="@nameof(Item.StackSize)" Caption="Stack size" />
@ -42,4 +42,10 @@
</DisplayTemplate> </DisplayTemplate>
</DataGridColumn> </DataGridColumn>
<DataGridColumn TItem="Item" Field="@nameof(Item.CreatedDate)" Caption="Created date" DisplayFormat="{0:d}" DisplayFormatProvider="@System.Globalization.CultureInfo.GetCultureInfo("fr-FR")"/> <DataGridColumn TItem="Item" Field="@nameof(Item.CreatedDate)" Caption="Created date" DisplayFormat="{0:d}" DisplayFormatProvider="@System.Globalization.CultureInfo.GetCultureInfo("fr-FR")"/>
<DataGridColumn TItem="Item" Field="@nameof(Item.Id)" Caption="Action">
<DisplayTemplate>
<a href="Edit/@(context.Id)" class="btn btn-primary"><i class="fa fa-edit"></i> Editer</a>
<button type="button" class="btn btn-primary" @onclick="() => OnDelete(context.Id)"><i class="fa fa-trash"></i> Supprimer</button>
</DisplayTemplate>
</DataGridColumn>
</DataGrid> </DataGrid>

@ -1,7 +1,11 @@
using System.Net.Http.Json; using System.Net.Http.Json;
using Blazored.LocalStorage; using Blazored.LocalStorage;
using Blazored.Modal;
using Blazored.Modal.Services;
using Blazorise.DataGrid; using Blazorise.DataGrid;
using BlazorProject.Modals;
using BlazorProject.Models; using BlazorProject.Models;
using BlazorProject.Services;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
@ -25,6 +29,12 @@ public partial class List
[Inject] [Inject]
public NavigationManager NavigationManager { get; set; } public NavigationManager NavigationManager { get; set; }
[CascadingParameter]
public IModalService Modal { get; set; }
[Inject]
public IDataService DataService { get; set; }
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
// Do not treat this action if is not the first render // Do not treat this action if is not the first render
@ -51,14 +61,24 @@ public partial class List
return; return;
} }
// When you use a real API, we use this follow code
//var response = await Http.GetJsonAsync<Data[]>( $"http://my-api/api/data?page={e.Page}&pageSize={e.PageSize}" );
var response = (await LocalStorage.GetItemAsync<Item[]>("data")).Skip((e.Page - 1) * e.PageSize).Take(e.PageSize).ToList();
if (!e.CancellationToken.IsCancellationRequested) if (!e.CancellationToken.IsCancellationRequested)
{ {
totalItem = (await LocalStorage.GetItemAsync<List<Item>>("data")).Count; items = await DataService.List(e.Page, e.PageSize);
items = new List<Item>(response); // an actual data for the current page totalItem = await DataService.Count();
} }
} }
private void OnDelete(int id)
{
var parameters = new ModalParameters();
parameters.Add(nameof(Item.Id), id);
var modal = Modal.Show<DeleteConfirmation>("Delete Confirmation", parameters);
var result = modal.Result;
DataService.Delete(id);
// Reload the page
NavigationManager.NavigateTo("list", true);
}
} }

@ -1,14 +1,18 @@
using Blazored.LocalStorage; using Blazored.LocalStorage;
using Blazored.Modal;
using Blazorise; using Blazorise;
using Blazorise.Bootstrap; using Blazorise.Bootstrap;
using Blazorise.Icons.FontAwesome; using Blazorise.Icons.FontAwesome;
using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using BlazorProject; using BlazorProject;
using BlazorProject.Services;
var builder = WebAssemblyHostBuilder.CreateDefault(args); var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.Services.AddBlazoredLocalStorage(); builder.Services.AddBlazoredLocalStorage();
builder.Services.AddBlazoredModal();
builder.Services.AddScoped<IDataService, DataLocalService>();
builder.Services builder.Services
.AddBlazorise() .AddBlazorise()
.AddBootstrapProviders() .AddBootstrapProviders()

@ -0,0 +1,178 @@
using System.Net.Http.Json;
using Blazored.LocalStorage;
using BlazorProject.Factories;
using BlazorProject.Models;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
namespace BlazorProject.Services;
public class DataLocalService : IDataService
{
private readonly HttpClient _http;
private readonly ILocalStorageService _localStorage;
private readonly NavigationManager _navigationManager;
private readonly IWebAssemblyHostEnvironment _webHostEnvironment;
public DataLocalService(
ILocalStorageService localStorage,
HttpClient http,
IWebAssemblyHostEnvironment webHostEnvironment,
NavigationManager navigationManager)
{
_localStorage = localStorage;
_http = http;
_webHostEnvironment = webHostEnvironment;
_navigationManager = navigationManager;
}
public async Task Add(ItemModel model)
{
// Get the current data
var currentData = await _localStorage.GetItemAsync<List<Item>>("data");
// Simulate the Id
model.Id = currentData.Max(s => s.Id) + 1;
// Add the item to the current data
currentData.Add(ItemFactory.Create(model));
// Save the image
var imagePathInfo = new DirectoryInfo($"{_webHostEnvironment.BaseAddress}/images");
// Check if the folder "images" exist
if (!imagePathInfo.Exists)
{
imagePathInfo.Create();
}
// Determine the image name
var fileName = new FileInfo($"{imagePathInfo}/{model.Name}.png");
// Write the file content
await File.WriteAllBytesAsync(fileName.FullName, model.ImageContent);
// Save the data
await _localStorage.SetItemAsync("data", currentData);
}
public async Task<int> Count()
{
// Load data from the local storage
var currentData = await _localStorage.GetItemAsync<Item[]>("data");
// Check if data exist in the local storage
if (currentData == null)
{
// this code add in the local storage the fake data
var originalData = await _http.GetFromJsonAsync<Item[]>($"{_navigationManager.BaseUri}fake-data.json");
await _localStorage.SetItemAsync("data", originalData);
}
return (await _localStorage.GetItemAsync<Item[]>("data")).Length;
}
public async Task<List<Item>> List(int currentPage, int pageSize)
{
// Load data from the local storage
var currentData = await _localStorage.GetItemAsync<Item[]>("data");
// Check if data exist in the local storage
if (currentData == null)
{
// this code add in the local storage the fake data
var originalData = await _http.GetFromJsonAsync<Item[]>($"{_navigationManager.BaseUri}fake-data.json");
await _localStorage.SetItemAsync("data", originalData);
}
return (await _localStorage.GetItemAsync<Item[]>("data")).Skip((currentPage - 1) * pageSize).Take(pageSize).ToList();
}
public async Task<Item> GetById(int id)
{
// Get the current data
var currentData = await _localStorage.GetItemAsync<List<Item>>("data");
// Get the item int the list
var item = currentData.FirstOrDefault(w => w.Id == id);
// Check if item exist
if (item == null)
{
throw new Exception($"Unable to found the item with ID: {id}");
}
return item;
}
public async Task Update(int id, ItemModel model)
{
// Get the current data
var currentData = await _localStorage.GetItemAsync<List<Item>>("data");
// Get the item int the list
var item = currentData.FirstOrDefault(w => w.Id == id);
// Check if item exist
if (item == null)
{
throw new Exception($"Unable to found the item with ID: {id}");
}
// Save the image
var imagePathInfo = new DirectoryInfo($"{_webHostEnvironment.BaseAddress}/images");
// Check if the folder "images" exist
if (!imagePathInfo.Exists)
{
imagePathInfo.Create();
}
// Delete the previous image
if (item.Name != model.Name)
{
var oldFileName = new FileInfo($"{imagePathInfo}/{item.Name}.png");
if (oldFileName.Exists)
{
File.Delete(oldFileName.FullName);
}
}
// Determine the image name
var fileName = new FileInfo($"{imagePathInfo}/{model.Name}.png");
// Write the file content
await File.WriteAllBytesAsync(fileName.FullName, model.ImageContent);
// Modify the content of the item
ItemFactory.Update(item, model);
// Save the data
await _localStorage.SetItemAsync("data", currentData);
}
public async Task Delete(int id)
{
// Get the current data
var currentData = await _localStorage.GetItemAsync<List<Item>>("data");
// Get the item int the list
var item = currentData.FirstOrDefault(w => w.Id == id);
// Delete item in
currentData.Remove(item);
// Delete the image
var imagePathInfo = new DirectoryInfo($"{_webHostEnvironment.BaseAddress}/images");
var fileName = new FileInfo($"{imagePathInfo}/{item.Name}.png");
if (fileName.Exists)
{
File.Delete(fileName.FullName);
}
// Save the data
await _localStorage.SetItemAsync("data", currentData);
}
}

@ -0,0 +1,18 @@
using BlazorProject.Models;
namespace BlazorProject.Services;
public interface IDataService
{
Task Add(ItemModel model);
Task<int> Count();
Task<List<Item>> List(int currentPage, int pageSize);
Task<Item> GetById(int id);
Task Update(int id, ItemModel model);
Task Delete(int id);
}

@ -9,3 +9,5 @@
@using BlazorProject @using BlazorProject
@using BlazorProject.Layout @using BlazorProject.Layout
@using Blazorise.DataGrid @using Blazorise.DataGrid
@using Blazored.Modal
@using Blazored.Modal.Services

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Before

Width:  |  Height:  |  Size: 220 KiB

After

Width:  |  Height:  |  Size: 220 KiB

@ -13,6 +13,8 @@
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.15.4/css/all.css"> <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.15.4/css/all.css">
<link href="_content/Blazorise/blazorise.css" rel="stylesheet" /> <link href="_content/Blazorise/blazorise.css" rel="stylesheet" />
<link href="_content/Blazorise.Bootstrap/blazorise.bootstrap.css" rel="stylesheet" /> <link href="_content/Blazorise.Bootstrap/blazorise.bootstrap.css" rel="stylesheet" />
<link href="_content/Blazored.Modal/blazored-modal.css" rel="stylesheet" />
<script src="_content/Blazored.Modal/blazored.modal.js"></script>
</head> </head>
<body> <body>

Loading…
Cancel
Save