In an attempt to correct many perceived deficiencies in Entity Framework Core, Microsoft is introducing 40 breaking changes to EF Core 3. You can see the entire breaking changes list on Microsoft Docs, but here are some of the highlights.
Today we are releasing the first preview of Visual Studio 2017 version 15.5. You can either download it from here, or if you already have Preview installed, you’ll receive a notification that the update is available.
در اینجا توسط کامپوننت sidenav، کار نمایش لیست تماسها صورت میگیرد و نمایش این کامپوننت واکنشگرا است. به این معنا که در اندازههای صفحات نمایشی بزرگ، نمایان است و در صفحات نمایشی کوچک، مخفی خواهد شد. در بالای صفحه یک Toolbar قرار دارد که همیشه نمایان است و از آن برای نمایش گزینههای منوی برنامه استفاده میکنیم. همچنین ناحیهی main content را هم مشاهده میکنید که با انتخاب هر شخص از لیست تماسها، جزئیات او در این قسمت نمایش داده خواهد شد.
ایجاد ماژول مدیریت تماسها
در قسمت اول، برنامه را به همراه تنظیمات ابتدایی مسیریابی آن ایجاد کردیم که نتیجهی آن تولید فایل src\app\app-routing.module.ts میباشد:
ng new MaterialAngularClient --routing
ng g m ContactManager -m app.module --routing
این دستور ماژول جدید contact-manager را به همراه تنظیمات ابتدایی مسیریابی و همچنین به روز رسانی app.module، برای درج آن، ایجاد میکند. البته در این حالت نیاز است به app.module.ts مراجعه کرد و محل درج آنرا تغییر داد:
import { ContactManagerModule } from "./contact-manager/contact-manager.module"; @NgModule({ imports: [ BrowserModule, BrowserAnimationsModule, CoreModule, SharedModule.forRoot(), ContactManagerModule, AppRoutingModule ], }) export class AppModule { }
سپس دستور زیر را اجرا میکنیم تا کامپوننت contact-manager-app در ماژول contact-manager ایجاد شود:
ng g c contact-manager/ContactManagerApp --no-spec
CREATE src/app/contact-manager/contact-manager-app/contact-manager-app.component.html (38 bytes) CREATE src/app/contact-manager/contact-manager-app/contact-manager-app.component.ts (319 bytes) CREATE src/app/contact-manager/contact-manager-app/contact-manager-app.component.css (0 bytes) UPDATE src/app/contact-manager/contact-manager.module.ts (436 bytes)
این کامپوننت به عنوان میزبان سایر کامپوننتهایی که در مقدمهی بحث عنوان شدند، عمل میکند. این کامپوننتها را به صورت زیر در پوشهی components ایجاد میکنیم:
ng g c contact-manager/components/toolbar --no-spec ng g c contact-manager/components/main-content --no-spec ng g c contact-manager/components/sidenav --no-spec
تنظیمات مسیریابی برنامه
در ادامه به src\app\app-routing.module.ts مراجعه کرده و این ماژول جدید را به صورت lazy load معرفی میکنیم:
import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; const routes: Routes = [ { path: "contactmanager", loadChildren: "./contact-manager/contact-manager.module#ContactManagerModule" }, { path: "", redirectTo: "contactmanager", pathMatch: "full" }, { path: "**", redirectTo: "contactmanager" } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
سپس تنظیمات مسیریابی ماژول مدیریت تماسها را در فایل src\app\contact-manager\contact-manager-routing.module.ts به صورت زیر تغییر میدهیم:
import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; import { MainContentComponent } from "./components/main-content/main-content.component"; import { ContactManagerAppComponent } from "./contact-manager-app/contact-manager-app.component"; const routes: Routes = [ { path: "", component: ContactManagerAppComponent, children: [ { path: "", component: MainContentComponent } ] }, { path: "**", redirectTo: "" } ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule] }) export class ContactManagerRoutingModule { }
کامپوننت ContactManagerApp که کار هاست سایر کامپوننتهای این ماژول را بر عهده دارد، دارای router-outlet خاص خود خواهد بود. به همین جهت برای آن children تعریف شدهاست که مسیر پیشفرض آن، بارگذاری کامپوننت MainContent است.
بنابراین نیاز است به فایل contact-manager-app\contact-manager-app.component.html مراجعه و ابتدا منوی کنار صفحه را به آن افزود:
<app-sidenav></app-sidenav>
سپس در قالب sidenav\sidenav.component.html، کار تعریف toolbar و همچنین router-outlet را انجام میدهیم:
<app-toolbar></app-toolbar> <router-outlet></router-outlet>
معرفی Angular Material به ماژول جدید مدیریت تماسها
در قسمت اول، یک فایل material.module.ts را ایجاد کردیم که به همراه تعریف تمامی کامپوننتهای Angular Material بود. سپس آنرا به shared.module.ts افزودیم که حاوی تعریف ماژول فرمها و همچنین Flex Layout نیز هست. به همین جهت برای معرفی اینها به این ماژول جدید تنها کافی است در فایل src\app\contact-manager\contact-manager.module.ts در قسمت imports، کار معرفی SharedModule صورت گیرد:
import { SharedModule } from "../shared/shared.module"; @NgModule({ imports: [ CommonModule, SharedModule, ContactManagerRoutingModule ] }) export class ContactManagerModule { }
پس از اجرای برنامه مشاهده میکنید که ابتدا ماژول مدیریت تماسها بارگذاری شدهاست و سپس contact-manager-app عمل و sidenav را بارگذاری کرده و آن نیز سبب نمایش کامپوننت toolbar و سپس main-content شدهاست.
تنظیم طرحبندی برنامه توسط کامپوننتهای Angular Material و همچنین Flex Layout
پس از این تنظیمات اکنون نوبت به تنظیم طرحبندی برنامهاست و آنرا با قراردادن کامپوننت Sidenav بستهی Angular Material شروع میکنیم:
<mat-sidenav-container *ngIf="shouldRun"> <mat-sidenav mode="side" opened> Sidenav content </mat-sidenav> Primary content </mat-sidenav-container>
- Over: قسمت Sidenav content بر روی Primary content قرار میگیرد.
- Push: قسمت Sidenav content قسمت Primary content را از سر راه خود بر میدارد.
- Side: قسمت Sidenav content در کنار Primary content قرار میگیرد.
در اینجا از حالت Side، در صفحات نمایشی بزرگ (اولین تصویر این قسمت) و از حالت Over، در صفحات نمایشی موبایل (مانند تصویر زیر) استفاده خواهیم کرد.
در ابتدا کدهای کامل هر سه کامپوننت و سپس توضیحات آنها را مشاهده خواهید کرد:
تنظیم margin در CSS اصلی برنامه
زمانیکه sidenav و toolbar را بر روی صفحه قرار میدهیم، فاصلهای بین آنها و لبههای صفحه مشاهده میشود. برای اینکه این فاصله را به صفر برسانیم، به فایل src\styles.css مراجعه کرده و margin بدنهی صفحه را به صفر تنظیم میکنیم:
@import "~@angular/material/prebuilt-themes/indigo-pink.css"; body { margin: 0; }
طراحی قالب main content
<mat-card> <h1>Main content</h1> </mat-card>
sidenav\sidenav.component.css | sidenav\sidenav.component.html |
.app-sidenav-container { position: fixed; } .app-sidenav { width: 240px; } .wrapper { margin: 50px; } | <mat-sidenav-container fxLayout="row" fxFill> <mat-sidenav #sidenav fxFlex="1 1 100%" [opened]="!isScreenSmall" [mode]="isScreenSmall ? 'over' : 'side'"> <mat-toolbar color="primary"> Contacts </mat-toolbar> <mat-list> <mat-list-item>Item 1</mat-list-item> <mat-list-item>Item 2</mat-list-item> <mat-list-item>Item 3</mat-list-item> </mat-list> </mat-sidenav> <mat-sidenav-content fxLayout="column" fxFlex="1 1 100%" fxFill> <app-toolbar (toggleSidenav)="sidenav.toggle()"></app-toolbar> <div> <router-outlet></router-outlet> </div> </mat-sidenav-content> </mat-sidenav-container> |
- نمای کلی صفحه در این قسمت طراحی شدهاست. sidenav-container که در برگیرندهی اصلی است، به fxLayout از نوع row تنظیم شدهاست. یعنی mat-sidenav و mat-sidenav-content دو ستون آنرا از چپ به راست تشکیل میدهند و درون یک ردیف، سیلان خواهند یافت. همچنین میخواهیم این container کل صفحه را پر کند، به همین جهت از fxFill استفاده شدهاست. این fxFill اعمال شدهی به container، زمانی عمل خواهد کرد که position آن در css، به fixed تنظیم شود که اینکار در css این قالب و در کلاس app-sidenav-container آن انجام شدهاست.
- سپس toolbar و همچنین router-outlet که main content را نمایش میدهند، داخل sidenav-content قرار گرفتهاند و هر دو با هم، ستون دوم این طرحبندی را تشکیل میدهند. به همین جهت fxLayout آن به column تنظیم شدهاست (ستون اول آن، لیست تماسها است و ستون دوم آن، از دو ردیف toolbar و main-content تشکیل میشود).
- اگر دقت کنید یک template reference variable به نام sidenav# به container اعمال شدهاست. از آن، جهت باز و بسته کردن sidenav استفاده میشود:
<app-toolbar (toggleSidenav)="sidenav.toggle()"></app-toolbar>
- mat-sidenav از دو قسمت تشکیل شدهاست. بالای آن توسط mat-toolbar صرفا کلمهی Contacts نمایش داده میشود و سپس ذیل آن، لیست فرضی تماسها توسط کامپوننت mat-list قرار گرفتهاند (تا فعلا خالی نباشد. در قسمتهای بعدی آنرا پویا خواهیم کرد). رنگ تولبار آنرا ("color="primary) نیز به primary تنظیم کردهایم تا خاکستری پیشفرض آن نباشد.
- کار کلاس mat-elevation-z10 این است که بین sidenav و main-content یک سایهی سه بعدی را ایجاد کند که آنرا در تصاویر مشاهده میکنید. عددی که پس از z قرار میگیرد، میزان عمق سایه را مشخص میکند.
- این قسمت از sidenav به همراه دو خاصیت opened و همچنین mode است که به مقدار isScreenSmall عکس العمل نشان میدهند:
<mat-sidenav [opened]="!isScreenSmall" [mode]="isScreenSmall ? 'over' : 'side'">
محتویات فایل sidenav\sidenav.component.ts:
import { Component, OnDestroy, OnInit } from "@angular/core"; import { MediaChange, ObservableMedia } from "@angular/flex-layout"; import { Subscription } from "rxjs"; @Component({ selector: "app-sidenav", templateUrl: "./sidenav.component.html", styleUrls: ["./sidenav.component.css"] }) export class SidenavComponent implements OnInit, OnDestroy { isScreenSmall = false; watcher: Subscription; constructor(private media: ObservableMedia) { this.watcher = media.subscribe((change: MediaChange) => { this.isScreenSmall = change.mqAlias === "xs"; }); } ngOnInit() { } ngOnDestroy() { this.watcher.unsubscribe(); } }
تاثیر خاصیت isScreenSmall بر روی دو خاصیت opened و mode کامپوننت sidenav را در دو تصویر زیر مشاهده میکنید. اگر اندازهی صفحه کوچک شود، ابتدا sidenav مخفی میشود. اگر کاربر بر روی دکمهی منوی همبرگری کلیک کند، سبب نمایش مجدد sidenav، اینبار با حالت over و بر روی محتوای زیرین آن خواهد شد:
طراحی نوار ابزار واکنشگرا
کدهای قالب و css تولبار (ستون دوم طرحبندی کلی صفحه) را در ادامه مشاهده میکنید:
toolbar\toolbar.component.css | toolbar\toolbar.component.html |
.sidenav-toggle { padding: 0; margin: 8px; min-width:56px; } | <mat-toolbar color="primary"> <button mat-button fxHide fxHide.xs="false" class="sidenav-toggle" (click)="toggleSidenav.emit()"> <mat-icon>menu</mat-icon> </button> <span>Contact Manager</span> </mat-toolbar> |
با توجه به استفادهی از fxHide، یعنی دکمهی نمایش منوی همبرگری در تمام حالات مخفی خواهد بود. برای لغو آن و نمایش آن در حالت موبایل، از حالت واکنشگرای آن یعنی fxHide.xs استفاده میکنیم (قسمت «کار با API واکنشگرای Angular Flex layout» در مطلب قبلی این سری). به این ترتیب زمانیکه کاربر اندازهی صفحه را کوچک میکند و یا اندازهی واقعی صفحهی نمایش او کوچک است، این دکمه نمایان خواهد شد.
همچنین در sidenav یک چنین تعریفی را داریم:
<app-toolbar (toggleSidenav)="sidenav.toggle()"></app-toolbar>
محتویات فایل toolbar\toolbar.component.ts:
import { Component, EventEmitter, OnInit, Output } from "@angular/core"; @Component({ selector: "app-toolbar", templateUrl: "./toolbar.component.html", styleUrls: ["./toolbar.component.css"] }) export class ToolbarComponent implements OnInit { @Output() toggleSidenav = new EventEmitter<void>(); constructor() { } ngOnInit() { } }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MaterialAngularClient-02.zip
برای اجرای آن نیز ابتدا فایل restore.bat و سپس فایل ng-serve.bat را اجرا کنید. پس از اجرای برنامه، یکبار آنرا در حالت تمام صفحه و بار دیگر با کوچکتر کردن اندازهی مرورگر آزمایش کنید. در حالتیکه به اندازهی موبایل رسیدید، بر روی دکمهی همبرگری نمایان شده کلیک کنید تا عکس العمل آن و نمایش مجدد sidenav را در حالت over، مشاهده نمائید.
آشنایی با امکانات بستهی angular/platform-browser@
در ماژول angular/platform-browser@، دو سرویس Meta و Title، امکان تغییر پویای متاتگهای صفحهی جاری را مهیا میکنند. برای نمونه فرض کنید قصد دارید یک چنین متاتگهایی را به صفحهی جاری اضافه کنید:
<head> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="icon" type="image/x-icon" href="favicon.ico"> <title>newTitle ...</title> <base href="/"> <meta name="description" content="Angular meta service"> <meta name="author" content="DNT"> <meta name="keywords" content="Angular, Meta Service"> <meta name="twitter:card" content="summary"> <meta name="twitter:site" content="@my_site"> <meta name="twitter:title" content="Front-end Web Development"> <meta name="twitter:description" content="Learn frontend web development..."> <meta name="twitter:image" content="https://site/images/image.png"> <meta name="author" content="Other Author"> </head>
import { Component, OnInit } from "@angular/core"; import { Meta, Title } from "@angular/platform-browser"; @Component({ selector: "app-seo-tests", templateUrl: "./seo-tests.component.html", styleUrls: ["./seo-tests.component.css"] }) export class SeoTestsComponent implements OnInit { constructor(private metaService: Meta, private titleService: Title) { }
افزودن یک یا چند متاتگ
متد addTag سرویس Meta، کار افزودن پویای یک متا تگ جدید را به همراه ویژگیهای name و content آن، انجام میدهد. در ذیل چندین مثال از آنرا مشاهده میکنید. در اینجا یا میتوان از متد addTag استفاده کرد که تنها یک متاتگ را به صفحه اضافه میکند و یا از متد addTags کمک گرفت که میتواند آرایهای از متاتگها را به صورت پویا به صفحهی جاری اضافه کند:
// addTag & addTags this.metaService.addTag({ name: "description", content: "How to optimize your Angular App for search engine and other crawlers." }); this.metaService.addTag({ name: "author", content: "DNT" }); this.metaService.addTag({ name: "keywords", content: "Angular, Meta Service" }); // Or this.metaService.addTags([ { name: "description", content: "How to optimize your Angular App for search engine and other crawlers." }, { name: "author", content: "DNT" }, { name: "keywords", content: "Angular, Meta Service" } ], false); // --> forceCreation = false this.metaService.addTag({ name: "twitter:card", content: "summary_large_image" }); this.metaService.addTag({ name: "twitter:site", content: "@my_site" }); this.metaService.addTag({ name: "twitter:title", content: "Front-end Web Development" }); this.metaService.addTag({ name: "twitter:description", content: "Learn frontend web development..." }); this.metaService.addTag({ name: "twitter:image", content: "https://site/images/image.png" }); // Or this.metaService.addTags([ { name: "twitter:card", content: "summary_large_image" }, { name: "twitter:site", content: "@my_site" }, ], false); // --> forceCreation = false
دریافت محتوای متاتگهای موجود
با استفاده از متد getTag میتوان یک متاتگ مشخص را به صورت HTMLMetaElement دریافت کرد:
// getTag & getTags const viewport = this.metaService.getTag("name=viewport"); if (viewport) { console.log(viewport.content); // width=device-width, initial-scale=1 } const author = this.metaService.getTag("name=author"); if (author) { console.log(author.content); // DNT } this.metaService.addTag({ name: "author", content: "DNT" }); this.metaService.addTag({ name: "author", content: "Other Author" }, true); const authors = this.metaService.getTags("name=author"); console.log(authors[0]); // <meta name="author" content="DNT"> console.log(authors[1]); // <meta name="author" content="Other Author">
به روز رسانی متاتگهای موجود
میتوان از متد updateTag برای تغییر محتوای متاتگی موجود، استفاده کرد:
// updateTag this.metaService.addTag({ name: "twitter:card", content: "summary_large_image" }); this.metaService.updateTag({ name: "twitter:card", content: "summary" }, `name='twitter:card'`); this.metaService.updateTag({ name: "description", content: "Angular meta service" });
حذف تگهای موجود
در اینجا میتوان از دو متد removeTag که یک attribute-selector را دریافت میکند و یا removeTagElement که یک HTMLMetaElement را توسط متد getTag دریافت میکند، برای حذف کامل این تگها استفاده کرد:
// removeTag & removeTagElement this.metaService.removeTag("charset"); // Or const chartsetTag = this.metaService.getTag("charset"); if (chartsetTag) { this.metaService.removeTagElement(chartsetTag); }
تنظیم عنوان صفحهی جاری
سرویس توکار دیگری به نام Title امکان تغییر عنوان صفحهی جاری را به صورت پویا میسر میکند:
// Setting the browser page Title in an Angular app const currentTitle = this.titleService.getTitle(); console.log(currentTitle); this.titleService.setTitle("newTitle ...");
طراحی سرویسی برای افزودن پویای متاتگها به صفحات مختلف سایت
میتوان شبیه به مطلب «نمایش Breadcrumbs در برنامههای Angular» به قسمت data مسیریابی، اطلاعات عنوان صفحه و همچنین metaTags آنرا اضافه کرد:
const routes: Routes = [ { path: "seo", component: SeoTestsComponent, data: { title: "Page Title", metaTags: { description: "Page Description or some content here", keywords: "some, keywords, here, separated, by, a comma" } } } ];
به همین جهت سرویس SEO را در مسیر src\app\core\seo-service.ts به صورت ذیل ایجاد میکنیم:
import { Injectable } from "@angular/core"; import { Title, Meta } from "@angular/platform-browser"; import { Router, NavigationEnd, ActivatedRouteSnapshot } from "@angular/router"; @Injectable() export class SeoService { constructor(private titleService: Title, private metaService: Meta, private router: Router) { } enableSeo() { this.router.events .filter(event => event instanceof NavigationEnd) .distinctUntilChanged() .subscribe(() => { this.addMetaData(this.router.routerState.snapshot.root); }); } private addMetaData(root: ActivatedRouteSnapshot): void { if (root.children && root.children.length) { this.addMetaData(root.children[0]); } else if (root.data) { this.setTitle(root.data); this.setMetaTags(root.data); } } private setMetaTags(routeData: { [name: string]: any; }) { const routeDataMetaTagsKey = "metaTags"; const metaTags = routeData[routeDataMetaTagsKey]; if (!metaTags) { return; } for (const tag in metaTags) { if (metaTags.hasOwnProperty(tag)) { const newTag = { name: tag, content: metaTags[tag] }; console.log("new tag", newTag); this.metaService.addTag(newTag); } } } private setTitle(routeData: { [name: string]: any; }) { const routeDataTitleKey = "title"; const title = routeData[routeDataTitleKey]; if (title) { console.log("new title", title); this.titleService.setTitle(title); } } }
در اینجا در ابتدای کار مشترک رخداد NavigationEnd سیستم مسیریابی خواهیم شد:
this.router.events .filter(event => event instanceof NavigationEnd) .distinctUntilChanged() .subscribe(() => { this.addMetaData(this.router.routerState.snapshot.root); });
سپس در این متد خاصیت data مسیرنهایی را خوانده و کلیدهای title و metaTags آنرا استخراج میکنیم و سپس توسط متدهای this.titleService.setTitle و this.metaService.addTag، این عنوان و متاتگهای جدید را به صورت پویا به صفحه اضافه خواهیم کرد.
پس از تعریف این سرویس، برای معرفی آن به برنامه، ابتدا آنرا به قسمت providers مربوط به CoreModule اضافه میکنیم:
import { SeoService } from "./seo-service"; @NgModule({ providers: [ SeoService ] }) export class CoreModule {}
import { SeoService } from "./core/seo-service"; export class AppComponent { constructor(private seoService: SeoService) { this.seoService.enableSeo(); } }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
یک نکتهی تکمیلی: روش طراحی binding دو طرفه در Blazor SSR
در نکتهی قبل عنوان شد که مقدمات طراحی binding دو طرفه، داشتن حداقل سه خاصیت زیر در یک کامپوننت سفارشی است:
[Parameter] public T? Value { set; get; } [Parameter] public EventCallback<T?> ValueChanged { get; set; } [Parameter] public Expression<Func<T?>> ValueExpression { get; set; } = default!;
اگر این خواص را به کامپوننتهای توکار خود Blazor متصل کنیم (مانند InputBox آن و مابقی آنها)، نیازی به کدنویسی بیشتری ندارند و کار میکنند. اما اگر قرار است از یک input سادهی Html ای استفاده کنیم، نیاز است ValueChanged آنرا اینبار در متد OnInitialized فراخوانی کنیم؛ چون در زمان post-back به سرور است که مقدار آن در اختیار مصرف کنندهی کامپوننت قرار میگیرد. این مورد، مهمترین تفاوت نحوهی طراحی binding دوطرفه در Blazor SSR با مابقی حالات و نگارشهای Blazor است.
بررسی وقوع post-back به سرور به دو روش زیر میسر است:
الف) بررسی کنیم که آیا HttpPost ای رخدادهاست؟ سپس در همین لحظه، متد ValueChanged.InvokeAsync را فراخوانی کنیم:
[CascadingParameter] internal HttpContext HttpContext { set; get; } = null!; protected override void OnInitialized() { base.OnInitialized(); if (HttpContext.IsPostRequest()) { SetValueCallback(Value); } } private void SetValueCallback(string value) { if (!ValueChanged.HasDelegate) { return; } _ = ValueChanged.InvokeAsync(value); }
در این مثال نحوهی فعالسازی ارسال اطلاعات از یک کامپوننت سفارشی را به مصرف کنندهی آن ملاحظه میکنید. اینکار در قسمت OnInitialized و فقط در صورت ارسال اطلاعات به سمت سرور، فعال خواهد شد.
ب) میتوان در قسمت OnInitialized، بررسی کرد که آیا درخواست جاری به همراه اطلاعات یک فرم ارسال شدهی به سمت سرور است یا خیر؟ روش کار به صورت زیر است:
protected override void OnInitialized() { base.OnInitialized(); if (HttpContext.Request.HasFormContentType && HttpContext.Request.Form.TryGetValue(ValueField.HtmlFieldName, out var data)) { SetValueCallback(data.ToString()); } }
در اینجا از ValueField.HtmlFieldName که در نکتهی قبلی معرفی BlazorHtmlField به آن اشاره شد، جهت یافتن نام واقعی فیلد ورودی، استفاده شدهاست.