Toko Online Sisi Klien Blazor: Bagian 3 - Showcase Produk



Halo, Habr! Saya terus melakukan toko online di Blazor. Di bagian ini, saya akan berbicara tentang bagaimana saya menambahkan showcase barang ke dalamnya dan membuat komponen saya. Untuk detailnya, selamat datang di kucing.

Kandungan



Referensi


→  Sumber
Gambar pada Register Docker

Pembaruan


Microsoft menambahkan perpustakaan otorisasi mereka Blazor WebAssembly aplikasi mandiri dengan perpustakaan Otentikasi .

Kami juga menambahkan kemampuan untuk membuat aplikasi dengan PWA Build Progressive Web Applications.



Anda dapat menginstalnya dengan mengklik tanda tambah di chrome:



Tampaknya seperti ini:



Sekarang Anda dapat dengan mudah men- debug kode WebAssembly Debug .

Menambahkan kemampuan untuk melakukan async Task Main dan menghapus Startup:

    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>();
        }
    }

Secara umum, orang-orang kecil dan menurut pendapat saya layak mendapat tanda bintang .

Komponen


Paginator


Bertanggung jawab untuk paging.

Model:

    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 + Pisau Cukur:

<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;
            }
        }
    }
}

Kesalahan


Bertanggung jawab untuk menampilkan kesalahan kepada pengguna.

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; }
}

Pemimpin SortableTableHeader


Bertanggung jawab untuk menyortir data dalam tabel.

Model:

    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

Ini adalah parameter umum yang tipenya akan memiliki kunci yang dengannya kami akan mengidentifikasi judul kolom yang ditekan. Sejauh ini, tidak ada cara untuk menentukan batasan untuk itu. Jika memungkinkan, saya akan menggantung sesuatu seperti di mana TId: IEquatable

Atribut Validasi


Validasi bawaan untuk formulir di Blazor bekerja melalui atribut. Saya menambahkan sendiri.
Dia memeriksa bahwa nilai propertinya lebih besar daripada nilai propertinya yang namanya dia berikan sebagai parameter. Saya menggunakannya untuk memverifikasi bahwa harga maksimum lebih besar dari harga 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 });
        }
    }

Halaman Daftar Produk


Model:

    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;
        }
    }

Pisau cukur:

@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>

Versi Sudut


PrimeNG yang digunakan . Blazor WASM 1.47 waktu boot versus 0,35 detik mendukung Angular:


All Articles