ASP.NET MVC #17
اگر به سازندهی پیشفرض کلاس Startup یک برنامهی وب دقت کنید، چنین تزریق وابستگی در قالب ابتدایی آن وجود دارد:
public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; }
الف) چه سرویسهای پیشفرض دیگری را نیز میتوان در اینجا تزریق کرد؟
ب) آیا میتوان سرویسهای سفارشی تهیه شدهی توسط خودمان را نیز در اینجا تزریق کرد؟
الف) بر روی ابتدای متد ConfigureServices کلاس Startup یک break-point را قرار دهید. لیست پارامتر services آن، شامل سرویسهای پیشفرضی است که قابلیت تزریق وابستگیها را در سازندهی این کلاس دارند و بیش از 40 کلاس هستند.
ب) برای این منظور به فایل Program.cs مراجعه کرده و سرویس سفارشی خود را به صورت زیر، توسط متد ConfigureServices آن، اضافه کنید:
using CoreIocServices; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; namespace CoreIocSample02 { public class Program { public static void Main(string[] args) { CreateWebHostBuilder(args).Build().Run(); } public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .ConfigureServices(serviceCollection => { serviceCollection.AddScoped<ISomeService, SomeService>(); }) .UseStartup<Startup>(); } }
namespace CoreIocSample02 { public class Startup { private readonly ISomeService _someService; public Startup(IConfiguration configuration, ISomeService someService) { Configuration = configuration; _someService = someService; } public IConfiguration Configuration { get; }
CoffeeScript #11
کامپایل خودکار CoffeeScript
همانطور که گفته شده CoffeeScript یک لایه میان شما و جاوااسکریپت است و هر زمان که فایل CoffeeScript تغییر کرد، باید به صورت دستی آن را کامپایل کرد. خوشبختانه CoffeeScript روشهای دیگری را برای کامپایل کردن دارد که به وسیله آن میتوان چرخهی توسعه را بسیار سادهتر نمود.
در قسمت اول گفته شد، برای کامپایل فایل CoffeeScript با استفاده از coffee به صورت زیر عمل میکردیم:
coffee --compile --output lib src
حال به کامپایل خودکار CoffeeScript توجه کنید.
Cake
Cake یک سیستم فوق العاده ساده برای کامپایل خودکار است که مانند Make و Rake عمل میکند. این کتابخانه همراه پکیج coffee-script npm نصب میشود و برای استفاده با فراخوانی cake اجرا میشود.
برای ایجاد فایل tasks در cake که Cakefile نامیده میشود، میتوان از خود CoffeeScript استفاده کرد. برای اجرای cake با استفاده از دستور [cake [task] [options میتوان عمل کرد. برای اطلاع از لیست امکانات cake کافی است دستور cake را به تنهایی اجرا کنید.
وظایف را میتوان با استفاده از تابع task، با ارسال نام و توضیحات (اختیاری) و تابع callback، تعریف کرد. به مثال زیر توجه کنید:
fs = require 'fs' {print} = require 'sys' {spawn} = require 'child_process' build = (callback) -> coffee = spawn 'coffee', ['-c', '-o', 'lib', 'src'] coffee.stderr.on 'data', (data) -> process.stderr.write data.toString() coffee.stdout.on 'data', (data) -> print data.toString() coffee.on 'exit', (code) -> callback?() if code is 0 task 'build', 'Build lib/ from src/', -> build()
همان طور که مشاهده میکنید پس از تغییر در فایل CoffeeScript باید به صورت دستی cake build را فراخوانی کنیم که این دور از حالت ایده آل است.
خوشبختانه دستور coffee پارامتر دیگری به نام watch-- دارد که به وسیله آن میتوان تمامی تغییرات یک پوشه را زیر نظر گرفت و در صورت نیاز دوباره کامپایل انجام شود. به مثال زیر توجه کنید:
task 'watch', 'Watch src/ for changes', -> coffee = spawn 'coffee', ['-w', '-c', '-o', 'lib', 'src'] coffee.stderr.on 'data', (data) -> process.stderr.write data.toString() coffee.stdout.on 'data', (data) -> print data.toString()
task 'open', 'Open index.html', -> # First open, then watch spawn 'open', 'index.html' invoke 'watch'
option '-o', '--output [DIR]', 'output dir' task 'build', 'Build lib/ from src/', -> # Now we have access to a `options` object coffee = spawn 'coffee', ['-c', '-o', options.output or 'lib', 'src'] coffee.stderr.on 'data', (data) -> process.stderr.write data.toString() coffee.stdout.on 'data', (data) -> print data.toString()
Cake یک روش عالی برای انجام وظایف معمول به صورت خودکار است، مانند کامپایل فایلهای CoffeeScript است. همچنین برای آشنایی بیشتر میتوانید به سورس cake نگاهی کنید.
با کلیک بر روی لینک منوی نمایش لیست محصولات، ابتدا قاب خالی لیست محصولات نمایش داده میشود:
سپس بعد از یک ثانیه، شاهد بارگذاری اطلاعات جدول لیست محصولات خواهید بود. این یک ثانیه تاخیر را نیز به عمد توسط منبع داده درون حافظهای برنامه ایجاد کردیم، تا بتوان شرایط دنیای واقعی را شبیه سازی کرد:
InMemoryWebApiModule.forRoot(ProductData, { delay: 1000 }),
ارسال اطلاعات ثابت به مسیرهای مختلف برنامه
روشهای متعددی برای ارسال اطلاعات به مسیرهای مختلف برنامه وجود دارند که تعدادی از آنها را مانند پارامترهای اختیاری، پارامترهای اجباری و پارامترهای کوئری، در قسمت قبل بررسی کردیم. روش دیگری را که در اینجا میتوان بکار برد، استفاده از خاصیت data تعاریف مسیریابی برنامه است:
{ path: 'products', component: ProductListComponent, data: { pageTitle: 'Product List'} },
برای خواندن این اطلاعات ثابت میتوان از شیء route.snapshot سرویس ActivatedRoute استفاده کرد:
this.pageTitle = this.route.snapshot.data['pageTitle'];
پیش بارگذاری اطلاعات پویای مسیرهای مختلف برنامه
زمانیکه به صفحهی جزئیات یک محصول مراجعه میکنیم، ابتدا این کامپوننت آغاز شده و قالب آن نمایش داده میشود. سپس در متد ngOnInit آن کار درخواست اطلاعات از سرور و نمایش آن صورت خواهد گرفت. در این بین، چون زمانی بین درخواست اطلاعات از سرور و دریافت آن صرف میشود، کاربر ابتدا شاهد قالب خالی کامپوننت، به همراه برچسبهای مختلف آن خواهد بود که فاقد اطلاعات هستند و پس از مدتی این اطلاعات نمایش داده میشوند.
برای حل این مشکل از سرویسی به نام Route Resolver استفاده میشود. در این حالت زمانیکه کاربر صفحهی جزئیات یک محصول را درخواست میکند، ابتدا مسیریابی آن فعال شده و سپس سرویس Route Resolver اجرا میشود که کار آن درخواست اطلاعات از وب سرور است. در این حالت پس از دریافت اطلاعات از سرور، کار فعالسازی کامپوننت صورت میگیرد. به این ترتیب قالب کاملا آمادهی کامپوننت، به همراه اطلاعات مرتبط با آن، به کاربر نمایش داده خواهد شد.
بدون استفادهی از Route Resolver، کامپوننت کلاس، پس از آغاز آن، اطلاعات را دریافت میکند. اما با بکارگیری Route Resolver، این سرویس ویژهاست که پیش از هر مرحلهی دیگری اطلاعات را دریافت میکند.
پیاده سازی یک Route Resolver شامل سه مرحلهاست:
الف) ایجاد و ثبت سرویس Route Resolver
ب) معرفی Route Resolver به تنظیمات مسیریابی
ج) خواندن اطلاعات دریافتی توسط Route Resolver به کمک سرویس ActivatedRoute
ایجاد سرویس Route Resolver
یک Route Resolver به صورت یک سرویس جدید ایجاد میشود:
> ng g s product/ProductResolver -m product/product.module installing service create src\app\product\product-resolver.service.spec.ts create src\app\product\product-resolver.service.ts update src\app\product\product.module.ts
providers: [ProductService, ProductResolverService]
فایل src\app\product\product-resolver.service.ts را به نحو ذیل تکمیل کنید:
import { Injectable } from '@angular/core'; import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/catch'; import 'rxjs/add/observable/of'; import 'rxjs/add/operator/map'; import { ProductService } from './product.service'; import { IProduct } from './iproduct'; @Injectable() export class ProductResolverService implements Resolve<IProduct> { constructor(private productService: ProductService, private router: Router) { } resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<IProduct> { let id = route.params['id']; if (isNaN(id)) { console.log(`Product id was not a number: ${id}`); this.router.navigate(['/products']); return Observable.of(null); } return this.productService.getProduct(+id) .map(product => { if (product) { return product; } console.log(`Product was not found: ${id}`); this.router.navigate(['/products']); return null; }) .catch(error => { console.log(`Retrieval error: ${error}`); this.router.navigate(['/products']); return Observable.of(null); }); } }
مرحلهی اول تعریف یک سرویس Route Resolver، پیاده سازی اینترفیس جنریک Resolve است:
export class ProductResolverService implements Resolve<IProduct> {
این اینترفیس پیاده سازی متد resolve را با امضایی که مشاهده میکنید، درخواست میکند:
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<IProduct> {
RouterStateSnapshot وضعیت مسیریاب را در این لحظه در اختیار این سرویس قرار میدهد.
خروجی این متد یک Observable است؛ از نوع اطلاعاتی که دریافت میکند. زمانیکه مسیریابی فعال میشود، متد resolve را فراخوانی کرده و منتظر پایان کار Observable آن میشود. پس از آن است که کامپوننت این مسیریابی را فعالسازی خواهد کرد.
در پیاده سازی متد resolve، تعدادی اعتبارسنجی اطلاعات را نیز مشاهده میکنید. برای مثال اگر id وارد شده، عددی نباشد، در اینجا فرصت خواهیم داشت پیش از فعالسازی کامپوننت نمایش جزئیات یک محصول، کاربر را به صفحهای دیگر هدایت کنیم.
پس از آن نیاز به دریافت اطلاعات محصول درخواست شده، از REST Web API برنامه است. به همین جهت سرویس ProductService را که در قسمت قبل معرفی کردیم، به سازندهی کلاس تزریق کردهایم تا از طریق متد getProduct آن، کار دریافت اطلاعات یک محصول را انجام دهیم.
در اینجا متد getProduct(+id) به همراه عملگر + است تا id دریافتی را به عدد تبدیل کند. سپس بر روی این متد، عملگر map فراخوانی شدهاست. به این ترتیب میتوان به اطلاعات دریافتی از سرور، پیش از بازگشت آن به فراخوان متد resolve، دسترسی یافت. به این ترتیب در اینجا نیز میتوان یک سری اعتبارسنجی را انجام داد. برای مثال آیا id دریافتی، متناظر با محصولی در سمت سرور است یا خیر؟
map operator خروجی را به صورت یک observable بازگشت میدهد. به همین جهت در اینجا نیازی به ذکر return Observable.of نیست.
معرفی Route Resolver به تنظیمات مسیریابی
بعد از پیاده سازی سرویس Route Resolver، نیاز است آنرا به تنظیمات مسیریابی برنامه اضافه کنیم. به همین جهت فایل src\app\product\product-routing.module.ts را گشوده و تنظیمات آنرا به شکل زیر تغییر دهید:
import { ProductResolverService } from './product-resolver.service'; const routes: Routes = [ { path: 'products', component: ProductListComponent }, { path: 'products/:id', component: ProductDetailComponent, resolve: { product: ProductResolverService } }, { path: 'products/:id/edit', component: ProductEditComponent, resolve: { product: ProductResolverService } } ];
در اینجا هر تعداد Route Resolver مورد نیاز را میتوان تعریف کرد. برای مثال اگر مسیریابی خاصی، اطلاعات دیگری را نیز از سرویس خاصی دریافت میکند، میتوان یک جفت کلید/مقدار دیگر را نیز برای آن تعریف کرد. فقط باید دقت داشت که keyها باید منحصربفرد باشند.
به این ترتیب اطمینان حاصل خوهیم کرد که اطلاعات مورد نیاز این مسیریابیها، پیش از فعالسازی کامپوننت آنها، از REST Web API برنامه دریافت میشوند.
خواندن اطلاعات دریافتی توسط Route Resolver به کمک سرویس ActivatedRoute
پس از تعریف سرویس Route Resolver سفارشی خود و معرفی آن به تنظیمات مسیریابی برنامه، قسمت نهایی این عملیات، خواندن این اطلاعات پیش واکشی شدهاست. به همین جهت فایل src\app\product\product-detail\product-detail.component.ts را گشوده و محتوای آنرا به نحو ذیل اصلاح کنید:
constructor(private route: ActivatedRoute) { } ngOnInit(): void { this.product = this.route.snapshot.data['product']; }
- همانطور که مشاهده میکنید، دیگر در این کامپوننت نیازی به تزریق سرویس ProductService نبوده و قسمت دریافت اطلاعات آن از طریق این سرویس، حذف شدهاست.
برای آزمایش آن، لیست محصولات را مشاهده کرده و سپس بر روی لینک مشاهدهی جزئیات یک محصول کلیک کنید. البته در اینجا چون هنوز Route Resolver ایی را برای پیش دریافت لیست محصولات ایجاد نکردهایم، ابتدا قاب خالی لیست محصولات نمایش داده میشود و سپس لیست محصولات. اما دیگر صفحهی نمایش جزئیات یک محصول، این چنین نیست. ابتدا یک وقفهی یک ثانیهای را حس خواهید کرد و سپس صفحهی کامل جزئیات یک محصول نمایان میشود.
یک نکته: اگر یک سرویس Route Resolver، در دو کامپوننت مختلف استفاده شود، اطلاعات آن، بین این دو کامپوننت به اشتراک گذاشته خواهد شد.
مرحلهی بعد، ویرایش فایل src\app\product\product-edit\product-edit.component.ts است تا کامپوننت ویرایش جزئیات اطلاعات نیز بتواند از قابلیت پیش واکشی اطلاعات استفاده کند. در اینجا هنوز نیاز به سرویس ProductService است تا بتوان اطلاعات را ذخیره و یا حذف کرد. تنها قسمتی که باید تغییر کند، حذف متد getProduct و تغییر متد ngOnInit است:
ngOnInit(): void { this.route.data.subscribe(data => { this.onProductRetrieved(data['product']); }); }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-routing-lab-03.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
نحوهی سفارشی سازی کلاس پایهی تمام Viewهای برنامه و معرفی inherits@
در نگارشهای پیشین ASP.NET MVC، امکان تعویض کلاس پایهی Viewها، در فایل web.config واقع در پوشهی ریشهی Views وجود داشت. با حذف این فایل و ساده سازی و محول کردن مسئولیتهای آن به فایل جدید view imports، اینبار برای تعریف کلاس پایهی viewها میتوان به صورت ذیل عمل کرد:
using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Razor; namespace Core1RtmEmptyTest.StartupCustomizations { public abstract class MyCustomBaseView<TModel> : RazorPage<TModel> { public bool IsAuthenticated() { return Context.User.Identity.IsAuthenticated; } #pragma warning disable 1998 public override async Task ExecuteAsync() { } #pragma warning restore 1998 } }
پس از تعریف این کلاس، برای ثبت و معرفی آن به فایل ViewImports.cshtml_ مراجعه کنید و این یک سطر را به ابتدای آن اضافه نمائید:
@inherits Core1RtmEmptyTest.StartupCustomizations.MyCustomBaseView<TModel>
برای نمونه پس از سفارشی سازی صفحهی پایهی تمام Viewها، اکنون یک سطر ذیل را در هر view ایی میتوان تعریف و استفاده کرد:
Is Current User Authenticated? @IsAuthenticated()
معرفی functions@
دایرکتیو جدید functions@، بسیار شبیه است به دایرکتیو قدیمی و حذف شدهی helper@، که در نگارشهای پیشین Razor معرفی شده بود:
@functions { public string Test() { return message; } readonly string message = "test"; }
@Test() <br /> @message
معرفی inject@
توسط دایرکتیو جدید inject@، یک خاصیت عمومی به ASP.NET Core اعلام میشود و سپس مقدار دهی آن بر اساس تنظیمات IoC Container برنامه به صورت خودکار صورت خواهد گرفت. برای مثال زمانیکه میخواهیم به سرویس توکار HostingEnvironment در یک View دسترسی پیدا کنیم، میتوان در ابتدای آن نوشت:
@inject Microsoft.AspNetCore.Hosting.IHostingEnvironment Host;
[Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] public Microsoft.AspNetCore.Hosting.IHostingEnvironment Host { get; private set; }
اکنون برای استفادهی از آن خواهیم داشت:
<div> Running in @Host.EnvironmentName </div>
به علاوه باید دقت داشت اگر تعریف inject@ فوق را در فایل view import قرار دهیم، این سرویس در اختیار تمام Viewهای برنامه قرار خواهد گرفت و دیگر نیازی به قرار دادن آن در یک کلاس پایهی سفارشی نیست.
یکی از مفیدترین استفادههای از قابلیت تزریق سرویسها در Viewها میتواند دسترسی به سرویس تامین تنظیمات برنامه باشد (که در مورد نحوهی تامین آن در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایلهای config» بیشتر بحث شد):
@inject IOptions<SmtpConfig> Settings;
حالتهای مختلف ذخیره سازی اطلاعات در مرورگر کاربر
Web Storage و یا Client-side storage در دو حالت کلی session storage و local storage قابل دسترسی است:
الف) session storage
در این حالت اطلاعات ذخیره شدهی در session storage، پس از بسته شدن مرورگر، به صورت خودکار حذف خواهند شد.
ب) local storage
اطلاعات ذخیره شدهی در local storage پس از بسته شدن مرورگر نیز باقی مانده و قابل دسترسی و بازیابی مجدد هستند. تاریخ انقضای آنها صرفا بر اساس خالی شدن دستی کش مرورگر توسط کاربر و یا حذف دستی اطلاعات آن توسط کدهای برنامه تعیین میشود.
هر دو حالت فوق به صورت ایزوله ارائه میشوند؛ با محدودیت حجم 10 مگابایت (جمع حجم نهایی هر دو حالت با هم، محدود به 10 مگابایت است). به این معنا که برنامههای هر دومین، تنها به محل ذخیره سازی خاص همان دومین دسترسی خواهند داشت.
همچنین API دسترسی به آنها synchronous است و کار کردن با آنها سادهاست.
البته Client-side storage به دو مورد فوق خلاصه نمیشود و شامل File Storage ،WebSQL ،IndexedDB و کوکیهای مرورگر نیز هست.
- File Storage هنوز مراحل آزمایشی خودش را طی میکند و مناسب برنامههای دنیای واقعی نیست.
- WebSQL قرار بود بر اساس بانک اطلاعاتی معروف SQLite ارائه شود؛ اما W3C در سال 2010 این استاندارد را منسوخ شده اعلام کرد و با IndexedDB جایگزین شد. دسترسی به آن async است و میتواند موضوع بحثی مجزا باشد.
- کوکیهای مرورگرها نیز یکی دیگر از روشهای ذخیره سازی اطلاعات در مرورگرها هستند و تنها به ذخیره سازی حداکثر 4096 بایت اطلاعات محدود هستند. کوکیها نیز همانند local storage پس از بسته شدن مرورگر باقی میمانند؛ اما برخلاف آن، دارای تاریخ انقضاء و همچنین قابلیت ارسال بین دومینها را نیز دارا میباشند. اگر تاریخ انقضای یک کوکی تعیین نشود، همانند session storage، در پایان کار مرورگر و بسته شدن آن، حذف خواهد شد.
تهیه یک سرویس Angular برای کار با Web Storage
جهت کپسوله سازی نحوهی کار با session storage و local storage میتوان سرویسی را برای اینکار تهیه کرد:
import { Injectable } from "@angular/core"; @Injectable() export class BrowserStorageService { getSession(key: string): any { const data = window.sessionStorage.getItem(key); return JSON.parse(data); } setSession(key: string, value: any): void { const data = value === undefined ? null : JSON.stringify(value); window.sessionStorage.setItem(key, data); } removeSession(key: string): void { window.sessionStorage.removeItem(key); } removeAllSessions(): void { for (const key in window.sessionStorage) { if (window.sessionStorage.hasOwnProperty(key)) { this.removeSession(key); } } } getLocal(key: string): any { const data = window.localStorage.getItem(key); return JSON.parse(data); } setLocal(key: string, value: any): void { const data = value === undefined ? null : JSON.stringify(value); window.localStorage.setItem(key, data); } removeLocal(key: string): void { window.localStorage.removeItem(key); } removeAllLocals(): void { for (const key in window.localStorage) { if (window.localStorage.hasOwnProperty(key)) { this.removeLocal(key); } } } }
در حالت setItem اطلاعاتی را که مرورگرها ذخیره میکنند باید رشتهای باشد. به همین جهت توسط متد JSON.stringify میتوان یک شیء را تبدیل به رشته کرد و ذخیره نمود و در حالت getItem توسط متد JSON.parse، میتوان این رشته را مجددا به همان شیء پیشین خود تبدیل کرد و بازگشت داد.
محل صحیح تعریف BrowserStorageService
همانطور که در مطلب «سازماندهی برنامههای Angular توسط ماژولها» بررسی شد، محل صحیح تعریف این سرویس سراسری مشترک در بین کامپوننتها و ماژولهای برنامه، در CoreModule و پوشهی src\app\core\browser-storage.service.ts است:
import { BrowserStorageService } from "./browser-storage.service"; import { NgModule } from "@angular/core"; import { CommonModule } from "@angular/common"; import { RouterModule } from "@angular/router"; @NgModule({ imports: [CommonModule, RouterModule], exports: [], // components that are used in app.component.ts will be listed here. declarations: [], // components that are used in app.component.ts will be listed here. providers: [BrowserStorageService] // singleton services of the whole app will be listed here. }) export class CoreModule { };
و CoreModule نیز به AppModule اضافه میشود:
import { CoreModule } from "./core/core.module"; @NgModule({ imports: [ //... CoreModule, //... RouterModule.forRoot(appRoutes) ], //... }) export class AppModule { }
بنابراین یکی دیگر از روشهای به اشتراک گذاری اطلاعات در بین قسمتهای مختلف برنامه، ذخیره سازی آنها در session/local storage و سپس بازیابی آنها بر اساس کلیدهای مشخص آنها است.
مثالی از نحوهی کاربرد BrowserStorageService
برای آزمایش سرویس تهیه شده، از کامپوننت و قالب ذیل استفاده خواهیم کرد. در اینجا سرویس BrowserStorageService به سازندهی کلاس تزریق شدهاست و سپس دو حالت session storage و local storage مورد بررسی قرار گرفتهاند:
import { BrowserStorageService } from "./../../core/browser-storage.service"; import { Component, OnInit } from "@angular/core"; @Component({ selector: "app-browser-storage-sample-test", templateUrl: "./browser-storage-sample-test.component.html", styleUrls: ["./browser-storage-sample-test.component.css"] }) export class BrowserStorageSampleTestComponent implements OnInit { fromSessionStorage = ""; fromLocalStorage = "" sessionStorageKey = "sessionStorageKey1"; localStorageKey = "localStorageKey1" constructor(private browserStorage: BrowserStorageService) { } ngOnInit() { } sessionStorageSetItem() { this.browserStorage.setSession(this.sessionStorageKey, "Val1"); } sessionStorageGetItem() { this.fromSessionStorage = this.browserStorage.getSession(this.sessionStorageKey); } localStorageSetItem() { this.browserStorage.setLocal(this.localStorageKey, { key1: "val1", key2: 2 }); } localStorageGetItem() { this.fromLocalStorage = JSON.stringify(this.browserStorage.getLocal(this.localStorageKey)); } }
<h1>Browser storage sample</h1> <div class="panel"> <button class="btn btn-primary" (click)="sessionStorageSetItem()" type="button">sessionStorage -> Set Item</button> <button class="btn btn-success" (click)="sessionStorageGetItem()" type="button">sessionStorage -> Get Item</button> <div class="alert alert-info" *ngIf="fromSessionStorage"> {{fromSessionStorage}} </div> </div> <div class="panel"> <button class="btn btn-warning" (click)="localStorageSetItem()" type="button">localStorage -> Set Item</button> <button class="btn btn-success" (click)="localStorageGetItem()" type="button">localStorage -> Get Item</button> <div class="alert alert-info" *ngIf="fromLocalStorage"> {{fromLocalStorage}} </div> </div>
در این حالت اگر برنامه را اجرا کنیم، یک چنین خروجی قابل مشاهده خواهد بود:
و اگر به برگهی Application کنسول ابزارهای توسعه دهندههای مرورگرها نیز مراجعه کنیم، این مقادیر ثبت شده را در دو حالت استفادهی از session storage و local storage، میتوان مشاهده کرد:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
20- :link
<style> :link { color: red; } </style> <a href="page1.html">Link 1</a> <a>Link 2</a> <a href="page2.html">Link 3</a>
|
|
|
|
| Selector | نسخه CSS |
3.1 | 9.6 | 7.0 | 2.0 | 4.0 | :link | 1 |
21- :visited
<style> :visited { color: green; } </style> <a href="page1.html">Link 1</a> <a>Link 2</a> <a href="page2.html">Link 3</a>
|
|
|
|
| Selector | نسخه CSS |
3.1 | 9.6 | 7.0 | 2.0 | 4.0 | :visited | 1 |
22- :focus
<style> :focus { background: yellow; } </style> <input type="text"/> <input type="password"/> <input type="text" />
|
|
|
|
| Selector | نسخه CSS |
3.1 | 9.6 | 8.0 | 2.0 | 4.0 | :focus | 2 |
23- :hover
<style> input:hover { background: yellow; } </style> <input type="text" /> <input type="password" /> <input type="text" />
|
|
|
|
| Selector | نسخه CSS |
3.1 | 9.6 | 7.0 | 2.0 | 4.0 | :hover | 1 |
24- :active
<style> button:active { background: yellow; } </style> <button>Button 1</button> <button>Button 2</button>
|
|
|
|
| Selector | نسخه CSS |
3.1 | 9.6 | 7.0 | 2.0 | 4.0 | :active | 1 |
25- :target
<style> :target { color: green; } </style> <h2><a href="#part1">Link 1</a></h2> <h2><a href="#part2">Link 2</a></h2> <p>This is a paragraph</p> <h1><a name="part1">Part 1</a></h1> <p> Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas porttitor congue massa. Fusce posuere, magna sed pulvinar ultricies, purus lectus malesuada libero, sit amet commodo magna eros quis urna. </p> <h1><a name="part2">Part 2</a></h1> <p> Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas porttitor congue massa. Fusce posuere, magna sed pulvinar ultricies, purus lectus malesuada libero, sit amet commodo magna eros quis urna. </p>
|
|
|
|
| Selector | نسخه CSS |
3.2 | 9.6 | 9.0 | 3.5 | 4.0 | :target | 3 |
26- :first-letter
<style> :first-letter { font-size: xx-large; } </style> <p> Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas porttitor congue massa. Fusce posuere, magna sed pulvinar ultricies, purus lectus malesuada libero, sit amet commodo magna eros quis urna. </p>
|
|
|
|
| Selector | نسخه CSS |
1.0 | 7.0 | 9.0 | 1.0 | 1.0 | :first-letter | 1 |
27- :first-line
<style> :first-line { color: red; } </style> <p> Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas porttitor congue massa. Fusce posuere, magna sed pulvinar ultricies, purus lectus malesuada libero, sit amet commodo magna eros quis urna. Nunc viverra imperdiet enim. Fusce est. Vivamus a tellus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Proin pharetra nonummy pede. Mauris et orci. Aenean nec lorem. </p>
|
|
|
|
| Selector | نسخه CSS |
1.0 | 7.0 | 9.0 | 1.0 | 1.0 | :first-letter | 1 |
28- :empty
<style> :empty { background: gray; } </style> <table border="1" cellpadding="10" cellspacing="10"> <tr> <td>A</td> <td>B</td> <td></td> </tr> <tr> <td>C</td> <td></td> <td>D</td> </tr> <tr> <td></td> <td>E</td> <td>F</td> </tr> </table>
|
|
|
|
| Selector | نسخه CSS |
3.2 | 9.6 | 9.0 | 3.5 | 4.0 | :target | 3 |
29- :dir(direction)
<style> :dir(rtl) { color: red; } </style> <div dir="rtl">متن 1</div> <div>متن 2</div>
|
|
|
|
| Selector | نسخه CSS |
No | No | No | No | No | :dir(direction) | 4 |
30- :lang(language1, language2,...)
<style> :lang(en) { color: red; } </style> <div lang="en">Text 1</div> <div>Text 2</div> <div lang="en">Text 3</div>
|
|
|
|
| Selector | نسخه CSS |
3.1 | 9.6 | 8.0 | 2.0 | 4.0 | :lang(language1) | 2 |
|
|
|
|
| Selector | نسخه CSS |
No | No | No | No | No | :lang(language1, language2,...) | 4 |
افزودن فیلد آپلود تصاویر، به فرم ثبت اطلاعات یک اتاق
در ادامه به کامپوننت Pages\HotelRoom\HotelRoomUpsert.razor که تا این قسمت آنرا تکمیل کردهایم مراجعه کرده و فیلد جدید InputFile را ذیل قسمت ثبت توضیحات، اضافه میکنیم:
<div class="form-group"> <InputFile OnChange="HandleImageUpload" multiple></InputFile> </div> @code { private async Task HandleImageUpload(InputFileChangeEventArgs args) { } }
- در این کامپوننت، رویداد OnChange، پس از تغییر مجموعهی فایلهای اضافه شدهی به آن، فراخوانی میشود و آرگومانی از نوع InputFileChangeEventArgs را دریافت میکند.
افزودن لیست فایلهای انتخابی به HotelRoomDTO
تا اینجا اگر به BlazorServer.Models\HotelRoomDTO.cs مراجعه کنیم (کلاسی که مدل UI فرم ثبت اطلاعات اتاق را فراهم میکند)، امکان افزودن لیست تصاویر انتخابی به آن وجود ندارد. به همین جهت در این کلاس، تغییر زیر را اعمال میکنیم:
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace BlazorServer.Models { public class HotelRoomDTO { // ... public virtual ICollection<HotelRoomImageDTO> HotelRoomImages { get; set; } = new List<HotelRoomImageDTO>(); } }
تکمیل متد رویدادگردان HandleImageUpload
در ادامه، لیست فایلهای انتخاب شدهی توسط کاربر را دریافت کرده و آنها را آپلود میکنیم:
@inject IHotelRoomService HotelRoomService @inject NavigationManager NavigationManager @inject IJSRuntime JsRuntime @inject IFileUploadService FileUploadService @inject IWebHostEnvironment WebHostEnvironment @code { // ... private async Task HandleImageUpload(InputFileChangeEventArgs args) { var files = args.GetMultipleFiles(maximumFileCount: 5); if (args.FileCount == 0 || files.Count == 0) { return; } var allowedExtensions = new List<string> { ".jpg", ".png", ".jpeg" }; if(!files.Any(file => allowedExtensions.Contains(Path.GetExtension(file.Name), StringComparer.OrdinalIgnoreCase))) { await JsRuntime.ToastrError("Please select .jpg/.jpeg/.png files only."); return; } foreach (var file in files) { var uploadedImageUrl = await FileUploadService.UploadFileAsync(file, WebHostEnvironment.WebRootPath, "Uploads"); HotelRoomModel.HotelRoomImages.Add(new HotelRoomImageDTO { RoomImageUrl = uploadedImageUrl }); } } }
@using Microsoft.AspNetCore.Hosting @using System.Linq @using System.IO
- متد ()args.GetMultipleFiles، امکان دسترسی به فایلهای انتخابی توسط کاربر را میسر میکند که خروجی آن از نوع <IReadOnlyList<IBrowserFile است. در قسمت قبل، سرویس آپلود فایلهایی را که تکمیل کردیم، امکان آپلود یک IBrowserFile را به سرور میسر میکند. اگر متد ()GetMultipleFiles را بدون پارامتری فراخوانی کنیم، حداکثر 10 فایل را قبول میکند و اگر تعداد بیشتری انتخاب شده باشد، یک استثناء را صادر خواهد کرد.
- سپس بر اساس پسوند فایلهای دریافتی، آنها را صرفا به فایلهای تصویری محدود کردهایم.
- در آخر، لیست فایلهای دریافتی را یکی یکی به سرور آپلود کرده و Url دسترسی به آنها را به لیست HotelRoomImages اضافه میکنیم. فایلهای آپلود شده در پوشهی BlazorServer.App\wwwroot\Uploads قابل مشاهده هستند.
نمایش فایلهای انتخاب شدهی توسط کاربر
در ادامه میخواهیم پس از آپلود فایلها، آنها را در ذیل کامپوننت InputFile نمایش دهیم. برای اینکار در ابتدا به فایل wwwroot\css\site.css مراجعه کرده و شیوه نامهی نمایش تصاویر و عناوین آنها را اضافه میکنیم:
.room-image { display: block; width: 100%; height: 150px; background-size: cover !important; border: 3px solid green; position: relative; } .room-image-title { position: absolute; top: 0; right: 0; background-color: green; color: white; padding: 0px 6px; display: inline-block; }
<div class="form-group"> <InputFile OnChange="HandleImageUpload" multiple></InputFile> <div class="row"> @if (HotelRoomModel.HotelRoomImages.Count > 0) { var serial = 1; foreach (var roomImage in HotelRoomModel.HotelRoomImages) { <div class="col-md-2 mt-3"> <div class="room-image" style="background: url('@roomImage.RoomImageUrl') 50% 50%; "> <span class="room-image-title">@serial</span> </div> <button type="button" class="btn btn-outline-danger btn-block mt-4">Delete</button> </div> serial++; } } </div> </div>
ذخیره سازی اطلاعات تصاویر آپلودی یک اتاق در بانک اطلاعاتی
تا اینجا موفق شدیم تصاویر انتخابی کاربر را آپلود کرده و همچنین لیست آنها را نیز نمایش دهیم. در ادامه نیاز است تا این اطلاعات را در بانک اطلاعاتی ثبت کنیم. به همین جهت ابتدا سرویس IHotelRoomImageService را که در قسمت قبل تکمیل کردیم، به کامپوننت جاری تزریق میکنیم و سپس با استفاده از متد CreateHotelRoomImageAsync، رکوردهای تصویر متناظر با اتاق ثبت شده را اضافه میکنیم:
// ... @inject IHotelRoomImageService HotelRoomImageService @code { // ... private async Task AddHotelRoomImageAsync(HotelRoomDTO roomDto) { foreach (var imageDto in HotelRoomModel.HotelRoomImages) { imageDto.RoomId = roomDto.Id; await HotelRoomImageService.CreateHotelRoomImageAsync(imageDto); } } }
HotelRoomModel.HotelRoomImages.Add(new HotelRoomImageDTO { RoomImageUrl = uploadedImageUrl });
محل فراخوانی AddHotelRoomImageAsync فوق، در متد HandleHotelRoomUpsert است که در قسمتهای قبل تکمیل کردیم. در اینجا پس از ثبت اطلاعات اتاق در بانک اطلاعاتی است که به Id آن دسترسی پیدا میکنیم:
private async Task HandleHotelRoomUpsert() { // ... // Create Mode var createdRoomDto = await HotelRoomService.CreateHotelRoomAsync(HotelRoomModel); await AddHotelRoomImageAsync(createdRoomDto); await JsRuntime.ToastrSuccess($"The `{HotelRoomModel.Name}` created successfully."); // ... }
یک نکته: در انتهای بحث خواهیم دید که اینکار غیرضروری است و با وجود رابطهی one-to-many تعریف شدهی توسط EF-Core، اگر لیست HotelRoomImages موجودیت اتاق تعریف شده و در حال ثبت نیز مقدار دهی شده باشد، به صورت خودکار جزئی از این رابطه و تنها در یک رفت و برگشت، ثبت میشود. یعنی همان متد CreateHotelRoomAsync، قابلیت ثبت خودکار اطلاعات خاصیت HotelRoomImages موجودیت اتاق را نیز دارا است.
نمایش تصاویر یک اتاق، در حالت ویرایش رکورد آن
تا اینجا فقط حالت ثبت یک رکورد جدید را پوشش دادیم. در این حالت اگر به لیست اتاقهای ثبت شده مراجعه کرده و بر روی دکمهی edit یکی از آنها کلیک کنیم، به صفحهی ویرایش رکورد منتقل خواهیم شد؛ اما این صفحه، فاقد اطلاعات تصاویر منتسب به آن رکورد است.
علت اینجا است که در حین ویرایش اطلاعات، در متد OnInitializedAsync، هرچند اطلاعات یک اتاق را از بانک اطلاعاتی دریافت کرده و آنرا تبدیل به Dto آن میکنیم که سبب نمایش جزئیات هر خاصیت در فیلد متصل به آن در فرم جاری میشود:
protected override async Task OnInitializedAsync() { if (Id.HasValue) { // Update Mode Title = "Update"; HotelRoomModel = await HotelRoomService.GetHotelRoomAsync(Id.Value); } // ... }
بنابراین به فایل BlazorServer\BlazorServer.Services\HotelRoomService.cs مراجعه کرده و تغییرات زیر را اعمال میکنیم:
namespace BlazorServer.Services { public class HotelRoomService : IHotelRoomService { // ... public IAsyncEnumerable<HotelRoomDTO> GetAllHotelRoomsAsync() { return _dbContext.HotelRooms .Include(x => x.HotelRoomImages) .ProjectTo<HotelRoomDTO>(_mapperConfiguration) .AsAsyncEnumerable(); } public Task<HotelRoomDTO> GetHotelRoomAsync(int roomId) { return _dbContext.HotelRooms .Include(x => x.HotelRoomImages) .ProjectTo<HotelRoomDTO>(_mapperConfiguration) .FirstOrDefaultAsync(x => x.Id == roomId); } } }
افزودن تصاویر جدید، در حین ویرایش یک رکورد
پس از نمایش لیست تصاویر منتسب به یک اتاق در حال ویرایش، اکنون میخواهیم در همین حالت اگر کاربر تصویر جدیدی را انتخاب کرد، این تصویر را نیز به لیست تصاویر ثبت شدهی در بانک اطلاعاتی اضافه کنیم. برای اینکار نیز به متد HandleHotelRoomUpsert مراجعه کرده و از متد AddHotelRoomImageAsync در قسمت به روز رسانی آن استفاده میکنیم:
private async Task HandleHotelRoomUpsert() { //... // Update Mode var updatedRoomDto = await HotelRoomService.UpdateHotelRoomAsync(HotelRoomModel.Id, HotelRoomModel); await AddHotelRoomImageAsync(updatedRoomDto); await JsRuntime.ToastrSuccess($"The `{HotelRoomModel.Name}` updated successfully."); //... }
برای رفع این مشکل میتوان در متد AddHotelRoomImageAsync، جدید بودن یک تصویر را بر اساس RoomId آن بررسی کرد. اگر این RoomId مساوی صفر بود، یعنی تازه به لیست اضافه شدهاست و حاصل بارگذاری اولیهی فرم ویرایش اطلاعات نیست:
private async Task AddHotelRoomImageAsync(HotelRoomDTO roomDto) { foreach (var imageDto in HotelRoomModel.HotelRoomImages.Where(x => x.RoomId == 0)) { imageDto.RoomId = roomDto.Id; await HotelRoomImageService.CreateHotelRoomImageAsync(imageDto); } }
یک نکته: متد AddHotelRoomImageAsync اضافی است!
چون از AutoMapper استفاده میکنیم، در ابتدای متد ثبت یک اتاق، کار نگاشت DTO، به موجودیت متناظر با آن انجام میشود:
public async Task<HotelRoomDTO> CreateHotelRoomAsync(HotelRoomDTO hotelRoomDTO) { var hotelRoom = _mapper.Map<HotelRoom>(hotelRoomDTO);
مقدار دهی RoomId یک تصویر، در اینجا غیرضروری است؛ چون RoomId و Room، به عنوان کلید خارجی این رابطه تعریف شدهاند که در اینجا Room یک تصویر، دقیقا همین اتاق در حال ثبت است و EF Core در حین ثبت نهایی، آنرا به صورت خودکار در تمام تصاویر مرتبط نیز مقدار دهی میکند.
یعنی نیازی به چندین بار رفت و برگشت تعریف شدهی در متد AddHotelRoomImageAsync نیست و اساسا نیازی به آن نیست؛ نه برای ثبت و نه برای ویرایش اطلاعات!
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-17.zip
Asp.Net Identity #3
using System.Web; using System.Web.Mvc; using Microsoft.AspNet.Identity.Owin; using Users.Infrastructure; namespace Users.Controllers { public class HomeController : Controller { private AppUserManager UserManager { get { return HttpContext.GetOwinContext().GetUserManager<AppUserManager>(); } } // GET: Home public ActionResult Index() { return View(UserManager.Users); } }
@using Users.Models @model IEnumerable<AppUser> @{ ViewBag.Title = "Index"; } <div class="panel panel-primary"> <div class="panel-heading"> User Accounts </div> <table class="table table-striped"> <tr><th>ID</th><th>Name</th><th>Email</th></tr> @if (!Model.Any()) { <tr><td colspan="3" class="text-center">No User Accounts</td></tr> } else { foreach (AppUser user in Model) { <tr> <td>@user.Id</td> <td>@user.UserName</td> <td>@user.Email</td> </tr> } } </table> </div> @Html.ActionLink("Create", "CreateUser", null, new { @class = "btn btn-primary" })
نحوهی ساخت یک کاربر جدید
namespace Users.Models { public class CreateModel { [Required] public string Name { get; set; } [Required] public string Email { get; set; } [Required] public string Password { get; set; } } }
public ActionResult CreateUser() { return View(); } [HttpPost] public async Task<ActionResult> CreateUser(CreateModel model) { if (!ModelState.IsValid) return View(model); var user = new AppUser { UserName = model.Name, Email = model.Email }; var result = await UserManager.CreateAsync(user, model.Password); if (result.Succeeded) { return RedirectToAction("Index"); } foreach (var error in result.Errors) { ModelState.AddModelError("", error); } return View(model); }
@model Users.ViewModels.CreateModel @Html.ValidationSummary(false) @using (Html.BeginForm()) { <div class="form-group"> <label>Name</label> @Html.TextBoxFor(x => x.UserName, new { @class = "form-control" }) </div> <div class="form-group"> <label>Email</label> @Html.TextBoxFor(x => x.Email, new { @class = "form-control" }) </div> <div class="form-group"> <label>Password</label> @Html.PasswordFor(x => x.Password, new { @class = "form-control" }) </div> <button type="submit" class="btn btn-primary">Create</button> @Html.ActionLink("Cancel", "Index", null, new { @class = "btn btn-default" }) }
اعتبار سنجی رمز
var manager = new AppUserManager(new UserStore<AppUser>(db)) { PasswordValidator = new PasswordValidator { RequiredLength = 6, RequireNonLetterOrDigit = false, RequireDigit = false, RequireLowercase = true, RequireUppercase = true } };
فقط دوستان توجه داشته باشید که کد بالا را در متد Create از کلاس AppUserManager استفاده کنید.
اعتبار سنجی نام کاربری
برای اعبارسنجی نام کاربری از کلاس UserValidator به صورت زیر استفاده میکنیم:
manager.UserValidator = new UserValidator<AppUser>(manager) { AllowOnlyAlphanumericUserNames = true, RequireUniqueEmail = true };
کد بالا را نیز در متد Create از کلاس AppUserManager قرار میدهیم.