Boutique en ligne côté client Blazor: Partie 3 - Présentation des produits



Bonjour, Habr! Je continue de faire de la boutique en ligne sur Blazor. Dans cette partie, je vais vous expliquer comment j'y ai ajouté une vitrine de produits et fabriqué mes composants. Pour plus de détails, bienvenue au chat.

Contenu



Références


→  Sources
Images sur le registre Docker

Mises à jour


Microsoft a ajouté sa bibliothèque d'autorisations Blazor WebAssembly app autonome avec la bibliothèque d'authentification .

Nous avons également ajouté la possibilité de créer des applications avec PWA créer des applications Web Progressive.



Vous pouvez l' installer en cliquant sur le signe plus en chrome:



Il ressemble à ceci:



Maintenant , vous pouvez facilement le débogage de débogage WebAssembly . Code

Ajout de la possibilité de faire async tâche principale et désinstaller démarrage:

    public class Program
    {
        public static async Task Main(string[] args)
        {
            Console.WriteLine("START MAIN");
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("app");
            await ConfigureServices(builder.Services);
            await builder.Build().RunAsync();
            Console.WriteLine("END MAIN");
        }

        private static async Task<ConfigModel> GetConfig(IServiceCollection services)
        {
            using (var provider = services.BuildServiceProvider())
            {
                var nm = provider.GetRequiredService<NavigationManager>();
                var uri = nm.BaseUri;
                Console.WriteLine($"BASE URI: {uri}");
                var url = $"{(uri.EndsWith('/') ? uri : uri + "/")}api/v1/config";
                using var client = new HttpClient();
                return await client.GetJsonAsync<ConfigModel>(url);
            }
        }

        private static async Task ConfigureServices(IServiceCollection services)
        {
            services.AddBaseAddressHttpClient();

            var cfg = await GetConfig(services);
            services.AddScoped<ConfigModel>(s => cfg);
            Console.WriteLine($"SSO URI IN STARTUP: {cfg?.SsoUri}");
            services.AddOidcAuthentication(x =>
            {
                x.ProviderOptions.Authority = cfg.SsoUri;
                x.ProviderOptions.ClientId = "spaBlazorClient";
                x.ProviderOptions.ResponseType = "code";
                x.ProviderOptions.DefaultScopes.Add("api");
                x.UserOptions.RoleClaim = "role";
            });
            services.AddTransient<IAuthorizedHttpClientProvider, AuthorizedHttpClientProvider>();
            services.AddTransient<IHttpService, HttpService>();
            services.AddTransient<IApiRepository, ApiRepository>();
        }
    }

En général, les petits gars et à mon avis méritent un astérisque .

Composants


Paginateur


Responsable de la pagination.

Modèle:

    public sealed class PaginatorModel
    {
        public int Size { get; set; }
        public int CurrentPage { get; set; }
        public int ItemsPerPage { get; set; } = 10;
        public int ItemsTotalCount { get; set; }
    }

ViewModel + Razor:

<ul class="pagination justify-content-center mx-3 my-3">
    <li class="page-item">
        <a class="page-link" href="#" @onclick="@(async e => await LoadPage(First))" @onclick:preventDefault><<</a>
    </li>
    <li class="page-item">
        <a class="page-link" href="#" @onclick="@(async e => await LoadPage(Prev))" @onclick:preventDefault><</a>
    </li>
    @{
        foreach (var p in Pages)
        {
            <li class=@(Model.CurrentPage == p ? "page-item active" : "page-item")>
                <a class="page-link" @onclick="@(async e => await LoadPage(p))" href="#" @onclick:preventDefault>@(p + 1)</a>
            </li>
        }
    }
    <li class="page-item"><a class="page-link" href="#" @onclick="@(async e => await LoadPage(Next))" @onclick:preventDefault>></a></li>
    <li class="page-item"><a class="page-link" href="#" @onclick="@(async e => await LoadPage(Last))" @onclick:preventDefault>>></a></li>
    <li class="page-item">
        <select id="size" class="form-control" value="@Model.ItemsPerPage" @onchange="@OnItemsPerPageChanged">
            <option value=10 selected>10</option>
            <option value=20>20</option>
            <option value=40>40</option>
            <option value=80>80</option>
        </select>
    </li>
