کدهای سمت سرور دریافت تصویر
در اینجا کدهای سمت سرور برنامه، نکتهی خاصی را به همراه نداشته و صرفا یک تصویر ساده (محتوای باینری) را بازگشت میدهد:
using Microsoft.AspNetCore.Mvc; namespace AngularTemplateDrivenFormsLab.Controllers { [Route("api/[controller]")] public class ShowImagesController : Controller { [HttpGet("[action]")] public IActionResult GetImage() { return File("~/assets/resume.png", "image/png"); } } }
سرویس دریافت محتوای باینری در برنامههای Angular
برای اینکه HttpClient برنامههای Angular بتواند محتوای باینری را بجای محتوای JSON پیشفرض آن دریافت کند، نیاز است نوع خروجی سمت سرور آنرا به blob تنظیم کرد:
import { Injectable } from "@angular/core"; import { Observable } from "rxjs/Observable"; import { HttpClient } from "@angular/common/http"; @Injectable() export class DownloadBinaryDataService { constructor(private httpClient: HttpClient) { } public getImage(): Observable<Blob> { return this.httpClient.get("/api/ShowImages/GetImage", { responseType: "blob" }); } }
ساخت URL برای دسترسی به اطلاعات باینری
تمام مرورگرهای جدید از ایجاد URL برای اشیاء Blob دریافتی از سمت سرور، توسط متد توکار URL.createObjectURL پشتیبانی میکنند. این متد، شیء URL را از شیء window جاری دریافت میکند و سپس اطلاعات باینری را دریافت کرده و آدرسی را جهت دسترسی موقت به آن تولید میکند.
بنابراین ابتدا سرویس جدیدی را در مسیر src\app\core\window.service.ts جهت دسترسی به این شیء پنجره ایجاد میکنیم:
import { Injectable } from "@angular/core"; function getWindow(): any { return window; } @Injectable() export class WindowRefService { get nativeWindow(): any { return getWindow(); } }
چون این سرویس، یک سرویس سراسری است، آنرا در قسمت providers مربوط به CoreModule ثبت خواهیم کرد تا در تمام برنامه قابل دسترسی شود:
import { WindowRefService } from "./window.service"; @NgModule({ providers: [ WindowRefService ] }) export class CoreModule {}
const urlCreator = this.nativeWindow.URL; imageBlobUrl = urlCreator.createObjectURL(imageDataBlob);
اصلاح Content Security Policy سمت سرور جهت نمایش تصاویر blob
زمانیکه از متد createObjectURL استفاده میشود، یک نمونه آدرس تولیدی آن چنین شکلی را پیدا میکند:
<img src="blob:http://localhost:5000/9d4bae44-00bc-4ed8-9f27-cac2de5ecd5d">
img-src 'self' data: blob:
دریافت تصویر از سرور و نمایش آن در برنامهی Angular
پس از این مقدمات، کامپوننتی که یک تصویر را از سمت سرور دریافت کرده و نمایش میدهد، چنین کدی را خواهد داشت:
import { WindowRefService } from "./../../core/window.service"; import { DownloadBinaryDataService } from "app/angular-http-client-blob/download-binary-data.service"; import { Component, OnInit, ViewChild, ElementRef } from "@angular/core"; import { DomSanitizer, SafeUrl } from "@angular/platform-browser"; @Component({ templateUrl: "./download-blobs.component.html", styleUrls: ["./download-blobs.component.css"] }) export class DownloadBlobsComponent implements OnInit { @ViewChild("sampleImage1") sampleImage1: ElementRef; private nativeWindow: Window; imageBlobUrl: string; sanitizedImageBlobUrl: SafeUrl; constructor(private downloadService: DownloadBinaryDataService, private windowRefService: WindowRefService, private sanitizer: DomSanitizer) { } ngOnInit() { this.nativeWindow = this.windowRefService.nativeWindow; this.downloadService.getImage().subscribe(imageDataBlob => { const urlCreator = this.nativeWindow.URL; this.imageBlobUrl = urlCreator.createObjectURL(imageDataBlob); // doesn't work -> <img [src]="imageBlobUrl"> this.sampleImage1.nativeElement.src = this.imageBlobUrl; // works -> <img #sampleImage1> this.sanitizedImageBlobUrl = this.sanitizer.bypassSecurityTrustUrl(this.imageBlobUrl); // works -> <img [src]="sanitizedImageBlobUrl"> }); } }
<h1>Angular HttpClient Blob</h1> <h4>#sampleImage1</h4> <img #sampleImage1> <h4>imageBlobUrl</h4> <img [src]="imageBlobUrl"> <h4>sanitizedImageBlobUrl</h4> <img [src]="sanitizedImageBlobUrl">
سپس مشترک متد getImage دریافت تصویر شده و اطلاعات نهایی آنرا به صورت imageDataBlob دریافت میکنیم.
این اطلاعات باینری را به متد createObjectURL ارسال کرده و آدرس موقتی این تصویر را در مرورگر بدست میآوریم.
در ادامه سه روش کار با این URL نهایی را بررسی کردهایم:
- دسترسی مستقیم به imageBlobUrl
this.imageBlobUrl = urlCreator.createObjectURL(imageDataBlob); // doesn't work -> <img [src]="imageBlobUrl">
<h4>imageBlobUrl</h4> <img [src]="imageBlobUrl">
<img _ngcontent-c1="" src="unsafe:blob:http://localhost:5000/a4505339-5da2-4303-949c-8e6a7cfff2fc">
Refused to load the image 'unsafe:blob:http://localhost:5000/a4505339-5da2-4303-949c-8e6a7cfff2fc' because it violates the following Content Security Policy directive: "img-src 'self' data: blob:".
- برای رفع این مشکل، میتوان از سرویس DomSanitizer آن که به سازندهی کلاس تزریق شدهاست استفاده کرد:
this.sanitizedImageBlobUrl = this.sanitizer.bypassSecurityTrustUrl(this.imageBlobUrl); // works -> <img [src]="sanitizedImageBlobUrl">
<img [src]="sanitizedImageBlobUrl">
<img _ngcontent-c1="" src="blob:http://localhost:5000/a4505339-5da2-4303-949c-8e6a7cfff2fc">
- روش دیگر نمایش این تصویر، انتساب این آدرس به شیء بومی این تصویر است. برای اینکار در قالب این کامپوننت، یک template reference variable را به img نسبت میدهیم:
<img #sampleImage1>
@ViewChild("sampleImage1") sampleImage1: ElementRef;
this.sampleImage1.nativeElement.src = this.imageBlobUrl; // works -> <img #sampleImage1>
در نهایت Angular یک چنین خروجی را برای نمایش اینگونه تصاویر، بر اساس کدهای فوق رندر میکند:
<ng-component _nghost-c1=""><h1 _ngcontent-c1="">Angular HttpClient Blob</h1> <h4 _ngcontent-c1="">#sampleImage1</h4> <img _ngcontent-c1="" src="blob:http://localhost:5000/a4505339-5da2-4303-949c-8e6a7cfff2fc"> <h4 _ngcontent-c1="">imageBlobUrl</h4> <img _ngcontent-c1="" src="unsafe:blob:http://localhost:5000/a4505339-5da2-4303-949c-8e6a7cfff2fc"> <h4 _ngcontent-c1="">sanitizedImageBlobUrl</h4> <img _ngcontent-c1="" src="blob:http://localhost:5000/a4505339-5da2-4303-949c-8e6a7cfff2fc"> </ng-component>
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید.
بررسی ساختار یک قالب 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 قابل دسترسی است.
- اگر از attribute routing استفاده میکنید، متد MapControllers را فراخوانی کنید که البته به صورت ضمنی هم فراخوانی میشود.
- متد MapRoute پیشین به MapControllerRoute تغییر نام یافتهاست.
- متد MapAreaRoute پیشین به MapAreaControllerRoute تغییر نام یافتهاست.
علت این نوع نامگذاریهای صریح، این است که اکنون سیستم مسیریابی، فراتر از سیستم MVC را پشتیبانی میکند.
- همانند سابق متدهای MapControllerRoute/MapAreaControllerRoute/MapDefaultControllerRoute به ترتیبی که تعریف میشوند، تاثیرگذار خواهند بود.
در مثال زیر:
public void Configure(IApplicationBuilder app) { ... app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); endpoints.MapAreaControllerRoute( "admin", "admin", "Admin/{controller=Home}/{action=Index}/{id?}"); endpoints.MapControllerRoute( "default", "{controller=Home}/{action=Index}/{id?}"); }); }
- متد MapAreaControllerRoute، مسیریابی سنتی کنترلرهای درون یک Area را تعریف میکند.
- متد MapControllerRoute، مسیریابی سنتی کنترلرها را تعریف میکند.
اگر از Razor pages استفاده میکنید، در اینجا باید متد MapRazorPages را نیز فراخوانی کنید:
public void Configure(IApplicationBuilder app) { ... app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); }); }
1- چگونه Areaهای استاندارد را تبدیل به یک افزونهی مجزا و منتقل شدهی به یک اسمبلی دیگر کنیم.
2- چگونه ساختار پایهای را جهت تامین نیازهای هر افزونه جهت تزریق وابستگیها تا ثبت مسیریابیها و امثال آن تدارک ببینیم.
3- چگونه فایلهای CSS ، JS و همچنین تصاویر ثابت هر افزونه را داخل اسمبلی آن قرار دهیم تا دیگر نیازی به ارائهی مجزای آنها نباشد.
4- چگونه Entity Framework Code-First را با این طراحی یکپارچه کرده و از آن جهت یافتن خودکار مدلها و موجودیتهای خاص هر افزونه استفاده کنیم؛ به همراه مباحث Migrations خودکار و همچنین پیاده سازی الگوی واحد کار.
در مطلب جاری، موارد اول و دوم بررسی خواهند شد. پیشنیازهای آن مطالب ذیل هستند:
الف) منظور از یک Area چیست؟
ب) توزیع پروژههای ASP.NET MVC بدون ارائه فایلهای View آن
ج) آشنایی با تزریق وابستگیها در ASP.NET MVC و همچنین اصول طراحی یک سیستم افزونه پذیر به کمک StructureMap
د) آشنایی با رخدادهای Build
تبدیل یک Area به یک افزونهی مستقل
روشهای زیادی برای خارج کردن Areaهای استاندارد ASP.NET MVC از یک پروژه و قرار دادن آنها در اسمبلیهای دیگر وجود دارند؛ اما در حال حاضر تنها روشی که نگهداری میشود و همچنین اعضای آن همان اعضای تیم نیوگت و ASP.NET MVC هستند، همان روش استفاده از Razor Generator است.
بنابراین ساختار ابتدایی پروژهی افزونه پذیر ما به صورت ذیل خواهد بود:
1) ابتدا افزونهی Razor Generator را نصب کنید.
2) سپس یک پروژهی معمولی ASP.NET MVC را آغاز کنید. در این سری نام MvcPluginMasterApp برای آن در نظر گرفته شدهاست.
3) در ادامه یک پروژهی معمولی دیگر ASP.NET MVC را نیز به پروژهی جاری اضافه کنید. برای مثال نام آن در اینجا MvcPluginMasterApp.Plugin1 تنظیم شدهاست.
4) به پروژهی MvcPluginMasterApp.Plugin1 یک Area جدید و معمولی را به نام NewsArea اضافه کنید.
5) از پروژهی افزونه، تمام پوشههای غیر Area را حذف کنید. پوشههای Controllers و Models و Views حذف خواهند شد. همچنین فایل global.asax آنرا نیز حذف کنید. هر افزونه، کنترلرها و Viewهای خود را از طریق Area مرتبط دریافت میکند و در این حالت دیگر نیازی به پوشههای Controllers و Models و Views واقع شده در ریشهی اصلی پروژهی افزونه نیست.
6) در ادامه کنسول پاور شل نیوگت را باز کرده و دستور ذیل را صادر کنید:
PM> Install-Package RazorGenerator.Mvc
همانطور که در تصویر نیز مشخص شدهاست، برای اجرای دستور نصب RazorGenerator.Mvc نیاز است هربار پروژهی پیش فرض را تغییر دهید.
7) اکنون پس از نصب RazorGenerator.Mvc، نوبت به اجرای آن بر روی هر دو پروژهی اصلی و افزونه است:
PM> Enable-RazorGenerator
همچنین هربار که View جدیدی اضافه میشود نیز باید اینکار را تکرار کنید یا اینکه مطابق شکل زیر، به خواص View جدید مراجعه کرده و Custom tool آنرا به صورت دستی به RazorGenerator تنظیم نمائید. دستور Enable-RazorGenerator اینکار را به صورت خودکار انجام میدهد.
تا اینجا موفق شدیم Viewهای افزونه را داخل فایل dll آن مدفون کنیم. به این ترتیب با کپی کردن افزونه به پوشهی bin پروژهی اصلی، دیگر نیازی به ارائهی فایلهای View آن نیست و تمام اطلاعات کنترلرها، مدلها و Viewها به صورت یکجا از فایل dll افزونهی ارائه شده خوانده میشوند.
کپی کردن خودکار افزونه به پوشهی Bin پروژهی اصلی
پس از اینکه ساختار اصلی کار شکل گرفت، هربار پس از کامپایل افزونه (یا افزونهها)، نیاز است فایلهای پوشهی bin آنرا به پوشهی bin پروژهی اصلی کپی کنیم (پروژهی اصلی در این حالت هیچ ارجاع مستقیمی را به افزونهی جدید نخواهد داشت). برای خودکار سازی این کار، به خواص پروژهی افزونه مراجعه کرده و قسمت Build events آنرا به نحو ذیل تنظیم کنید:
در اینجا دستور ذیل در قسمت Post-build event نوشته شده است:
Copy "$(ProjectDir)$(OutDir)$(TargetName).*" "$(SolutionDir)MvcPluginMasterApp\bin\"
تنظیم فضاهای نام کلیه مسیریابیهای پروژه
در همین حالت اگر پروژه را اجرا کنید، موتور ASP.NET MVC به صورت خودکار اطلاعات افزونهی کپی شده به پوشهی bin را دریافت و به Application domain جاری اعمال میکند؛ برای اینکار نیازی به کد نویسی اضافهتری نیست و خودکار است. برای آزمایش آن فقط کافی است یک break point را داخل کلاس RazorGeneratorMvcStart افزونه قرار دهید.
اما ... پس از اجرا، بلافاصله پیام تداخل فضاهای نام را دریافت میکنید. خطاهای حاصل عنوان میکند که در App domain جاری، دو کنترلر Home وجود دارند؛ یکی در پروژهی اصلی و دیگری در پروژهی افزونه و مشخص نیست که مسیریابیها باید به کدامیک ختم شوند.
برای رفع این مشکل، به فایل NewsAreaAreaRegistration.cs پروژهی افزونه مراجعه کرده و مسیریابی آنرا به نحو ذیل تکمیل کنید تا فضای نام اختصاصی این Area صریحا مشخص گردد.
using System.Web.Mvc; namespace MvcPluginMasterApp.Plugin1.Areas.NewsArea { public class NewsAreaAreaRegistration : AreaRegistration { public override string AreaName { get { return "NewsArea"; } } public override void RegisterArea(AreaRegistrationContext context) { context.MapRoute( "NewsArea_default", "NewsArea/{controller}/{action}/{id}", // تکمیل نام کنترلر پیش فرض new { controller = "Home", action = "Index", id = UrlParameter.Optional }, // مشخص کردن فضای نام مرتبط جهت جلوگیری از تداخل با سایر قسمتهای برنامه namespaces: new[] { string.Format("{0}.Controllers", this.GetType().Namespace) } ); } } }
using System.Web.Mvc; using System.Web.Routing; namespace MvcPluginMasterApp { public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }, // مشخص کردن فضای نام مرتبط جهت جلوگیری از تداخل با سایر قسمتهای برنامه namespaces: new[] { string.Format("{0}.Controllers", typeof(RouteConfig).Namespace) } ); } } }
طراحی قرارداد پایه افزونهها
تا اینجا با نحوهی تشکیل ساختار هر پروژهی افزونه آشنا شدیم. اما هر افزونه در آینده نیاز به مواردی مانند منوی اختصاصی در منوی اصلی سایت، تنظیمات مسیریابی اختصاصی، تنظیمات EF و امثال آن نیز خواهد داشت. به همین منظور، یک پروژهی class library جدید را به نام MvcPluginMasterApp.PluginsBase آغاز کنید.
سپس قرار داد IPlugin را به نحو ذیل به آن اضافه نمائید:
using System; using System.Reflection; using System.Web.Optimization; using System.Web.Routing; using StructureMap; namespace MvcPluginMasterApp.PluginsBase { public interface IPlugin { EfBootstrapper GetEfBootstrapper(); MenuItem GetMenuItem(RequestContext requestContext); void RegisterBundles(BundleCollection bundles); void RegisterRoutes(RouteCollection routes); void RegisterServices(IContainer container); } public class EfBootstrapper { /// <summary> /// Assemblies containing EntityTypeConfiguration classes. /// </summary> public Assembly[] ConfigurationsAssemblies { get; set; } /// <summary> /// Domain classes. /// </summary> public Type[] DomainEntities { get; set; } /// <summary> /// Custom Seed method. /// </summary> //public Action<IUnitOfWork> DatabaseSeeder { get; set; } } public class MenuItem { public string Name { set; get; } public string Url { set; get; } } }
PM> install-package EntityFramework PM> install-package Microsoft.AspNet.Web.Optimization PM> install-package structuremap.web
توضیحات قرار داد IPlugin
از این پس هر افزونه باید دارای کلاسی باشد که از اینترفیس IPlugin مشتق میشود. برای مثال فعلا کلاس ذیل را به افزونهی پروژه اضافه نمائید:
using System.Web.Mvc; using System.Web.Optimization; using System.Web.Routing; using MvcPluginMasterApp.PluginsBase; using StructureMap; namespace MvcPluginMasterApp.Plugin1 { public class Plugin1 : IPlugin { public EfBootstrapper GetEfBootstrapper() { return null; } public MenuItem GetMenuItem(RequestContext requestContext) { return new MenuItem { Name = "Plugin 1", Url = new UrlHelper(requestContext).Action("Index", "Home", new { area = "NewsArea" }) }; } public void RegisterBundles(BundleCollection bundles) { //todo: ... } public void RegisterRoutes(RouteCollection routes) { //todo: add custom routes. } public void RegisterServices(IContainer container) { // todo: add custom services. container.Configure(cfg => { //cfg.For<INewsService>().Use<EfNewsService>(); }); } } }
برای اینکه هر افزونه در منوی اصلی ظاهر شود، نیاز به یک نام، به همراه آدرسی به صفحهی اصلی آن خواهد داشت. به همین جهت در متد GetMenuItem نحوهی ساخت آدرسی را به اکشن متد Index کنترلر Home واقع در Areaایی به نام NewsArea، مشاهده میکنید.
بارگذاری و تشخیص خودکار افزونهها
پس از اینکه هر افزونه دارای کلاسی مشتق شده از قرارداد IPlugin شد، نیاز است آنها را به صورت خودکار یافته و سپس پردازش کنیم. اینکار را به کتابخانهی StructureMap واگذار خواهیم کرد. برای این منظور پروژهی جدیدی را به نام MvcPluginMasterApp.IoCConfig آغاز کرده و سپس تنظیمات آنرا به نحو ذیل تغییر دهید:
using System; using System.IO; using System.Threading; using System.Web; using MvcPluginMasterApp.PluginsBase; using StructureMap; using StructureMap.Graph; namespace MvcPluginMasterApp.IoCConfig { public static class SmObjectFactory { private static readonly Lazy<Container> _containerBuilder = new Lazy<Container>(defaultContainer, LazyThreadSafetyMode.ExecutionAndPublication); public static IContainer Container { get { return _containerBuilder.Value; } } private static Container defaultContainer() { return new Container(cfg => { cfg.Scan(scanner => { scanner.AssembliesFromPath( path: Path.Combine(HttpRuntime.AppDomainAppPath, "bin"), // یک اسمبلی نباید دوبار بارگذاری شود assemblyFilter: assembly => { return !assembly.FullName.Equals(typeof(IPlugin).Assembly.FullName); }); scanner.WithDefaultConventions(); //Connects 'IName' interface to 'Name' class automatically. scanner.AddAllTypesOf<IPlugin>().NameBy(item => item.FullName); }); }); } } }
PM> install-package EntityFramework PM> install-package structuremap.web
کاری که در کلاس SmObjectFactory انجام شده، بسیار ساده است. مسیر پوشهی Bin پروژهی اصلی به structuremap معرفی شدهاست. سپس به آن گفتهایم که تنها اسمبلیهایی را که دارای اینترفیس IPlugin هستند، به صورت خودکار بارگذاری کن. در ادامه تمام نوعهای IPlugin را نیز به صورت خودکار یافته و در مخزن تنظیمات خود، اضافه کن.
تامین نیازهای مسیریابی و Bundling هر افزونه به صورت خودکار
در ادامه به پروژهی اصلی مراجعه کرده و در پوشهی App_Start آن کلاس ذیل را اضافه کنید:
using System.Linq; using System.Web.Optimization; using System.Web.Routing; using MvcPluginMasterApp; using MvcPluginMasterApp.IoCConfig; using MvcPluginMasterApp.PluginsBase; [assembly: WebActivatorEx.PostApplicationStartMethod(typeof(PluginsStart), "Start")] namespace MvcPluginMasterApp { public static class PluginsStart { public static void Start() { var plugins = SmObjectFactory.Container.GetAllInstances<IPlugin>().ToList(); foreach (var plugin in plugins) { plugin.RegisterServices(SmObjectFactory.Container); plugin.RegisterRoutes(RouteTable.Routes); plugin.RegisterBundles(BundleTable.Bundles); } } } }
دراینجا با استفاده از کتابخانهای به نام WebActivatorEx (که باز هم توسط نویسندگان اصلی Razor Generator تهیه شدهاست)، یک متد PostApplicationStartMethod سفارشی را تعریف کردهایم.
مزیت استفاده از اینکار این است که فایل Global.asax.cs برنامه شلوغ نخواهد شد. در غیر اینصورت باید تمام این کدها را در انتهای متد Application_Start قرار میدادیم.
در اینجا با استفاده از structuremap، تمام افزونههای موجود به صورت خودکار بررسی شده و سپس پیشنیازهای مسیریابی و Bundling و همچنین تنظیمات IoC Container مورد نیاز آنها به هر افزونه به صورت مستقل، تزریق خواهد شد.
اضافه کردن منوهای خودکار افزونهها به پروژهی اصلی
پس از اینکه کار پردازش اولیهی IPluginها به پایان رسید، اکنون نوبت به نمایش آدرس اختصاصی هر افزونه در منوی اصلی سایت است. برای این منظور فایل جدیدی را به نام PluginsMenu.cshtml_، در پوشهی shared پروژهی اصلی اضافه کنید؛ با این محتوا:
@using MvcPluginMasterApp.IoCConfig @using MvcPluginMasterApp.PluginsBase @{ var plugins = SmObjectFactory.Container.GetAllInstances<IPlugin>().ToList(); } @foreach (var plugin in plugins) { var menuItem = plugin.GetMenuItem(this.Request.RequestContext); <li> <a href="@menuItem.Url">@menuItem.Name</a> </li> }
سپس به فایل Layout.cshtml_ پروژهی اصلی مراجعه و توسط فراخوانی Html.RenderPartial، آنرا در بین سایر آیتمهای منوی اصلی اضافه میکنیم:
<div class="navbar navbar-inverse navbar-fixed-top"> <div class="container"> <div class="navbar-header"> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse"> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> @Html.ActionLink("MvcPlugin Master App", "Index", "Home", new { area = "" }, new { @class = "navbar-brand" }) </div> <div class="navbar-collapse collapse"> <ul class="nav navbar-nav"> <li>@Html.ActionLink("Master App/Home", "Index", "Home", new {area = ""}, null)</li> @{ Html.RenderPartial("_PluginsMenu"); } </ul> </div> </div> </div>
بنابراین به صورت خلاصه
1) هر افزونه، یک پروژهی کامل ASP.NET MVC است که پوشههای ریشهی اصلی آن حذف شدهاند و اطلاعات آن توسط یک Area جدید تامین میشوند.
2) تنظیم فضای نام مسیریابیهای تمام پروژهها را فراموش نکنید. در غیر اینصورت شاهد تداخل پردازش کنترلرهای هم نام خواهید بود.
3) جهت سهولت کار، میتوان فایلهای bin هر افزونه را توسط رخداد post-build، به پوشهی bin پروژهی اصلی کپی کرد.
4) Viewهای هر افزونه توسط Razor Generator در فایل dll آن مدفون خواهند شد.
5) هر افزونه باید دارای کلاسی باشد که اینترفیس IPlugin را پیاده سازی میکند. از این اینترفیس برای ثبت اطلاعات هر افزونه یا دریافت اطلاعات سفارشی از آن کمک میگیریم.
6) با استفاده از استراکچرمپ و قرارداد IPlugin، منوهای هر افزونه را به صورت خودکار یافته و سپس به فایل layout اصلی اضافه میکنیم.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید:
MvcPluginMasterApp-Part1.zip
> npm install @angular/animations@latest --save
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; @NgModule({ imports: [ //… BrowserAnimationsModule ], })
در اینجا مثال سادهای را مشاهده خواهید کرد که انتقال (transition) المنت li را از حالت active به inactive، با یک انمیشن ساده همراه خواهد کرد. محل تعریف انیمیشنها در Angular، داخل متادیتای component@ و توسط خصوصیت animations میباشد. در ابتدای کار باید توابع مختص انیمیشن را در فایل component مورد نظر خود توسط دستور import به شکل زیر وارد کنیم:
import { trigger, state, style, transition, animate } from '@angular/animations';
import { Component } from '@angular/core'; import { trigger,state,style,transition,animate } from '@angular/animations'; @Component({ selector: 'app-root', template: ` <div style='width:50%; margin:auto'> <ul> <li [@myState]="currentState" style='width:100px; padding:10px; list-style-type: none' (click)="toggleState()"> {{currentState}} </li> </ul> </div> `, styleUrls: ['./app.component.css'], animations: [ trigger('myState', [ state('inactive', style({ backgroundColor: '#eee', transform: 'scale(1)' })), state('active', style({ backgroundColor: '#cfd8dc', transform: 'scale(1.1)' })), transition('inactive => active', animate('100ms ease-in')), transition('active => inactive', animate('100ms ease-out')) ]) ] }) export class AppComponent { currentState: string = 'inactive'; toggleState():void{ this.currentState = this.currentState === 'inactive' ? 'active' : 'inactive'; } }
توضیحات تکمیلی
trigger: جهت تعریف یک انیمیشن، از تابع trigger استفاده میکنیم. این تابع استفاده شدهی در خصوصیت animations، در پارامتر اول خود، یک نام یکتا را دریافت خواهد کرد و در پارامتر بعدی، لیستی از توابع وارد شده مختص انیمیشن را دریافت میکند.
نکته: خصوصیت animations، قابلیت دریافت چندین تابع trigger را برای داشتن چندین انیمیشن مجزا، دارا است.
state: با استفاده از این تابع قادر به تعریف وضعیتهای مختلف خواهیم بود. انیمشن در Angular بر دو منطق وضعیت (state) و گذار (transition) پیاده سازی میشود. به عبارت دیگر انیمیشن در Angular بر روی گذار (transition) بین وضعیتها (states) قابل تعریف هستند. این تابع در پارامتر اول خود یک نام و در پارامتر دوم خود تابع style را دریافت میکند.
style: با استفاده از این تابع قادر به تعریف استایلی برای وضعیت تعریف شده خواهیم بود.
transition: برای ساخت انیمیشن واقعی مورد استفاده قرار میگیرد. این تابع در پارامتر اول خود، مسیر حرکت بین دو حالت (state) را به صورت یک رشته دریافت کرده و در پارامتر دوم خود، تابع animate را دریافت میکند. در این مثال مسیر حرکت به صورت 'inactive => active' تعریف شده است. یعنی هنگام گذار از وضعیت inactive به وضعیت active، تابع animate در پارامتر دوم اجرا خواهد شد.
animate: این تابع دو پارامتر timing و styles را دریافت میکند. timing مقداری از جنس رشته (string) است که میتواند ترکیبی از مدت زمان (duration) با مقدار اختیاری تاخیر(delay) و مقدار easing باشد. به عنوان مثال مقدار کامل زیر را درنظر بگیرید:
'1s 100ms ease-out'
در این اینجا مدت زمان، برابر ۱ ثانیه، تاخیر، ۱۰۰ میلی ثانیه و easing، مقدار ease-out خواهد بود و به معنی اجرای انیمیشن با افکت ease-out و در مدت زمان ۱ ثانیه و با تاخیر در شروع به مدت ۱۰۰ میلی ثانیه میباشد. در صورتیکه به این پارامتر مقداری عددی را ارسال کنیم، عدد به عنوان مدت زمان (duration) و بر مبنای واحد میلی ثانیه در نظر گرفته خواهد شد. تمامی مقادیر زیر برای ارسال به این پارامتر معتبر میباشند:
500 // duration=500ms "1s" // duration=1s "100ms 0.5s" // duration=100ms, delay=0.5s "5s ease" // duration=5s, easing=ease "5s 10ms cubic-bezier(.17,.67,.88,.1)" // duration=5s, delay=10ms, easing=cubic-bezier(.17,067,.88,.1)
پارامتر styles در تابع animate یکی از توابع style یا keyframes میباشد. البته این پارامتر اختیاری است و در قسمت نکات تکمیلی توضیح داده خواهد شد. در مثال بالا از این پارامتر صرف نظر شده است.
برای متصل کردن انیمیشن تعریف شده به المنتهای موجود در صفحه، از خصوصیت [triggerName@] استفاده کنید. trigger تعریف شده در قطعه کد بالا myState نام دارد. بنابراین برای اینکه المنت li در گذار بین حالتها، این انیمیشن را داشته باشد، باید [myState@] را به المنت خود اضافه کنید. همچنین مقدار حالت جاری را باید برای این خصوصیت تامین کرد. این مقدار میتواند یک رشته استاتیک یا یک عبارت محاسبه شده توسط یک تابع یا یک متغیر باشد.
همانطور که در مثال بالا مشاهده میکنید، با استفاده از متغیر currentState، المنت li در ابتدا در حالت inactive قرار دارد. با کلیک اول بر روی المنت، تابع toggleState باعث تغییر وضعیت المنت از وضعیت inactive به وضعیت active خواهد شد (inactive=>active) بنابراین انیمیشن زیر اجرا خواهد شد.
transition('inactive => active', animate('100ms ease-in'))
با کلیک دوباره، المنت از وضعیت active به inactive خواهد رفت (active=>inactive)، بنابراین انیمیشن زیر اجرا میشود.
نکات تکمیلی
transition('inactive => active, active => inactive', animate('100ms ease-out'))
یا به شکل سادهتر:
transition('inactive <=> active', animate('100ms ease-out'))
کاراکتر * جایگزین تمامی حالتهای موجود در برنامه خواهد بود. برای مثال:
'* <= active': بر روی تمامی گذارهای از حالت active به هر حالت دیگر، اعمال خواهد شد.
'active <= *': بر روی تمامی گذارهای از هر حالت به حالت active، اعمال خواهد شد.
'* <= *': بر روی تمامی گذارها اعمال خواهد شد.
همانطور که قبلا ذکر شد، تابع animate در پارامتر دوم خود یک تابع style یا keyframes را دریافت میکند. در صورتیکه بخواهیم در مدت زمان اجرای انیمیشن بر روی المنت، استایلی را نیز همراه کنیم، میتوانیم از تابع style استفاده کنیم. اما این استایل بعد از اتمام انیمیشن بر روی المنت باقی نخواهد ماند؛ چون حالت (state) مقصد برای خود استایل تعریف شدهاست. علاوه بر این، در تابع transition میتوان به شکل زیر یک استایل خطی را نیز تعریف کرد:
transition('inactive => active', [ style({ backgroundColor: '#cfd8dc', transform: 'scale(1.3)' }), animate('80ms ease-in', style({ backgroundColor: '#eee', transform: 'scale(1)' })) ]),
این تعریف استایل در تابع transition در شروع گذار بلافاصله بر روی المنت اعمال خواهد شد و در طول انیمیشن استایل را از دست خواهد داد و به استایل مقصد خواهد رسید.
در اینجا یک مثال بسیار ساده از نحوه پیاده سازی انیمیشن در Angular را مورد برسی قرار دادیم. در بخش بعدی نحوهی پیاده سازی انیمیشنهای پیشرفته که قابلیت اعمال بر روی المنتها را هنگام اضافه شدن به صفحه و ترک صفحه را خواهند داشت، مورد بررسی قرار میدهیم.