اشتراکها
فرض کنید کدی مانند زیر را در یک کامپوننت داریم و انتظار این است که با کلیک بر روی Section2، به بخش مورد نظر اسکرول شویم:
اما متاسفانه در Blazor Server تا نسخه فعلی آن (نسخه هفت)، این کار ساده به راحتی امکانپذیر نیست. همانطور که ملاحظه میکنید، به دو روش، نویگیشن انجام شدهاست؛ اما هیچیک ما را به هدف نمیرسانند. دلیل این موضوع، رفتار Blazor Server در بارگذاری صفحات میباشد. در حقیقت المانها موقع بارگذاری، هنوز در صفحه وجود ندارند. در واقع ابتدا نیاز است که اتصال SignalR برقرار شود و سپس دادهها از سرور دریافت شوند (مگر در حالت pre-rendered که مشکلات خاص خود را در پی دارد).
سپس کد جاوا اسکریپتی زیر را در جایی قبل از فراخوانی <script src="_framework/blazor.server.js"></script> قرار میدهیم (برای مثال اگر میخواهیم در اکثر صفحات از آن بهره ببریم، آن را در layout.cshtmlـ قرار میدهیم).
حال در هر کامپوننتی که نیاز به استفاده از لنگر (anchor) داریم، به شکل زیر عمل میکنیم:
و پیرو آن، صفحهی موردنظر برای استفاده از لنگر نیز به شکل زیر تغییر خواهد کرد:
@page "/test" <nav> <!-- یک روش --> <a href="#section2">Section2</a> <!-- روش دیگر --> <NavLink href="#section2">Section2</NavLink> </nav> @* ... *@ <h2 id="section2">It's Section2.</h2> @* ... *@
برای انجام این کار دو روش وجود دارد؛ یکی بر پایهی جاوااسکریپت است و دیگری توسط توابع داخلی Blazor JS.
روش جاوااسکریپتی
ابتدا یک کامپوننت را به نام AnchorNavigation ایجاد مینماییم:
@inject IJSRuntime JSRuntime @inject NavigationManager NavigationManager @implements IDisposable @code { protected override void OnInitialized() { NavigationManager.LocationChanged += OnLocationChanged; } protected override async Task OnAfterRenderAsync(bool firstRender) { await ScrollToFragment(); } public void Dispose() { NavigationManager.LocationChanged -= OnLocationChanged; } private async void OnLocationChanged(object sender, LocationChangedEventArgs e) { await ScrollToFragment(); } private async Task ScrollToFragment() { var uri = new Uri(NavigationManager.Uri, UriKind.Absolute); var fragment = uri.Fragment; if (fragment.StartsWith('#')) { // Handle text fragment (https://example.org/#test:~:text=foo) // https://github.com/WICG/scroll-to-text-fragment/ var elementId = fragment.Substring(1); var index = elementId.IndexOf(":~:", StringComparison.Ordinal); if (index > 0) { elementId = elementId.Substring(0, index); } if (!string.IsNullOrEmpty(elementId)) { await JSRuntime.InvokeVoidAsync("BlazorScrollToId", elementId); } } } }
function BlazorScrollToId(id) { const element = document.getElementById(id); if (element instanceof HTMLElement) { element.scrollIntoView({ behavior: "smooth", block: "start", inline: "nearest" }); } }
@page "/" <PageTitle>Index</PageTitle> <a href="#section2"> <h1>Section2</h1> </a> <SurveyPrompt Title="How is Blazor working for you?" /> <div style="height: 2000px"> </div> <div id="section2"> <h2>It's Section2. </h2> </div> <AnchorNavigation />
روش استفاده از توابع داخلی Blazor JS
می توان از ElementReference و FocusAsync که در حقیقت مربوط به خود Blazor JS میباشند استفاده نمود. اینبار کدهای کامپوننت AnchorNavigation را به شکل زیر تغییر میدهیم:
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.Routing; using System.Diagnostics.CodeAnalysis; namespace TestAnchorNavigation; public class AnchorNavigation: ComponentBase, IDisposable { private bool _setFocus; [Inject] private NavigationManager NavManager { get; set; } = default!; [Parameter] public RenderFragment? ChildContent { get; set; } [Parameter] public string? BookmarkName { get; set; } [DisallowNull] public ElementReference? Element { get; private set; } protected override void BuildRenderTree(RenderTreeBuilder builder) { builder.OpenElement(0, "span"); builder.AddAttribute(2, "tabindex", "-1"); builder.AddContent(3, this.ChildContent); builder.AddElementReferenceCapture(4, this.SetReference); builder.CloseElement(); } protected override void OnInitialized() => NavManager.LocationChanged += this.OnLocationChanged; protected override void OnParametersSet() => _setFocus = this.IsMe(); private void SetReference(ElementReference reference) => this.Element = reference; private void OnLocationChanged(object? sender, LocationChangedEventArgs e) { if (this.IsMe()) { _setFocus = true; this.StateHasChanged(); } } protected async override Task OnAfterRenderAsync(bool firstRender) { if (_setFocus) await this.Element!.Value.FocusAsync(false); _setFocus = false; } private bool IsMe() { string? elementId = null; var uri = new Uri(this.NavManager.Uri, UriKind.Absolute); if (uri.Fragment.StartsWith('#')) { elementId = uri.Fragment.Substring(1); return elementId == BookmarkName; } return false; } public void Dispose() => NavManager.LocationChanged -= this.OnLocationChanged; }
@page "/" <PageTitle>Index</PageTitle> <NavLink href="#section2"> <h1>Section2</h1> </NavLink> <SurveyPrompt Title="How is Blazor working for you?" /> <div style="height: 2000px"> </div> <AnchorNavigation BookmarkName="section2"> <h2>It's Section2. </h2> </AnchorNavigation>
The desktop is here to stay. Sam Basu reviews how you can take advantage of the latest in .NET technologies and still build the apps your customers demand.
Sam covers Windows Forms, XAML (UWP,
WinUI), Progressive Web Apps, Electron, Blazor Mobile Bindings, .NET
MAUI and building for Mac OS and Linux.
اشتراکها
ASP.NET Core 5 Preview 6 منتشر شد
.NET 5 Preview 6 is now available and is ready for evaluation. Here’s what’s new in this release:
- Blazor WebAssembly template now included
- JSON extension methods for
HttpRequest
andHttpResponse
- Extension method to allow anonymous access to an endpoint
- Custom handling of authorization failures
- SignalR Hub filters
در قسمت قبل، نحوهی افزودن قالب ابتدایی ASP.NET Core Identity را به یک برنامهی Blazor Server بررسی کردیم. در این مطلب، قسمتهای ورود و خروج آنرا به همراه نمایش قسمتی از صفحه، تنها به کاربران اعتبارسنجی شده، بررسی میکنیم تا روش دسترسی به اطلاعات ASP.NET Core Identity را در یک برنامهی Blazor Server یکپارچه شدهی با آن، مطالعه کنیم.
نمایش قسمتی از صفحه بر اساس وضعیت اعتبارسنجی کاربر
فرض کنید میخواهیم در کامپوننت Shared\LoginDisplay.razor که در قسمت قبل آنرا اضافه کردیم، لینکهای ثبت نام و لاگین را به کاربران غیر اعتبارسنجی شده (هنوز لاگین نکرده) نمایش دهیم و اگر کاربر، اعتبارسنجی شده بود (لاگین کرده بود)، لینک خروج را به او نمایش دهیم. برای این منظور کامپوننت Shared\LoginDisplay.razor را به صورت زیر تغییر میدهیم:
AuthorizeView، یکی از کامپوننتهای استاندارد Blazor Server است. زمانیکه کاربری به سیستم لاگین کرده باشد، فرگمنت Authorized و در غیر اینصورت قسمت NotAuthorized آنرا مشاهده خواهد کرد.
البته اگر برنامه را در همین حالت اجرا کنیم، به استثنای زیر خواهیم رسید:
برای رفع این مشکل و ارائهی AuthenticationState به تمام کامپوننتهای یک برنامهی Blazor Server، نیاز است از کامپوننت CascadingAuthenticationState استفاده کرد. در مورد پارامترهای آبشاری، در قسمت نهم این سری بیشتر بحث شد و هدف از آن، ارائهی یکسری اطلاعات، به تمام زیر کامپوننتهای یک کامپوننت والد است؛ بدون اینکه نیاز باشد مدام این پارامترها را در هر زیر کامپوننتی، تعریف و تنظیم کنیم. همینقدر که آنها را در بالاترین سطح سلسله مراتب کامپوننتهای تعریف شده تعریف کردیم، در تمام زیر کامپوننتهای آن نیز در دسترس خواهند بود.
بنابراین به فایل BlazorServer.App\App.razor که محل تعریف ریشهی مسیریابی برنامهاست، مراجعه کرده و کامپوننت آنرا با کامپوننت توکار CascadingAuthenticationState محصور میکنیم:
اینکار سبب میشود تا اطلاعات AuthenticationState، بین تمام کامپوننتهای یک برنامهی Blazor به اشتراک گذاشته شود.
اکنون اگر برنامه را اجرا کنیم، مشاهده خواهیم کرد که در اولین بار مراجعهی به آن (پیش از لاگین)، لینک به صفحهی خروج، نمایش داده نشدهاست؛ چون آنرا در فرگمنت مخصوص Authorized قرار دادیم:
آزمایش نمایش منوی خروج برنامه
برای آزمایش برنامه، نیاز است ابتدا یک کاربر جدید را ثبت کنیم؛ چون هنوز هیچ کاربری در آن ثبت نشدهاست و همچنین کاربر پیشفرضی را هم به همراه ندارد. در مورد روش ثبت کاربران پیشفرض ASP.NET Core Identity، میتوانید به مطلب «بازنویسی متد مقدار دهی اولیهی کاربر ادمین در ASP.NET Core Identity توسط متد HasData در EF Core» مراجعه کنید و تمام نکات آن، در اینجا هم صادق است (چون پایهی سیستم Identity مورد استفاده، یکی است و هدف ما در اینجا بیشتر بررسی نکات یکپارچه سازی آن با Blazor Server است و نه مرور تمام نکات ریز Identity).
بنابراین ابتدا از منوی بالای صفحه، گزینهی Register را انتخاب کرده و کاربری را ثبت میکنیم. پس از ثبت نام، بلافاصله به منوی جدید زیر میرسیم که در آن گزینههای ورود و ثبت نام، مخفی شدهاند و اکنون گزینهی خروج از سیستم را نمایش میدهد:
بهبود تجربهی کاربری خروج از سیستم
در همین حال که گزینهی خروج نمایش داده شدهاست، اگر بر روی لینک آن کلیک کنیم، ابتدا ما را به صفحهی مجزای logout هدایت میکند. سپس باید در این صفحه، مجددا بر روی لینک logout بالای آن کلیک کنیم. زمانیکه اینکار را انجام دادیم، اکنون صفحهی دیگری را نمایش میدهد که به همراه پیام «خروج موفقیت آمیز از سیستم» است! در این پروسه، کاربر احساس میکند که کاملا از برنامهی اصلی خارج شدهاست و همچنین مراحل طولانی را نیز باید طی کند.
مدیریت این مراحل توسط دو فایل زیر انجام میشوند:
Areas\Identity\Pages\Account\Logout.cshtml
Areas\Identity\Pages\Account\Logout.cshtml.cs
میخواهیم کدهای این دو فایل را به نحوی تغییر دهیم که اگر کاربری بر روی لینک logout برنامهی اصلی کلیک کرد، به صورت خودکار logout شده و سپس مجددا به صفحهی اصلی برنامهی Blazor Server هدایت شود و مجبور نباشد تا مراحل طولانی یاد شده را تکرار کند.
به همین جهت ابتدا فایل Logout.cshtml.cs را حذف میکنیم؛ چون نیازی به آن نداریم. سپس محتوای فایل Logout.cshtml را به صورت زیر تغییر میدهیم:
با استفاده از سرویس SignInManager در ASP.NET Core Identity میتوان یک کاربر را logout کرد که نمونهای از آنرا در اینجا مشاهده میکنید. در این حالت بررسی میشود که آیا کاربر جاری، به سیستم وارد شدهاست؟ اگر بله، کوکیهای او حذف شده و سپس به صفحهی اصلی برنامه، Redirect میشود. به این ترتیب به تجربهی کاربری خروج بهتری خواهیم رسید.
نمایش User Claims، در یک برنامهی Blazor Server
سیستم ASP.NET Core Identity، بر اساس User Claims کار میکند؛ اطلاعات بیشتر. پس از استفاده از CascadingAuthenticationState در بالاترین سطح برنامه، اطلاعات آن در سراسر برنامهی Blazor Server هم قابل دسترسی است. برای مثال در کامپوننت Shared\LoginDisplay.razor، به نحو زیر میتوان نام کاربر ثبت نام شده را که یکی از User Claims او است، نمایش داد:
محدود کردن دسترسی به صفحات برنامه تنها برای کاربران اعتبارسنجی شده
پس از لاگین موفق به سیستم، اکنون میخواهیم دسترسی به صفحات تعریف اتاقها و یا امکانات رفاهی هتل را تنها به کاربران لاگین شده، محدود کنیم. برای اینکار تنها کافی است از ویژگی Authorize استفاده کنیم. برای مثال به کامپوننت Pages\HotelRoom\HotelRoomList.razor مراجعه کرده و یک سطر زیر را به آن اضافه میکنیم:
دسترسی به کامپوننتی که دارای دایرکتیو فوق باشد، تنها مختص به کاربران اعتبارسنجی شدهی سیستم است.
مشکل! با اینکه تمام کامپوننتهای مثال جاری را به ویژگی Authorize مزین کردهایم، اما ... کار نمیکند! و هنوز هم میتوان بدون لاگین به سیستم، به محتوای آنها دسترسی داشت.
برای رفع این مشکل، مجددا نیاز است کامپوننت BlazorServer.App\App.razor را ویرایش کرد:
در اینجا RouteView پیشین را کامنت کرده و با AuthorizeRouteView، جایگزین کردهایم. کار آن فعالسازی پردازش ویژگی Authorize افزوده شدهی به کامپوننتهای برنامهاست. همچنین در اینجا محتوای سفارشی را که در صورت درخواست یک چنین کامپوننتهایی نمایش داده میشود، در فرگمنت NotAuthorized مشاهده میکنید؛ که حتی میتواند یک کامپوننت مجزا هم باشد:
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-22.zip
نمایش قسمتی از صفحه بر اساس وضعیت اعتبارسنجی کاربر
فرض کنید میخواهیم در کامپوننت Shared\LoginDisplay.razor که در قسمت قبل آنرا اضافه کردیم، لینکهای ثبت نام و لاگین را به کاربران غیر اعتبارسنجی شده (هنوز لاگین نکرده) نمایش دهیم و اگر کاربر، اعتبارسنجی شده بود (لاگین کرده بود)، لینک خروج را به او نمایش دهیم. برای این منظور کامپوننت Shared\LoginDisplay.razor را به صورت زیر تغییر میدهیم:
<AuthorizeView> <Authorized> <a href="Identity/Account/Logout">Logout</a> </Authorized> <NotAuthorized> <a href="Identity/Account/Register">Register</a> <a href="Identity/Account/Login">Login</a> </NotAuthorized> </AuthorizeView>
البته اگر برنامه را در همین حالت اجرا کنیم، به استثنای زیر خواهیم رسید:
InvalidOperationException: Authorization requires a cascading parameter of type Task<AuthenticationState>. Consider using CascadingAuthenticationState to supply this. Microsoft.AspNetCore.Components.Authorization.AuthorizeViewCore.OnParametersSetAsync()
بنابراین به فایل BlazorServer.App\App.razor که محل تعریف ریشهی مسیریابی برنامهاست، مراجعه کرده و کامپوننت آنرا با کامپوننت توکار CascadingAuthenticationState محصور میکنیم:
<CascadingAuthenticationState> <Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true"> <Found Context="routeData"> <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /> </Found> <NotFound> <LayoutView Layout="@typeof(MainLayout)"> <p>Sorry, there's nothing at this address.</p> </LayoutView> </NotFound> </Router> </CascadingAuthenticationState>
اکنون اگر برنامه را اجرا کنیم، مشاهده خواهیم کرد که در اولین بار مراجعهی به آن (پیش از لاگین)، لینک به صفحهی خروج، نمایش داده نشدهاست؛ چون آنرا در فرگمنت مخصوص Authorized قرار دادیم:
آزمایش نمایش منوی خروج برنامه
برای آزمایش برنامه، نیاز است ابتدا یک کاربر جدید را ثبت کنیم؛ چون هنوز هیچ کاربری در آن ثبت نشدهاست و همچنین کاربر پیشفرضی را هم به همراه ندارد. در مورد روش ثبت کاربران پیشفرض ASP.NET Core Identity، میتوانید به مطلب «بازنویسی متد مقدار دهی اولیهی کاربر ادمین در ASP.NET Core Identity توسط متد HasData در EF Core» مراجعه کنید و تمام نکات آن، در اینجا هم صادق است (چون پایهی سیستم Identity مورد استفاده، یکی است و هدف ما در اینجا بیشتر بررسی نکات یکپارچه سازی آن با Blazor Server است و نه مرور تمام نکات ریز Identity).
بنابراین ابتدا از منوی بالای صفحه، گزینهی Register را انتخاب کرده و کاربری را ثبت میکنیم. پس از ثبت نام، بلافاصله به منوی جدید زیر میرسیم که در آن گزینههای ورود و ثبت نام، مخفی شدهاند و اکنون گزینهی خروج از سیستم را نمایش میدهد:
بهبود تجربهی کاربری خروج از سیستم
در همین حال که گزینهی خروج نمایش داده شدهاست، اگر بر روی لینک آن کلیک کنیم، ابتدا ما را به صفحهی مجزای logout هدایت میکند. سپس باید در این صفحه، مجددا بر روی لینک logout بالای آن کلیک کنیم. زمانیکه اینکار را انجام دادیم، اکنون صفحهی دیگری را نمایش میدهد که به همراه پیام «خروج موفقیت آمیز از سیستم» است! در این پروسه، کاربر احساس میکند که کاملا از برنامهی اصلی خارج شدهاست و همچنین مراحل طولانی را نیز باید طی کند.
مدیریت این مراحل توسط دو فایل زیر انجام میشوند:
Areas\Identity\Pages\Account\Logout.cshtml
Areas\Identity\Pages\Account\Logout.cshtml.cs
میخواهیم کدهای این دو فایل را به نحوی تغییر دهیم که اگر کاربری بر روی لینک logout برنامهی اصلی کلیک کرد، به صورت خودکار logout شده و سپس مجددا به صفحهی اصلی برنامهی Blazor Server هدایت شود و مجبور نباشد تا مراحل طولانی یاد شده را تکرار کند.
به همین جهت ابتدا فایل Logout.cshtml.cs را حذف میکنیم؛ چون نیازی به آن نداریم. سپس محتوای فایل Logout.cshtml را به صورت زیر تغییر میدهیم:
@page @using Microsoft.AspNetCore.Identity @inject SignInManager<IdentityUser> SignInManager @functions { public async Task<IActionResult> OnGet() { if (SignInManager.IsSignedIn(User)) { <p>You have successfully logged out of the application.</p> await SignInManager.SignOutAsync(); } return Redirect("~/"); } }
نمایش User Claims، در یک برنامهی Blazor Server
سیستم ASP.NET Core Identity، بر اساس User Claims کار میکند؛ اطلاعات بیشتر. پس از استفاده از CascadingAuthenticationState در بالاترین سطح برنامه، اطلاعات آن در سراسر برنامهی Blazor Server هم قابل دسترسی است. برای مثال در کامپوننت Shared\LoginDisplay.razor، به نحو زیر میتوان نام کاربر ثبت نام شده را که یکی از User Claims او است، نمایش داد:
<AuthorizeView> <Authorized> Hello, @context.User.Identity.Name <a href="Identity/Account/Logout">Logout</a> </Authorized>
محدود کردن دسترسی به صفحات برنامه تنها برای کاربران اعتبارسنجی شده
پس از لاگین موفق به سیستم، اکنون میخواهیم دسترسی به صفحات تعریف اتاقها و یا امکانات رفاهی هتل را تنها به کاربران لاگین شده، محدود کنیم. برای اینکار تنها کافی است از ویژگی Authorize استفاده کنیم. برای مثال به کامپوننت Pages\HotelRoom\HotelRoomList.razor مراجعه کرده و یک سطر زیر را به آن اضافه میکنیم:
@attribute [Authorize]
مشکل! با اینکه تمام کامپوننتهای مثال جاری را به ویژگی Authorize مزین کردهایم، اما ... کار نمیکند! و هنوز هم میتوان بدون لاگین به سیستم، به محتوای آنها دسترسی داشت.
برای رفع این مشکل، مجددا نیاز است کامپوننت BlazorServer.App\App.razor را ویرایش کرد:
<CascadingAuthenticationState> <Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true"> <Found Context="routeData"> @*<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />*@ <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"> <NotAuthorized> <p>Sorry, you do not have access to this page</p> </NotAuthorized> </AuthorizeRouteView> </Found> <NotFound> <LayoutView Layout="@typeof(MainLayout)"> <p>Sorry, there's nothing at this address.</p> </LayoutView> </NotFound> </Router> </CascadingAuthenticationState>
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-22.zip
امکان تهیه Custom Elements در NET 6 Blazor.
در ادامه یک کامپوننت Todo ایجاد خواهیم کرد:
در نهایت درون فایل App.js میتوانیم از کامپوننت Todo استفاده کنیم:
در آخرین نسخه Blazor این امکان فراهم شده است که بتوانیم از کامپوننتهای Blazor درون پروژههای React/Vue, Angular, ... استفاده کنیم (+). البته این فیچر هنوز به صورت آزمایشی میباشد و ممکن است API آن تغییر کند.
در ادامه یک مثال از این قابلیت را مشاهده خواهید کرد.
ایجاد پروژه Blazor
یک دایرکتوری ایجاد کرده و درون آن یک پروژه blazorwasm با نام blazor_wasm ایجاد کنید:
dotnet new blazorwasm blazor_wasm
برای استفاده از این فیچر میبایست پکیج Microsoft.AspNetCore.Components.CustomElements را نصب کنیم:
dotnet add package Microsoft.AspNetCore.Components.CustomElements --version 0.1.0-alpha.21466.1
@page "/todo" <PageTitle>Todo</PageTitle> <h1>Todo (@todos.Count(todo => !todo.IsDone))</h1> <ul> @foreach (var todo in todos) { <li> <input type="checkbox" @bind="todo.IsDone" /> <input @bind="todo.Title" /> </li> } </ul> <input placeholder="Something todo" @bind="newTodo" /> <button @onclick="AddTodo">Add todo</button> @code { public class TodoItem { public string? Title { get; set; } public bool IsDone { get; set; } } private List<TodoItem> todos = new(); private string? newTodo; private async void AddTodo(MouseEventArgs e) { if (!string.IsNullOrWhiteSpace(newTodo)) { todos.Add(new TodoItem { Title = newTodo }); newTodo = string.Empty; } } }
برای تبدیل کامپوننت فوق به یک Custom Element درون فایل Program.cs خط زیر را اضافه میکنیم:
builder.RootComponents.RegisterAsCustomElement<Todo>("todo-element");
استفاده از کامپوننت فوق درون یک پروژه React
npx create-react-app blazor_react && cd blazor_react
برای استفاده از Custom Element موردنظر دو خط زیر را به فایل public/index.html اضافه میکنیم:
<script src="_content/Microsoft.AspNetCore.Components.CustomElements/BlazorCustomElements.js"></script> <script src="_framework/blazor.webassembly.js"></script>
همچنین لازم است یک پراکسی نیز درون پروژه ایجاد کنیم (درون فایل package.json)؛ با اینکار اسکریپتهای موردنیاز فوق از سمت سرور دریافت خواهند شد:
"proxy": "BLAZOR_APP_ADDRESS", // for example: http://localhost:5269
function App() { return ( <div className="App"> <todo-element /> </div> ); } export default App;
نظرات اشتراکها
پیاده سازی چندمستاجری با EF Core و Blazor Server
سلام، پس با این وجود و چیزی که من تا به اینجا متوجه شدم نرم افزارهای سازمانی که میخوایم با بلیزر و البته با استفاده از استراتژی multi-Tenancy طراحی و پیاده سازی کنیم باید با Blazor Server انجام شوند و نه با WASM...درسته؟؟
اشتراکها