</ul>
//ViewModel
@code 
{
    [Parameter]
    public EventCallback OnPageChanged { get; set; }

    [Parameter]
    public PaginatorModel Model { get; set; }

    public async Task LoadPage(int page)
    {
        Model.CurrentPage = page;
        await OnPageChanged.InvokeAsync(null);
    }

    public async Task OnItemsPerPageChanged(ChangeEventArgs x)
    {
        Model.ItemsPerPage = int.Parse(x.Value.ToString());
        await OnPageChanged.InvokeAsync(null);
    }

    public int First => 0;
    public int Prev => Math.Max(Model.CurrentPage - 1, 0);
    public int Next => Math.Min(Model.CurrentPage + 1, Math.Max(PageCount - 1, 0));
    public int Last => Math.Max(PageCount - 1, 0);

    public int PageCount
    {
        get
        {
            if (Model.ItemsPerPage < 1 || Model.ItemsTotalCount < 1)
                return 0;
            var count = (Model.ItemsTotalCount / Model.ItemsPerPage);
            if ((Model.ItemsTotalCount % Model.ItemsPerPage) > 0)
                count++;
            return count;

        }
    }

    public IEnumerable<int> Pages
    {
        get
        {
            var half = Model.Size / 2;
            var reminder = Model.Size % 2;
            var max = Math.Min(Model.CurrentPage + half + Math.Max((half - Model.CurrentPage), 0) + reminder, PageCount);
            var min = Math.Max(max - Model.Size, 0);
            for (int i = min; i < max; i++)
            {
                yield return i;
            }
        }
    }
}

Erreur


Responsable d'afficher les erreurs à l'utilisateur.

ViewMode + Razor:

@if (!string.IsNullOrWhiteSpace(Model))
{
    <div class="text-danger">
        <h4> </h4>
        <p>           :</p>
        <p>@Model</p>
    </div>
}

//ViewModel
@code {
    [Parameter]
    public string Model { get; set; }
}

SortableTableHeader


Responsable du tri des données dans une table.

Modèle:

    public sealed class SortableTableHeaderModel<TId>
    {
        public TId Current { get; set; }
        public Dictionary<TId, string> Headers { get; set; }
        public bool Descending { get; set; }
    }

ViewMode + Razor:

@typeparam TId

<thead>
    <tr>
        @foreach (var kv in Model.Headers)
        {
            <th @onclick="@(x=>Sort(kv.Key))">
                @kv.Value
                <span class="@GetClass(kv.Key)"></span>
            </th>
        }
    </tr>
</thead>

//ViewModel
@code
 {
    public Task Sort(TId id)
    {
        Model.Current = id;
        Model.Descending = !Model.Descending;
        return Sorted.InvokeAsync(null);
    }

    public string GetClass(TId id)
    {
        if (!id.Equals(Model.Current))
            return "d-none";
        return Model.Descending ? "oi oi-caret-bottom" : "oi oi-caret-top";
    }

    [Parameter]
    public SortableTableHeaderModel<TId> Model { get; set; }


    [Parameter]
    public EventCallback Sorted { get; set; }
}

@typeparam TId

Il s'agit d'un paramètre générique dont le type aura une clé par laquelle nous identifierons l'en-tête de colonne enfoncé. Jusqu'à présent, il n'y a aucun moyen de spécifier des restrictions pour cela. Si possible, je suspendrais quelque chose comme où TId: IEquatable

Attribut de validation


La validation intégrée des formulaires dans Blazor fonctionne via des attributs. J'ai ajouté le mien.
Il vérifie que les valeurs de sa propriété sont supérieures à la valeur de la propriété dont le nom lui a été donné en paramètre. Je l'utilise pour vérifier que le prix maximum est supérieur au prix minimum.

    [AttributeUsage(AttributeTargets.Property)]
    public class GreaterOrEqualToAttribute : ValidationAttribute
    {
        public string FieldName { get; }
        public string DisplayName { get; }

        public GreaterOrEqualToAttribute(string fieldName, string displayName)
        {
            FieldName = fieldName;
            DisplayName = displayName;
        }

        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            if (value is null)
                return ValidationResult.Success;
            PropertyInfo otherPropertyInfo = validationContext.ObjectType.GetProperty(FieldName);
            if (otherPropertyInfo == null)
                return Fail(validationContext);
            var otherPropertyValue = otherPropertyInfo.GetValue(validationContext.ObjectInstance, null);
            if (Comparer.Default.Compare(value, otherPropertyValue) >= 0)
                return ValidationResult.Success;
            return Fail(validationContext);
        }

        private ValidationResult Fail(ValidationContext validationContext)
        {
            return new ValidationResult($"      {DisplayName}",
                new[] { validationContext.MemberName });
        }
    }

Page de liste des produits


