TDD در دات نت، قسمت اول
dependency injection به زبان ساده
طراحی Responsive در وب
- Transient
- Scoped
- Singleton
- Constructor Injection
- Property Injection
- Method Injection
- Action & Handler Injection
Microsoft.AspNetCore.Mvc
public class AccountController : Controller { public IActionResult ConfirmEmail([FromServices] IEmailSender emailSender) { // emailSender.SendAsync(...); } }
public IActionResult About([FromServices] IDateTime dateTime) { ViewData["Message"] = $"Current server time: {dateTime.Now}"; return View(); }
[HttpGet] public ProductModel GetProduct([FromServices] IProductModelRequestService productModelRequest) { return productModelRequest.Value; }
Command ما که نقش ایجاد یک مشتری را داشت ( CreateCustomerCommand )، هیچ Validation ای برای اعتبار سنجی مقادیر ورودی از سمت کاربر را ندارد و کاربر با هر مقادیری میتواند این Command را فراخوانی کند. در این قسمت با استفاده از کتابخانه Fluent Validation امکان اعتبار سنجی را به Commandهای خود اضافه میکنیم.
در ابتدا با استفاده از دستور زیر ، این کتابخانه را داخل پروژه خود نصب میکنیم:
Install-Package FluentValidation.AspNetCore
بعد از افزودن این کتابخانه، باید آن را داخل DI Container خود Register کنیم:
services.AddMvc() .AddFluentValidation(cfg => cfg.RegisterValidatorsFromAssemblyContaining<Startup>());
کلاس جدیدی با نام CreateCustomerCommandValidator ایجاد میکنیم و از کلاس AbstractValidator مربوط به Fluent Validation ارث بری میکنیم تا منطق اعتبارسنجی برای CreateCustomerCommand را داخل آن تعریف نماییم :
public class CreateCustomerCommandValidator : AbstractValidator<CreateCustomerCommand> { public CreateCustomerCommandValidator() { RuleFor(customer => customer.FirstName).NotEmpty(); RuleFor(customer => customer.LastName).NotEmpty(); } }
اگر برنامه را اجرا و CreateCustomerCommand را با مقادیر خالی فراخوانی کنیم، خواهید دید که بلافاصله با چنین خطایی مواجه خواهید شد که نشان میدهد Fluent Validation بدرستی وظیفه اعتبارسنجی ورودیها را انجام داده است:
Error: Bad Request { "LastName": [ "'Last Name' must not be empty." ], "FirstName": [ "'First Name' must not be empty." ] }
* نکته : تمامی اعتبارسنجیهای سطحی ( Superficial Validation ) مانند خالی نبودن مقادیر، اعتبارسنجی تاریخها، اعتبارسنجی ایمیل و ... باید قبل از Handle شدن Commandها صورت گیرد و در صورت ناموفق بودن اعتبارسنجی، نباید وارد متد Handle در Commandها شویم. ( Fail Fast Principle )
Events
برای حل این مشکل میتوانیم از Eventها استفاده کنیم. Eventها خبری را به Subscriber های خود میدهند. در فریمورک MediatR، ارسال و Handle کردن Eventها، با دو interface صورت میگیرد: INotification و INotificationHandler
بر خلاف Commandها که فقط یک Handler میتوانند داشته باشند، Event ها میتوانند چندین Handler داشته باشند. مزیت داشتن چند Subscriber برای Eventها این است که شما علاوه بر اینکه میتوانید Subscriber ای داشته باشید که وظیفه ارسال Email برای مشتری را بر عهده داشته باشد، Subscriber دیگری داشته باشید که اطلاعات مشتری جدید را Log کند.
ابتدا کلاس CustomerCreatedEvent را ایجاد و از INotification ارث بری میکنیم. این کلاس منتشر کننده یک اتفاق است که آن را Producer مینامند :
public class CustomerCreatedEvent : INotification { public CustomerCreatedEvent(string firstName, string lastName, DateTime registrationDate) { FirstName = firstName; LastName = lastName; RegistrationDate = registrationDate; } public string FirstName { get; } public string LastName { get; } public DateTime RegistrationDate { get; } }
سپس دو Handler برای این Event مینویسیم. Handler اول وظیفه ارسال ایمیل را بر عهده خواهد داشت :
public class CustomerCreatedEmailSenderHandler : INotificationHandler<CustomerCreatedEvent> { public Task Handle(CustomerCreatedEvent notification, CancellationToken cancellationToken) { // IMessageSender.Send($"Welcome {notification.FirstName} {notification.LastName} !"); return Task.CompletedTask; } }
و Handler دوم، وظیفه Log کردن اطلاعات مشتری ثبت شده را بر عهده خواهد داشت:
public class CustomerCreatedLoggerHandler : INotificationHandler<CustomerCreatedEvent> { readonly ILogger<CustomerCreatedLoggerHandler> _logger; public CustomerCreatedLoggerHandler(ILogger<CustomerCreatedLoggerHandler> logger) { _logger = logger; } public Task Handle(CustomerCreatedEvent notification, CancellationToken cancellationToken) { _logger.LogInformation($"New customer has been created at {notification.RegistrationDate}: {notification.FirstName} {notification.LastName}"); return Task.CompletedTask; } }
در نهایت کافیست داخل CreateCustomerCommandHandler که در قسمت قبل آن را ایجاد کردیم، متد Handle را ویرایش و با استفاده از متد Publish موجود در اینترفیس IMediator، این Event را Raise کنیم :
public class CreateCustomerCommandHandler : IRequestHandler<CreateCustomerCommand, CustomerDto> { readonly ApplicationDbContext _context; readonly IMapper _mapper; readonly IMediator _mediator; public CreateCustomerCommandHandler(ApplicationDbContext context, IMapper mapper, IMediator mediator) { _context = context; _mapper = mapper; _mediator = mediator; } public async Task<CustomerDto> Handle(CreateCustomerCommand createCustomerCommand, CancellationToken cancellationToken) { Customer customer = _mapper.Map<Customer>(createCustomerCommand); await _context.Customers.AddAsync(customer, cancellationToken); await _context.SaveChangesAsync(cancellationToken); // Raising Event ... await _mediator.Publish(new CustomerCreatedEvent(customer.FirstName, customer.LastName, customer.RegistrationDate), cancellationToken); return _mapper.Map<CustomerDto>(customer); } }
برنامه را اجرا و روی دو NotificationHandler خود Breakpoint قرار دهید. اگر api/Customers را برای ایجاد یک مشتری جدید فراخوانی کنید، بعد از ثبت نام موفق مشتری، خواهید دید که هر دو Handler شما Raise میشوند و اطلاعات مشتری را که با LogHandler خود داخل Console لاگ کردیم، خواهیم دید:
info: MediatrTutorial.Features.Customer.Events.CustomerCreated.CustomerCreatedLoggerHandler[0] New customer has been created at 2/1/2019 11:40:48 PM: Moien Tajik
* نکته : در این قسمت از آموزش برای Log کردن اطلاعات از یک Notification استفاده کردیم. اگر تعداد Commandهای ما در برنامه بیشتر شوند، به ازای هر Command مجبور به ایجاد یک Notification و NotificationHandler خواهیم بود که منطق کار آنها بسیار شبیه به یک دیگر است.
در مقاله بعدی با استفاده از Behaviors موجود در MediatR که AOP را پیاده سازی میکند، این موارد تکراری را از بین خواهیم برد.
CQRS
* نکته : Naming Convention مورد استفاده برای Commandها به صورت دستوری است و کار Command در نام آن مشخص است؛ مثال : RegisterUser, SendForgottenPasswordEmail, PlaceOrder
3- این جداسازی باعث تمرکز بیشتر شما بر روی قسمتهای مختلف برنامه میشود؛ بخشهایی که وضعیت سیستم را تغییر میدهند از بخشهایی که صرفا دادههایی را خوانده و نمایش میدهند، بطور کامل جدا شدهاند و بهراحتی قابلیت تغییر هرکدام از این بخشها را خواهید داشت.
Events
Eventها میتوانند چندین Consumer داشته باشند؛ بنابراین میتوانیم یک EventHandler را برای UserRegistered بنویسیم که Email ارسال کند و EventHandler دیگری ایجاد کنیم که Notification ای را برای کاربر بفرستد.
Event Sourcing
مزیت Event Sourcing این است که State برنامه را در زمانهای مختلفی نگه داشتهایم و میتوانیم وضعیت سیستم را در تاریخی مشخص، پیدا کنیم و در صورت بهوجود آمدن مشکلی در سیستم، وضعیت آن را تا قبل از به مشکل خوردن، بررسی کنیم.
بعنوان مثال مبلغ یک حساب بانکی را در نظر بگیرید. یکی از راههای بهروز نگه داشتن این مبلغ بعد از هر تراکنش، در نظر گرفتن یک فیلد برای مبلغ و انجام عمل Update بعد از هر تراکنش بطور مستقیم برروی آن است. در این روش بهدلیل آپدیت کردن مستقیم این فیلد داخل دیتابیس، ما وضعیت قبلی (مبلغ قبلی) را از دست خواهیم داد و برای رسیدن به مبلغ قبلی مجبور به زدن چندین کوئری دیتابیسی و دریافت تراکنشهای قبلی و ... برای رسیدن به وضعیت قبلی سیستم هستیم.
روش دیگری وجود دارد که بجای بهروزرسانی مداوم state جاری، تمام Event هایی که در آن تراکنشی داخل سیستم رخ داده و این تراکنش State برنامه را تحت تاثیر خود قرار دادهاست، داخل یک دیتابیس اضافه نماییم. در این صورت بدلیل داشتن تمام رویدادهای اتفاق افتادهی در برنامه، میتوان وضعیت جاری سیستم را شبیه سازی و متوجه شد.
* در این سری آموزشی از دیتابیس Event Store برای پیاده سازی Event Sourcing استفاده خواهیم کرد.
در مقالهی بعدی، امکانات فریمورک MediatR را بررسی خواهیم کرد.
در نسخههای قبل از Angular CLI 6.0، صرفا امکان Bundle کردن جداگانهی ماژولهایی که در قسمت loadChildren مرتبط با تنظیمات مسیریابی ذکر شده بودند، وجود داشت. بنابراین در برخی از شرایط اگر نیاز به امکان بارگذاری ماژولی به صورت Lazy load بود، باید از سیستم مسیریابی استفاده میشد یا اینکه با یکسری ترفند، CLI و Webpack را مجبور به ساخت فایل chunk جداگانه برای ماژول مورد نظر میکردید. از زمان انتشار Angular CLI 6.0 امکان Lazy loading پویا نیز مهیا میباشد؛ به این ترتیب بدون وابستگی به سیستم مسیریابی، باز هم میتوان از مزایای Lazy loading بهره برد. در این مطلب روش استفاده از این قابلیت و همچنین نحوهی بارگذاری پویای یک کامپوننت مرتبط با یک ماژول Lazy load شده را بررسی خواهیم کرد. برای این منظور در ادامه با ایجاد یک TabLayout با استفاده از Angular Material Tabs با یکی از موارد پر استفادهی قابلیت مذکور آشنا خواهیم شد.
پیش نیازها
- مسیر آموزشی «+AngularJS 2.0»
- مسیر آموزشی «سری آموزشی Angular CLI»
- مسیر آموزشی «مسیریابی در Angular»
- مسیر آموزشی «کتابخانه Angular Material 6x»
کار را با طراحی و پیاده سازی TabService شروع میکنیم. برای این منظور یک سرویس را در فولدر services موجود در کنار CoreModule ایجاد خواهیم کرد؛ به این جهت ابتدا مدلهای زیر را خواهیم داشت:
import { Type, ValueProvider } from '@angular/core'; export interface OpenNewTabModel { label: string; componentType: Type<any>; iconName: string; modulePath?: string; data?: ValueProvider[]; }
import { TabItemComponent } from './tab-item-component'; export interface TabItem { label: string; iconName: string; component: TabItemComponent; }
OpenNewTabModel برای ارسال داده توسط مصرف کننده از این سرویس در نظر گرفته شده است. ولی واسط TabItem دارای خصوصیاتی میباشد که ما برای نمایش یک برگهی جدید نیازمندیم. TabItemComponent نیز دارای خصوصیاتی است که مورد نیاز دایرکتیو« NgComponentOutlet» است.
import { Injector, NgModuleFactory, Type } from '@angular/core'; export interface TabItemComponent { componentType: Type<any>; moduleFactory?: NgModuleFactory<any>; injector: Injector; }
import { BehaviorSubject, Observable } from 'rxjs'; import { Injectable, Injector, NgModuleFactory, NgModuleFactoryLoader } from '@angular/core'; import { OpenNewTabModel } from '../models/open-new-tab-model'; import { TabItem } from '../models/tab-item'; @Injectable({ providedIn: 'root' }) export class TabService { private tabItemSubject: BehaviorSubject<TabItem[]> = new BehaviorSubject< TabItem[] >([]); constructor( private loader: NgModuleFactoryLoader, private injector: Injector ) {} get tabItems$(): Observable<TabItem[]> { return this.tabItemSubject.asObservable(); } open(newTab: OpenNewTabModel) { if (newTab.modulePath) { this.loader .load(newTab.modulePath) .then((moduleFactory: NgModuleFactory<any>) => { this.openInternal(newTab, moduleFactory); }); } else { this.openInternal(newTab); } } private openInternal(newTab: OpenNewTabModel, moduleFactory?: NgModuleFactory<any>) { const newTabItem: TabItem = { label: newTab.label, iconName: newTab.iconName, component: { componentType: newTab.componentType, moduleFactory: moduleFactory, injector: newTab.data ? Injector.create(newTab.data, this.injector) : this.injector } }; this.tabItemSubject.getValue().push(newTabItem); this.tabItemSubject.next(this.tabItemSubject.getValue()); } close(index: number) { this.tabItemSubject.getValue().splice(index, 1); this.tabItemSubject.next(this.tabItemSubject.getValue()); } }
{ provide: NgModuleFactoryLoader, useClass: SystemJsNgModuleLoader }
import { Component, OnInit } from '@angular/core'; import { TabService } from './../../../services/tab.service'; @Component({ selector: 'app-tabs', templateUrl: './tabs.component.html', styleUrls: ['./tabs.component.scss'] }) export class TabsComponent implements OnInit { constructor(public service: TabService) {} ngOnInit() {} }
<mat-tab-group> <mat-tab *ngFor="let tabItem of (service.tabItems$ | async); let index = index" > <ng-template mat-tab-label> <mat-icon class="icon" aria-label="icon for tab" >{{tabItem.iconName}}</mat-icon> <span class="full">{{ tabItem.label }}</span> <mat-icon class="close" (click)="service.close(index)" aria-label="close tab button" >close</mat-icon > <!-- </button> --> </ng-template> <ng-container *ngIf="tabItem.component.moduleFactory"> <ng-container *ngComponentOutlet=" tabItem.component.componentType; ngModuleFactory: tabItem.component.moduleFactory; injector: tabItem.component.injector " > </ng-container> </ng-container> <ng-container *ngIf="!tabItem.component.moduleFactory"> <ng-container *ngComponentOutlet=" tabItem.component.componentType; injector: tabItem.component.injector " > </ng-container> </ng-container> </mat-tab> </mat-tab-group>
در تکه کد بالا، ابتدا با استفاده از وهله تزریق شده TabService در کامپوننت مذکور، به شکل زیر، مشترک تغییرات لیست برگهها شدهایم و با استفاده از دایرکتیو *ngFor به ازای تک تک tabItemهای درخواست شده برای گشوده شدن، به شکل زیر کار وهله سازی پویا از کامپوننت مشخص شده انجام میشود:
<ng-container *ngComponentOutlet="tabItem.component.componentType; ngModuleFactory: tabItem.component.moduleFactory; injector: tabItem.component.injector"> </ng-container>
خوب، با استفاده از آنچه تا اینجای مطلب بررسی شد، میتوان یک سیستم راهبری مبتنی بر Tab را نیز برپا کرد که مطلب جدایی را میطلبد. برای تکمیل مکانیزم بارگذاری پویای ماژولها، نیاز است تا مسیر ماژول مورد نظر را در فایل angular.json و بخش lazyModules به شکل زیر معرفی کنید:
"build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist/MaterialAngularTabLayout", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "src/tsconfig.app.json", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", "src/styles.scss" ], "lazyModules": [ "src/app/lazy/lazy.module" ], "scripts": [] },
به عنوان مثال قصد داریم ماژول LazyModule را به صورت پویا بارگذاری کرده و LazyComponent موجود در این ماژول را به صورت پویا در برگهی جدیدی نمایش دهیم. برای این منظور کدهای فایل AppComponent.ts را به شکل زیر تغییر خواهیم داد:
import { Component } from '@angular/core'; import { IdModel } from './core/models/id-model'; import { LazyComponent } from './lazy/lazy.component'; import { OpenNewTabModel } from './core/models/open-new-tab-model'; import { TabService } from './core/services/tab.service'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent { title = 'MaterialAngularTabLayout'; constructor(private tabService: TabService) {} loadLazyComponent() { this.tabService.open(<OpenNewTabModel>{ label: 'Loaded Lazy Component', iconName: 'thumb_up', componentType: LazyComponent, modulePath: 'src/app/lazy/lazy.module#LazyModule', data: [{ provide: IdModel, useValue: <IdModel>{ id: 1 } }] }); } }
در تکه کد بالا با تزریق TabService به سازندهی آن، قصد گشودن برگهی جدیدی را توسط متد open آن، داریم. در بدنهی متد loadLazyComponent یک شیء با قرارداد OpenNewTabModel ایجاد شده و به عنوان آرگومان به متد open ارسال شده است. توجه داشته باشید که modulePath اینجا نیز به مانند خصوصیت loadChildren مرتبط با اشیاء مسیریابی، باید مقدار دهی شود. همچنین قصد داشتیم اطلاعاتی را نیز به کامپوننت مورد نظر ارسال کنیم؛ همانند مکانیزم مسیریابی که با پارامترها این چنین کارهایی صورت میپذیرد. در اینجا از یک کلاس به شکل زیر استفاده شدهاست:
export class IdModel { constructor(public id: number) {} }
در این صورت پیاده سازی LazyComponent نیز به شکل زیر خواهد بود:
import { Component, OnInit } from '@angular/core'; import { IdModel } from './../core/models/id-model'; @Component({ selector: 'app-lazy', templateUrl: './lazy.component.html', styleUrls: ['./lazy.component.scss'] }) export class LazyComponent implements OnInit { constructor(private model: IdModel) {} ngOnInit() { console.log(this.model); } }
البته فراموش نکنید کامپوننتی را که نیاز است به صورت پویا بارگذاری شود، در قسمت entryComponents مرتبط با NgModule متناظر به شکل نیز معرفی کنید:
import { CommonModule } from '@angular/common'; import { LazyComponent } from './lazy.component'; import { NgModule } from '@angular/core'; @NgModule({ imports: [CommonModule], declarations: [LazyComponent], entryComponents: [LazyComponent] }) export class LazyModule {}
با خروجی زیر:
و chunk تولید شده برای ماژول مورد نظر:
در صورتیکه در حالت production پروژه را بیلد کنید، هش مرتبط برای chunk تولید شده نیز ایجاد خواهد شد.
کدهای کامل این قسمت را میتوانید از اینجا دریافت کنید.
Install-Package Microsoft.AspNetCore.Mvc.Versioning
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddApiVersioning(); // ... }
services.AddApiVersioning(opt => { opt.AssumeDefaultVersionWhenUnspecified = true; opt.DefaultApiVersion = new ApiVersion(1, 0); });
- api/foo?api-version=1.0/
opt.DefaultApiVersion = new ApiVersion(new DateTime(2018, 10, 22));
- api/foo?api-version=2018-10-22/
URL Path Segment Versioning
[Route("api/v{version:apiVersion}/[controller]")] public class FooController : ControllerBase { public ActionResult<IEnumerable<string>> Get() { return new[] { "value1", "value2" }; } }
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddApiVersioning(opt => opt.ApiVersionReader = new HeaderApiVersionReader("api-version")); }
Deprecating
[ApiVersion("2")] [ApiVersion("1", Deprecated = true)] [Route("api/v{version:apiVersion}/[controller]")] public class FooController : ControllerBase { [HttpGet] public string Get() => "I'm deprecated, Bye bye :("; [HttpGet, MapToApiVersion("2.0")] public string GetV2() => "Hello world ! :D"; }
services.AddApiVersioning(opt => { opt.DefaultApiVersion = new ApiVersion(1, 0); opt.AssumeDefaultVersionWhenUnspecified = true; opt.ReportApiVersions = true; });
Ignoring Versioning
[ApiVersionNeutral] [Route("api/[controller]")] public class BarController : ControllerBase { public string Get() => HttpContext.GetRequestedApiVersion().ToString(); }
مطلب تکمیلی:
namespace TestVersioning.Controllers.V1 { [ApiVersion("1", Deprecated = true)] [Route("api/v{version:apiVersion}/[controller]")] public class FooController : ControllerBase { public string Get() => "I'm deprecated, Bye bye :("; } } namespace TestVersioning.Controllers.V2 { [ApiVersion("2")] [Route("api/v{version:apiVersion}/[controller]")] public class FooController : ControllerBase { public string GetV2() => "Hello world ! :D"; } }
بررسی ساختار یک قالب Angular Material
قالب، مجموعهای از رنگها است که به کامپوننتهای Angular Material اعمال میشود. هر قالب از چندین جعبهرنگ یا palette تشکیل میشود:
- primary palette: به صورت گستردهای در تمام کامپوننتها مورد استفادهاست.
- accent palette: به المانهای تعاملی انتساب داده میشود.
- warn palette: برای نمایش خطاها و اخطارها بکار میرود.
- foreground palette: برای متون و آیکنها استفاده میشود.
- background palette: برای پسزمینهی المانها بکار میرود.
روش انتخاب این جعبه رنگها نیز به صورت زیر است:
<mat-card> Main Theme: <button mat-raised-button color="primary"> Primary </button> <button mat-raised-button color="accent"> Accent </button> <button mat-raised-button color="warn"> Warning </button> </mat-card>
همانطور که در قسمت اول این سری نیز بررسی کردیم، بستهی Angular Material به همراه چندین قالب از پیش طراحی شدهاست (قالبهای از پیش آمادهی متریال را در پوشهی node_modules\@angular\material\prebuilt-themes میتوانید مشاهده کنید) و در حین اجرای برنامه تنها یکی از آنها که در فایل styles.css ذکر شدهاست، مورد استفاده قرار میگیرد.
اگر نیاز به سفارشی سازی بیشتری وجود داشته باشد، میتوان قالبهای ویژهی خود را نیز طراحی کرد. این قالب جدید باید mat-core() sass mixin را import کند که حاوی تمام شیوهنامههای مشترک بین کامپوننتها است. این مورد باید تنها یکبار به کل برنامه الحاق شود تا حجم آنرا بیش از اندازه زیاد نکند. سپس این قالب سفارشی، جعبه رنگهای خاص خودش را معرفی میکند. در ادامه این جعبه رنگها توسط توابع mat-light-theme و یا mat-dark-theme ترکیب شده و مورد استفاده قرار میگیرند. سپس این قالب را include خواهیم کرد. به این ترتیب یک قالب سفارشی Angular Material، چنین طرحی را دارد:
@import '~@angular/material/theming'; @include mat-core(); $candy-app-primary: mat-palette($mat-indigo); $candy-app-accent: mat-palette($mat-pink, A200, A100, A400); $candy-app-warn: mat-palette($mat-red); $candy-app-theme: mat-light-theme($candy-app-primary, $candy-app-accent, $candy-app-warn); @include angular-material-theme($candy-app-theme);
ایجاد یک قالب سفارشی جدید Angular Material
برای ایجاد یک قالب سفارشی نیاز است از فایلهای sass استفاده کرد. بنابراین بهترین روش ایجاد برنامههای Angular Material در ابتدای کار، ذکر صریح نوع style مورد استفاده به sass است:
ng new MyProjectName --style=sass
"styles": [ "node_modules/material-design-icons/iconfont/material-icons.css", "src/styles.css", "src/custom.theme.scss" ],
پس از افزودن و تنظیم فایل custom.theme.scss، به فایل styles.css مراجعه کرده و قالب فعلی را به صورت comment در میآوریم:
/* @import "~@angular/material/prebuilt-themes/indigo-pink.css"; */ body { margin: 0; }
@import '~@angular/material/theming'; @include mat-core(); $my-app-primary: mat-palette($mat-blue-grey); $my-app-accent: mat-palette($mat-pink, 500, 900, A100); $my-app-warn: mat-palette($mat-deep-orange); $my-app-theme: mat-light-theme($my-app-primary, $my-app-accent, $my-app-warn); @include angular-material-theme($my-app-theme); .alternate-theme { $alternate-primary: mat-palette($mat-light-blue); $alternate-accent: mat-palette($mat-yellow, 400); $alternate-theme: mat-light-theme($alternate-primary, $alternate-accent); @include angular-material-theme($alternate-theme); }
در اینجا روش تعریف یک قالب دوم (alternate-theme) را نیز مشاهده میکنید. علت تعریف قالب دوم در همین فایل جاری، کاهش حجم نهایی برنامه است. از این جهت که اگر alternate-themeها را در فایلهای scss دیگری قرار دهیم، مجبور به import تعاریف اولیهی قالبهای Angular Material در هرکدام به صورت جداگانهای خواهیم بود که حجم قابل ملاحظهای را به خود اختصاص میدهند. به همین جهت قالبهای دیگر را نیز در همینجا به صورت کلاسهای ثانویه تعریف خواهیم کرد.
در این حالت روش استفادهی از این قالب ثانویه به صورت زیر میباشد:
<mat-card class="alternate-theme"> Alternate Theme: <button mat-raised-button color="primary"> Primary </button> <button mat-raised-button color="accent"> Accent </button> <button mat-raised-button color="warn"> Warning </button> </mat-card>
پس از افزودن فایل src\custom.theme.scss به برنامه، اگر آنرا اجرا کنیم به خروجی زیر خواهیم رسید:
افزودن امکان انتخاب پویای قالبها به برنامه
قصد داریم به منوی برنامه که اکنون گزینهی new contact را به همراه دارد، گزینهی toggle theme را هم جهت تغییر پویای قالب اصلی برنامه اضافه کنیم. به همین جهت فایل toolbar.component.html را گشوده و به صورت زیر تغییر میدهیم:
<mat-menu #menu="matMenu"> <button mat-menu-item (click)="openAddContactDialog()">New Contact</button> <button mat-menu-item (click)="toggleTheme.emit()">Toggle theme</button> </mat-menu>
بنابراین جهت تبادل اطلاعات بین toolbar و sidenav از یک رخداد استفاده خواهیم کرد. برای این منظور فایل toolbar.component.ts را گشوده و این رخداد را به آن اضافه میکنیم:
export class ToolbarComponent implements OnInit { @Output() toggleTheme = new EventEmitter<void>();
<app-toolbar (toggleTheme)="toggleTheme()" (toggleSidenav)="sidenav.toggle()"></app-toolbar>
export class SidenavComponent { isAlternateTheme = false; toggleTheme() { this.isAlternateTheme = !this.isAlternateTheme; } }
<mat-sidenav-container fxLayout="row" class="app-sidenav-container" fxFill [class.alternate-theme]="isAlternateTheme">
افزودن پشتیبانی از راست به چپ به قالب برنامه
اگر به mat-sidenav-container ویژگی dir=rtl را اضافه کنیم، قالب برنامه راست به چپ خواهد شد. در ادامه میخواهیم شبیه به حالت تغییر پویای قالب سایت، گزینهای را به منوی برنامه جهت تغییر جهت برنامه نیز اضافه کنیم. برای این منظور به قالب toolbar.component.html مراجعه کرده و گزینهی Toggle dir را به آن اضافه میکنیم:
<mat-menu #menu="matMenu"> <button mat-menu-item (click)="openAddContactDialog()">New Contact</button> <button mat-menu-item (click)="toggleTheme.emit()">Toggle theme</button> <button mat-menu-item (click)="toggleDir.emit()">Toggle dir</button> </mat-menu>
export class ToolbarComponent implements OnInit { @Output() toggleDir = new EventEmitter<void>();
<app-toolbar (toggleDir)="toggleDir()" (toggleTheme)="toggleTheme()" (toggleSidenav)="sidenav.toggle()"></app-toolbar>
export class SidenavComponent implements OnInit, OnDestroy { dir = "ltr"; toggleDir() { this.dir = this.dir === "ltr" ? "rtl" : "ltr"; } }
<mat-sidenav-container fxLayout="row" class="app-sidenav-container" fxFill [dir]="dir" [class.alternate-theme]="isAlternateTheme">
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MaterialAngularClient-06.zip
برای اجرای آن:
الف) ابتدا به پوشهی src\MaterialAngularClient وارد شده و فایلهای restore.bat و ng-build-dev.bat را اجرا کنید.
ب) سپس به پوشهی src\MaterialAspNetCoreBackend\MaterialAspNetCoreBackend.WebApp وارد شده و فایلهای restore.bat و dotnet_run.bat را اجرا کنید.
اکنون برنامه در آدرس https://localhost:5001 قابل دسترسی است.