рдмреНрд▓реЗрдЬрд╝рд░ рдХреНрд▓рд╛рдЗрдВрдЯ рд╕рд╛рдЗрдб рдСрдирд▓рд╛рдЗрди рд╕реНрдЯреЛрд░: рднрд╛рдЧ 3 - рдЙрддреНрдкрд╛рдж рд╢реЛрдХреЗрд╕



рдирдорд╕реНрдХрд╛рд░, рд╣реЗрдмреНрд░! рдореИрдВ рдмреНрд▓реЗрдЬрд╝рд░ рдкрд░ рдСрдирд▓рд╛рдЗрди рд╕реНрдЯреЛрд░ рдХрд░рдирд╛ рдЬрд╛рд░реА рд░рдЦрддрд╛ рд╣реВрдВред рдЗрд╕ рднрд╛рдЧ рдореЗрдВ, рдореИрдВ рдЗрд╕ рдмрд╛рд░реЗ рдореЗрдВ рдмрд╛рдд рдХрд░реВрдБрдЧрд╛ рдХрд┐ рдХреИрд╕реЗ рдореИрдВрдиреЗ рдЗрд╕рдореЗрдВ рд╕рд╛рдорд╛рди рдХрд╛ рдкреНрд░рджрд░реНрд╢рди рдЬреЛрдбрд╝рд╛ рдФрд░ рдЕрдкрдиреЗ рдШрдЯрдХ рдмрдирд╛рдПред рд╡рд┐рд╡рд░рдг рдХреЗ рд▓рд┐рдП, рдмрд┐рд▓реНрд▓реА рдореЗрдВ рдЖрдкрдХрд╛ рд╕реНрд╡рд╛рдЧрдд рд╣реИред

рд╕рд╛рдордЧреНрд░реА



рд╕рдВрджрд░реНрдн


тЖТ  рд╕реНрд░реЛрдд
тЖТ рдбреЛрдХрд░ рд░рдЬрд┐рд╕реНрдЯреНрд░реА рдкрд░ рдЫрд╡рд┐рдпрд╛рдВ

рдЕрдкрдбреЗрдЯ


Microsoft рдиреЗ рдСрдереЗрдВрдЯрд┐рдХреЗрд╢рди рд▓рд╛рдЗрдмреНрд░реЗрд░реА рдХреЗ рд╕рд╛рде рдЕрдкрдиреЗ рдСрдерд░рд╛рдЗрдЬреЗрд╢рди рд▓рд╛рдЗрдмреНрд░реЗрд░реА Blazor WebAssembly рд╕реНрдЯреИрдВрдбрдЕрд▓реЛрди рдРрдк рдХреЛ рдЬреЛрдбрд╝рд╛ ред

рд╣рдордиреЗ PWA рдмрд┐рд▓реНрдб рдкреНрд░реЛрдЧреНрд░реЗрд╕рд┐рд╡ рд╡реЗрдм рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреЗ рд╕рд╛рде рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдмрдирд╛рдиреЗ рдХреА рдХреНрд╖рдорддрд╛ рднреА рдЬреЛрдбрд╝реА рд╣реИ ред



рдЖрдк рдХреНрд░реЛрдо рдореЗрдВ рдкреНрд▓рд╕ рд╕рд╛рдЗрди рдкрд░ рдХреНрд▓рд┐рдХ рдХрд░рдХреЗ рдЗрд╕реЗ рдЗрдВрд╕реНрдЯреЙрд▓ рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ:



рдпрд╣ рдЗрд╕ рддрд░рд╣ рджрд┐рдЦрддрд╛ рд╣реИ:



рдЕрдм рдЖрдк рдЖрд╕рд╛рдиреА рд╕реЗ рдбрд┐рдмрдЧ рд╡реЗрдмрдЕрд╡реЗрд╢рди рдХреЛрдб рдХреЛ рдбреАрдмрдЧ рдХрд░ рд╕рдХрддреЗ рд╣реИрдВред

рдЯрд╛рд╕реНрдХ рдЯрд╛рд╕реНрдХ рдореЗрди рдХрд░рдиреЗ рдХреА рдХреНрд╖рдорддрд╛ рдЬреЛрдбрд╝рд╛ рдФрд░ рд╕реНрдЯрд╛рд░реНрдЯрдЕрдк рдХреА рд╕реНрдерд╛рдкрдирд╛ рд░рджреНрдж рдХрд░реЗрдВ:

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

рд╕рд╛рдорд╛рдиреНрдп рддреМрд░ рдкрд░, рдХреНрд╖реБрджреНрд░ рд╕рд╛рдерд┐рдпреЛрдВ рдФрд░ рдореЗрд░реА рд░рд╛рдп рдореЗрдВ рддрд╛рд░рд╛рдВрдХрди рдпреЛрдЧреНрдп рд╣реИ ред

рдЕрд╡рдпрд╡


paginator


рдкреЗрдЬрд┐рдВрдЧ рдХреЗ рд▓рд┐рдП рдЬрд┐рдореНрдореЗрджрд╛рд░ред

рдирдореВрдирд╛:

    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 + рд░реЗрдЬрд░:

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

рддреНрд░реБрдЯрд┐


