рдирдорд╕реНрдХрд╛рд░, рд╣реЗрдмреНрд░! рдореИрдВ рдмреНрд▓реЗрдЬрд╝рд░ рдкрд░ рдСрдирд▓рд╛рдЗрди рд╕реНрдЯреЛрд░ рдХрд░рдирд╛ рдЬрд╛рд░реА рд░рдЦрддрд╛ рд╣реВрдВред рдЗрд╕ рднрд╛рдЧ рдореЗрдВ, рдореИрдВ рдЗрд╕ рдмрд╛рд░реЗ рдореЗрдВ рдмрд╛рдд рдХрд░реВрдБрдЧрд╛ рдХрд┐ рдХреИрд╕реЗ рдореИрдВрдиреЗ рдЗрд╕рдореЗрдВ рд╕рд╛рдорд╛рди рдХрд╛ рдкреНрд░рджрд░реНрд╢рди рдЬреЛрдбрд╝рд╛ рдФрд░ рдЕрдкрдиреЗ рдШрдЯрдХ рдмрдирд╛рдПред рд╡рд┐рд╡рд░рдг рдХреЗ рд▓рд┐рдП, рдмрд┐рд▓реНрд▓реА рдореЗрдВ рдЖрдкрдХрд╛ рд╕реНрд╡рд╛рдЧрдд рд╣реИредрд╕рд╛рдордЧреНрд░реА
рд╕рдВрджрд░реНрдн
тЖТ рд╕реНрд░реЛрддтЖТ рдбреЛрдХрд░ рд░рдЬрд┐рд╕реНрдЯреНрд░реА рдкрд░ рдЫрд╡рд┐рдпрд╛рдВрдЕрдкрдбреЗрдЯ
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>
@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>
}
@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>
@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 рд╕реЗрдХрдВрдб рдХреЛрдгреАрдп рдХреЗ рдкрдХреНрд╖ рдореЗрдВ: