نظرات مطالب
مطلب مفصلی در مورد جزئیات تغییرات مسیریاب جدید AngularJS 2
Angular 2 New Router Overview (خلاصه کاربردی از دیدگاه ارتقاء کدها)
Routing in Angular 2 revisited
Angular 2 New Router Overview (خلاصه کاربردی از دیدگاه ارتقاء کدها)
Routing in Angular 2 revisited
نظرات مطالب
AngularJS #1
knockoutjs کتاب خانه ای برای انجام DataBinding در وب هست، یعنی برای مقید سازی اشیاء جاوا اسکریپت به کنترلهای html.
AngularJS یک Toolset برای درست کردن framework و برنامههای تک صفحه ای میباشد، angular قابلیت مقید سازی دادهها را هم فراهم میکند.
لطفا برای اطلاعات بیشتر این مطلب را مطالعه کنید.
با تشکر از مطلب مفید شما
این کتابخانه داره از کتابخانه ی jQuery File Upload استفاده میکنه. به نظرم کسی نحوهی استفاده از این کتاب خانه را با توجه به sampleهای خود سایتش یاد بگیره خیلی بهتره. نسخهی angularjs هم داره.
نظرات مطالب
مسیریابی در AngularJs #بخش اول
نمیشه مستقیما. چون مسیر کاری MVC از اکشن متد و کنترلرها شروع میشه و نه از Viewها. Viewهای قرار گرفته در پوشه Views دارای web.config عدم دسترسی از خارج از سایت هستند. کدهای angularjs هم سمت کاربر هست و نه سمت سرور.
پس از ایجاد AuthService در قسمت قبل، اکنون میخواهیم از آن برای تکمیل صفحهی ورود به سیستم و همچنین تغییر منوی بالای برنامه یا همان کامپوننت header استفاده کنیم.
ایجاد ماژول Dashboard و تعریف کامپوننت صفحهی محافظت شده
قصد داریم پس از لاگین موفق، کاربر را به یک صفحهی محافظت شده هدایت کنیم. به همین جهت ماژول جدید Dashboard را به همراه کامپوننت یاد شده، به برنامه اضافه میکنیم:
پس از اجرای این دستورات، ابتدا به فایل app.module.ts مراجعه کرده و تعریف این ماژول را که به صورت خودکار به قسمت imports اضافه شدهاست، به قبل از AppRoutingModule منتقل میکنیم تا مسیریابیهای آن توسط catch all ماژول AppRouting لغو نشوند:
همچنین به فایل dashboard-routing.module.ts ایجاد شده مراجعه کرده و مسیریابی کامپوننت جدید ProtectedPage را اضافه میکنیم:
ایجاد صفحهی ورود به سیستم
در قسمت اول این سری، کارهای «ایجاد ماژول Authentication و تعریف کامپوننت لاگین» انجام شدند. اکنون میخواهیم کامپوننت خالی لاگین را به نحو ذیل تکمیل کنیم:
مدل login را همان اینترفیس Credentials تعریف شدهی در قسمت قبل درنظر گرفتهایم. به همراه دو خاصیت error جهت نمایش خطاها در ذیل قسمت لاگین و همچنین ذخیره سازی returnUrl در صورت وجود:
AuthService را به سازندهی کامپوننت لاگین تزریق کردهایم تا بتوان از متدهای login و Logout آن استفاده کرد. همچنین از سرویس Router برای استفادهی از متد navigate آن استفاده میکنیم و از سرویس ActivatedRoute برای دریافت کوئری استرینگ returnUrl، در صورت وجود، کمک خواهیم گرفت.
اکنون متد ارسال این فرم چنین شکلی را پیدا میکند:
در اینجا توسط وهلهی تزریق شدهی this.authService، کار فراخوانی متد login و ارسال شیء Credentials به سمت سرور صورت میگیرد. خروجی متد login یک Observable از نوع boolean است. بنابراین در صورت true بودن این مقدار و یا موفقیت آمیز بودن عملیات لاگین، کاربر را به یکی از دو صفحهی protectedPage و یا this.returnUrl (در صورت وجود)، هدایت خواهیم کرد.
صفحهی خالی protectedPage را در ابتدای بحث، در ذیل ماژول Dashboard ایجاد کردیم.
در سمت سرور هم در صورت شکست اعتبارسنجی کاربر، یک return Unauthorized صورت میگیرد که معادل error.status === 401 کدهای فوق است و در اینجا در قسمت خطای عملیات بررسی شدهاست.
قالب این کامپوننت نیز به صورت ذیل به model از نوع Credentials آن متصل شدهاست:
در اینجا خاصیتهای نام کاربری، کلمهی عبور و همچنین rememberMe مدل، از کاربر دریافت میشوند؛ به همراه بررسی اعتبارسنجی سمت کلاینت آنها؛ با این شکل:
برای آزمایش برنامه، نام کاربری Vahid و کلمهی عبور 1234 را وارد کنید.
تکمیل کامپوننت Header برنامه
در ادامه، پس از لاگین موفق شخص، میخواهیم صفحهی protectedPage را نمایش دهیم:
در این صفحه، Login از منوی سایت حذف شدهاست و بجای آن Logout به همراه «نام نمایشی کاربر» ظاهر شدهاند. همچنین توکن decode شده به همراه تاریخ انقضای آن نمایش داده شدهاند.
برای پیاده سازی این موارد، ابتدا از کامپوننت Header شروع میکنیم:
این کامپوننت وضعیت گزارش شدهی ورود شخص را توسط خاصیت isLoggedIn در اختیار قالب خود قرار میدهد. برای این منظور به سرویس AuthService تزریق شدهی در سازندهی خود نیاز دارد.
اکنون در روال رخدادگردان ngOnInit، مشترک authStatus میشود که یک BehaviorSubject است و از آن جهت صدور رخدادهای authService به تمام کامپوننتهای مشترک به آن استفاده کردهایم:
Status بازگشت داده شدهی توسط آن از نوع boolean است و در صورت true بودن، خاصیت isLoggedIn را نیز true میکند که از آن در قالب این کامپوننت برای نمایش و یا مخفی کردن لینکهای login و logout استفاده خواهیم کرد.
همچنین اگر این وضعیت true باشد، مقدار DisplayName کاربر را نیز از سرویس authService دریافت کرده و توسط خاصیت this.displayName در اختیار قالب Header قرار میدهیم.
در آخر برای جلوگیری از نشتی حافظه، ضروری است اشتراک به authStatus، در روال رخدادگردان ngOnDestroy لغو شود:
همچنین در قالب Header، مدیریت دکمهی Logout را نیز انجام خواهیم داد:
با این مقدمات، قالب Header اکنون به صورت ذیل تغییر میکند:
در اینجا توسط ngIfها وضعیت خاصیت isLoggedIn این کامپوننت را بررسی میکنیم. اگر true باشد، Logoff به همراه نام نمایشی کاربر را در منوی راهبری سایت ظاهر خواهیم کرد و در غیراینصورت لینک به صفحهی Login را نمایش میدهیم.
تکمیل کامپوننت صفحهی محافظت شده
در تصویر قبل، نمایش توکن decode شده را نیز مشاهده کردید. این نمایش توسط کامپوننت صفحهی محافظت شده، مدیریت میشود:
در اینجا به کمک سرویس تزریقی AuthService، یکبار با استفاده از متد getDecodedAccessToken آن، مقدار اصلی توکن را و بار دیگر توسط متد getAccessTokenExpirationDate آن، تاریخ انقضای توکن دریافتی از سمت سرور را نمایش میدهیم؛ با این قالب:
کدهای کامل این سری را از اینجا میتوانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژهی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشهی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.
ایجاد ماژول Dashboard و تعریف کامپوننت صفحهی محافظت شده
قصد داریم پس از لاگین موفق، کاربر را به یک صفحهی محافظت شده هدایت کنیم. به همین جهت ماژول جدید Dashboard را به همراه کامپوننت یاد شده، به برنامه اضافه میکنیم:
>ng g m Dashboard -m app.module --routing >ng g c Dashboard/ProtectedPage
import { DashboardModule } from "./dashboard/dashboard.module"; @NgModule({ imports: [ //... DashboardModule, AppRoutingModule ] }) export class AppModule { }
import { ProtectedPageComponent } from "./protected-page/protected-page.component"; const routes: Routes = [ { path: "protectedPage", component: ProtectedPageComponent } ];
ایجاد صفحهی ورود به سیستم
در قسمت اول این سری، کارهای «ایجاد ماژول Authentication و تعریف کامپوننت لاگین» انجام شدند. اکنون میخواهیم کامپوننت خالی لاگین را به نحو ذیل تکمیل کنیم:
export class LoginComponent implements OnInit { model: Credentials = { username: "", password: "", rememberMe: false }; error = ""; returnUrl: string;
constructor( private authService: AuthService, private router: Router, private route: ActivatedRoute) { } ngOnInit() { // reset the login status this.authService.logout(false); // get the return url from route parameters this.returnUrl = this.route.snapshot.queryParams["returnUrl"]; }
اکنون متد ارسال این فرم چنین شکلی را پیدا میکند:
submitForm(form: NgForm) { this.error = ""; this.authService.login(this.model) .subscribe(isLoggedIn => { if (isLoggedIn) { if (this.returnUrl) { this.router.navigate([this.returnUrl]); } else { this.router.navigate(["/protectedPage"]); } } }, (error: HttpErrorResponse) => { console.log("Login error", error); if (error.status === 401) { this.error = "Invalid User name or Password. Please try again."; } else { this.error = `${error.statusText}: ${error.message}`; } }); }
صفحهی خالی protectedPage را در ابتدای بحث، در ذیل ماژول Dashboard ایجاد کردیم.
در سمت سرور هم در صورت شکست اعتبارسنجی کاربر، یک return Unauthorized صورت میگیرد که معادل error.status === 401 کدهای فوق است و در اینجا در قسمت خطای عملیات بررسی شدهاست.
قالب این کامپوننت نیز به صورت ذیل به model از نوع Credentials آن متصل شدهاست:
<div class="panel panel-default"> <div class="panel-heading"> <h2 class="panel-title">Login</h2> </div> <div class="panel-body"> <form #form="ngForm" (submit)="submitForm(form)" novalidate> <div class="form-group" [class.has-error]="username.invalid && username.touched"> <label for="username">User name</label> <input id="username" type="text" required name="username" [(ngModel)]="model.username" #username="ngModel" class="form-control" placeholder="User name"> <div *ngIf="username.invalid && username.touched"> <div class="alert alert-danger" *ngIf="username.errors['required']"> Name is required. </div> </div> </div> <div class="form-group" [class.has-error]="password.invalid && password.touched"> <label for="password">Password</label> <input id="password" type="password" required name="password" [(ngModel)]="model.password" #password="ngModel" class="form-control" placeholder="Password"> <div *ngIf="password.invalid && password.touched"> <div class="alert alert-danger" *ngIf="password.errors['required']"> Password is required. </div> </div> </div> <div class="checkbox"> <label> <input type="checkbox" name="rememberMe" [(ngModel)]="model.rememberMe"> Remember me </label> </div> <div class="form-group"> <button type="submit" class="btn btn-primary" [disabled]="form.invalid ">Login</button> </div> <div *ngIf="error" class="alert alert-danger " role="alert "> {{error}} </div> </form> </div> </div>
برای آزمایش برنامه، نام کاربری Vahid و کلمهی عبور 1234 را وارد کنید.
تکمیل کامپوننت Header برنامه
در ادامه، پس از لاگین موفق شخص، میخواهیم صفحهی protectedPage را نمایش دهیم:
در این صفحه، Login از منوی سایت حذف شدهاست و بجای آن Logout به همراه «نام نمایشی کاربر» ظاهر شدهاند. همچنین توکن decode شده به همراه تاریخ انقضای آن نمایش داده شدهاند.
برای پیاده سازی این موارد، ابتدا از کامپوننت Header شروع میکنیم:
export class HeaderComponent implements OnInit, OnDestroy { title = "Angular.Jwt.Core"; isLoggedIn: boolean; subscription: Subscription; displayName: string; constructor(private authService: AuthService) { }
اکنون در روال رخدادگردان ngOnInit، مشترک authStatus میشود که یک BehaviorSubject است و از آن جهت صدور رخدادهای authService به تمام کامپوننتهای مشترک به آن استفاده کردهایم:
ngOnInit() { this.subscription = this.authService.authStatus$.subscribe(status => { this.isLoggedIn = status; if (status) { this.displayName = this.authService.getDisplayName(); } }); }
همچنین اگر این وضعیت true باشد، مقدار DisplayName کاربر را نیز از سرویس authService دریافت کرده و توسط خاصیت this.displayName در اختیار قالب Header قرار میدهیم.
در آخر برای جلوگیری از نشتی حافظه، ضروری است اشتراک به authStatus، در روال رخدادگردان ngOnDestroy لغو شود:
ngOnDestroy() { // prevent memory leak when component is destroyed this.subscription.unsubscribe(); }
همچنین در قالب Header، مدیریت دکمهی Logout را نیز انجام خواهیم داد:
logout() { this.authService.logout(true); }
با این مقدمات، قالب Header اکنون به صورت ذیل تغییر میکند:
<nav class="navbar navbar-default"> <div class="container-fluid"> <div class="navbar-header"> <a class="navbar-brand" [routerLink]="['/']">{{title}}</a> </div> <ul class="nav navbar-nav"> <li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }"> <a class="nav-link" [routerLink]="['/welcome']">Home</a> </li> <li *ngIf="!isLoggedIn" class="nav-item" routerLinkActive="active"> <a class="nav-link" queryParamsHandling="merge" [routerLink]="['/login']">Login</a> </li> <li *ngIf="isLoggedIn" class="nav-item" routerLinkActive="active"> <a class="nav-link" (click)="logout()">Logoff [{{displayName}}]</a> </li> <li *ngIf="isLoggedIn" class="nav-item" routerLinkActive="active"> <a class="nav-link" [routerLink]="['/protectedPage']">Protected Page</a> </li> </ul> </div> </nav>
تکمیل کامپوننت صفحهی محافظت شده
در تصویر قبل، نمایش توکن decode شده را نیز مشاهده کردید. این نمایش توسط کامپوننت صفحهی محافظت شده، مدیریت میشود:
import { Component, OnInit } from "@angular/core"; import { AuthService } from "../../core/services/auth.service"; @Component({ selector: "app-protected-page", templateUrl: "./protected-page.component.html", styleUrls: ["./protected-page.component.css"] }) export class ProtectedPageComponent implements OnInit { decodedAccessToken: any = {}; accessTokenExpirationDate: Date = null; constructor(private authService: AuthService) { } ngOnInit() { this.decodedAccessToken = this.authService.getDecodedAccessToken(); this.accessTokenExpirationDate = this.authService.getAccessTokenExpirationDate(); } }
<h1> Decoded Access Token </h1> <div class="alert alert-info"> <label> Access Token Expiration Date:</label> {{accessTokenExpirationDate}} </div> <div> <pre>{{decodedAccessToken | json}}</pre> </div>
کدهای کامل این سری را از اینجا میتوانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژهی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشهی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.
اشتراکها
نگاهی به جاوااسکریپت Bind در 5 دقیقه
در قسمت قبل مشاهده کردیم که چگونه میتوان کل برنامه را به صورت سراسری، تعاملی کرد تا بتوان توسط آن، Blazor Server سنتی را شبیه سازی نمود؛ اما ... آیا واقعا نیاز است چنین کاری را انجام دهیم؟! چون در این صورت از قابلیتهای جدید SSR به همراه Blazor 8x محروم میشویم. اگر کل قابلیتهای تعاملی مورد نیاز ما در حد یک فرم و ارسال اطلاعات آن به سمت سرور است، میتوان در Blazor 8x هنوز هم در همان حالت SSR قرار گرفت و از فرمهای جدید تعاملی آن استفاده کرد تا برای پردازش چنین مواردی، نیازی به برقراری اتصال دائم SignalR نباشد. جزئیات نحوهی کار با اینگونه فرمها را در ادامه بررسی میکنیم.
امکان تعریف HTML Forms استاندارد در Blazor 8x
فرمهای استاندارد HTML، پیش از ظهور جاوااسکریپت و SPAها وجود داشتند (دقیقا همان زمانیکه که فقط مفهوم SSR وجود خارجی داشت) و هنوز هم جزء مهمی از اغلب برنامههای وب را تشکیل میدهند. با ارائهی دات نت 8 و قابلیت server side rendering آن، کامپوننتهای برنامه، فقط یکبار در سمت سرور رندر شده و HTML سادهی آنها به سمت مرورگر کاربر بازگشت داده میشود. در این حالت، فرمهای استاندارد HTML، امکان دریافت ورودیهای کاربر و ارسال دادههای آنها را به سمت سرور میسر میکنند (چون دیگر خبری از اتصال دائم SignalR نیست و باید اطلاعات را به همان نحو استاندارد پروتکل HTTP، به سمت سرور Post کرد). در دات نت 8، دو راهحل برای کار با فرمها در برنامههای Blazor وجود دارد: استفاده از EditForm خود Blazor و یا استفاده از HTML forms استاندارد و ساده، به همان نحوی که بوده و هست.
روش کار با EditForm در برنامههای Blazor SSR
البته ما قصد استفاده از فرمهای سادهی HTML را در اینجا نداریم و ترجیح میدهیم که از همان EditForm استفاده کنیم. EditForms در Blazor بسیار مفید بوده و امکان بایند خواص یک مدل را به اجزای مختلف ورودیهای تعریف شدهی در آن میسر میکند و همچنین قابلیتهایی مانند اعتبارسنجی و امثال آنرا نیز به همراه دارد (اطلاعات بیشتر). اما چگونه میتوان از این امکان در برنامههای Blazor SSR نیز استفاده کرد؟
برای این منظور، ابتدا مثالی را به صورت زیر تکمیل میکنیم (که بر اساس قالب dotnet new blazor --interactivity Server تهیه شده) و سپس توضیحات آن ارائه خواهد شد:
الف) تهیه یک مدل برای تعریف محلهای مرتبط با یک سفارش در فایل Models/OrderPlace.cs
ب) تهیهی یک کامپوننت Editor برای دریافت اطلاعات آدرس فوق در فایل Components\Pages\Chekout\AddressEntry.razor
ج) استفاده از مدل و ادیتور فوق در یک EditForm تغییر یافته برای کار با برنامههای Blazor SSR در فایل Components\Pages\Chekout\Checkout.razor
توضیحات:
باید بخاطر داشت که این فرم بر اساس حالت Server Side Rendering در اختیار مرورگر کاربر قرار میگیرد. یعنی برای بار اول، یک HTML خالص، در سمت سرور بر اساس اطلاعات آن تهیه شده و بازگشت داده میشود و زمانیکه به کاربر نمایش داده شد، دیگر برخلاف Blazor Server پیشین، اتصال SignalR ای وجود ندارد تا قابلیتهای تعاملی آنرا مدیریت کند. در این حالت اگر به view source صفحهی جاری رجوع کنیم، چنین خروجی قابل مشاهدهاست:
یعنی زمانیکه این فرم به سمت سرور ارسال میشود، همان HTTP POST استاندارد رخ میدهد و برای اینکار، نیازی به اتصال وبسوکت SignalR ندارد.
این EditForm تعریف شده، دو قسمت اضافهتر را نسبت به EditFormهای نگارشهای قبلی Blazor دارد:
در اینجا نوع HTTP Method ارسال فرم، مشخص شده و همچنین یک FormName نیز تعریف شدهاست. علت اینجا است که Blazor باید بتواند اطلاعات POST شده و دریافتی در سمت سرور را به کامپوننت متناظری نگاشت کند؛ به همین جهت این نامگذاری، ضروری است.
همانطور که در نحوهی تعریف فرم HTML ای فوق مشخص است، فیلد مخفی handler_، کار متمایز ساختن این فرم را به عهده داشته و از مقدار آن در سمت سرور جهت یافتن کامپوننت متناظر، استفاده خواهد شد.
همچنین برای دریافت و پردازش این اطلاعات در سمت سرور، تنها کافی است خاصیت مرتبط با آنرا با ویژگی SupplyParameterFromForm مزین کنیم:
جریان کاری این فرم به صورت خلاصه به نحو زیر است (که در آن متد OnInitialized دوبار فراخوانی میشود و باید به آن دقت داشت):
- در بار اول نمایش این صفحه (با فراخوانی مسیر /checkout در مرورگر)، متد OnInitialized فراخوانی شده و در آن، مقدار شیء PlaceModel نال است.
- بنابراین به متد GetOrderPlace مراجعه کرده و اطلاعاتی را دریافت میکند؛ برای مثال، این اطلاعات را از سرویسی میخواند.
- پس از پایان هر روال رخدادگردانی در Blazor، در پشت صحنه به صورت خودکار، متد تغییر حالت جاری کامپوننت (متد StateHasChanged) هم فراخوانی میشود. این فراخوانی خودکار، باعث رندر مجدد UI آن بر اساس اطلاعات جدید خواهد شد. یعنی قسمتهای نمایش فرم و نمایش اطلاعات ارسالی، یکبار ارزیابی شده و در صورت برقراری شرطها، نمایش داده میشوند.
- در ادامه، کاربر فرم را پر کرده و به سمت سرور POST میکند.
- پیش از هر رخدادی، خواص شیء PlaceModel به علت مزین بودن به ویژگی SupplyParameterFromForm، بر اساس اطلاعات ارسالی به سرور، مقدار دهی میشوند.
- سپس متد OnInitialized فراخوانی شده و چون اینبار مقدار PlaceModel نال نیست، به متد GetOrderPlace جهت دریافت مقادیر ابتدایی خود مراجعه نمیکند. سطر تعریف شدهی در متد OnInitialized فقط زمانی سبب مقدار دهی شیء PlaceModel میشود که مقدار این شیء، نال باشد (یعنی فقط در اولین بار نمایش صفحه)؛ اما اگر این مقدار توسط پارامتر مزین شدهی به SupplyParameterFromForm به علت ارسال دادههای فرم به سرور، مقدار دهی شده باشد، دیگر به منبع دادهی ابتدایی رجوع نمیکند.
- چون متد رخدادگردان OnInitialized فراخوانی شده، پس از پایان آن (و فراخوانی خودکار متد StateHasChanged در انتهای آن)، یکبار دیگر کار رندر UI فرم جاری بر اساس اطلاعات جدید، انجام خواهد شد.
- اکنون است که پس از طی این رخدادها، متد رویدادگردان SubmitOrder فراخوانی میشود. یعنی زمانیکه این متد فراخوانی میشود، شیء PlaceModel بر اساس اطلاعات رسیدهی از طرف کاربر، مقدار دهی شده و آمادهی استفاده است (برای مثال آمادهی ذخیره سازی در بانک اطلاعاتی؛ با فراخوانی سرویسی در اینجا).
- پس از پایان فراخوانی متد رویدادگردان SubmitOrder، به علت تغییر حالت کامپوننت (و فراخوانی خودکار متد StateHasChanged در انتهای آن)، یکبار دیگر نیز کار رندر UI فرم جاری بر اساس اطلاعات جدید انجام خواهد شد. یعنی اینبار قسمت Order Summary نمایش داده میشود.
مدیریت تداخل نامهای HTML Forms در Blazor 8x SSR
تمام فرمهایی که به این صورت در برنامههای Blazor SSR مدیریت میشوند، باید دارای نام منحصربفردی که توسط خاصیت FormName مشخص میشود، باشند. برای جلوگیری از این تداخل نامها، کامپوننت جدیدی به نام FormMappingScope معرفی شدهاست که نمونهای از آنرا در فایل فرضی Components\Pages\Chekout\CheckoutForm.razor تعریف شدهی به صورت زیر مشاهده میکنید:
در اینجا ابتدا ویژگی page@ کامپوننت CheckoutForm را حذف کرده و آنرا تبدیل به یک کامپوننت معمولی بدون قابلیت مسیریابی کردهایم. سپس آنرا توسط کامپوننت FormMappingScope در صفحهای دیگر معرفی و محصور میکنیم.
اکنون اگر برنامه را اجرا کرده و خروجی HTML آنرا بررسی کنیم، به فرم زیر خواهیم رسید:
همانطور که ملاحظه میکنید، اینبار مقدار فیلد مخفی handler_ که کار متمایز ساختن این فرم را به عهده دارد و از آن در سمت سرور جهت یافتن کامپوننت متناظری استفاده میشود، با حالتیکه از کامپوننت FormMappingScope استفاده نشده بود، متفاوت است و نام FormMappingScope را در ابتدای خود به همراه دارد تا به این نحو، از تداخل احتمالی نامهای فرمها جلوگیری شود.
یک نکته: اگر به تگهای فرم HTML ای فوق دقت کنید، به همراه یک anti-forgery token نیز هست که کار تولید و مدیریت آن، به صورت خودکار صورت میگیرد و میانافزاری نیز برای آن طراحی شده که در فایل Program.cs برنامه، به صورت app.UseAntiforgery بکارگرفته شدهاست.
یک نکته: در Blazor 8x SSR میتوان بجای EditForm، از همان HTML form متداول هم استفاده کرد
اگر بخواهیم بجای استفاده از EditForm، از فرمهای استاندارد HTML هم در حالت SSR استفاده کنیم، این کار میسر بوده و روش کار به صورت زیر است:
در اینجا ذکر دایرکتیوهای onsubmit@ و formname@ را (شبیه به خواص و رویدادگردانهای مشابهی در EditForm) به همراه ذکر صریح کامپوننت AntiforgeryToken، مشاهده میکنید. در حین استفاده از EditForm، نیازی به درج این کامپوننت نیست و به صورت خودکار اضافه میشود.
پردازش فرمهای GET در Blazor 8x
در حالتیکه از فرمهای استاندارد HTML ای استفاده میشود، ممکن است method فرم، بجای post، حالت get باشد که نتایج آن به صورت کوئری استرینگ در نوار آدرس مرورگر ظاهر میشوند؛ مانند جستجوی گوگل که اشخاص میتوانند کوئری استرینگ و لینک نهایی را به اشتراک بگذارند. روش پردازش یک چنین فرمهایی به صورت زیر است:
در اینجا از ویژگی SupplyParameterFromQuery برای دریافت کوئری استرینگ استفاده شده و چون نام پارامتر تعریف شده با نام input فرم یکی نیست، این نام به صورت صریحی توسط خاصیت Name آن مشخص شدهاست.
یک ابتکار! تعاملی کردن قسمتی از صفحه بدون فعالسازی کامل Blazor Server و یا Blazor WASM کامل
این دکمهی قرار گرفتهی در یک صفحهی SSR را ملاحظه کنید:
در اینجا میخواهیم، اگر کاربری بر روی آن کلیک کرد، روال رویدادگردان منتسب به onclick اجرا شود. اما ... اگر در این حالت برنامه را اجرا کرده و بر روی دکمهی Log out کلیک کنیم، هیچ اتفاقی رخ نمیدهد! یعنی روال رویدادگران BeginSignOut اصلا اجرا نمیشود. علت اینجا است که صفحات SSR، در نهایت یک static HTML بیشتر نیستند و فاقد قابلیتهای تعاملی، مانند واکنش نشان دادن به کلیک بر روی یک دکمه هستند. برای رفع این مشکل یا میتوان این قسمت از صفحه را کاملا تعاملی کرد که روش انجام آنرا در قسمتهای بعدی با جزئیات کاملی بررسی میکنیم و یا ... میتوان این دکمه را داخل یک فرم جدید تعاملی به صورت زیر محصور کرد:
در این حالت چون این فرم، از نوع فرمهای جدید تعاملی است، برای پردازش آن نیازی به اتصال دائم SignalR و یا فعالسازی یک وباسمبلی نیست. پردازش آن بر اساس استاندارد HTTP Post و فرمهای آن، صورت گرفته و به این ترتیب میتوان عملکرد onclick@ کاملا تعاملی را با یک فرم تعاملی جدید، شبیه سازی کرد.
یک نکته: میتوان حالت post-back مانند فرمهای تعاملی Blazor 8x را تغییر داد.
به همراه ویژگیهای جدید مرتبط با صفحات SSR، ویژگی هدایت بهبودیافته هم وجود دارد که جزئیات بیشتر آنرا در قسمتهای بعدی این سری بررسی میکنیم. برای نمونه اگر مثال این قسمت را اجرا کنید، فرم آن به همراه یک post-back مانند به سمت سرور است که کاملا قابل احساس است؛ این رفتار هرچند استاندارد است، اما بیشباهت به برنامههای MVC ، Razor pages و یا وبفرمها نیست و با فرمهای بیصدا و سریع نگارشهای قبلی Blazor متفاوت است. در Blazor8x میتوان این نوع ارسال اطلاعات را Ajax ای هم کرد که به آن enhanced navigation میگویند. برای اینکار فقط کافی است ویژگی Enhance را به تگ EditForm اضافه کرد و یا ویژگی جدید data-enhance را به تگهای فرمهای استاندارد HTML ای افزود. پس از آن اگر برنامه را اجرا کنیم، دیگر یک post-back استاندارد وبفرمها مشاهده نمیشود و رفتار این صفحه بسیار سریع، نرم و روان خواهد بود.
در اینجا تنها تغییری که حاصل شده، اضافه شدن ویژگی Enhance به المان EditForm است. این ویژگی به صورت پیشفرض غیرفعال است که جزئیات بیشتر آنرا در قسمتهای بعدی بررسی خواهیم کرد.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید: Blazor8x-Server-Normal.zip
امکان تعریف HTML Forms استاندارد در Blazor 8x
فرمهای استاندارد HTML، پیش از ظهور جاوااسکریپت و SPAها وجود داشتند (دقیقا همان زمانیکه که فقط مفهوم SSR وجود خارجی داشت) و هنوز هم جزء مهمی از اغلب برنامههای وب را تشکیل میدهند. با ارائهی دات نت 8 و قابلیت server side rendering آن، کامپوننتهای برنامه، فقط یکبار در سمت سرور رندر شده و HTML سادهی آنها به سمت مرورگر کاربر بازگشت داده میشود. در این حالت، فرمهای استاندارد HTML، امکان دریافت ورودیهای کاربر و ارسال دادههای آنها را به سمت سرور میسر میکنند (چون دیگر خبری از اتصال دائم SignalR نیست و باید اطلاعات را به همان نحو استاندارد پروتکل HTTP، به سمت سرور Post کرد). در دات نت 8، دو راهحل برای کار با فرمها در برنامههای Blazor وجود دارد: استفاده از EditForm خود Blazor و یا استفاده از HTML forms استاندارد و ساده، به همان نحوی که بوده و هست.
روش کار با EditForm در برنامههای Blazor SSR
البته ما قصد استفاده از فرمهای سادهی HTML را در اینجا نداریم و ترجیح میدهیم که از همان EditForm استفاده کنیم. EditForms در Blazor بسیار مفید بوده و امکان بایند خواص یک مدل را به اجزای مختلف ورودیهای تعریف شدهی در آن میسر میکند و همچنین قابلیتهایی مانند اعتبارسنجی و امثال آنرا نیز به همراه دارد (اطلاعات بیشتر). اما چگونه میتوان از این امکان در برنامههای Blazor SSR نیز استفاده کرد؟
برای این منظور، ابتدا مثالی را به صورت زیر تکمیل میکنیم (که بر اساس قالب dotnet new blazor --interactivity Server تهیه شده) و سپس توضیحات آن ارائه خواهد شد:
الف) تهیه یک مدل برای تعریف محلهای مرتبط با یک سفارش در فایل Models/OrderPlace.cs
using System.ComponentModel.DataAnnotations; namespace Models; public record OrderPlace { public Address BillingAddress { get; set; } = new(); public Address ShippingAddress { get; set; } = new(); } public class Address { [Required] public string Name { get; set; } = default!; public string? AddressLine1 { get; set; } public string? AddressLine2 { get; set; } public string? City { get; set; } [Required] public string PostCode { get; set; } = default!; }
ب) تهیهی یک کامپوننت Editor برای دریافت اطلاعات آدرس فوق در فایل Components\Pages\Chekout\AddressEntry.razor
@inherits Editor<Models.Address> <div> <label>Name</label> <InputText @bind-Value="Value.Name"/> </div> <div> <label>Address 1</label> <InputText @bind-Value="Value.AddressLine1"/> </div> <div> <label>Address 2</label> <InputText @bind-Value="Value.AddressLine2"/> </div> <div> <label>City</label> <InputText @bind-Value="Value.City"/> </div> <div> <label>Post Code</label> <InputText @bind-Value="Value.PostCode"/> </div>
ج) استفاده از مدل و ادیتور فوق در یک EditForm تغییر یافته برای کار با برنامههای Blazor SSR در فایل Components\Pages\Chekout\Checkout.razor
@page "/checkout" @using Models @if (!_submitted && PlaceModel != null) { <EditForm Model="PlaceModel" method="post" OnValidSubmit="SubmitOrder" FormName="checkout"> <DataAnnotationsValidator/> <h4>Bill To:</h4> <AddressEntry @bind-Value="PlaceModel.BillingAddress"/> <h4>Ship To:</h4> <AddressEntry @bind-Value="PlaceModel.ShippingAddress"/> <button type="submit">Submit</button> <ValidationSummary/> </EditForm> } @if (_submitted && PlaceModel != null) { <div> <h2>Order Summary</h2> <h3>Shipping To:</h3> <dl> <dt>Name</dt> <dd>@PlaceModel.BillingAddress.Name</dd> <dt>Address 1</dt> <dd>@PlaceModel.BillingAddress.AddressLine1</dd> <dt>Address 2</dt> <dd>@PlaceModel.BillingAddress.AddressLine2</dd> <dt>City</dt> <dd>@PlaceModel.BillingAddress.City</dd> <dt>Post Code</dt> <dd>@PlaceModel.BillingAddress.PostCode</dd> </dl> </div> } @code { bool _submitted; [SupplyParameterFromForm] public OrderPlace? PlaceModel { get; set; } protected override void OnInitialized() { PlaceModel ??= GetOrderPlace(); } private void SubmitOrder() { _submitted = true; } private static OrderPlace GetOrderPlace() => new() { BillingAddress = new Address { PostCode = "12345", Name = "Test 1", }, ShippingAddress = new Address { PostCode = "67890", Name = "Test 2", }, }; }
باید بخاطر داشت که این فرم بر اساس حالت Server Side Rendering در اختیار مرورگر کاربر قرار میگیرد. یعنی برای بار اول، یک HTML خالص، در سمت سرور بر اساس اطلاعات آن تهیه شده و بازگشت داده میشود و زمانیکه به کاربر نمایش داده شد، دیگر برخلاف Blazor Server پیشین، اتصال SignalR ای وجود ندارد تا قابلیتهای تعاملی آنرا مدیریت کند. در این حالت اگر به view source صفحهی جاری رجوع کنیم، چنین خروجی قابل مشاهدهاست:
<form method="post"> <input type="hidden" name="_handler" value="checkout" /> <input type="hidden" name="__RequestVerificationToken" value="CfDxxx" /> . . . <button type="submit">Submit</button> </form>
این EditForm تعریف شده، دو قسمت اضافهتر را نسبت به EditFormهای نگارشهای قبلی Blazor دارد:
<EditForm Model="PlaceModel" method="post" OnValidSubmit="SubmitOrder" FormName="checkout">
همانطور که در نحوهی تعریف فرم HTML ای فوق مشخص است، فیلد مخفی handler_، کار متمایز ساختن این فرم را به عهده داشته و از مقدار آن در سمت سرور جهت یافتن کامپوننت متناظر، استفاده خواهد شد.
همچنین برای دریافت و پردازش این اطلاعات در سمت سرور، تنها کافی است خاصیت مرتبط با آنرا با ویژگی SupplyParameterFromForm مزین کنیم:
[SupplyParameterFromForm] public OrderPlace? PlaceModel { get; set; }
جریان کاری این فرم به صورت خلاصه به نحو زیر است (که در آن متد OnInitialized دوبار فراخوانی میشود و باید به آن دقت داشت):
- در بار اول نمایش این صفحه (با فراخوانی مسیر /checkout در مرورگر)، متد OnInitialized فراخوانی شده و در آن، مقدار شیء PlaceModel نال است.
- بنابراین به متد GetOrderPlace مراجعه کرده و اطلاعاتی را دریافت میکند؛ برای مثال، این اطلاعات را از سرویسی میخواند.
- پس از پایان هر روال رخدادگردانی در Blazor، در پشت صحنه به صورت خودکار، متد تغییر حالت جاری کامپوننت (متد StateHasChanged) هم فراخوانی میشود. این فراخوانی خودکار، باعث رندر مجدد UI آن بر اساس اطلاعات جدید خواهد شد. یعنی قسمتهای نمایش فرم و نمایش اطلاعات ارسالی، یکبار ارزیابی شده و در صورت برقراری شرطها، نمایش داده میشوند.
- در ادامه، کاربر فرم را پر کرده و به سمت سرور POST میکند.
- پیش از هر رخدادی، خواص شیء PlaceModel به علت مزین بودن به ویژگی SupplyParameterFromForm، بر اساس اطلاعات ارسالی به سرور، مقدار دهی میشوند.
- سپس متد OnInitialized فراخوانی شده و چون اینبار مقدار PlaceModel نال نیست، به متد GetOrderPlace جهت دریافت مقادیر ابتدایی خود مراجعه نمیکند. سطر تعریف شدهی در متد OnInitialized فقط زمانی سبب مقدار دهی شیء PlaceModel میشود که مقدار این شیء، نال باشد (یعنی فقط در اولین بار نمایش صفحه)؛ اما اگر این مقدار توسط پارامتر مزین شدهی به SupplyParameterFromForm به علت ارسال دادههای فرم به سرور، مقدار دهی شده باشد، دیگر به منبع دادهی ابتدایی رجوع نمیکند.
- چون متد رخدادگردان OnInitialized فراخوانی شده، پس از پایان آن (و فراخوانی خودکار متد StateHasChanged در انتهای آن)، یکبار دیگر کار رندر UI فرم جاری بر اساس اطلاعات جدید، انجام خواهد شد.
- اکنون است که پس از طی این رخدادها، متد رویدادگردان SubmitOrder فراخوانی میشود. یعنی زمانیکه این متد فراخوانی میشود، شیء PlaceModel بر اساس اطلاعات رسیدهی از طرف کاربر، مقدار دهی شده و آمادهی استفاده است (برای مثال آمادهی ذخیره سازی در بانک اطلاعاتی؛ با فراخوانی سرویسی در اینجا).
- پس از پایان فراخوانی متد رویدادگردان SubmitOrder، به علت تغییر حالت کامپوننت (و فراخوانی خودکار متد StateHasChanged در انتهای آن)، یکبار دیگر نیز کار رندر UI فرم جاری بر اساس اطلاعات جدید انجام خواهد شد. یعنی اینبار قسمت Order Summary نمایش داده میشود.
مدیریت تداخل نامهای HTML Forms در Blazor 8x SSR
تمام فرمهایی که به این صورت در برنامههای Blazor SSR مدیریت میشوند، باید دارای نام منحصربفردی که توسط خاصیت FormName مشخص میشود، باشند. برای جلوگیری از این تداخل نامها، کامپوننت جدیدی به نام FormMappingScope معرفی شدهاست که نمونهای از آنرا در فایل فرضی Components\Pages\Chekout\CheckoutForm.razor تعریف شدهی به صورت زیر مشاهده میکنید:
@page "/checkout" <FormMappingScope Name="store-checkout"> <CheckoutForm /> </FormMappingScope>
اکنون اگر برنامه را اجرا کرده و خروجی HTML آنرا بررسی کنیم، به فرم زیر خواهیم رسید:
<form method="post"> <input type="hidden" name="_handler" value="[store-checkout]checkout" /> <input type="hidden" name="__RequestVerificationToken" value="CfDxxxxx" /> . . . <button type="submit">Submit</button> </form>
یک نکته: اگر به تگهای فرم HTML ای فوق دقت کنید، به همراه یک anti-forgery token نیز هست که کار تولید و مدیریت آن، به صورت خودکار صورت میگیرد و میانافزاری نیز برای آن طراحی شده که در فایل Program.cs برنامه، به صورت app.UseAntiforgery بکارگرفته شدهاست.
یک نکته: در Blazor 8x SSR میتوان بجای EditForm، از همان HTML form متداول هم استفاده کرد
اگر بخواهیم بجای استفاده از EditForm، از فرمهای استاندارد HTML هم در حالت SSR استفاده کنیم، این کار میسر بوده و روش کار به صورت زیر است:
<form method="post" @onsubmit="SaveData" @formname="MyFormName"> <AntiforgeryToken /> <InputText @bind-Value="Name" /> <button>Submit</button> </form>
پردازش فرمهای GET در Blazor 8x
در حالتیکه از فرمهای استاندارد HTML ای استفاده میشود، ممکن است method فرم، بجای post، حالت get باشد که نتایج آن به صورت کوئری استرینگ در نوار آدرس مرورگر ظاهر میشوند؛ مانند جستجوی گوگل که اشخاص میتوانند کوئری استرینگ و لینک نهایی را به اشتراک بگذارند. روش پردازش یک چنین فرمهایی به صورت زیر است:
@page "/" <form method="GET"> <input type="text" name="q"/> <button type="submit">Search</button> </form> @code { [SupplyParameterFromQuery(Name="q")] public string SearchTerm { get; set; } protected override async Task OnInitializedAsync() { // do something with the search term } }
یک ابتکار! تعاملی کردن قسمتی از صفحه بدون فعالسازی کامل Blazor Server و یا Blazor WASM کامل
این دکمهی قرار گرفتهی در یک صفحهی SSR را ملاحظه کنید:
<button class="nav-link border-0" @onclick="BeginSignOut">Log out</button>
<EditForm Context="ctx" FormName="LogoutForm" method="post" Model="@Foo" OnValidSubmit="BeginSignOut"> <button type="submit" class="nav-link border-0">Log out</button> </EditForm> @code{ [SupplyParameterFromForm(Name = "LogoutForm")] public string? Foo { get; set; } protected override void OnInitialized() => Foo = ""; async Task BeginSignOut() { // TODO: SignOutAsync(); // TODO: NavigateTo("/authentication/logout"); } }
یک نکته: میتوان حالت post-back مانند فرمهای تعاملی Blazor 8x را تغییر داد.
به همراه ویژگیهای جدید مرتبط با صفحات SSR، ویژگی هدایت بهبودیافته هم وجود دارد که جزئیات بیشتر آنرا در قسمتهای بعدی این سری بررسی میکنیم. برای نمونه اگر مثال این قسمت را اجرا کنید، فرم آن به همراه یک post-back مانند به سمت سرور است که کاملا قابل احساس است؛ این رفتار هرچند استاندارد است، اما بیشباهت به برنامههای MVC ، Razor pages و یا وبفرمها نیست و با فرمهای بیصدا و سریع نگارشهای قبلی Blazor متفاوت است. در Blazor8x میتوان این نوع ارسال اطلاعات را Ajax ای هم کرد که به آن enhanced navigation میگویند. برای اینکار فقط کافی است ویژگی Enhance را به تگ EditForm اضافه کرد و یا ویژگی جدید data-enhance را به تگهای فرمهای استاندارد HTML ای افزود. پس از آن اگر برنامه را اجرا کنیم، دیگر یک post-back استاندارد وبفرمها مشاهده نمیشود و رفتار این صفحه بسیار سریع، نرم و روان خواهد بود.
<EditForm Model="PlaceModel" method="post" OnValidSubmit="SubmitOrder" FormName="checkout" Enhance>
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید: Blazor8x-Server-Normal.zip
نظرات مطالب
ASP.NET MVC #12
سلام آقای نصیری
در قسمتی که از radio استفاده کردید وقتی که model رو از نوع bool قرار میدم error مید
در قسمتی که از radio استفاده کردید وقتی که model رو از نوع bool قرار میدم error مید
The model item passed into the dictionary is null
وقتی create فراخوانی میشه و بعدش partial رو فراخوانی میکنه . مدل partial view نال هست
اما وقتی که partial view رو بدون مدل set کردم مشکلی پیش نیومد و به فیلدم bind شد
میشه لطف کنید بگید اشکال کارم کجاست
اما وقتی که partial view رو بدون مدل set کردم مشکلی پیش نیومد و به فیلدم bind شد
میشه لطف کنید بگید اشکال کارم کجاست