Modèle:

    public sealed class ProductsModel
    {
        public PageResultDto<ProductDto> Items { get; set; } = new PageResultDto<ProductDto>()
        {
            TotalCount = 0,
            Value = new List<ProductDto>()
        };
        public bool IsLoaded { get; set; }
        public PaginatorModel Paginator { get; set; } = new PaginatorModel()
        {
            ItemsTotalCount = 0,
            Size = 5,
            ItemsPerPage = 10,
            CurrentPage = 0
        };
        public SortableTableHeaderModel<ProductOrderBy> TableHeaderModel { get; set; } = new SortableTableHeaderModel<ProductOrderBy>()
        {
            Current = ProductOrderBy.Id,
            Descending = false,
            Headers = new Dictionary<ProductOrderBy, string>()
            {
                {ProductOrderBy.Name,"Title" },
                { ProductOrderBy.Price, "Price"}
            }
        };
        public string HandledErrors { get; set; }
        public string Title { get; set; }
        public decimal MinPrice { get; set; } = 0m;
        public decimal? MaxPrice { get; set; }
    }

ViewModel:

    public class ProductsViewModel : ComponentBase
    {
        protected override async Task OnInitializedAsync()
        {
            await LoadFromServerAsync();
        }

        [Inject]
        public IApiRepository Repository { get; set; }
        public ProductsModel Model { get; set; } = new ProductsModel();
        [StringLength(30, ErrorMessage = "     30 ")]
        public string Title { get; set; }
        [GreaterOrEqualTo(nameof(Min), "0")]
        public decimal MinPrice { get; set; } = 0m;
        public decimal Min => 0m;
        [GreaterOrEqualTo(nameof(MinPrice), "Min Price")]
        public decimal? MaxPrice { get; set; }
        public int Skip => Model.Paginator.ItemsPerPage * Model.Paginator.CurrentPage;
        public int Take => Model.Paginator.ItemsPerPage;

        public async Task HandleValidSubmit()
        {
            Model.Title = Title;
            Model.MinPrice = MinPrice;
            Model.MaxPrice = MaxPrice;
            Model.Paginator.CurrentPage = 0;
            await LoadFromServerAsync();
        }

        public async Task LoadPage()
        {
            await LoadFromServerAsync();
        }

        public async Task HandleSort()
        {
            await LoadFromServerAsync();
        }

        private async Task LoadFromServerAsync()
        {
            Model.IsLoaded = false;
            var dto = new ProductsFilterDto()
            {
                Descending = Model.TableHeaderModel.Descending,
                MinPrice = Model.MinPrice,
                MaxPrice = Model.MaxPrice ?? decimal.MaxValue,
                OrderBy = Model.TableHeaderModel.Current,
                Skip = Skip,
                Take = Take,
                Title = Model.Title
            };
            var (r, e) = await Repository.GetFiltered(dto);
            Model.HandledErrors = e;
            Model.Items = r ?? new PageResultDto<ProductDto>();
            Model.Paginator.ItemsTotalCount = Model.Items.TotalCount;
            Model.IsLoaded = true;
        }
    }

Rasoir:

@inherits ProductsViewModel
@page "/products"

<h3>Products</h3>
<div class="jumbotron col-md-6">
    <EditForm Model="@this" OnValidSubmit="@HandleValidSubmit">
        <DataAnnotationsValidator />
        <div class="form-row">
            <div class="form-group col-md-12">
                <label for="title">Title</label>
                <InputText id="title" @bind-Value="@Title" class="form-control" />
                <ValidationMessage For="@(() =>Title)" />
            </div>
        </div>
        <div class="form-row">
            <div class="form-group col-md-6">
                <label for="min">Min Price</label>
                <InputNumber id="min" @bind-Value="@MinPrice" class="form-control" TValue="decimal" />
                <ValidationMessage For="@(() => MinPrice)" />
            </div>
            <div class="form-group col-md-6">
                <label for="max">Max Price</label>
                <InputNumber id="max" @bind-Value="@MaxPrice" class="form-control" TValue="decimal?" />
                <ValidationMessage For="@(() =>MaxPrice)" />
            </div>
        </div>
        <button type="submit" class="btn btn-primary" disabled="@(!context.Validate())">Submit</button>
    </EditForm>
</div>
<nav aria-label="Table pages">
    <Paginator OnPageChanged="@LoadPage" Model="@Model.Paginator" />
</nav>
<div>
    <Error Model="@Model.HandledErrors" />
</div>
<div class="table-responsive">
    <table class="table">
        <SortableTableHeader Sorted="@HandleSort" Model="@Model.TableHeaderModel" TId="ProductOrderBy" />
        <tbody>
            @if (Model.IsLoaded)
            {
                @foreach (var product in Model.Items.Value)
                {
                    <tr>
                        <td>@product.Title</td>
                        <td>@product.Price</td>
                    </tr>
                }
            }
            else
            {
                <tr>
                    <td>
                        <p><em>Loading...</em></p>
                    </td>
                </tr>
            }
        </tbody>
    </table>
</div>

Version angulaire


PrimeNG utilisé . Blazor WASM 1,47 temps de démarrage contre 0,35 secondes en faveur d'Angular:


All Articles