рдЙрдкрдпреЛрдЧрдХрд░реНрддрд╛ рдХреЛ рддреНрд░реБрдЯрд┐рдпреЛрдВ рдХреЛ рдкреНрд░рджрд░реНрд╢рд┐рдд рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдЬрд┐рдореНрдореЗрджрд╛рд░ред

ViewMode + рд░реЗрдЬрд░:

@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


рдПрдХ рддрд╛рд▓рд┐рдХрд╛ рдореЗрдВ рдбреЗрдЯрд╛ рд╕реЙрд░реНрдЯ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдЬрд┐рдореНрдореЗрджрд╛рд░ред

рдирдореВрдирд╛:

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

ViewMode + рд░реЗрдЬрд░:

@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

рдпрд╣ рдПрдХ рд╕рд╛рдорд╛рдиреНрдп рдкреИрд░рд╛рдореАрдЯрд░ рд╣реИ рдЬрд┐рд╕рдХреЗ рдкреНрд░рдХрд╛рд░ рдореЗрдВ рдПрдХ рдХреБрдВрдЬреА рд╣реЛрдЧреА рдЬрд┐рд╕рдХреЗ рджреНрд╡рд╛рд░рд╛ рд╣рдо рджрдмрд╛рдП рдЧрдП рдХреЙрд▓рдо рд╢реАрд░реНрд╖рдХ рдХреА рдкрд╣рдЪрд╛рди рдХрд░реЗрдВрдЧреЗред рдЕрдм рддрдХ, рдЗрд╕рдХреЗ рд▓рд┐рдП рдкреНрд░рддрд┐рдмрдВрдзреЛрдВ рдХреЛ рдирд┐рд░реНрджрд┐рд╖реНрдЯ рдХрд░рдиреЗ рдХрд╛ рдХреЛрдИ рддрд░реАрдХрд╛ рдирд╣реАрдВ рд╣реИред рдпрджрд┐ рд╕рдВрднрд╡ рд╣реЛ рддреЛ, рдореИрдВ рдХреБрдЫ рдРрд╕реА рдЬрдЧрд╣ рд▓рдЯрдХрд╛рдКрдВрдЧрд╛ рдЬрд╣рд╛рдВ TId: IEquatable рд╣реИ

рдорд╛рдиреНрдпрддрд╛ рдЧреБрдг


рдмреНрд▓реЗрдЬрд╝рд░ рдореЗрдВ рд░реВрдкреЛрдВ рдХреЗ рд▓рд┐рдП рдЕрдВрддрд░реНрдирд┐рд╣рд┐рдд рд╕рддреНрдпрд╛рдкрди рд╡рд┐рд╢реЗрд╖рддрд╛рдУрдВ рдХреЗ рдорд╛рдзреНрдпрдо рд╕реЗ рдХрд╛рдо рдХрд░рддрд╛ рд╣реИред рдореИрдВрдиреЗ рдЕрдкрдирд╛ рдЬреЛрдбрд╝рд╛ред
рд╡рд╣ рдЬрд╛рдБрдЪрддрд╛ рд╣реИ рдХрд┐ рдЙрд╕рдХреА рд╕рдВрдкрддреНрддрд┐ рдХрд╛ рдореВрд▓реНрдп рдЙрд╕ рд╕рдВрдкрддреНрддрд┐ рдХреЗ рдореВрд▓реНрдп рд╕реЗ рдЕрдзрд┐рдХ рд╣реИ рдЬрд┐рд╕рдХрд╛ рдирд╛рдо рдЙрд╕реЗ рдПрдХ рдкреИрд░рд╛рдореАрдЯрд░ рдХреЗ рд░реВрдк рдореЗрдВ рджрд┐рдпрд╛ рдЧрдпрд╛ рдерд╛ред рдореИрдВ рдЗрд╕рдХрд╛ рдЙрдкрдпреЛрдЧ рдпрд╣ рд╕рддреНрдпрд╛рдкрд┐рдд рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдХрд░рддрд╛ рд╣реВрдВ рдХрд┐ рдЕрдзрд┐рдХрддрдо рдореВрд▓реНрдп рдиреНрдпреВрдирддрдо рдореВрд▓реНрдп рд╕реЗ рдЕрдзрд┐рдХ рд╣реИред

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

рдЙрддреНрдкрд╛рдж рд▓рд┐рд╕реНрдЯрд┐рдВрдЧ рдкреГрд╖реНрда


рдирдореВрдирд╛:

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

рдЙрд╕реНрддрд░рд╛:

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

рдХреЛрдгреАрдп рд╕рдВрд╕реНрдХрд░рдг


рдкреНрд░рд╛рдЗрдордПрдирдЬреА рдХрд╛ рдЗрд╕реНрддреЗрдорд╛рд▓ рдХрд┐рдпрд╛ ред рдмреНрд▓реЗрдЬрд╝рд░ WASM 1.47 рдмреВрдЯ рд╕рдордп рдмрдирд╛рдо 0.35 рд╕реЗрдХрдВрдб рдХреЛрдгреАрдп рдХреЗ рдкрдХреНрд╖ рдореЗрдВ:


All Articles