{"id":301124,"date":"2020-04-01T21:00:39","date_gmt":"2020-04-01T21:00:39","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=301124"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=301124","title":{"rendered":"Blazor Client Side \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442 \u041c\u0430\u0433\u0430\u0437\u0438\u043d: \u0427\u0430\u0441\u0442\u044c 3 \u2014 \u0412\u0438\u0442\u0440\u0438\u043d\u0430 \u0442\u043e\u0432\u0430\u0440\u043e\u0432"},"content":{"rendered":"\n<div class=\"post__text post__text-html post__text_v1\" id=\"post-content-body\" data-io-article-url=\"https:\/\/habr.com\/ru\/post\/494612\/\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/webt\/kz\/ip\/hb\/kziphbtuihaxaaa3ue93cdyfhhs.png\"><\/p>\n<p>  \u041f\u0440\u0438\u0432\u0435\u0442, \u0425\u0430\u0431\u0440! \u041f\u0440\u043e\u0434\u043e\u043b\u0436\u0430\u044e \u0434\u0435\u043b\u0430\u0442\u044c \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442 \u043c\u0430\u0433\u0430\u0437\u0438\u043d \u043d\u0430 Blazor. \u0412 \u044d\u0442\u043e\u0439 \u0447\u0430\u0441\u0442\u0438 \u0440\u0430\u0441\u0441\u043a\u0430\u0436\u0443 \u043e \u0442\u043e\u043c \u043a\u0430\u043a \u0434\u043e\u0431\u0430\u0432\u0438\u043b \u0432 \u043d\u0435\u0433\u043e \u0432\u0438\u0442\u0440\u0438\u043d\u0443 \u0442\u043e\u0432\u0430\u0440\u043e\u0432 \u0438 \u0441\u0434\u0435\u043b\u0430\u043b \u0441\u0432\u043e\u0438 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044b. \u0417\u0430 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u044f\u043c\u0438 \u0434\u043e\u0431\u0440\u043e \u043f\u043e\u0436\u0430\u043b\u043e\u0432\u0430\u0442\u044c \u043f\u043e\u0434 \u043a\u0430\u0442. <br \/>  <a name=\"habracut\"><\/a>  <\/p>\n<h2>\u0421\u043e\u0434\u0435\u0440\u0436\u0430\u043d\u0438\u0435<\/h2>\n<p>  <\/p>\n<ul>\n<li><a href=\"https:\/\/habr.com\/ru\/post\/463197\/\">Blazor + MVVM = Silverlight \u043d\u0430\u043d\u043e\u0441\u0438\u0442 \u043e\u0442\u0432\u0435\u0442\u043d\u044b\u0439 \u0443\u0434\u0430\u0440, \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u0434\u0440\u0435\u0432\u043d\u0435\u0435 \u0437\u043b\u043e \u043d\u0435\u043f\u043e\u0431\u0435\u0434\u0438\u043c\u043e<\/a>  <\/li>\n<li><a href=\"https:\/\/habr.com\/ru\/post\/484596\/\">Blazor Client Side \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442 \u041c\u0430\u0433\u0430\u0437\u0438\u043d: \u0427\u0430\u0441\u0442\u044c 1 \u2014 \u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f oidc (oauth2) + Identity Server4<\/a><\/li>\n<li><a href=\"https:\/\/habr.com\/ru\/post\/484782\/\">Blazor Client Side \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442 \u041c\u0430\u0433\u0430\u0437\u0438\u043d: \u0427\u0430\u0441\u0442\u044c 2 \u2014 CI\/CD<\/a><\/li>\n<li>Blazor Client Side \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442 \u041c\u0430\u0433\u0430\u0437\u0438\u043d: \u0427\u0430\u0441\u0442\u044c 3 \u2014 \u0412\u0438\u0442\u0440\u0438\u043d\u0430 \u0442\u043e\u0432\u0430\u0440\u043e\u0432<\/li>\n<\/ul>\n<p>  <\/p>\n<h2>\u0421\u0441\u044b\u043b\u043a\u0438<\/h2>\n<p>  <a href=\"https:\/\/gitlab.com\/VictorWinbringer\/blazoreshop\" rel=\"nofollow\">\u0418\u0441\u0445\u043e\u0434\u043d\u0438\u043a\u0438<\/a><br \/>  <a href=\"https:\/\/hub.docker.com\/u\/victorcallidus\" rel=\"nofollow\">\u041e\u0431\u0440\u0430\u0437\u044b \u043d\u0430 Docker Registry <\/a><\/p>\n<h2>\u041e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f<\/h2>\n<p>  \u041c\u0430\u0439\u043a\u0440\u043e\u0441\u043e\u0444\u0442 \u0434\u043e\u0431\u0430\u0432\u0438\u043b\u0438 \u0441\u0432\u043e\u044e \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0443 \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 <a href=\"https:\/\/docs.microsoft.com\/en-us\/aspnet\/core\/security\/blazor\/webassembly\/standalone-with-authentication-library?view=aspnetcore-3.1\" rel=\"nofollow\">Blazor WebAssembly standalone app with the Authentication library<\/a>.<\/p>\n<p>  \u0422\u0430\u043a \u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u043b\u0438 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0441 PWA <a href=\"https:\/\/docs.microsoft.com\/en-us\/aspnet\/core\/blazor\/progressive-web-app?view=aspnetcore-3.1&amp;tabs=visual-studio\" rel=\"nofollow\">Build Progressive Web Applications<\/a><\/p>\n<p>  <img decoding=\"async\" src=\"https:\/\/habrastorage.org\/webt\/ro\/yu\/rs\/royursvmcb0vaztdhw8rk-n_exi.png\"><\/p>\n<p>  \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u043c\u043e\u0436\u043d\u043e \u043d\u0430\u0436\u0430\u0432 \u043d\u0430 \u043f\u043b\u044e\u0441\u0438\u043a \u0432 \u0445\u0440\u043e\u043c\u0435:<\/p>\n<p>  <img decoding=\"async\" src=\"https:\/\/habrastorage.org\/webt\/nt\/vw\/gc\/ntvwgczspjgw_puep9gqtoijilg.png\"><\/p>\n<p>  \u0412\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u043e\u043d\u043e \u0432\u043e\u0442 \u0442\u0430\u043a:<\/p>\n<p>  <img decoding=\"async\" src=\"https:\/\/habrastorage.org\/webt\/pa\/pp\/zn\/pappznezyrfiwkvi7br_x0lwyi4.png\"><\/p>\n<p>  \u0422\u0430\u043a \u0436\u0435 \u0442\u0435\u043f\u0435\u0440\u044c \u043c\u043e\u0436\u043d\u043e \u0443\u0434\u043e\u0431\u043d\u043e \u043e\u0442\u043b\u0430\u0436\u0438\u0432\u0430\u0442\u044c \u043a\u043e\u0434 <a href=\"https:\/\/docs.microsoft.com\/en-us\/aspnet\/core\/blazor\/debug?view=aspnetcore-3.1\" rel=\"nofollow\">Debug WebAssembly<\/a><\/p>\n<p>  \u0414\u043e\u0431\u0430\u0432\u0438\u043b\u0438 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u0434\u0435\u043b\u0430\u0442\u044c async Task Main \u0438 \u0443\u0434\u0430\u043b\u0438\u043b\u0438 Startup:<\/p>\n<pre><code class=\"cs\">    public class Program     {         public static async Task Main(string[] args)         {             Console.WriteLine(&quot;START MAIN&quot;);             var builder = WebAssemblyHostBuilder.CreateDefault(args);             builder.RootComponents.Add&lt;App&gt;(&quot;app&quot;);             await ConfigureServices(builder.Services);             await builder.Build().RunAsync();             Console.WriteLine(&quot;END MAIN&quot;);         }          private static async Task&lt;ConfigModel&gt; GetConfig(IServiceCollection services)         {             using (var provider = services.BuildServiceProvider())             {                 var nm = provider.GetRequiredService&lt;NavigationManager&gt;();                 var uri = nm.BaseUri;                 Console.WriteLine($&quot;BASE URI: {uri}&quot;);                 var url = $&quot;{(uri.EndsWith('\/') ? uri : uri + &quot;\/&quot;)}api\/v1\/config&quot;;                 using var client = new HttpClient();                 return await client.GetJsonAsync&lt;ConfigModel&gt;(url);             }         }          private static async Task ConfigureServices(IServiceCollection services)         {             services.AddBaseAddressHttpClient();              var cfg = await GetConfig(services);             services.AddScoped&lt;ConfigModel&gt;(s =&gt; cfg);             Console.WriteLine($&quot;SSO URI IN STARTUP: {cfg?.SsoUri}&quot;);             services.AddOidcAuthentication(x =&gt;             {                 x.ProviderOptions.Authority = cfg.SsoUri;                 x.ProviderOptions.ClientId = &quot;spaBlazorClient&quot;;                 x.ProviderOptions.ResponseType = &quot;code&quot;;                 x.ProviderOptions.DefaultScopes.Add(&quot;api&quot;);                 x.UserOptions.RoleClaim = &quot;role&quot;;             });             services.AddTransient&lt;IAuthorizedHttpClientProvider, AuthorizedHttpClientProvider&gt;();             services.AddTransient&lt;IHttpService, HttpService&gt;();             services.AddTransient&lt;IApiRepository, ApiRepository&gt;();         }     } <\/code><\/pre>\n<p>  \u0412 \u043e\u0431\u0449\u0435\u043c \u043c\u0435\u043b\u043a\u043e\u043c\u044f\u0433\u043a\u0438\u0435 \u043c\u043e\u043b\u043e\u0434\u0446\u044b \u0438 \u043f\u043e \u043c\u043e\u0435\u043c\u0443 \u0437\u0430\u0441\u043b\u0443\u0436\u0438\u0432\u0430\u044e\u0442 <a href=\"https:\/\/github.com\/dotnet\/aspnetcore\" rel=\"nofollow\">\u0437\u0432\u0435\u0437\u0434\u043e\u0447\u043a\u0443<\/a>.<\/p>\n<h2>\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044b<\/h2>\n<p>  <\/p>\n<h3>Paginator<\/h3>\n<p>  \u041e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0437\u0430 \u043f\u043e\u0441\u0442\u0440\u0430\u043d\u0438\u0447\u043d\u0443\u044e \u043d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u044e.<\/p>\n<p>  \u041c\u043e\u0434\u0435\u043b\u044c:<\/p>\n<pre><code class=\"cs\">    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; }     } <\/code><\/pre>\n<p>  ViewModel + Razor:<\/p>\n<pre><code class=\"cs\">&lt;ul class=&quot;pagination justify-content-center mx-3 my-3&quot;&gt;     &lt;li class=&quot;page-item&quot;&gt;         &lt;a class=&quot;page-link&quot; href=&quot;#&quot; @onclick=&quot;@(async e =&gt; await LoadPage(First))&quot; @onclick:preventDefault&gt;&lt;&lt;&lt;\/a&gt;     &lt;\/li&gt;     &lt;li class=&quot;page-item&quot;&gt;         &lt;a class=&quot;page-link&quot; href=&quot;#&quot; @onclick=&quot;@(async e =&gt; await LoadPage(Prev))&quot; @onclick:preventDefault&gt;&lt;&lt;\/a&gt;     &lt;\/li&gt;     @{         foreach (var p in Pages)         {             &lt;li class=@(Model.CurrentPage == p ? &quot;page-item active&quot; : &quot;page-item&quot;)&gt;                 &lt;a class=&quot;page-link&quot; @onclick=&quot;@(async e =&gt; await LoadPage(p))&quot; href=&quot;#&quot; @onclick:preventDefault&gt;@(p + 1)&lt;\/a&gt;             &lt;\/li&gt;         }     }     &lt;li class=&quot;page-item&quot;&gt;&lt;a class=&quot;page-link&quot; href=&quot;#&quot; @onclick=&quot;@(async e =&gt; await LoadPage(Next))&quot; @onclick:preventDefault&gt;&gt;&lt;\/a&gt;&lt;\/li&gt;     &lt;li class=&quot;page-item&quot;&gt;&lt;a class=&quot;page-link&quot; href=&quot;#&quot; @onclick=&quot;@(async e =&gt; await LoadPage(Last))&quot; @onclick:preventDefault&gt;&gt;&gt;&lt;\/a&gt;&lt;\/li&gt;     &lt;li class=&quot;page-item&quot;&gt;         &lt;select id=&quot;size&quot; class=&quot;form-control&quot; value=&quot;@Model.ItemsPerPage&quot; @onchange=&quot;@OnItemsPerPageChanged&quot;&gt;             &lt;option value=10 selected&gt;10&lt;\/option&gt;             &lt;option value=20&gt;20&lt;\/option&gt;             &lt;option value=40&gt;40&lt;\/option&gt;             &lt;option value=80&gt;80&lt;\/option&gt;         &lt;\/select&gt;     &lt;\/li&gt; &lt;\/ul&gt; \/\/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 =&gt; 0;     public int Prev =&gt; Math.Max(Model.CurrentPage - 1, 0);     public int Next =&gt; Math.Min(Model.CurrentPage + 1, Math.Max(PageCount - 1, 0));     public int Last =&gt; Math.Max(PageCount - 1, 0);      public int PageCount     {         get         {             if (Model.ItemsPerPage &lt; 1 || Model.ItemsTotalCount &lt; 1)                 return 0;             var count = (Model.ItemsTotalCount \/ Model.ItemsPerPage);             if ((Model.ItemsTotalCount % Model.ItemsPerPage) &gt; 0)                 count++;             return count;          }     }      public IEnumerable&lt;int&gt; 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 &lt; max; i++)             {                 yield return i;             }         }     } } <\/code><\/pre>\n<p>  <\/p>\n<h3>Error<\/h3>\n<p>  \u041e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0437\u0430 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u043e\u0448\u0438\u0431\u043e\u043a \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044e.<\/p>\n<p>  ViewMode + Razor:<\/p>\n<pre><code class=\"cs\">@if (!string.IsNullOrWhiteSpace(Model)) {     &lt;div class=&quot;text-danger&quot;&gt;         &lt;h4&gt;\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430&lt;\/h4&gt;         &lt;p&gt;\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 \u0432\u043a\u043b\u0430\u0434\u043a\u0443 \u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u0437\u0436\u0435 \u0438\u043b\u0438 \u043e\u0431\u0440\u0430\u0442\u0438\u0442\u0435\u0441\u044c \u0432 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0443 \u0441 \u0442\u0435\u043a\u0441\u0442\u043e\u043c \u043e\u0448\u0438\u0431\u043a\u0438:&lt;\/p&gt;         &lt;p&gt;@Model&lt;\/p&gt;     &lt;\/div&gt; }  \/\/ViewModel @code {     [Parameter]     public string Model { get; set; } } <\/code><\/pre>\n<p>  <\/p>\n<h3>SortableTableHeader<\/h3>\n<p>  \u041e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0437\u0430 \u0441\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u043a\u0443 \u0434\u0430\u043d\u043d\u044b\u0445 \u0432 \u0442\u0430\u0431\u043b\u0438\u0446\u0435.<\/p>\n<p>  \u041c\u043e\u0434\u0435\u043b\u044c:<\/p>\n<pre><code class=\"cs\">    public sealed class SortableTableHeaderModel&lt;TId&gt;     {         public TId Current { get; set; }         public Dictionary&lt;TId, string&gt; Headers { get; set; }         public bool Descending { get; set; }     } <\/code><\/pre>\n<p>  ViewMode + Razor:<\/p>\n<pre><code class=\"cs\">@typeparam TId  &lt;thead&gt;     &lt;tr&gt;         @foreach (var kv in Model.Headers)         {             &lt;th @onclick=&quot;@(x=&gt;Sort(kv.Key))&quot;&gt;                 @kv.Value                 &lt;span class=&quot;@GetClass(kv.Key)&quot;&gt;&lt;\/span&gt;             &lt;\/th&gt;         }     &lt;\/tr&gt; &lt;\/thead&gt;  \/\/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 &quot;d-none&quot;;         return Model.Descending ? &quot;oi oi-caret-bottom&quot; : &quot;oi oi-caret-top&quot;;     }      [Parameter]     public SortableTableHeaderModel&lt;TId&gt; Model { get; set; }       [Parameter]     public EventCallback Sorted { get; set; } } <\/code><\/pre>\n<p>  <\/p>\n<pre><code class=\"cs\">@typeparam TId <\/code><\/pre>\n<p>  \u042d\u0442\u043e \u0433\u0435\u043d\u0435\u0440\u0438\u043a \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \u0442\u0438\u043f \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0431\u0443\u0434\u0435\u0442 \u0438\u043c\u0435\u0442\u044c \u043a\u043b\u044e\u0447 \u043f\u043e \u043a\u043e\u0442\u043e\u0440\u043e\u043c\u0443 \u043c\u044b \u0431\u0443\u0434\u0435\u0442 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043d\u0430\u0436\u0430\u0442\u044b\u0439 \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a \u0441\u0442\u043e\u043b\u0431\u0446\u0430. \u041f\u043e\u043a\u0430 \u0447\u0442\u043e \u043d\u0435\u0442 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u0438 \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u044f \u0434\u043b\u044f \u043d\u0435\u0433\u043e. \u0415\u0441\u043b\u0438 \u0431\u044b\u043b\u0430 \u0431\u044b \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u044f \u0431\u044b \u043f\u043e\u0432\u0435\u0441\u0438\u043b \u0447\u0442\u043e-\u0442\u043e \u0432\u0440\u043e\u0434\u0435 where TId:IEquatable <\/p>\n<h2>\u0410\u0442\u0440\u0438\u0431\u0443\u0442 \u0434\u043b\u044f \u0432\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u0438<\/h2>\n<p>  \u0412\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u0430\u044f \u0432\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u044f \u0434\u043b\u044f \u0444\u043e\u0440\u043c \u0432 Blazor \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0447\u0435\u0440\u0435\u0437 \u0430\u0442\u0440\u0438\u0431\u0443\u0442\u044b. \u042f \u0434\u043e\u0431\u0430\u0432\u0438\u043b \u0441\u0432\u043e\u0439 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439.<br \/>  \u041e\u043d \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442 \u0447\u0442\u043e \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0443 \u0435\u0433\u043e \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u0430 \u0431\u043e\u043b\u044c\u0448\u0435 \u0447\u0435\u043c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0435\u043c\u0443 \u043f\u0435\u0440\u0435\u0434\u0430\u043b\u0438 \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430. \u042f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e \u0435\u0433\u043e \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u0447\u0442\u043e \u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430\u044f \u0446\u0435\u043d\u0430 \u0431\u043e\u043b\u044c\u0448\u0435 \u043c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0439 \u0446\u0435\u043d\u044b.<\/p>\n<pre><code class=\"cs\">    [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) &gt;= 0)                 return ValidationResult.Success;             return Fail(validationContext);         }          private ValidationResult Fail(ValidationContext validationContext)         {             return new ValidationResult($&quot;\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u0431\u043e\u043b\u044c\u0448\u0435 \u0438\u043b\u0438 \u0440\u0430\u0432\u043d\u043e {DisplayName}&quot;,                 new[] { validationContext.MemberName });         }     } <\/code><\/pre>\n<p>  <\/p>\n<h2>\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u0430 \u0441\u043e \u0441\u043f\u0438\u0441\u043a\u043e\u043c \u043f\u0440\u043e\u0434\u0443\u043a\u0442\u043e\u0432<\/h2>\n<p>  \u041c\u043e\u0434\u0435\u043b\u044c:<\/p>\n<pre><code class=\"cs\">    public sealed class ProductsModel     {         public PageResultDto&lt;ProductDto&gt; Items { get; set; } = new PageResultDto&lt;ProductDto&gt;()         {             TotalCount = 0,             Value = new List&lt;ProductDto&gt;()         };         public bool IsLoaded { get; set; }         public PaginatorModel Paginator { get; set; } = new PaginatorModel()         {             ItemsTotalCount = 0,             Size = 5,             ItemsPerPage = 10,             CurrentPage = 0         };         public SortableTableHeaderModel&lt;ProductOrderBy&gt; TableHeaderModel { get; set; } = new SortableTableHeaderModel&lt;ProductOrderBy&gt;()         {             Current = ProductOrderBy.Id,             Descending = false,             Headers = new Dictionary&lt;ProductOrderBy, string&gt;()             {                 {ProductOrderBy.Name,&quot;Title&quot; },                 { ProductOrderBy.Price, &quot;Price&quot;}             }         };         public string HandledErrors { get; set; }         public string Title { get; set; }         public decimal MinPrice { get; set; } = 0m;         public decimal? MaxPrice { get; set; }     } <\/code><\/pre>\n<p>  ViewModel:<\/p>\n<pre><code class=\"cs\">    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 = &quot;\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u043f\u0440\u043e\u0434\u0443\u043a\u0442\u0430 \u0434\u043e\u043b\u0436\u043d\u043e \u0431\u044b\u0442\u044c \u043a\u043e\u0440\u043e\u0447\u0435 30 \u0441\u0438\u043c\u0432\u043e\u043b\u043e\u0432&quot;)]         public string Title { get; set; }         [GreaterOrEqualTo(nameof(Min), &quot;0&quot;)]         public decimal MinPrice { get; set; } = 0m;         public decimal Min =&gt; 0m;         [GreaterOrEqualTo(nameof(MinPrice), &quot;Min Price&quot;)]         public decimal? MaxPrice { get; set; }         public int Skip =&gt; Model.Paginator.ItemsPerPage * Model.Paginator.CurrentPage;         public int Take =&gt; 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&lt;ProductDto&gt;();             Model.Paginator.ItemsTotalCount = Model.Items.TotalCount;             Model.IsLoaded = true;         }     } <\/code><\/pre>\n<p>  Razor:<\/p>\n<pre><code class=\"cs\">@inherits ProductsViewModel @page &quot;\/products&quot;  &lt;h3&gt;Products&lt;\/h3&gt; &lt;div class=&quot;jumbotron col-md-6&quot;&gt;     &lt;EditForm Model=&quot;@this&quot; OnValidSubmit=&quot;@HandleValidSubmit&quot;&gt;         &lt;DataAnnotationsValidator \/&gt;         &lt;div class=&quot;form-row&quot;&gt;             &lt;div class=&quot;form-group col-md-12&quot;&gt;                 &lt;label for=&quot;title&quot;&gt;Title&lt;\/label&gt;                 &lt;InputText id=&quot;title&quot; @bind-Value=&quot;@Title&quot; class=&quot;form-control&quot; \/&gt;                 &lt;ValidationMessage For=&quot;@(() =&gt;Title)&quot; \/&gt;             &lt;\/div&gt;         &lt;\/div&gt;         &lt;div class=&quot;form-row&quot;&gt;             &lt;div class=&quot;form-group col-md-6&quot;&gt;                 &lt;label for=&quot;min&quot;&gt;Min Price&lt;\/label&gt;                 &lt;InputNumber id=&quot;min&quot; @bind-Value=&quot;@MinPrice&quot; class=&quot;form-control&quot; TValue=&quot;decimal&quot; \/&gt;                 &lt;ValidationMessage For=&quot;@(() =&gt; MinPrice)&quot; \/&gt;             &lt;\/div&gt;             &lt;div class=&quot;form-group col-md-6&quot;&gt;                 &lt;label for=&quot;max&quot;&gt;Max Price&lt;\/label&gt;                 &lt;InputNumber id=&quot;max&quot; @bind-Value=&quot;@MaxPrice&quot; class=&quot;form-control&quot; TValue=&quot;decimal?&quot; \/&gt;                 &lt;ValidationMessage For=&quot;@(() =&gt;MaxPrice)&quot; \/&gt;             &lt;\/div&gt;         &lt;\/div&gt;         &lt;button type=&quot;submit&quot; class=&quot;btn btn-primary&quot; disabled=&quot;@(!context.Validate())&quot;&gt;Submit&lt;\/button&gt;     &lt;\/EditForm&gt; &lt;\/div&gt; &lt;nav aria-label=&quot;Table pages&quot;&gt;     &lt;Paginator OnPageChanged=&quot;@LoadPage&quot; Model=&quot;@Model.Paginator&quot; \/&gt; &lt;\/nav&gt; &lt;div&gt;     &lt;Error Model=&quot;@Model.HandledErrors&quot; \/&gt; &lt;\/div&gt; &lt;div class=&quot;table-responsive&quot;&gt;     &lt;table class=&quot;table&quot;&gt;         &lt;SortableTableHeader Sorted=&quot;@HandleSort&quot; Model=&quot;@Model.TableHeaderModel&quot; TId=&quot;ProductOrderBy&quot; \/&gt;         &lt;tbody&gt;             @if (Model.IsLoaded)             {                 @foreach (var product in Model.Items.Value)                 {                     &lt;tr&gt;                         &lt;td&gt;@product.Title&lt;\/td&gt;                         &lt;td&gt;@product.Price&lt;\/td&gt;                     &lt;\/tr&gt;                 }             }             else             {                 &lt;tr&gt;                     &lt;td&gt;                         &lt;p&gt;&lt;em&gt;Loading...&lt;\/em&gt;&lt;\/p&gt;                     &lt;\/td&gt;                 &lt;\/tr&gt;             }         &lt;\/tbody&gt;     &lt;\/table&gt; &lt;\/div&gt; <\/code><\/pre>\n<p>  <\/p>\n<h2>\u0412\u0435\u0440\u0441\u0438\u044f \u043d\u0430 Angular<\/h2>\n<p>  \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043b <a href=\"https:\/\/www.primefaces.org\/primeng\/showcase\/#\/table\/page\" rel=\"nofollow\">PrimeNG<\/a> \u0412\u0440\u0435\u043c\u044f \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 Blazor WASM 1.47 \u043f\u0440\u043e\u0442\u0438\u0432 0.35 \u0441\u0435\u043a\u0443\u043d\u0434 \u0432 \u043f\u043e\u043b\u044c\u0437\u0443 \u0410\u043d\u0433\u0443\u043b\u044f\u0440\u0430:<\/p>\n<p>  <img decoding=\"async\" src=\"https:\/\/habrastorage.org\/webt\/14\/h2\/1_\/14h21_jy9dsl-qeprapml1yvmiy.png\"><\/div>\n<p> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/post\/494612\/\"> https:\/\/habr.com\/ru\/post\/494612\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"\n<div class=\"post__text post__text-html post__text_v1\" id=\"post-content-body\" data-io-article-url=\"https:\/\/habr.com\/ru\/post\/494612\/\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/webt\/kz\/ip\/hb\/kziphbtuihaxaaa3ue93cdyfhhs.png\"><\/p>\n<p>  \u041f\u0440\u0438\u0432\u0435\u0442, \u0425\u0430\u0431\u0440! \u041f\u0440\u043e\u0434\u043e\u043b\u0436\u0430\u044e \u0434\u0435\u043b\u0430\u0442\u044c \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442 \u043c\u0430\u0433\u0430\u0437\u0438\u043d \u043d\u0430 Blazor. \u0412 \u044d\u0442\u043e\u0439 \u0447\u0430\u0441\u0442\u0438 \u0440\u0430\u0441\u0441\u043a\u0430\u0436\u0443 \u043e \u0442\u043e\u043c \u043a\u0430\u043a \u0434\u043e\u0431\u0430\u0432\u0438\u043b \u0432 \u043d\u0435\u0433\u043e \u0432\u0438\u0442\u0440\u0438\u043d\u0443 \u0442\u043e\u0432\u0430\u0440\u043e\u0432 \u0438 \u0441\u0434\u0435\u043b\u0430\u043b \u0441\u0432\u043e\u0438 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044b. \u0417\u0430 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u044f\u043c\u0438 \u0434\u043e\u0431\u0440\u043e \u043f\u043e\u0436\u0430\u043b\u043e\u0432\u0430\u0442\u044c \u043f\u043e\u0434 \u043a\u0430\u0442.   <\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-301124","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/301124","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=301124"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/301124\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=301124"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=301124"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=301124"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}