اشتراکها
نظرات مطالب
Blazor 5x - قسمت اول - معرفی
با استفاده از Blazor هم میشه مثل آنگولار و ری اکت برنامه موبایل ساخت؟؟
نظرات مطالب
React 16x - قسمت 1 - معرفی و شروع به کار
به نظر من هم یادگیری آنگولار سختتر هست. البته کمی با الگو هاش آشنا بشید دیگر سختی ندارد.
در همان مثال فوق اگر projectId تقدم دارد، آنرا ابتدا ذکر کنید (در تعریف MapRoute ایی به نام IssueDetails که پیش از مسیریابی پیش فرض ذکر شده).
بعد امضای متد Details هم باید بر همین اساس و ترتیب تغییر کند.
سپس اگر نمیخواهید یکی از پارامترها را ذکر کنید؛ پارامترهای این متد را nullable تعریف کنید. به این ترتیب با مقدار دهی null به issueId در ActionLink، خودبخود در لینک تولیدی نهایی ظاهر نخواهد شد.
Details/{projectId}/{issueId}
سپس اگر نمیخواهید یکی از پارامترها را ذکر کنید؛ پارامترهای این متد را nullable تعریف کنید. به این ترتیب با مقدار دهی null به issueId در ActionLink، خودبخود در لینک تولیدی نهایی ظاهر نخواهد شد.
در قسمت سوم، کار ورود به سیستم و امکان مشاهدهی صفحهی محافظت شدهی پس از لاگین را پیاده سازی کردیم. در این قسمت میخواهیم امکان دسترسی به مسیر http://localhost:4200/protectedPage را کنترل کنیم. تا اینجا اگر کاربر بدون لاگین کردن نیز این مسیر را درخواست کند، میتواند حداقل ساختار ابتدایی آنرا مشاهده کند که باید مدیریت شود و این مدیریت دسترسی میتواند بر اساس وضعیت لاگین کاربر و همچنین نقشهای او در سیستم باشد:
طراحی بخشهایی از این قسمت، از پروژهی «کنترل دسترسیها در Angular با استفاده از Ng2Permission» ایده گرفته شدهاند.
استخراج اطلاعات کاربر وارد شدهی به سیستم از توکن دسترسی او
یکی از روشهای دسترسی به اطلاعات کاربر در سمت کلاینت، مانند نقشهای او، تدارک متدی در سمت سرور و بازگشت Claims او به سمت کلاینت است:
اما با توجه به اینکه در زمان لاگین، نقشهای او (و سایر Claims دلخواه) نیز به توکن دسترسی نهایی اضافه میشوند، میتوان این کوئری گرفتن مدام از سرور را تبدیل به روش بسیار سریعتر استخراج آنها از توکنی که هم اکنون در کش مرورگر ذخیره شدهاست، کرد.
همچنین باید دقت داشت چون این توکن دارای امضای دیجیتال است، کوچکترین تغییری در آن در سمت کاربر، سبب برگشت خوردن خودکار درخواست ارسالی به سمت سرور میشود (یکی از مراحل اعتبارسنجی کاربر در سمت سرور، اعتبارسنجی توکن دریافتی (قسمت cfg.TokenValidationParameters) و همچنین بررسی خودکار امضای دیجیتال آن است). بنابراین نگرانی از این بابت وجود ندارد.
اگر اطلاعات کاربر در سمت سرور تغییر کنند، با اولین درخواست ارسالی به سمت سرور، رخداد OnTokenValidated وارد عمل شده و درخواست ارسالی را برگشت میزند (در مورد پیاده سازی سمت سرور این مورد، در مطلب «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity» بیشتر بحث شدهاست). در این حالت کاربر مجبور به لاگین مجدد خواهد شد که این مورد سبب به روز رسانی خودکار اطلاعات توکنهای ذخیره شدهی او در مرورگر نیز میشود.
اگر از قسمت دوم این سری بهخاطر داشته باشید، توکن decode شدهی برنامه، چنین شکلی را دارد:
به همین جهت متدی را برای تبدیل این اطلاعات به شیء کاربر، ایجاد خواهیم کرد و در سراسر برنامه از این اطلاعات آماده، برای تعیین دسترسی او به قسمتهای مختلف برنامهی سمت کلاینت، استفاده میکنیم.
برای این منظور اینترفیس src\app\core\models\auth-user.ts را به صورت ذیل ایجاد میکنیم:
پس از آن متد getAuthUser را جهت استخراج خواص ویژهی توکن دسترسی decode شدهی فوق، به صورت ذیل به سرویس Auth اضافه میکنیم:
کار با این متد بسیار سریع است و نیازی به رفت و برگشتی به سمت سرور ندارد؛ چون تمام اطلاعات استخراجی توسط آن هم اکنون در کش سریع مرورگر کلاینت موجود هستند. استفادهی از متد Object.freeze هم سبب read-only شدن این خروجی میشود.
همچنین در اینجا تمام نقشهای دریافتی، تبدیل به LowerCase شدهاند. با اینکار مقایسهی بعدی آنها با نقشهای درخواستی، حساس به بزرگی و کوچکی حروف نخواهد بود.
تعریف نقشهای دسترسی به مسیرهای مختلف سمت کلاینت
مرسوم است اطلاعات اضافی مرتبط با هر مسیر را به خاصیت data آن route انتساب میدهند. به همین جهت به فایل dashboard-routing.module.ts مراجعه کرده و نقشهای مجاز به دسترسی به مسیر protectedPage را به خاصیت data آن به صورت ذیل اضافه میکنیم:
که در اینجا ساختار اینترفیس AuthGuardPermission، در فایل جدید app\core\models\auth-guard-permission.ts به صورت ذیل تعریف شدهاست:
به این ترتیب هر قسمتی از برنامه که نیاز به اطلاعات سطوح دسترسی مسیری را داشت، ابتدا به دنبال route.data["permission"] خواهد گشت (کلیدی به نام permission در خاصیت data یک مسیر) و سپس اطلاعات آنرا بر اساس ساختار اینترفیس AuthGuardPermission، تحلیل میکند.
در اینجا تنها باید یکی از خواص permittedRoles (نقشهای مجاز به دسترسی/صدور دسترسی فقط برای این نقشهای مشخص، منهای مابقی) و یا deniedRoles (نقشهای غیرمجاز به دسترسی/دسترسی همهی نقشهای ممکن، منهای این نقشهای تعیین شده)، مقدار دهی شوند.
افزودن کامپوننت «دسترسی ندارید» به ماژول Authentication
در ادامه میخواهیم اگر کاربری به مسیری دسترسی نداشت، به صورت خودکار به صفحهی «دسترسی ندارید» هدایت شود. به همین جهت این کامپوننت را به صورت ذیل به ماژول authentication اضافه میکنیم:
با این خروجی که سبب درج خودکار آن در قسمت declaration فایل authentication.module نیز میشود:
سپس به فایل authentication-routing.module.ts مراجعه کرده و مسیریابی آنرا نیز اضافه میکنیم:
قالب access-denied.component.html را نیز به صورت ذیل تکمیل میکنیم:
که دکمهی Back آن به کمک سرویس Location، صورت ذیل پیاده سازی شدهاست:
در اینجا اگر کاربر به سیستم وارد نشده باشد، دکمهی لاگین نیز به او نمایش داده میشود. همچنین وجود "queryParamsHandling="merge در لینک مراجعهی به صفحهی لاگین، سبب خواهد شد تا query string موجود در صفحه نیز حفظ شود و به صفحهی لاگین انتقال پیدا کند. در صفحهی لاگین نیز جهت پردازش این نوع کوئری استرینگها، تمهیدات لازم درنظر گرفته شدهاند.
دکمهی back آن نیز توسط سرویس توکار Location واقع در مسیر angular/common@ پیاده سازی شدهاست.
ایجاد یک محافظ مسیر سمت کلاینت برای بررسی وضعیت کاربر جاری و همچنین نقشهای او
پس از تعریف متد getAuthUser و استخراج اطلاعات کاربر از توکن دسترسی دریافتی که شامل نقشهای او نیز میشود، اکنون میتوان متد بررسی این نقشها را نیز به سرویس Auth اضافه کرد:
متد some در جاوا اسکریپت شبیه به متد Any در C# LINQ عمل میکند. همچنین در مقایسهی صورت گرفته، با توجه به اینکه user.roles را پیشتر به LowerCase تبدیل کردیم، حساسیتی بین نقش Admin و admin و کلا کوچکی و بزرگی نام نقشها وجود ندارد.
اکنون در هر قسمتی از برنامه که نیاز به بررسی امکان دسترسی یک کاربر به نقش یا نقشهایی خاص وجود داشته باشد، میتوان AuthService را به سازندهی آن تزریق و سپس از متد فوق جهت بررسی نهایی، استفاده کرد.
در ادامه یک Route Guard جدید را در مسیر app\core\services\auth.guard.ts ایجاد میکنیم. کار آن بررسی خودکار امکان دسترسی به یک مسیر درخواستی است:
در اینجا در ابتدا وضعیت لاگین کاربر بررسی میگردد. این وضعیت نیز از طریق سرویس Auth که به سازندهی کلاس تزریق شدهاست، تامین میشود. اگر کاربر هنوز لاگین نکرده باشد، به صفحهی عدم دسترسی هدایت خواهد شد.
سپس خاصیت permission اطلاعات مسیر استخراج میشود. اگر چنین مقداری وجود نداشت، همینجا کار با موفقیت خاتمه پیدا میکند.
در آخر وضعیت دسترسی به نقشهای استخراجی deniedRoles و permittedRoles که از اطلاعات مسیر دریافت شدند، توسط متد isAuthUserInRoles سرویس Auth بررسی میشوند.
در متد showAccessDenied کار ارسال آدرس درخواستی (state.url) به صورت یک کوئری استرینگ (queryParams) با کلید returnUrl به صفحهی accessDenied صورت میگیرد. در این صفحه نیز دکمهی لاگین به همراه "queryParamsHandling="merge است. یعنی کامپوننت لاگین برنامه، کوئری استرینگ returnUrl را دریافت میکند:
و پس از لاگین موفق، در صورت وجود این کوئری استرینگ، هدایت خودکار کاربر، به مسیر returnUrl پیشین صورت خواهد گرفت:
محل معرفی این AuthGuard جدید که در حقیقت یک سرویس است، در ماژول Core، در قسمت providers آن، به صورت ذیل میباشد:
در آخر برای اعمال این Guard جدید، به فایل dashboard-routing.module.ts مراجعه کرده و خاصیت canActivate را مقدار دهی میکنیم:
به این ترتیب با درخواست این مسیر، پیش از فعالسازی و نمایش آن، توسط AuthGuard معرفی شدهی به آن، کار بررسی وضعیت کاربر و نقشهای او که از خاصیت permission خاصیت data دریافت میشوند، صورت گرفته و اگر عملیات تعیین اعتبار اطلاعات با موفقیت به پایان رسید، آنگاه کاربر مجوز دسترسی به این قسمت از برنامه را خواهد یافت.
اگر قصد آزمایش آنرا داشتید، فقط کافی است بجای نقش Admin، مثلا Admin1 را در permittedRoles مقدار دهی کنید، تا صفحهی access denied را در صورت درخواست مسیر protectedPage، بتوان مشاهده کرد.
کدهای کامل این سری را از اینجا میتوانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژهی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشهی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.
طراحی بخشهایی از این قسمت، از پروژهی «کنترل دسترسیها در Angular با استفاده از Ng2Permission» ایده گرفته شدهاند.
استخراج اطلاعات کاربر وارد شدهی به سیستم از توکن دسترسی او
یکی از روشهای دسترسی به اطلاعات کاربر در سمت کلاینت، مانند نقشهای او، تدارک متدی در سمت سرور و بازگشت Claims او به سمت کلاینت است:
public IActionResult Get() { var user = this.User.Identity as ClaimsIdentity; var config = new { userName = user.Name, roles = user.Claims.Where(x => x.Type == ClaimTypes.Role).Select(x => x.Value).ToList() }; return Ok(config); }
همچنین باید دقت داشت چون این توکن دارای امضای دیجیتال است، کوچکترین تغییری در آن در سمت کاربر، سبب برگشت خوردن خودکار درخواست ارسالی به سمت سرور میشود (یکی از مراحل اعتبارسنجی کاربر در سمت سرور، اعتبارسنجی توکن دریافتی (قسمت cfg.TokenValidationParameters) و همچنین بررسی خودکار امضای دیجیتال آن است). بنابراین نگرانی از این بابت وجود ندارد.
اگر اطلاعات کاربر در سمت سرور تغییر کنند، با اولین درخواست ارسالی به سمت سرور، رخداد OnTokenValidated وارد عمل شده و درخواست ارسالی را برگشت میزند (در مورد پیاده سازی سمت سرور این مورد، در مطلب «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity» بیشتر بحث شدهاست). در این حالت کاربر مجبور به لاگین مجدد خواهد شد که این مورد سبب به روز رسانی خودکار اطلاعات توکنهای ذخیره شدهی او در مرورگر نیز میشود.
اگر از قسمت دوم این سری بهخاطر داشته باشید، توکن decode شدهی برنامه، چنین شکلی را دارد:
{ "jti": "d1272eb5-1061-45bd-9209-3ccbc6ddcf0a", "iss": "http://localhost/", "iat": 1513070340, "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "1", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "Vahid", "DisplayName": "وحید", "http://schemas.microsoft.com/ws/2008/06/identity/claims/serialnumber": "709b64868a1d4d108ee58369f5c3c1f3", "http://schemas.microsoft.com/ws/2008/06/identity/claims/userdata": "1", "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": [ "Admin", "User" ], "nbf": 1513070340, "exp": 1513070460, "aud": "Any" }
برای این منظور اینترفیس src\app\core\models\auth-user.ts را به صورت ذیل ایجاد میکنیم:
export interface AuthUser { userId: string; userName: string; displayName: string; roles: string[]; }
getAuthUser(): AuthUser { if (!this.isLoggedIn()) { return null; } const decodedToken = this.getDecodedAccessToken(); let roles = decodedToken["http://schemas.microsoft.com/ws/2008/06/identity/claims/role"]; if (roles) { roles = roles.map(role => role.toLowerCase()); } return Object.freeze({ userId: decodedToken["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"], userName: decodedToken["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"], displayName: decodedToken["DisplayName"], roles: roles }); }
همچنین در اینجا تمام نقشهای دریافتی، تبدیل به LowerCase شدهاند. با اینکار مقایسهی بعدی آنها با نقشهای درخواستی، حساس به بزرگی و کوچکی حروف نخواهد بود.
تعریف نقشهای دسترسی به مسیرهای مختلف سمت کلاینت
مرسوم است اطلاعات اضافی مرتبط با هر مسیر را به خاصیت data آن route انتساب میدهند. به همین جهت به فایل dashboard-routing.module.ts مراجعه کرده و نقشهای مجاز به دسترسی به مسیر protectedPage را به خاصیت data آن به صورت ذیل اضافه میکنیم:
import { ProtectedPageComponent } from "./protected-page/protected-page.component"; import { AuthGuardPermission } from "../core/models/auth-guard-permission"; const routes: Routes = [ { path: "protectedPage", component: ProtectedPageComponent, data: { permission: { permittedRoles: ["Admin"], deniedRoles: null } as AuthGuardPermission } } ];
export interface AuthGuardPermission { permittedRoles?: string[]; deniedRoles?: string[]; }
در اینجا تنها باید یکی از خواص permittedRoles (نقشهای مجاز به دسترسی/صدور دسترسی فقط برای این نقشهای مشخص، منهای مابقی) و یا deniedRoles (نقشهای غیرمجاز به دسترسی/دسترسی همهی نقشهای ممکن، منهای این نقشهای تعیین شده)، مقدار دهی شوند.
افزودن کامپوننت «دسترسی ندارید» به ماژول Authentication
در ادامه میخواهیم اگر کاربری به مسیری دسترسی نداشت، به صورت خودکار به صفحهی «دسترسی ندارید» هدایت شود. به همین جهت این کامپوننت را به صورت ذیل به ماژول authentication اضافه میکنیم:
>ng g c Authentication/AccessDenied
AccessDenied create src/app/Authentication/access-denied/access-denied.component.html (32 bytes) create src/app/Authentication/access-denied/access-denied.component.ts (296 bytes) create src/app/Authentication/access-denied/access-denied.component.css (0 bytes) update src/app/Authentication/authentication.module.ts (550 bytes)
import { LoginComponent } from "./login/login.component"; import { AccessDeniedComponent } from "./access-denied/access-denied.component"; const routes: Routes = [ { path: "login", component: LoginComponent }, { path: "accessDenied", component: AccessDeniedComponent } ];
<h1 class="text-danger"> <span class="glyphicon glyphicon-ban-circle"></span> Access Denied </h1> <p>Sorry! You don't have access to this page.</p> <button class="btn btn-default" (click)="goBack()"> <span class="glyphicon glyphicon-arrow-left"></span> Back </button> <button *ngIf="!isAuthenticated" class="btn btn-success" [routerLink]="['/login']" queryParamsHandling="merge"> Login </button>
import { Component, OnInit } from "@angular/core"; import { Location } from "@angular/common"; import { AuthService } from "../../core/services/auth.service"; @Component({ selector: "app-access-denied", templateUrl: "./access-denied.component.html", styleUrls: ["./access-denied.component.css"] }) export class AccessDeniedComponent implements OnInit { isAuthenticated = false; constructor( private location: Location, private authService: AuthService ) { } ngOnInit() { this.isAuthenticated = this.authService.isLoggedIn(); } goBack() { this.location.back(); // <-- go back to previous location on cancel } }
در اینجا اگر کاربر به سیستم وارد نشده باشد، دکمهی لاگین نیز به او نمایش داده میشود. همچنین وجود "queryParamsHandling="merge در لینک مراجعهی به صفحهی لاگین، سبب خواهد شد تا query string موجود در صفحه نیز حفظ شود و به صفحهی لاگین انتقال پیدا کند. در صفحهی لاگین نیز جهت پردازش این نوع کوئری استرینگها، تمهیدات لازم درنظر گرفته شدهاند.
دکمهی back آن نیز توسط سرویس توکار Location واقع در مسیر angular/common@ پیاده سازی شدهاست.
ایجاد یک محافظ مسیر سمت کلاینت برای بررسی وضعیت کاربر جاری و همچنین نقشهای او
پس از تعریف متد getAuthUser و استخراج اطلاعات کاربر از توکن دسترسی دریافتی که شامل نقشهای او نیز میشود، اکنون میتوان متد بررسی این نقشها را نیز به سرویس Auth اضافه کرد:
isAuthUserInRoles(requiredRoles: string[]): boolean { const user = this.getAuthUser(); if (!user || !user.roles) { return false; } return requiredRoles.some(requiredRole => user.roles.indexOf(requiredRole.toLowerCase()) >= 0); } isAuthUserInRole(requiredRole: string): boolean { return this.isAuthUserInRoles([requiredRole]); }
اکنون در هر قسمتی از برنامه که نیاز به بررسی امکان دسترسی یک کاربر به نقش یا نقشهایی خاص وجود داشته باشد، میتوان AuthService را به سازندهی آن تزریق و سپس از متد فوق جهت بررسی نهایی، استفاده کرد.
در ادامه یک Route Guard جدید را در مسیر app\core\services\auth.guard.ts ایجاد میکنیم. کار آن بررسی خودکار امکان دسترسی به یک مسیر درخواستی است:
import { Injectable } from "@angular/core"; import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from "@angular/router"; import { AuthService } from "./auth.service"; import { AuthGuardPermission } from "../models/auth-guard-permission"; @Injectable() export class AuthGuard implements CanActivate { constructor(private authService: AuthService, private router: Router) { } canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { if (!this.authService.isLoggedIn()) { this.showAccessDenied(state); return false; } const permissionData = route.data["permission"] as AuthGuardPermission; if (!permissionData) { return true; } if (Array.isArray(permissionData.deniedRoles) && Array.isArray(permissionData.permittedRoles)) { throw new Error("Don't set both 'deniedRoles' and 'permittedRoles' in route data."); } if (Array.isArray(permissionData.permittedRoles)) { const isInRole = this.authService.isAuthUserInRoles(permissionData.permittedRoles); if (isInRole) { return true; } this.showAccessDenied(state); return false; } if (Array.isArray(permissionData.deniedRoles)) { const isInRole = this.authService.isAuthUserInRoles(permissionData.deniedRoles); if (!isInRole) { return true; } this.showAccessDenied(state); return false; } } private showAccessDenied(state: RouterStateSnapshot) { this.router.navigate(["/accessDenied"], { queryParams: { returnUrl: state.url } }); } }
سپس خاصیت permission اطلاعات مسیر استخراج میشود. اگر چنین مقداری وجود نداشت، همینجا کار با موفقیت خاتمه پیدا میکند.
در آخر وضعیت دسترسی به نقشهای استخراجی deniedRoles و permittedRoles که از اطلاعات مسیر دریافت شدند، توسط متد isAuthUserInRoles سرویس Auth بررسی میشوند.
در متد showAccessDenied کار ارسال آدرس درخواستی (state.url) به صورت یک کوئری استرینگ (queryParams) با کلید returnUrl به صفحهی accessDenied صورت میگیرد. در این صفحه نیز دکمهی لاگین به همراه "queryParamsHandling="merge است. یعنی کامپوننت لاگین برنامه، کوئری استرینگ returnUrl را دریافت میکند:
this.returnUrl = this.route.snapshot.queryParams["returnUrl"];
if (this.returnUrl) { this.router.navigate([this.returnUrl]); } else { this.router.navigate(["/protectedPage"]); }
محل معرفی این AuthGuard جدید که در حقیقت یک سرویس است، در ماژول Core، در قسمت providers آن، به صورت ذیل میباشد:
import { AuthGuard } from "./services/auth.guard"; @NgModule({ providers: [ AuthGuard ] }) export class CoreModule {}
import { ProtectedPageComponent } from "./protected-page/protected-page.component"; import { AuthGuardPermission } from "../core/models/auth-guard-permission"; import { AuthGuard } from "../core/services/auth.guard"; const routes: Routes = [ { path: "protectedPage", component: ProtectedPageComponent, data: { permission: { permittedRoles: ["Admin"], deniedRoles: null } as AuthGuardPermission }, canActivate: [AuthGuard] } ];
اگر قصد آزمایش آنرا داشتید، فقط کافی است بجای نقش Admin، مثلا Admin1 را در permittedRoles مقدار دهی کنید، تا صفحهی access denied را در صورت درخواست مسیر protectedPage، بتوان مشاهده کرد.
کدهای کامل این سری را از اینجا میتوانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژهی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشهی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.
نظرات مطالب
بررسی angular.bootstrap
مثال بالا نمونه ای از یک برنامه کوچک است. بهتر است هر نکته و سرنخ در طی یک پست ارائه شود به جای ذکر تاریخچه، هستی و چیستی مسائل در طی یک پست.
اشتراکها
ReSharper 2017.2 منتشر شد
ReSharper 2017.2 understands .NET Core 2.0and C# 7.1, gets better at C# 7.0,provides more code inspections and context actions including new IEnumerable
inspections, delivers many powerful navigation and search improvements, brings new C# typing assists, levels up support for TypeScript, JavaScript, JSON and Angular.
پس از ایجاد کنترلرها، در این قسمت سعی خواهیم کرد تا آرایهای ثابت از مطالب و نظرات را در سایت نمایش دهیم. همچنین امکان ویرایش اطلاعات را نیز به این آرایههای جاوا اسکریپتی مدل، اضافه خواهیم کرد.
تعریف مدل سمت کاربر برنامه
فایل جدید Scripts\App\store.js را اضافه کرده و محتوای آنرا به نحو ذیل تغییر دهید:
در اینجا دو آرایه ثابت از اشیاء مطالب و نظرات را مشاهده میکنید.
سپس جهت استفاده از آن، تعریف مدخل آنرا به فایل index.html، پیش از تعاریف کنترلرها اضافه خواهیم کرد:
ویرایش قالب مطالب برای نمایش لیستی از عناوین ارسالی
قالب فعلی Scripts\Templates\posts.hbs صرفا دارای یک سری عنوان درج شده به صورت مستقیم در صفحه است. اکنون قصد داریم آنرا جهت نمایش لیستی از آرایه مطالب تغییر دهیم.
همانطور که در تصویر ملاحظه میکنید، با درخواست آدرس صفحهی مطالب، router آن مسیریابی متناظری را یافته و سپس بر این اساس، template، کنترلر و مدلی را انتخاب میکند. به صورت پیش فرض، قالب و کنترلر انتخاب شده، مواردی هستند همنام با مسیریابی جاری. اما مقدار پیش فرضی برای model وجود ندارد و باید آنرا به صورت دستی مشخص کرد.
برای این منظور فایل Scripts\Routes\posts.js را به پوشهی routes با محتوای ذیل اضافه کنید:
در اینجا صرفا جهت نمایش پیش فرضها و نحوهی کار یک route، دو خاصیت controllerName و renderTemplare آن نیز مقدار دهی شدهاند. این دو خاصیت به صورت پیش فرض، همنام مسیریابی جاری مقدار دهی میشوند و نیازی به ذکر صریح آنها نیست. اما خاصیت model یک مسیریابی است که باید دقیقا مشخص شود. در اینجا مقدار آنرا به آرایه posts تعریف شده در فایل Scripts\App\store.js تنظیم کردهایم. به این ترتیب مدل تعریف شده در اینجا، به صورت خودکار در کنترلر posts و قالب متناظر با آن، قابل استفاده خواهد بود.
همچنین اگر به خاطر داشته باشید، در پوشهی کنترلرها فایل posts.js تعریف نشدهاست. اگر اینکار صورت نگیرد، ember.js به صورت خودکار کنترلر پیش فرضی را ایجاد خواهد کرد. در کل، یک قالب هیچگاه به صورت مستقیم با مدل کار نمیکند. این کنترلر است که مدل را در اختیار یک قالب قرار میدهد.
سپس مدخل تعریف این فایل را به فایل index.html، پس از تعاریف کنترلرها اضافه نمائید:
اکنون فایل Scripts\Templates\posts.hbs را گشوده و به نحو ذیل، جهت نمایش عناوین مطالب، ویرایش کنید:
در این قالب، حلقهای بر روی عناصر model تشکیل شده و سپس خاصیت title هر عضو نمایش داده میشود.
نمایش لیست آخرین نظرات ارسالی
در ادامه قصد داریم تا آرایه comments ابتدای بحث را در صفحهای جدید نمایش دهیم. بنابراین نیاز است تا ابتدا مسیریابی آن تعریف شود. بنابراین فایل Scripts\App\router.js را گشوده و مسیریابی جدید recent-comments را به آن اضافه کنید:
سپس جهت تعیین مدل این مسیریابی جدید نیاز است تا فایل Scripts\Routes\recent-comments.js را در پوشهی routes با محتوای ذیل اضافه کرد:
در اینجا آرایه comments بازگشتی، همان آرایهای است که در ابتدای بحث در فایل Scripts\App\store.js تعریف کردیم.
همچنین نیاز است تا تعریف مدخل این فایل جدید را نیز به انتهای تعاریف مداخل فایل index.html اضافه کنیم:
اکنون قالب application واقع در فایل Scripts\Templates\application.hbs را جهت افزودن منوی مرتبط با این مسیریابی جدید، به نحو ذیل ویرایش خواهیم کرد:
و در آخر قالب جدید Scripts\Templates\recent-comments.hbs را برای نمایش لیست آخرین نظرات، با محتوای زیر اضافه میکنیم:
برای فعال شدن آن نیاز است تا تعریف این قالب جدید را به template loader برنامه، در فایل index.html اضافه کنیم:
نمایش مجزای هر مطلب در یک صفحهی جدید
تا اینجا در صفحهی اول سایت، لیست عناوین مطالب را نمایش دادیم. در ادامه نیاز است تا بتوان هر عنوان را به صفحهی متناظر و اختصاصی آن لینک کرد؛ برای مثال لینکی مانند http://localhost:25918/#/posts/3 به سومین مطلب ارسالی اشاره میکند. Ember.js به عدد 3 در اینجا، یک dynamic segment میگوید. از این جهت که مقدار آن بر اساس شماره مطلب درخواستی، متفاوت خواهد بود. برای پردازش این نوع آدرسها نیاز است مسیریابی ویژهای را تعریف کرد. فایل Scripts\App\router.js را گشوده و سپس مسیریابی post را به نحو ذیل به آن اضافه نمائید:
قسمت پویای مسیریابی با یک : مشخص میشود.
با توجه به اینکه این مسیریابی جدید post نام گرفت (جهت نمایش یک مطلب)، به صورت خودکار، کنترلر و قالبی به همین نام را بارگذاری میکند. همچنین مدل خود را نیز باید از مسیریابی خاص خود دریافت کند. بنابراین فایل جدید Scripts\Routes\post.js را در پوشهی routes با محتوای ذیل اضافه کنید:
در اینجا مدل مسیریابی post بر اساس پارامتری به نام params تعیین میشود. این پارامتر حاوی مقدار متغیر پویای post_id که در مسیریابی جدید post مشخص کردیم میباشد. در ادامه از آرایه posts تعریف شده در ابتدای بحث، توسط متد findBy که توسط Ember.js اضافه شدهاست، عنصری را که خاصیت id آن مساوی post_id دریافتی است، انتخاب کرده و به عنوان مقدار مدل بازگشت میدهیم.
برای مثال، جهت آدرس http://localhost:25918/#/posts/3، مقدار post_id به صورت خودکار به عدد 3 تنظیم میشود.
پس از آن نیاز است مدخل این فایل جدید را در صفحهی index.html نیز اضافه کنیم:
در ادامه برای نمایش اطلاعات مدل نیاز است قالب جدید Scripts\Templates\post.hbs را با محتوای زیر اضافه کنیم:
و template loader صفحهی index.html را نیز باید از وجود آن باخبر کرد:
اکنون به قالب Scripts\Templates\posts.hbs مراجعه کرده و هر عنوان را به مطلب متناظر با آن لینک میکنیم:
همانطور که ملاحظه میکنید، link-to امکان پذیرش id یک مطلب را به صورت متغیر نیز دارا است که سبب خواهد شد تا عناوین، به مطالب متناظر لینک شوند:
همچنین با کلیک بر روی هر عنوان نیز مطلب مرتبط نمایش داده خواهد شد:
افزودن امکان ویرایش مطالب
میخواهیم در صفحهی نمایش جزئیات یک مطلب، امکان ویرایش آنرا نیز فراهم کنیم. بنابراین فایل Scripts\Templates\post.hbs را گشوده و محتوای آنرا به نحو ذیل ویرایش کنید:
شبیه به این if و else را در قسمت قبل حین ایجاد صفحات about و یا contact نیز مشاهده کردهاید. در اینجا اگر خاصیت isEditing مساوی true باشد، فرم ویرایش اطلاعات ظاهر میشود و اگر خیر، محتوای مطلب جاری نمایش داده خواهد شد.
در فرم تعریف شده، المانهای ورودی اطلاعات از handlebar helperهای ویژهی input و textarea استفاده میکنند؛ بجای المانهای متداول HTML. همچنین value یکی به title و دیگری به body تنظیم شدهاست (خواص مدل ارائه شده توسط کنترلر متصل به قالب). این مقادیر نیز داخل '' قرار ندارند؛ به عبارتی در یک handlebar helper به عنوان متغیر در نظر گرفته میشوند. به این ترتیب اطلاعات کنترلر جاری، به این المانهای ورودی اطلاعات به صورت خودکار bind میشوند و برعکس. اگر کاربر مقادیر آنها را تغییر دهد، تغییرات نهایی به صورت خودکار به خواص متناظری در کنترلر جاری منعکس خواهند شد (two-way data binding).
دو دکمه نیز تعریف شدهاند که به اکشنهای save و edit متصل هستند.
بنابراین نیاز به یک کنترلر جدید، به نام post داریم تا بتوان رفتار قالب post را کنترل کرد. برای این منظور فایل جدید Scripts\Controllers\post.js را با محتوای ذیل ایجاد کنید:
همچنین مدخل تعریف آنرا نیز به فایل index.html اضافه نمائید (پس از تعاریف کنترلرهای موجود):
اگر به کدهای این کنترلر دقت کرده باشید، اینبار زیرکلاسی از ObjectController ایجاد شدهاست و نه Controller، مانند مثالهای قبل. ObjectController تغییرات رخ داده بر روی خواص مدل را که توسط کنترلر در معرض دید قالب قرار دادهاست، به صورت خودکار به مدل مرتبط نیز منعکس میکند (Ember.ObjectController.extend)؛ اما Controller خیر (Ember.Controller.extend). در اینجا مدل کنترلر، تنها «یک» شیء است که بر اساس id آن انتخاب شدهاست. به همین جهت از ObjectController برای ارائه two-way data binding کمک گرفته شد.
در ember.js، یک قالب تنها با کنترلر خودش دارای تبادل اطلاعات است. اگر این کنترلر از نوع ObjectController باشد، تغییرات خاصیتی در یک قالب، ابتدا به کنترلر آن منعکس میشود و سپس این کنترلر، در صورت یافتن معادلی از این خاصیت در مدل، آنرا به روز خواهد کرد. در حالت استفاده از Controller معمولی، صرفا تبادل اطلاعات بین قالب و کنترلر را شاهد خواهیم بود و نه بیشتر.
در ابتدای کار مقدار خاصیت isEditing مساوی false است. این مورد سبب میشود تا در بار اول بارگذاری اطلاعات یک مطلب انتخابی، صرفا عنوان و محتوای مطلب نمایش داده شوند؛ به همراه یک دکمهی edit. با کلیک بر روی دکمهی edit، مطابق کدهای کنترلر فوق، تنها خاصیت isEditing به true تنظیم میشود و در این حالت، بدنهی اصلی شرط if isEditing در قالب post، رندر خواهد شد.
برای مثال در ابتدا مطلب شماره یک را انتخاب میکنیم:
با کلیک بر روی دکمهی edit، فرم ویرایش ظاهر خواهد شد:
نکتهی جالب آن، مقدار دهی خودکار المانهای ویرایش اطلاعات است. در این حالت سعی کنید، عنوان مطلب جاری را اندکی ویرایش کنید:
با ویرایش عنوان، میتوان بلافاصله مقدار تغییر یافته را در برچسب عنوان مطلب نیز مشاهده کرد. این مورد دقیقا مفهوم two-way data binding و اتصال مقادیر value هر کدام از handlebar helperهای ویژهی input و textarea را به عناصر مدل ارائه شده توسط کنترلر post، بیان میکند.
در این حالت در کدهای متد save، تنها کافی است که خاصیت isEditing را به false تنظیم کنیم. زیرا کلیه مقادیر ویرایش شده توسط کاربر، در همان لحظه در برنامه منتشر شدهاند و نیاز به کار بیشتری برای اعمال تغییرات نیست.
اضافه کردن دکمهی مرتب سازی بر اساس عناوین، در صفحهی اول سایت
Ember.ObjectController.extend برای data bindg یک شیء کاربرد دارد. اگر قصد داشته باشیم با آرایهای از اشیاء کار کنیم میتوان از ArrayController استفاده کرد. فرض کنید در صفحهی اول سایت میخواهیم امکان مرتب سازی مطالب را بر اساس عنوان آنها اضافه کنیم. فایل Scripts\Templates\posts.hbs را گشوده و لینک Sort by title را به انتهای آن اضافه کنید:
در اینجا چون قصد تغییر رفتار قالب posts را توسط اکشن جدید sortByTitle داریم، نیاز است کنترلر متناظر با آنرا نیز اضافه کنیم. برای این منظور فایل جدید Scripts\Controllers\posts.js را به پوشهی کنترلرها اضافه کنید؛ با محتوای ذیل:
sortProperties جزو خواص کلاس پایه ArrayController است. اگر مانند سطر اول به صورت مستقیم مقدار دهی شود، خاصیت یا خواص پیش فرض مرتب سازی را مشخص میکند. اگر مانند اکشن sortByTitle توسط متد set مقدار دهی شود، امکان مرتب سازی تعاملی و با فرمان کاربر را فراهم خواهد کرد.
در ادامه، تعریف مدخل این کنترلر جدید را نیز باید به فایل index.html، اضافه کرد:
اگر برنامه را در این حالت اجرا کرده و بر روی دکمهی Sort by title کلیک کنید، اتفاقی رخ نمیدهد. علت اینجا است که ArrayController خروجی تغییر یافته خودش را توسط خاصیتی به نام arrangedContent در اختیار قالب خود قرار میدهد. بنابراین نیاز است فایل قالب Scripts\Templates\posts.hbs را به نحو ذیل ویرایش کرد:
اینبار کلیک بر روی دکمهی مرتب سازی بر اساس عناوین، هربار لیست موجود را به صورت صعودی و یا نزولی مرتب میکند.
یک نکته: حلقهی ویژهای به نام each
اگر قالب Scripts\Templates\posts.hbs را به نحو ذیل، با یک حلقهی each ساده بازنویسی کنید:
هم در حالت نمایش معمولی و هم در حالت استفاده از ArrayController برای نمایش اطلاعات مرتب شده، بدون مشکل کار میکند و نیازی به تغییر نخواهد داشت.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید:
EmberJS03_03.zip
تعریف مدل سمت کاربر برنامه
فایل جدید Scripts\App\store.js را اضافه کرده و محتوای آنرا به نحو ذیل تغییر دهید:
var posts = [ { id: '1', title: "Getting Started with Ember.js", body: "Bla bla bla 1." }, { id: '2', title: "Routes and Templates", body: "Bla bla bla 2." }, { id: '3', title: "Controllers", body: "Bla bla bla 3." } ]; var comments = [ { id: '1', postId: '3', text: 'Thanks!' }, { id: '2', postId: '3', text: 'Good to know that!' }, { id: '3', postId: '1', text: 'Great!' } ];
سپس جهت استفاده از آن، تعریف مدخل آنرا به فایل index.html، پیش از تعاریف کنترلرها اضافه خواهیم کرد:
<script src="Scripts/App/store.js" type="text/javascript"></script>
ویرایش قالب مطالب برای نمایش لیستی از عناوین ارسالی
قالب فعلی Scripts\Templates\posts.hbs صرفا دارای یک سری عنوان درج شده به صورت مستقیم در صفحه است. اکنون قصد داریم آنرا جهت نمایش لیستی از آرایه مطالب تغییر دهیم.
همانطور که در تصویر ملاحظه میکنید، با درخواست آدرس صفحهی مطالب، router آن مسیریابی متناظری را یافته و سپس بر این اساس، template، کنترلر و مدلی را انتخاب میکند. به صورت پیش فرض، قالب و کنترلر انتخاب شده، مواردی هستند همنام با مسیریابی جاری. اما مقدار پیش فرضی برای model وجود ندارد و باید آنرا به صورت دستی مشخص کرد.
برای این منظور فایل Scripts\Routes\posts.js را به پوشهی routes با محتوای ذیل اضافه کنید:
Blogger.PostsRoute = Ember.Route.extend({ controllerName: 'posts', renderTemplare: function () { this.render('posts'); }, model: function () { return posts; } });
همچنین اگر به خاطر داشته باشید، در پوشهی کنترلرها فایل posts.js تعریف نشدهاست. اگر اینکار صورت نگیرد، ember.js به صورت خودکار کنترلر پیش فرضی را ایجاد خواهد کرد. در کل، یک قالب هیچگاه به صورت مستقیم با مدل کار نمیکند. این کنترلر است که مدل را در اختیار یک قالب قرار میدهد.
سپس مدخل تعریف این فایل را به فایل index.html، پس از تعاریف کنترلرها اضافه نمائید:
<script src="Scripts/Routes/posts.js" type="text/javascript"></script>
اکنون فایل Scripts\Templates\posts.hbs را گشوده و به نحو ذیل، جهت نمایش عناوین مطالب، ویرایش کنید:
<h2>Emeber.js blog</h2> <ul> {{#each post in model}} <li>{{post.title}}</li> {{/each}} </ul>
نمایش لیست آخرین نظرات ارسالی
در ادامه قصد داریم تا آرایه comments ابتدای بحث را در صفحهای جدید نمایش دهیم. بنابراین نیاز است تا ابتدا مسیریابی آن تعریف شود. بنابراین فایل Scripts\App\router.js را گشوده و مسیریابی جدید recent-comments را به آن اضافه کنید:
Blogger.Router.map(function () { this.resource('posts', { path: '/' }); this.resource('about'); this.resource('contact', function () { this.resource('email'); this.resource('phone'); }); this.resource('recent-comments'); });
Blogger.RecentCommentsRoute = Ember.Route.extend({ model: function () { return comments; } });
همچنین نیاز است تا تعریف مدخل این فایل جدید را نیز به انتهای تعاریف مداخل فایل index.html اضافه کنیم:
<script src="Scripts/Routes/recent-comments.js" type="text/javascript"></script>
اکنون قالب application واقع در فایل Scripts\Templates\application.hbs را جهت افزودن منوی مرتبط با این مسیریابی جدید، به نحو ذیل ویرایش خواهیم کرد:
<div class='container'> <nav class='navbar navbar-default' role='navigation'> <ul class='nav navbar-nav'> <li>{{#link-to 'posts'}}Posts{{/link-to}}</li> <li>{{#link-to 'recent-comments'}}Recent comments{{/link-to}}</li> <li>{{#link-to 'about'}}About{{/link-to}}</li> <li>{{#link-to 'contact'}}Contact{{/link-to}}</li> </ul> </nav> {{outlet}} </div>
<h1>Recent comments</h1> <ul> {{#each comment in model}} <li>{{comment.text}}</li> {{/each}} </ul>
<script type="text/javascript"> EmberHandlebarsLoader.loadTemplates([ 'posts', 'about', 'application', 'contact', 'email', 'phone', 'recent-comments' ]); </script>
نمایش مجزای هر مطلب در یک صفحهی جدید
تا اینجا در صفحهی اول سایت، لیست عناوین مطالب را نمایش دادیم. در ادامه نیاز است تا بتوان هر عنوان را به صفحهی متناظر و اختصاصی آن لینک کرد؛ برای مثال لینکی مانند http://localhost:25918/#/posts/3 به سومین مطلب ارسالی اشاره میکند. Ember.js به عدد 3 در اینجا، یک dynamic segment میگوید. از این جهت که مقدار آن بر اساس شماره مطلب درخواستی، متفاوت خواهد بود. برای پردازش این نوع آدرسها نیاز است مسیریابی ویژهای را تعریف کرد. فایل Scripts\App\router.js را گشوده و سپس مسیریابی post را به نحو ذیل به آن اضافه نمائید:
Blogger.Router.map(function () { this.resource('posts', { path: '/' }); this.resource('about'); this.resource('contact', function () { this.resource('email'); this.resource('phone'); }); this.resource('recent-comments'); this.resource('post', { path: 'posts/:post_id' }); });
با توجه به اینکه این مسیریابی جدید post نام گرفت (جهت نمایش یک مطلب)، به صورت خودکار، کنترلر و قالبی به همین نام را بارگذاری میکند. همچنین مدل خود را نیز باید از مسیریابی خاص خود دریافت کند. بنابراین فایل جدید Scripts\Routes\post.js را در پوشهی routes با محتوای ذیل اضافه کنید:
Blogger.PostRoute = Ember.Route.extend({ model: function (params) { return posts.findBy('id', params.post_id); } });
برای مثال، جهت آدرس http://localhost:25918/#/posts/3، مقدار post_id به صورت خودکار به عدد 3 تنظیم میشود.
پس از آن نیاز است مدخل این فایل جدید را در صفحهی index.html نیز اضافه کنیم:
<script src="Scripts/Routes/post.js" type="text/javascript"></script>
در ادامه برای نمایش اطلاعات مدل نیاز است قالب جدید Scripts\Templates\post.hbs را با محتوای زیر اضافه کنیم:
<h1>{{title}}</h1> <p>{{body}}</p>
<script type="text/javascript"> EmberHandlebarsLoader.loadTemplates([ 'posts', 'about', 'application', 'contact', 'email', 'phone', 'recent-comments', 'post' ]); </script>
اکنون به قالب Scripts\Templates\posts.hbs مراجعه کرده و هر عنوان را به مطلب متناظر با آن لینک میکنیم:
<h2>Emeber.js blog</h2> <ul> {{#each post in model}} <li>{{#link-to 'post' post.id}}{{post.title}}{{/link-to}}</li> {{/each}} </ul>
همچنین با کلیک بر روی هر عنوان نیز مطلب مرتبط نمایش داده خواهد شد:
افزودن امکان ویرایش مطالب
میخواهیم در صفحهی نمایش جزئیات یک مطلب، امکان ویرایش آنرا نیز فراهم کنیم. بنابراین فایل Scripts\Templates\post.hbs را گشوده و محتوای آنرا به نحو ذیل ویرایش کنید:
<h2>{{title}}</h2> {{#if isEditing}} <form> <div class="form-group"> <label for="title">Title</label> {{input value=title id="title" class="form-control"}} </div> <div class="form-group"> <label for="body">Body</label> {{textarea value=body id="body" class="form-control" rows="5"}} </div> <button class="btn btn-primary" {{action 'save' }}>Save</button> </form> {{else}} <p>{{body}}</p> <button class="btn btn-primary" {{action 'edit' }}>Edit</button> {{/if}}
در فرم تعریف شده، المانهای ورودی اطلاعات از handlebar helperهای ویژهی input و textarea استفاده میکنند؛ بجای المانهای متداول HTML. همچنین value یکی به title و دیگری به body تنظیم شدهاست (خواص مدل ارائه شده توسط کنترلر متصل به قالب). این مقادیر نیز داخل '' قرار ندارند؛ به عبارتی در یک handlebar helper به عنوان متغیر در نظر گرفته میشوند. به این ترتیب اطلاعات کنترلر جاری، به این المانهای ورودی اطلاعات به صورت خودکار bind میشوند و برعکس. اگر کاربر مقادیر آنها را تغییر دهد، تغییرات نهایی به صورت خودکار به خواص متناظری در کنترلر جاری منعکس خواهند شد (two-way data binding).
دو دکمه نیز تعریف شدهاند که به اکشنهای save و edit متصل هستند.
بنابراین نیاز به یک کنترلر جدید، به نام post داریم تا بتوان رفتار قالب post را کنترل کرد. برای این منظور فایل جدید Scripts\Controllers\post.js را با محتوای ذیل ایجاد کنید:
Blogger.PostController = Ember.ObjectController.extend({ isEditing: false, actions: { edit: function () { this.set('isEditing', true); }, save: function () { this.set('isEditing', false); } } });
<script src="Scripts/Controllers/post.js" type="text/javascript"></script>
اگر به کدهای این کنترلر دقت کرده باشید، اینبار زیرکلاسی از ObjectController ایجاد شدهاست و نه Controller، مانند مثالهای قبل. ObjectController تغییرات رخ داده بر روی خواص مدل را که توسط کنترلر در معرض دید قالب قرار دادهاست، به صورت خودکار به مدل مرتبط نیز منعکس میکند (Ember.ObjectController.extend)؛ اما Controller خیر (Ember.Controller.extend). در اینجا مدل کنترلر، تنها «یک» شیء است که بر اساس id آن انتخاب شدهاست. به همین جهت از ObjectController برای ارائه two-way data binding کمک گرفته شد.
در ember.js، یک قالب تنها با کنترلر خودش دارای تبادل اطلاعات است. اگر این کنترلر از نوع ObjectController باشد، تغییرات خاصیتی در یک قالب، ابتدا به کنترلر آن منعکس میشود و سپس این کنترلر، در صورت یافتن معادلی از این خاصیت در مدل، آنرا به روز خواهد کرد. در حالت استفاده از Controller معمولی، صرفا تبادل اطلاعات بین قالب و کنترلر را شاهد خواهیم بود و نه بیشتر.
در ابتدای کار مقدار خاصیت isEditing مساوی false است. این مورد سبب میشود تا در بار اول بارگذاری اطلاعات یک مطلب انتخابی، صرفا عنوان و محتوای مطلب نمایش داده شوند؛ به همراه یک دکمهی edit. با کلیک بر روی دکمهی edit، مطابق کدهای کنترلر فوق، تنها خاصیت isEditing به true تنظیم میشود و در این حالت، بدنهی اصلی شرط if isEditing در قالب post، رندر خواهد شد.
برای مثال در ابتدا مطلب شماره یک را انتخاب میکنیم:
با کلیک بر روی دکمهی edit، فرم ویرایش ظاهر خواهد شد:
نکتهی جالب آن، مقدار دهی خودکار المانهای ویرایش اطلاعات است. در این حالت سعی کنید، عنوان مطلب جاری را اندکی ویرایش کنید:
با ویرایش عنوان، میتوان بلافاصله مقدار تغییر یافته را در برچسب عنوان مطلب نیز مشاهده کرد. این مورد دقیقا مفهوم two-way data binding و اتصال مقادیر value هر کدام از handlebar helperهای ویژهی input و textarea را به عناصر مدل ارائه شده توسط کنترلر post، بیان میکند.
در این حالت در کدهای متد save، تنها کافی است که خاصیت isEditing را به false تنظیم کنیم. زیرا کلیه مقادیر ویرایش شده توسط کاربر، در همان لحظه در برنامه منتشر شدهاند و نیاز به کار بیشتری برای اعمال تغییرات نیست.
اضافه کردن دکمهی مرتب سازی بر اساس عناوین، در صفحهی اول سایت
Ember.ObjectController.extend برای data bindg یک شیء کاربرد دارد. اگر قصد داشته باشیم با آرایهای از اشیاء کار کنیم میتوان از ArrayController استفاده کرد. فرض کنید در صفحهی اول سایت میخواهیم امکان مرتب سازی مطالب را بر اساس عنوان آنها اضافه کنیم. فایل Scripts\Templates\posts.hbs را گشوده و لینک Sort by title را به انتهای آن اضافه کنید:
<h2>Emeber.js blog</h2> <ul> {{#each post in model}} <li>{{#link-to 'post' post.id}}{{post.title}}{{/link-to}}</li> {{/each}} </ul> <a href="#" class="btn btn-primary" {{action 'sortByTitle'}}>Sort by title</a>
Blogger.PostsController = Ember.ArrayController.extend({ sortProperties: ['id'],// مقادیر پیش فرض مرتب سازی sortAscending: false, actions: { sortByTitle: function () { this.set('sortProperties', ['title']); this.set('sortAscending', !this.get('sortAscending')); } } });
در ادامه، تعریف مدخل این کنترلر جدید را نیز باید به فایل index.html، اضافه کرد:
<script src="Scripts/Controllers/posts.js" type="text/javascript"></script>
اگر برنامه را در این حالت اجرا کرده و بر روی دکمهی Sort by title کلیک کنید، اتفاقی رخ نمیدهد. علت اینجا است که ArrayController خروجی تغییر یافته خودش را توسط خاصیتی به نام arrangedContent در اختیار قالب خود قرار میدهد. بنابراین نیاز است فایل قالب Scripts\Templates\posts.hbs را به نحو ذیل ویرایش کرد:
<h2>Emeber.js blog</h2> <ul> {{#each post in arrangedContent}} <li>{{#link-to 'post' post.id}}{{post.title}}{{/link-to}}</li> {{/each}} </ul> <a href="#" class="btn btn-primary" {{action 'sortByTitle'}}>Sort by title</a>
یک نکته: حلقهی ویژهای به نام each
اگر قالب Scripts\Templates\posts.hbs را به نحو ذیل، با یک حلقهی each ساده بازنویسی کنید:
<h2>Ember.js blog</h2> <ul> {{#each}} <li>{{#link-to 'post' id}}{{title}}{{/link-to}}</li> {{/each}} </ul> <a href="#" class="btn btn-primary" {{action 'sortByTitle'}}>Sort by title</a>
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید:
EmberJS03_03.zip
مطالب دورهها
نگاهی به SignalR Hubs
Hubs کلاسهایی هستند جهت پیاده سازی push services در SignalR و همانطور که در قسمت قبل عنوان شد، در سطحی بالاتر از اتصال ماندگار (persistent connection) قرار میگیرند. کلاسهای Hubs بر مبنای یک سری قرار داد پیش فرض کار میکنند (ایده Convention-over-configuration) تا استفاده نهایی از آنها را سادهتر کنند.
Hubs به نوعی یک فریم ورک سطح بالای RPC نیز محسوب میشوند (Remote Procedure Calls) و آنرا برای انتقال انواع و اقسام دادهها بین سرور و کلاینت و یا فراخوانی متدی در سمت کلاینت یا سرور، بسیار مناسب میسازد. برای مثال اگر قرار باشد با persistent connection به صورت مستقیم کار کنیم، نیاز است تا بسیاری از مسایل serialization و deserialization اطلاعات را خودمان پیاده سازی و اعمال نمائیم.
قرار دادهای پیش فرض Hubs
- متدهای public کلاسهای Hubs از طریق دنیای خارج قابل فراخوانی هستند.
- ارسال اطلاعات به کلاینتها از طریق فراخوانی متدهای سمت کلاینت انجام خواهد شد. (نحوه تعریف این متدها در سمت سرور بر اساس قابلیتهای dynamic اضافه شده به دات نت 4 است که در ادامه در مورد آن بیشتر بحث خواهد شد)
مراحل اولیه نوشتن یک Hub
الف) یک کلاس Hub را تهیه کنید. این کلاس، از کلاس پایه Hub تعریف شده در فضای نام Microsoft.AspNet.SignalR باید مشتق شود. همچنین این کلاس میتواند توسط ویژگی خاصی به نام HubName نیز مزین گردد تا در حین برپایی اولیه سرویس، از طریق زیرساختهای SignalR به نامی دیگر (یک alias یا نام مستعار خاص) قابل شناسایی باشد. متدهای یک هاب میتوانند نوعهای ساده یا پیچیدهای را بازگشت دهند و همه چیز در اینجا نهایتا به فرمت JSON رد و بدل خواهد شد (فرمت پیش فرض که در پشت صحنه از کتابخانه معروف JSON.NET استفاده میکند؛ این کتابخانه سورس باز به دلیل کیفیت بالای آن، از زمان ارائه MVC4 به عنوان جزئی از مجموعه کارهای مایکروسافت قرار گرفته است).
ب) مسیریابی و Routing را تعریف و اصلاح نمائید.
و ... از نتیجه استفاده کنید.
تهیه اولین برنامه با SignalR
ابتدا یک پروژه خالی ASP.NET را آغاز کنید (مهم نیست MVC باشد یا WebForms). برای سادگی بیشتر، در اینجا یک ASP.NET Empty Web application درنظر گرفته شده است. در ادامه قصد داریم یک برنامه Chat را تهیه کنیم؛ از این جهت که توسط یک برنامه Chat بسیاری از مفاهیم مرتبط با SignalR را میتوان در عمل توضیح داد.
اگر از VS 2012 استفاده میکنید، گزینه SignalR Hub class جزئی از آیتمهای جدید قابل افزودن به پروژه است (منوی پروژه، گزینه new item آن) و پس از انتخاب این قالب خاص، تمامی ارجاعات لازم نیز به صورت خودکار به پروژه جاری اضافه خواهند شد.
و اگر از VS 2010 استفاده میکنید، نیاز است از طریق NuGet ارجاعات لازم را به پروژه خود اضافه نمائید:
اکنون یک کلاس خالی جدید را به نام ChatHub، به آن اضافه کنید. سپس کدهای آن را به نحو ذیل تغییر دهید:
همانطور که ملاحظه میکنید این کلاس از کلاس پایه Hub مشتق شده و توسط ویژگی HubName، نام مستعار chat را یافته است.
کلاس پایه Hub یک سری متد و خاصیت را در اختیار کلاسهای مشتق شده از آن قرار میدهد. سادهترین راه برای آشنایی با این متدها و خواص مهیا، کلیک راست بر روی نام کلاس پایه Hub و انتخاب گزینه Go to definition است.
برای نمونه در کلاس ChatHub فوق، از خاصیت Clients برای دسترسی به تمامی آنها و سپس فراخوانی متد dynamic ایی به نام hello که هنوز وجود خارجی ندارد، استفاده شده است.
اهمیتی ندارد که این کلاس در اسمبلی اصلی برنامه وب قرار گیرد یا مثلا در یک class library به نام Services. همینقدر که از کلاس Hub مشتق شود به صورت خودکار در ابتدای برنامه اسکن گردیده و یافت خواهد شد.
مرحله بعد، افزودن فایل global.asax به برنامه است. زیرا برای کار با SignalR نیاز است تنظیمات Routing و مسیریابی خاص آنرا اضافه نمائیم. پس از افرودن فایل global.asax، به فایل Global.asax.cs مراجعه کرده و در متد Application_Start آن تغییرات ذیل را اعمال نمائید:
یک نکته مهم
اگر از ASP.NET MVC استفاده میکنید، این تنظیم مسیریابی باید پیش از تعاریف پیش فرض موجود قرار گیرد. در غیراینصورت مسیریابیهای SignalR کار نخواهند کرد.
اکنون برای آزمایش برنامه، برنامه را اجرا کرده و مسیر ذیل را فراخوانی کنید:
در این حال اگر برنامه را برای مثال با مرورگر chrome باز کنید، در این آدرس، فایل جاوا اسکریپتی SignalR، قابل مشاهده خواهد بود. مرورگر IE پیغام میدهد که فایل را نمیتواند باز کند. اگر به انتهای خروجی آدرس مراجعه کنید، چنین سطری قابل مشاهده است:
و کلمه chat دقیقا از مقدار معرفی شده توسط ویژگی HubName دریافت گردیده است.
تا اینجا ما موفق شدیم اولین Hub خود را تشکیل دهیم.
بررسی پروتکل Hub
اکنون که اولین Hub خود را ایجاد کردهایم، بد نیست اندکی با زیر ساخت آن نیز آشنا شویم.
مطابق مسیریابی تعریف شده در Application_Start، مسیر ابتدایی دسترسی به SignalR با افزودن اسلش SignalR به انتهای مسیر ریشه سایت بدست میآید و اگر به این آدرس یک اسلش hubs را نیز اضافه کنیم، فایل js metadata مرتبط را نیز میتوان دریافت و مشاهده کرد.
زمانیکه یک کلاینت قصد اتصال به یک Hub را دارد، دو مرحله رخ خواهد داد:
الف) negotiate: در این حالت امکانات قابل پشتیبانی از طرف سرور مورد پرسش قرار میگیرند و سپس بهترین حالت انتقال، انتخاب میگردد. این انتخابها به ترتیب از چپ به راست خواهند بود:
به این معنا که اگر برای مثال امکانات Web sockets مهیا بود، در همینجا کار انتخاب نحوه انتقال اطلاعات خاتمه یافته و Web sockets انتخاب میشود.
تمام این مراحل نیز خودکار است و نیازی نیست تا برای تنظیمات آن کار خاصی صورت گیرد. البته در سمت کلاینت، امکان انتخاب یکی از موارد یاد شده به صورت صریح نیز وجود دارد.
ب) connect: اتصالی ماندگار برقرار میگردد.
در پروتکل Hub تمام اطلاعات JSON encoded هستند و یک سری مخففهایی را در این بین نیز ممکن است مشاهده نمائید که معنای آنها به شرح زیر است:
این مراحل را در قسمت بعد، پس از ایجاد یک کلاینت، بهتر میتوان توضیح داد.
روشهای مختلف ارسال اطلاعات به کلاینتها
به چندین روش میتوان اطلاعاتی را به کلاینتها ارسال کرد:
1) استفاده از خاصیت Clients موجود در کلاس Hub
2) استفاده از خواص و متدهای dynamic
در این حالت اطلاعات متد dynamic و پارامترهای آن به صورت JSON encoded به کلاینت ارسال میشوند (به همین جهت اهمیتی ندارند که در سرور وجود خارجی دارند یا خیر و به صورت dynamic تعریف شدهاند).
برای نمونه در اینجا متد hello به صورت dynamic تعریف شده است (جزئی از متدهای خاصیت All نیست و اصلا در سمت سرور وجود خارجی ندارد) و خواص Context و Clients، هر دو در کلاس پایه Hub قرار دارند.
حالت Clients.All به معنای ارسال پیامی به تمام کلاینتهای متصل به هاب ما هستند.
3) روشهای دیگر، استفاده از خاصیت dynamic دیگری به نام Caller است که میتوان بر روی آن متد دلخواهی را تعریف و فراخوانی کرد.
انجام اینکار با روش ارائه شده در سطر دومی که ملاحظه میکنید، در عمل یکی است؛ از این جهت که Context.ConnectionId همان ConnectionId فراخوان میباشد.
در اینجا پیامی صرفا به فراخوان جاری سرویس ارسال میگردد.
4) استفاده از خاصیت dynamic ایی به نام Clients.Others
در این حالت، پیام، به تمام کلاینتهای متصل، منهای کلاینت فراخوان ارسال میگردد.
5) استفاده از متد Clients.AllExcept
این متد میتواند آرایهای از ConnectionIdهایی را بپذیرد که قرار نیست پیام ارسالی ما را دریافت کنند.
6) ارسال اطلاعات به گروهها
تعداد مشخصی از ConnectionIdها یک گروه را تشکیل میدهند؛ مثلا اعضای یک chat room.
در اینجا نحوه الحاق یک کلاینت به یک room یا گروه را مشاهده میکنید. همچنین با مشخص بودن نام گروه، میتوان صرفا اطلاعاتی را به اعضای آن گروه خاص ارسال کرد.
خاصیت Group در کلاس پایه Hub تعریف شده است.
نکته مهمی را که در اینجا باید درنظر داشت این است که اطلاعات گروهها به صورت دائمی در سرور ذخیره نمیشوند. برای مثال اگر سرور ری استارت شود، این اطلاعات از دست خواهند رفت.
آشنایی با مراحل طول عمر یک Hub
اگر به تعاریف کلاس پایه Hub دقت کنیم:
در اینجا، تعدادی از متدها virtual تعریف شدهاند که تمامی آنها را در کلاس مشتق شده نهایی میتوان override و مورد استفاده قرار داد. به این ترتیب میتوان به اجزا و مراحل مختلف طول عمر یک Hub مانند برقراری اتصال یا قطع شدن آن، دسترسی یافت. تمام این متدها نیز با Task معرفی شدهاند؛ که معنای غیرهمزمان بودن پردازش آنها را بیان میکند.
تعدادی از این متدها را میتوان جهت مقاصد logging برنامه مورد استفاده قرار داد و یا در متد OnDisconnected اگر اطلاعاتی را در بانک اطلاعاتی ذخیره کردهایم، بر این اساس میتوان وضعیت نهایی را تغییر داد.
ارسال اطلاعات از یک Hub به Hub دیگر در برنامه
فرض کنید یک Hub دوم را به نام MinitorHub به برنامه اضافه کردهاید. اکنون قصد داریم از داخل ChatHub فوق، اطلاعاتی را به آن ارسال کنیم. روش کار به نحو زیر است:
در اینجا با override کردن OnDisconnected به رویداد خاتمه اتصال یک کلاینت دسترسی یافتهایم. سپس قصد داریم این اطلاعات را توسط متد sendMonitorData به Hub دومی به نام MonitorHub ارسال کنیم که نحوه پیاده سازی آنرا در کدهای فوق ملاحظه میکنید. GlobalHost.ConnectionManager یک dependency resolver توکار تعریف شده در SignalR است.
مورد استفاده دیگر این روش، ارسال اطلاعات به کلاینتها از طریق کدهای یک برنامه تحت وب است (که در همان پروژه هاب واقع شده است). برای مثال در یک اکشن متد یا یک روال رویدادگردان کلیک نیز میتوان از GlobalHost.ConnectionManager استفاده کرد.
Hubs به نوعی یک فریم ورک سطح بالای RPC نیز محسوب میشوند (Remote Procedure Calls) و آنرا برای انتقال انواع و اقسام دادهها بین سرور و کلاینت و یا فراخوانی متدی در سمت کلاینت یا سرور، بسیار مناسب میسازد. برای مثال اگر قرار باشد با persistent connection به صورت مستقیم کار کنیم، نیاز است تا بسیاری از مسایل serialization و deserialization اطلاعات را خودمان پیاده سازی و اعمال نمائیم.
قرار دادهای پیش فرض Hubs
- متدهای public کلاسهای Hubs از طریق دنیای خارج قابل فراخوانی هستند.
- ارسال اطلاعات به کلاینتها از طریق فراخوانی متدهای سمت کلاینت انجام خواهد شد. (نحوه تعریف این متدها در سمت سرور بر اساس قابلیتهای dynamic اضافه شده به دات نت 4 است که در ادامه در مورد آن بیشتر بحث خواهد شد)
مراحل اولیه نوشتن یک Hub
الف) یک کلاس Hub را تهیه کنید. این کلاس، از کلاس پایه Hub تعریف شده در فضای نام Microsoft.AspNet.SignalR باید مشتق شود. همچنین این کلاس میتواند توسط ویژگی خاصی به نام HubName نیز مزین گردد تا در حین برپایی اولیه سرویس، از طریق زیرساختهای SignalR به نامی دیگر (یک alias یا نام مستعار خاص) قابل شناسایی باشد. متدهای یک هاب میتوانند نوعهای ساده یا پیچیدهای را بازگشت دهند و همه چیز در اینجا نهایتا به فرمت JSON رد و بدل خواهد شد (فرمت پیش فرض که در پشت صحنه از کتابخانه معروف JSON.NET استفاده میکند؛ این کتابخانه سورس باز به دلیل کیفیت بالای آن، از زمان ارائه MVC4 به عنوان جزئی از مجموعه کارهای مایکروسافت قرار گرفته است).
ب) مسیریابی و Routing را تعریف و اصلاح نمائید.
و ... از نتیجه استفاده کنید.
تهیه اولین برنامه با SignalR
ابتدا یک پروژه خالی ASP.NET را آغاز کنید (مهم نیست MVC باشد یا WebForms). برای سادگی بیشتر، در اینجا یک ASP.NET Empty Web application درنظر گرفته شده است. در ادامه قصد داریم یک برنامه Chat را تهیه کنیم؛ از این جهت که توسط یک برنامه Chat بسیاری از مفاهیم مرتبط با SignalR را میتوان در عمل توضیح داد.
اگر از VS 2012 استفاده میکنید، گزینه SignalR Hub class جزئی از آیتمهای جدید قابل افزودن به پروژه است (منوی پروژه، گزینه new item آن) و پس از انتخاب این قالب خاص، تمامی ارجاعات لازم نیز به صورت خودکار به پروژه جاری اضافه خواهند شد.
و اگر از VS 2010 استفاده میکنید، نیاز است از طریق NuGet ارجاعات لازم را به پروژه خود اضافه نمائید:
PM> Install-Package Microsoft.AspNet.SignalR
using Microsoft.AspNet.SignalR; using Microsoft.AspNet.SignalR.Hubs; namespace SignalR02 { [HubName("chat")] public class ChatHub : Hub { public void SendMessage(string message) { Clients.All.hello(message); } } }
کلاس پایه Hub یک سری متد و خاصیت را در اختیار کلاسهای مشتق شده از آن قرار میدهد. سادهترین راه برای آشنایی با این متدها و خواص مهیا، کلیک راست بر روی نام کلاس پایه Hub و انتخاب گزینه Go to definition است.
برای نمونه در کلاس ChatHub فوق، از خاصیت Clients برای دسترسی به تمامی آنها و سپس فراخوانی متد dynamic ایی به نام hello که هنوز وجود خارجی ندارد، استفاده شده است.
اهمیتی ندارد که این کلاس در اسمبلی اصلی برنامه وب قرار گیرد یا مثلا در یک class library به نام Services. همینقدر که از کلاس Hub مشتق شود به صورت خودکار در ابتدای برنامه اسکن گردیده و یافت خواهد شد.
مرحله بعد، افزودن فایل global.asax به برنامه است. زیرا برای کار با SignalR نیاز است تنظیمات Routing و مسیریابی خاص آنرا اضافه نمائیم. پس از افرودن فایل global.asax، به فایل Global.asax.cs مراجعه کرده و در متد Application_Start آن تغییرات ذیل را اعمال نمائید:
using System; using System.Web; using System.Web.Routing; namespace SignalR02 { public class Global : HttpApplication { protected void Application_Start(object sender, EventArgs e) { // Register the default hubs route: ~/signalr RouteTable.Routes.MapHubs(); } } }
یک نکته مهم
اگر از ASP.NET MVC استفاده میکنید، این تنظیم مسیریابی باید پیش از تعاریف پیش فرض موجود قرار گیرد. در غیراینصورت مسیریابیهای SignalR کار نخواهند کرد.
اکنون برای آزمایش برنامه، برنامه را اجرا کرده و مسیر ذیل را فراخوانی کنید:
http://localhost/signalr/hubs
proxies.chat = this.createHubProxy('chat');
تا اینجا ما موفق شدیم اولین Hub خود را تشکیل دهیم.
بررسی پروتکل Hub
اکنون که اولین Hub خود را ایجاد کردهایم، بد نیست اندکی با زیر ساخت آن نیز آشنا شویم.
مطابق مسیریابی تعریف شده در Application_Start، مسیر ابتدایی دسترسی به SignalR با افزودن اسلش SignalR به انتهای مسیر ریشه سایت بدست میآید و اگر به این آدرس یک اسلش hubs را نیز اضافه کنیم، فایل js metadata مرتبط را نیز میتوان دریافت و مشاهده کرد.
زمانیکه یک کلاینت قصد اتصال به یک Hub را دارد، دو مرحله رخ خواهد داد:
الف) negotiate: در این حالت امکانات قابل پشتیبانی از طرف سرور مورد پرسش قرار میگیرند و سپس بهترین حالت انتقال، انتخاب میگردد. این انتخابها به ترتیب از چپ به راست خواهند بود:
Web socket -> SSE -> Forever frame -> long polling
به این معنا که اگر برای مثال امکانات Web sockets مهیا بود، در همینجا کار انتخاب نحوه انتقال اطلاعات خاتمه یافته و Web sockets انتخاب میشود.
تمام این مراحل نیز خودکار است و نیازی نیست تا برای تنظیمات آن کار خاصی صورت گیرد. البته در سمت کلاینت، امکان انتخاب یکی از موارد یاد شده به صورت صریح نیز وجود دارد.
ب) connect: اتصالی ماندگار برقرار میگردد.
در پروتکل Hub تمام اطلاعات JSON encoded هستند و یک سری مخففهایی را در این بین نیز ممکن است مشاهده نمائید که معنای آنها به شرح زیر است:
C: cursor M: Messages H: Hub name M: Method name A: Method args T: Time out D: Disconnect
روشهای مختلف ارسال اطلاعات به کلاینتها
به چندین روش میتوان اطلاعاتی را به کلاینتها ارسال کرد:
1) استفاده از خاصیت Clients موجود در کلاس Hub
2) استفاده از خواص و متدهای dynamic
در این حالت اطلاعات متد dynamic و پارامترهای آن به صورت JSON encoded به کلاینت ارسال میشوند (به همین جهت اهمیتی ندارند که در سرور وجود خارجی دارند یا خیر و به صورت dynamic تعریف شدهاند).
using Microsoft.AspNet.SignalR; using Microsoft.AspNet.SignalR.Hubs; namespace SignalR02 { [HubName("chat")] public class ChatHub : Hub { public void SendMessage(string message) { var msg = string.Format("{0}:{1}", Context.ConnectionId, message); Clients.All.hello(msg); } } }
حالت Clients.All به معنای ارسال پیامی به تمام کلاینتهای متصل به هاب ما هستند.
3) روشهای دیگر، استفاده از خاصیت dynamic دیگری به نام Caller است که میتوان بر روی آن متد دلخواهی را تعریف و فراخوانی کرد.
//این دو عبارت هر دو یکی هستند Clients.Caller.hello(msg); Clients.Client(Context.ConnectionId).hello(msg);
در اینجا پیامی صرفا به فراخوان جاری سرویس ارسال میگردد.
4) استفاده از خاصیت dynamic ایی به نام Clients.Others
Clients.Others.hello(msg);
5) استفاده از متد Clients.AllExcept
این متد میتواند آرایهای از ConnectionIdهایی را بپذیرد که قرار نیست پیام ارسالی ما را دریافت کنند.
6) ارسال اطلاعات به گروهها
تعداد مشخصی از ConnectionIdها یک گروه را تشکیل میدهند؛ مثلا اعضای یک chat room.
public void JoinRoom(string room) { this.Groups.Add(Context.ConnectionId, room); } public void SendMessageToRoom(string room, string msg) { this.Clients.Group(room).hello(msg); }
خاصیت Group در کلاس پایه Hub تعریف شده است.
نکته مهمی را که در اینجا باید درنظر داشت این است که اطلاعات گروهها به صورت دائمی در سرور ذخیره نمیشوند. برای مثال اگر سرور ری استارت شود، این اطلاعات از دست خواهند رفت.
آشنایی با مراحل طول عمر یک Hub
اگر به تعاریف کلاس پایه Hub دقت کنیم:
public abstract class Hub : IHub, IDisposable { protected Hub(); public HubConnectionContext Clients { get; set; } public HubCallerContext Context { get; set; } public IGroupManager Groups { get; set; } public void Dispose(); protected virtual void Dispose(bool disposing); public virtual Task OnConnected(); public virtual Task OnDisconnected(); public virtual Task OnReconnected(); }
تعدادی از این متدها را میتوان جهت مقاصد logging برنامه مورد استفاده قرار داد و یا در متد OnDisconnected اگر اطلاعاتی را در بانک اطلاعاتی ذخیره کردهایم، بر این اساس میتوان وضعیت نهایی را تغییر داد.
ارسال اطلاعات از یک Hub به Hub دیگر در برنامه
فرض کنید یک Hub دوم را به نام MinitorHub به برنامه اضافه کردهاید. اکنون قصد داریم از داخل ChatHub فوق، اطلاعاتی را به آن ارسال کنیم. روش کار به نحو زیر است:
public override System.Threading.Tasks.Task OnDisconnected() { sendMonitorData("OnDisconnected", Context.ConnectionId); return base.OnDisconnected(); } private void sendMonitorData(string type, string connection) { var ctx = GlobalHost.ConnectionManager.GetHubContext<MonitorHub>(); ctx.Clients.All.newEvenet(type, connection); }
مورد استفاده دیگر این روش، ارسال اطلاعات به کلاینتها از طریق کدهای یک برنامه تحت وب است (که در همان پروژه هاب واقع شده است). برای مثال در یک اکشن متد یا یک روال رویدادگردان کلیک نیز میتوان از GlobalHost.ConnectionManager استفاده کرد.