.
تولید یک پرووایدر منابع دیتابیسی - بخش سوم
برای پیادهسازی ویژگی بهروزرسانی ورودیهای منابع در زمان اجرا راهحلهای مخنلفی ممکن است به ذهن برنامهنویس خطور کند که هر کدام معایب و مزایای خودش را دارد. اما درنهایت بسته به شرایط موجود انتخاب روش مناسب برعهده خود برنامهنویس است.
مثلا برای پرووایدر سفارشی دیتابیسی تهیهشده در مطالب قبلی، تنها کافی است ابزاری تهیه شود تا به کاربران اجازه بهروزرسانی مقادیر موردنظرشان در دیتابیس را بدهد که کاری بسیار ساده است. بدین ترتیب بهروزرسانی این مقادیر در زمان اجرا کاری بسیار ابتدایی به نظر میرسد. اما در قسمت قبل نشان داده شد که برای بالا بردن بازدهی بهتر است که مقادیر موجود در دیتابیس در حافظه سرور کش شوند. استراتژی اولیه و سادهای نیز برای نحوه پیادهسازی این فرایند کشینگ ارائه شد. بنابراین باید امکاناتی فراهم شود تا درصورت تغییر مقادیر کششده در سمت دیتابیس، برنامه از این تغییرات آگاه شده و نسبت به بهروزرسانی این مقادیر در متغیر کشینگ اقدامات لازم را انجام دهد.
اما همانطور که در قسمت قبل نیز اشاره شد، نکتهای که باید درنظر داشت این است که مدیریت تمامی نمونههای تولیدشده از کلاسهای موردبحث کاملا برعهده ASP.NET است، بنابراین دسترسی مستقیمی به این نمونهها در بیرون و در زمان اجرا وجود ندارد تا این ویژگی را بتوان در مورد آنها پیاده کرد.
یکی از روشهای موجود برای حل این مشکل این است که مکانیزمی پیاده شود تا بتوان به تمامی نمونههای تولیدی از کلاس DbResourceManager در بیرون از محیط سیستم مدیریت منابع ASP.NET دسترسی داشت. مثلا یک کلاس حاول متغیری استاتیک جهت ذخیره نمونههای تولیدی از کلاس DbResourceManager، به کتابخانه خود اضافه کرد تا با استفاده از یکسری امکانات بتوان این نمونههای تولیدی را از تغییرات رخداده در سمت دیتابیس آگاه کرد. در این قسمت پیادهسازی این راهحل شرح داده میشود.
.
نکته: قبل از هرچیز برای مناسب شدن طراحی کتابخانه تولیدی و افزایش امنیت آن بهتر است تا سطح دسترسی تمامی کلاسهای پیادهسازی شده تا این مرحله به internal تغییر کند. ازآنجاکه سیستم مدیریت منابع ASP.NET از ریفلکشن برای تولید نمونههای موردنیاز خود استفاده میکند، بنابراین این تغییر تاثیری بر روند کاری آن نخواهد گذاشت.
.
نکته: با توجه به شرایط خاص موجود، ممکن است نامهای استفاده شده برای کلاسهای این کتابخانه کمی گیجکننده باشد. پس با دقت بیشتری به مطلب توجه کنید.
.
پیادهسازی امکان پاکسازی مقادیر کششده
برای اینکار باید تغییراتی در کلاس DbResourceManager داده شود تا بتوان این کلاس را از تغییرات بوجود آمده آگاه ساخت. روشی که من برای این کار درنظر گرفتم استفاده از یک اینترفیس حاوی اعضای موردنیاز برای پیادهسازی این امکان است تا مدیریت این ویژگی در ادامه راحتتر شود.
.
اینترفیس IDbCachedResourceManager
این اینترفیس به صورت زیر تعریف شده است:
namespace DbResourceProvider { internal interface IDbCachedResourceManager { string ResourceName { get; } void ClearAll(); void Clear(string culture); void Clear(string culture, string resourceKey); } }
در پراپرتی فقط خواندنی ResourceName نام منبع کش شده ذخیره خواهد شد.
متد ClearAll برای پاکسازی تمامی ورودیهای کششده استفاده میشود.
متدهای Clear برای پاکسازی ورودیهای کششده یک کالچر به خصوص و یا یک ورودی خاص استفاده میشود.
با استفاده از این اینترفیس، پیادهسازی کلاس DbResourceManager به صورت زیر تغییر میکند:
using System.Collections.Generic; using System.Globalization; using DbResourceProvider.Data; namespace DbResourceProvider { internal class DbResourceManager : IDbCachedResourceManager { private readonly string _resourceName; private readonly Dictionary<string, Dictionary<string, object>> _resourceCacheByCulture; public DbResourceManager(string resourceName) { _resourceName = resourceName; _resourceCacheByCulture = new Dictionary<string, Dictionary<string, object>>(); } public object GetObject(string resourceKey, CultureInfo culture) { ... } private object GetCachedObject(string resourceKey, string cultureName) { ... } #region Implementation of IDbCachedResourceManager public string ResourceName { get { return _resourceName; } } public void ClearAll() { lock (this) { _resourceCacheByCulture.Clear(); } } public void Clear(string culture) { lock (this) { if (!_resourceCacheByCulture.ContainsKey(culture)) return; _resourceCacheByCulture[culture].Clear(); } } public void Clear(string culture, string resourceKey) { lock (this) { if (!_resourceCacheByCulture.ContainsKey(culture)) return; _resourceCacheByCulture[culture].Remove(resourceKey); } } #endregion } }
اعضای اینترفیس IDbCachedResourceManager به صورت مناسبی در کد بالا پیادهسازی شدند. در تمام این پیادهسازیها مقادیر مربوطه از درون متغیر کشینگ پاک میشوند تا پس از اولین درخواست، بلافاصله از دیتابیس خوانده شوند. برای جلوگیری از دسترسی همزمان نیز از بلاک lock استفاده شده است.
برای استفاده از این امکانات جدید همانطور که در بالا نیز اشاره شد باید بتوان نمونههای تولیدی از کلاس DbResourceManager توسط ASP.NET درون متغیری استاتیک ذخیره شوند. برای اینکار از کلاس جدیدی با عنوان DbResourceCacheManager استفاده میشود که برخلاف تمام کلاسهای تعریفشده تا اینجا با سطح دسترسی public تعریف میشود.
کلاس DbResourceCacheManager
مدیریت نمونههای تولیدی از کلاس DbResourceManager در این کلاس انجام میشود. این کلاس پیادهسازی سادهای بهصورت زیر دارد:
using System.Collections.Generic; using System.Linq; namespace DbResourceProvider { public static class DbResourceCacheManager { internal static List<IDbCachedResourceManager> ResourceManagers { get; private set; } static DbResourceCacheManager() { ResourceManagers = new List<IDbCachedResourceManager>(); } public static void ClearAll() { ResourceManagers.ForEach(r => r.ClearAll()); } public static void Clear(string resourceName) { GetResouceManagers(resourceName).ForEach(r => r.ClearAll()); } public static void Clear(string resourceName, string culture) { GetResouceManagers(resourceName).ForEach(r => r.Clear(culture)); } public static void Clear(string resourceName, string culture, string resourceKey) { GetResouceManagers(resourceName).ForEach(r => r.Clear(culture, resourceKey)); } private static List<IDbCachedResourceManager> GetResouceManagers(string resourceName) { return ResourceManagers.Where(r => r.ResourceName.ToLower() == resourceName.ToLower()).ToList(); } } }
ازآنجاکه نیازی به تولید نمونه ای از این کلاس وجود ندارد، این کلاس به صورت استاتیک تعریف شده است. بنابراین تمام اعضای درون آن نیز استاتیک هستند.
از پراپرتی ResourceManagers برای نگهداری لیستی از نمونههای تولیدی از کلاس DbResourceManager استفاده میشود. این پراپرتی از نوع <List<IDbCachedResourceManager تعریف شده است و برای جلوگیری از دسترسی بیرونی، سطح دسترسی آن internal درنظر گرفته شده است.
در کانستراکتور استاتیک این کلاس (اطلاعات بیشتر درباره static constructor در اینجا) این پراپرتی با مقداردهی به یک نمونه تازه از لیست، اصطلاحا initialize میشود.
سایر متدها نیز برای فراخوانی متدهای موجود در اینترفیس IDbCachedResourceManager پیادهسازی شدهاند. تمامی این متدها دارای سطح دسترسی public هستند. همانطور که میبینید از خاصیت ResourceName برای مشخصکردن نمونه موردنظر استفاده شده است که دلیل آن در قسمت قبل شرح داده شده است.
دقت کنید که برای اطمینان از انتخاب درست همه موارد موجود در شرط انتخاب نمونه موردنظر در متد GetResouceManagers از متد ToLower برای هر دو سمت شرط استفاده شده است.
.
نکته مهم: درباره علت برگشت یک لیست از متد انتخاب نمونه موردنظر از کلاس DbResourceManager در کد بالا (یعنی متد GetResouceManagers) باید نکتهای اشاره شود. در قسمت قبل عنوان شد که سیستم مدیریت منابع ASP.NET نمونههای تولیدی از پرووایدرهای منابع را به ازای هر منبع کش میکند. اما یک نکته بسیار مهم که باید به آن توجه کرد این است که این کش برای «عبارات بومیسازی ضمنی» و نیز «متد مربوط به منابع محلی» موجود در کلاس HttpContext و یا نمونه مشابه آن در کلاس TemplateControl (همان متد GetLocalResourceObject که درباره این متدها در قسمت سوم این سری شرح داده شده است) از یکدیگر جدا هستند و استفاده از هریک از این دو روش موجب تولید یک نمونه مجزا از پرووایدر مربوطه میشود که متاسفانه کنترل آن از دست برنامه نویس خارج است. دقت کنید که این اتفاق برای منابع کلی رخ نمیدهد.
بنابراین برای پاک کردن مناسب ورودیهای کششده در کلاس فوق به جای استفاده از متد Single در انتخاب نمونه موردنظر از کلاس DbResourceManager (در متد GetResouceManagers) از متد Where استفاده شده و یک لیست برگشت داده میشود. چون با توجه به توضیح بالا امکان وجود دو نمونه DbResourceManager از یک منبع درخواستی محلی در لیست نمونههای نگهداری شده در این کلاس وجود دارد.
.
افزودن نمونهها به کلاس DbResourceCacheManager
برای نگهداری نمونههای تولید شده از DbResourceManager، باید در یک قسمت مناسب این نمونهها را به لیست مربوطه در کلاس DbResourceCacheManager اضافه کرد. بهترین مکان برای انجام این عمل در کلاس پایه BaseDbResourceProvider است که درخواست تولید نمونه را در متد EnsureResourceManager درصورت نال بودن آن میدهد. بنابراین این متد را به صورت زیر تغییر میدهیم:
private void EnsureResourceManager() { if (_resourceManager != null) return; { _resourceManager = CreateResourceManager(); DbResourceCacheManager.ResourceManagers.Add(_resourceManager); } }
تا اینجا کار پیادهسازی امکان مدیریت مقادیر کششده در کتابخانه تولیدی به پایان رسیده است.
.
استفاده از کلاس DbResourceCacheManager
پس از پیادهسازی تمامی موارد لازم، حالتی را درنظر بگیرید که مقادیر ورودیهای تعریف شده در منبع "dir1/page1.aspx" تغییر کرده است. بنابراین برای بروزرسانی مقادیر کششده کافی است تا از کدی مثل کد زیر استفاده شود:
DbResourceCacheManager.Clear("dir1/page1.aspx");
کد بالا کل ورودیهای کششده برای منبع "dir1/page1.aspx" را پاک میکند. برای پاک کردن کالچر یا یک ورودی خاص نیز میتوان از کدهایی مشابه زیر استفاده کرد:
DbResourceCacheManager.Clear("Default.aspx", "en-US"); DbResourceCacheManager.Clear("GlobalTexts", "en-US", "Yes");
.
دریافت کد پروژه
کد کامل پروژه DbResourceProvider به همراه مثال و اسکریپتهای دیتابیسی مربوطه از لینک زیر قابل دریافت است:
برای استفاده از این مثال ابتدا باید کتابخانه Entity Framework (با نام EntityFramework.dll) را مثلا از طریق نوگت دریافت کنید. نسخهای که من در این مثال استفاده کردم نسخه 4.4 با حجم حدود 1 مگابایت است.
نکته: در این کد یک بهبود جزئی اما مهم در کلاس ResourceData اعمال شده است. در قسمت سوم این سری، اشاره شد که نام ورودیهای منابع Case Sensitive نیست. بنابراین برای پیادهسازی این ویژگی، متدهای این کلاس باید به صورت زیر تغییر کنند:
public Resource GetResource(string resourceKey, string culture) { using (var data = new TestContext()) { return data.Resources.SingleOrDefault(r => r.Name.ToLower() == _resourceName.ToLower() && r.Key.ToLower() == resourceKey.ToLower() && r.Culture == culture); } } public List<Resource> GetResources(string culture) { using (var data = new TestContext()) { return data.Resources.Where(r => r.Name.ToLower() == _resourceName.ToLower() && r.Culture == culture).ToList(); } }
.
در آینده...
در ادامه مطالب، بحث تهیه پرووایدر سفارشی فایلهای resx. برای پیادهسازی امکان بهروزرسانی در زمان اجرا ارائه خواهد شد. بعد از پایان تهیه این پرووایدر سفارشی، این سری مطالب با ارائه نکات استفاده از این پرووایدرها در ASP.NET MVC پایان خواهد یافت.
.
منابع
تهیه سرویس اطلاعات پویای برنامه
سرویس Web API ارائه شدهی توسط ASP.NET Core در این برنامه، لیست کاربران را به همراه یادداشتهای آنها به سمت کلاینت باز میگرداند و ساختار موجودیتهای آنها به صورت زیر است:
موجودیت کاربر که یک رابطهی one-to-many را با UserNotes دارد:
using System; using System.Collections.Generic; namespace MaterialAspNetCoreBackend.DomainClasses { public class User { public User() { UserNotes = new HashSet<UserNote>(); } public int Id { set; get; } public DateTimeOffset BirthDate { set; get; } public string Name { set; get; } public string Avatar { set; get; } public string Bio { set; get; } public ICollection<UserNote> UserNotes { set; get; } } }
using System; namespace MaterialAspNetCoreBackend.DomainClasses { public class UserNote { public int Id { set; get; } public DateTimeOffset Date { set; get; } public string Title { set; get; } public User User { set; get; } public int UserId { set; get; } } }
public interface IUsersService { Task<List<User>> GetAllUsersIncludeNotesAsync(); Task<User> GetUserIncludeNotesAsync(int id); }
namespace MaterialAspNetCoreBackend.WebApp.Controllers { [Route("api/[controller]")] public class UsersController : Controller { private readonly IUsersService _usersService; public UsersController(IUsersService usersService) { _usersService = usersService ?? throw new ArgumentNullException(nameof(usersService)); } [HttpGet] public async Task<IActionResult> Get() { return Ok(await _usersService.GetAllUsersIncludeNotesAsync()); } [HttpGet("{id:int}")] public async Task<IActionResult> Get(int id) { return Ok(await _usersService.GetUserIncludeNotesAsync(id)); } } }
در این حالت اگر برنامه را اجرا کنیم، در مسیر زیر
https://localhost:5001/api/users
و آدرس https://localhost:5001/api/users/1 صرفا مشخصات اولین کاربر را بازگشت میدهد.
تنظیم محل تولید خروجی Angular CLI
ساختار پوشه بندی پروژهی جاری به صورت زیر است:
همانطور که ملاحظه میکنید، کلاینت Angular در یک پوشهاست و برنامهی سمت سرور ASP.NET Core در پوشهای دیگر. برای اینکه خروجی نهایی Angular CLI را به پوشهی wwwroot پروژهی وب کپی کنیم، فایل angular.json کلاینت Angular را به صورت زیر ویرایش میکنیم:
"build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "../MaterialAspNetCoreBackend/MaterialAspNetCoreBackend.WebApp/wwwroot",
ng build --no-delete-output-path --watch
dotnet watch run
بنابراین دو صفحهی کنسول مجزا را باز کنید. در اولی ng build (را با پارامترهای یاد شده در پوشهی MaterialAngularClient) و در دومی dotnet watch run را در پوشهی MaterialAspNetCoreBackend.WebApp اجرا نمائید.
هر دو دستور در حالت watch اجرا میشوند. مزیت مهم آن این است که اگر تغییر کوچکی را در هر کدام از پروژهها ایجاد کردید، صرفا همان قسمت کامپایل میشود و در نهایت سرعت کامپایل نهایی برنامه به شدت افزایش خواهد یافت.
تعریف معادلهای کلاسهای موجودیتهای سمت سرور، در برنامهی Angular
در ادامه پیش از تکمیل سرویس دریافت اطلاعات از سرور، نیاز است معادلهای کلاسهای موجودیتهای سمت سرور خود را به صورت اینترفیسهایی تایپاسکریپتی تعریف کنیم:
ng g i contact-manager/models/user ng g i contact-manager/models/user-note
محتویات فایل contact-manager\models\user-note.ts :
export interface UserNote { id: number; title: string; date: Date; userId: number; }
import { UserNote } from "./user-note"; export interface User { id: number; birthDate: Date; name: string; avatar: string; bio: string; userNotes: UserNote[]; }
ایجاد سرویس Angular دریافت اطلاعات از سرور
ساختار ابتدایی سرویس دریافت اطلاعات از سرور را توسط دستور زیر ایجاد میکنیم:
ng g s contact-manager/services/user --no-spec
import { Injectable } from "@angular/core"; @Injectable({ providedIn: "root" }) export class UserService { constructor() { } }
کدهای تکمیل شدهی UserService را در ذیل مشاهده میکنید:
import { HttpClient, HttpErrorResponse } from "@angular/common/http"; import { Injectable } from "@angular/core"; import { Observable, throwError } from "rxjs"; import { catchError, map } from "rxjs/operators"; import { User } from "../models/user"; @Injectable({ providedIn: "root" }) export class UserService { constructor(private http: HttpClient) { } getAllUsersIncludeNotes(): Observable<User[]> { return this.http .get<User[]>("/api/users").pipe( map(response => response || []), catchError((error: HttpErrorResponse) => throwError(error)) ); } getUserIncludeNotes(id: number): Observable<User> { return this.http .get<User>(`/api/users/${id}`).pipe( map(response => response || {} as User), catchError((error: HttpErrorResponse) => throwError(error)) ); } }
- متد getAllUsersIncludeNotes، لیست تمام کاربران را به همراه یادداشتهای آنها از سرور واکشی میکند.
- متد getUserIncludeNotes صرفا اطلاعات یک کاربر را به همراه یادداشتهای او از سرور دریافت میکند.
بارگذاری و معرفی فایل svg نمایش avatars کاربران به Angular Material
بستهی Angular Material و کامپوننت mat-icon آن به همراه یک MatIconRegistry نیز هست که قصد داریم از آن برای نمایش avatars کاربران استفاده کنیم.
در قسمت اول، نحوهی «افزودن آیکنهای متریال به برنامه» را بررسی کردیم که در آنجا آیکنهای مرتبط، از فایلهای قلم، دریافت و نمایش داده میشوند. این کامپوننت، علاوه بر قلم آیکنها، از فایلهای svg حاوی آیکنها نیز پشتیبانی میکند که یک نمونه از این فایلها در مسیر wwwroot\assets\avatars.svg فایل پیوستی انتهای مطلب کپی شدهاست (چون برنامهی وب ASP.NET Core، هاست برنامه است، این فایل را در آنجا کپی کردیم).
ساختار این فایل svg نیز به صورت زیر است:
<?xml version="1.0" encoding="utf-8"?> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <defs> <svg viewBox="0 0 128 128" height="100%" width="100%" pointer-events="none" display="block" id="user1" >
ابتدا به فایل contact-manager-app.component.ts مراجعه و سپس این کامپوننت آغازین ماژول مدیریت تماسها را با صورت زیر تکمیل میکنیم:
import { Component } from "@angular/core"; import { MatIconRegistry } from "@angular/material"; import { DomSanitizer } from "@angular/platform-browser"; @Component() export class ContactManagerAppComponent { constructor(iconRegistry: MatIconRegistry, sanitizer: DomSanitizer) { iconRegistry.addSvgIconSet(sanitizer.bypassSecurityTrustResourceUrl("assets/avatars.svg")); } }
در اینجا در صورتیکه فایل svg شما دارای یک تک آیکن است، روش ثبت آن به صورت زیر است:
iconRegistry.addSvgIcon( "unicorn", this.domSanitizer.bypassSecurityTrustResourceUrl("assets/unicorn_icon.svg") );
<mat-icon svgIcon="unicorn"></mat-icon>
یک نکته: پوشهی node_modules\material-design-icons به همراه تعداد قابل ملاحظهای فایل svg نیز هست.
نمایش لیست کاربران در sidenav
در ادامه به فایل sidenav\sidenav.component.ts مراجعه کرده و سرویس فوق را به آن جهت دریافت لیست کاربران، تزریق میکنیم:
import { User } from "../../models/user"; import { UserService } from "../../services/user.service"; @Component() export class SidenavComponent implements OnInit { users: User[] = []; constructor(private userService: UserService) { } ngOnInit() { this.userService.getAllUsersIncludeNotes() .subscribe(data => this.users = data); } }
اکنون میخواهیم از این اطلاعات جهت نمایش پویای آنها در sidenav استفاده کنیم. در قسمت قبل، جای آنها را در منوی سمت چپ صفحه به صورت زیر با اطلاعات ایستا مشخص کردیم:
<mat-list> <mat-list-item>Item 1</mat-list-item> <mat-list-item>Item 2</mat-list-item> <mat-list-item>Item 3</mat-list-item> </mat-list>
<mat-nav-list> <mat-list-item *ngFor="let user of users"> <a matLine href="#"> <mat-icon svgIcon="{{user.avatar}}"></mat-icon> {{ user.name }} </a> </mat-list-item> </mat-nav-list>
که در اینجا علاوه بر لیست کاربران که از سرویس Users دریافت شده، آیکن avatar آنها که از فایل assets/avatars.svg بارگذاری شده نیز قابل مشاهده است.
اتصال کاربران به صفحهی نمایش جزئیات آنها
در mat-nav-list فوق، فعلا هر کاربر به آدرس # لینک شدهاست. در ادامه میخواهیم با کمک سیستم مسیریابی، با کلیک بر روی نام هر کاربر، در سمت راست صفحه جزئیات او نیز نمایش داده شود:
<mat-nav-list> <mat-list-item *ngFor="let user of users"> <a matLine [routerLink]="['/contactmanager', user.id]"> <mat-icon svgIcon="{{user.avatar}}"></mat-icon> {{ user.name }} </a> </mat-list-item> </mat-nav-list>
const routes: Routes = [ { path: "", component: ContactManagerAppComponent, children: [ { path: ":id", component: MainContentComponent }, { path: "", component: MainContentComponent } ] }, { path: "**", redirectTo: "" } ];
این مشکل دو علت دارد:
الف) چون ContactManagerModule را به صورت lazy load تعریف کردهایم، دیگر نباید در لیست imports فایل AppModule ظاهر شود. بنابراین فایل app.module.ts را گشوده و سپس تعریف ContactManagerModule را هم از قسمت imports بالای صفحه و هم از قسمت imports ماژول حذف کنید؛ چون نیازی به آن نیست.
ب) برای مدیریت خواندن id کاربر، فایل main-content\main-content.component.ts را گشوده و به صورت زیر تکمیل میکنیم:
import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { User } from "../../models/user"; import { UserService } from "../../services/user.service"; @Component({ selector: "app-main-content", templateUrl: "./main-content.component.html", styleUrls: ["./main-content.component.css"] }) export class MainContentComponent implements OnInit { user: User; constructor(private route: ActivatedRoute, private userService: UserService) { } ngOnInit() { this.route.params.subscribe(params => { this.user = null; const id = params["id"]; if (!id) { return; } this.userService.getUserIncludeNotes(id) .subscribe(data => this.user = data); }); } }
اکنون میتوان از اطلاعات این user دریافتی، در قالب این کامپوننت و یا همان فایل main-content.component.html استفاده کرد:
<div *ngIf="!user"> <mat-spinner></mat-spinner> </div> <div *ngIf="user"> <mat-card> <mat-card-header> <mat-icon mat-card-avatar svgIcon="{{user.avatar}}"></mat-icon> <mat-card-title> <h2>{{ user.name }}</h2> </mat-card-title> <mat-card-subtitle> Birthday {{ user.birthDate | date:'d LLLL' }} </mat-card-subtitle> </mat-card-header> <mat-card-content> <mat-tab-group> <mat-tab label="Bio"> <p> {{user.bio}} </p> </mat-tab> <!-- <mat-tab label="Notes"></mat-tab> --> </mat-tab-group> </mat-card-content> </mat-card> </div>
همچنین mat-card را هم بر اساس مثال مستندات آن، ابتدا کپی و سپس سفارشی سازی کردهایم (اگر دقت کنید، هر کامپوننت آن سه برگهی overview، سپس API و در آخر Example را به همراه دارد). این روشی است که همواره میتوان با کامپوننتهای این مجموعه انجام داد. ابتدا مثالی را در مستندات آن پیدا میکنیم که مناسب کار ما باشد. سپس سورس آنرا از همانجا کپی و در برنامه قرار میدهیم و در آخر آنرا بر اساس اطلاعات خود سفارشی سازی میکنیم.
نمایش جزئیات اولین کاربر در حین بارگذاری اولیهی برنامه
تا اینجای کار اگر برنامه را از ابتدا بارگذاری کنیم، mat-spinner قسمت نمایش جزئیات تماسها ظاهر میشود و همانطور باقی میماند، با اینکه هنوز موردی انتخاب نشدهاست. برای رفع آن به کامپوننت sidnav مراجعه کرده و در لحظهی بارگذاری اطلاعات، اولین مورد را به صورت دستی نمایش میدهیم:
import { Router } from "@angular/router"; @Component() export class SidenavComponent implements OnInit, OnDestroy { users: User[] = []; constructor(private userService: UserService, private router: Router) { } ngOnInit() { this.userService.getAllUsersIncludeNotes() .subscribe(data => { this.users = data; if (data && data.length > 0 && !this.router.navigated) { this.router.navigate(["/contactmanager", data[0].id]); } }); } }
البته روش دیگر مدیریت این حالت، حذف کدهای فوق و تبدیل کدهای کامپوننت main-content به صورت زیر است:
let id = params['id']; if (!id) id = 1;
بستن خودکار sidenav در حالت نمایش موبایل
اگر اندازهی صفحهی نمایشی را کوچکتر کنیم، قسمت sidenav در حالت over نمایان خواهد شد. در این حالت اگر آیتمهای آنرا انتخاب کنیم، هرچند آنها نمایش داده میشوند، اما زیر این sidenav مخفی باقی خواهند ماند:
بنابراین در جهت بهبود کاربری این قسمت بهتر است با کلیک کاربر بر روی sidenav و گزینههای آن، این قسمت بسته شده و ناحیهی زیر آن نمایش داده شود.
در کدهای قالب sidenav، یک template reference variable برای آن به نام sidenav درنظر گرفته شدهاست:
<mat-sidenav #sidenav
import { MatSidenav } from "@angular/material"; @Component() export class SidenavComponent implements OnInit, OnDestroy { @ViewChild(MatSidenav) sidenav: MatSidenav;
ngOnInit() { this.router.events.subscribe(() => { if (this.isScreenSmall) { this.sidenav.close(); } }); }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MaterialAngularClient-03.zip
برای اجرای آن:
الف) ابتدا به پوشهی src\MaterialAngularClient وارد شده و فایلهای restore.bat و ng-build-dev.bat را اجرا کنید.
ب) سپس به پوشهی src\MaterialAspNetCoreBackend\MaterialAspNetCoreBackend.WebApp وارد شده و فایلهای restore.bat و dotnet_run.bat را اجرا کنید.
اکنون برنامه در آدرس https://localhost:5001 قابل دسترسی است.
// Extending the built-in validator validateEmail(user, errors);
Ionic , react native , flutter , xamarin ….
مزایای نوشتن یک اپلیکیشن با فریم ورکها:
1- نوشتن کد به مراتب آسانتر است.
۲- چون اکثر فریم ورکها، فریم ورکهای جاوا اسکریپتی هستند، سرعت بالایی هنگام run کردن برنامه دارند ولی در build آخر و خروجی نهایی بر روی پلتفرم، این سرعت کندتر میشود.
۳- cossplatform بودن. با طراحی یک اپلیکیشن برای یک پلتفرم میتوان همزمان برای پلتفرمهای دیگر خروجی گرفت.
۱- برنامه نویس را محدود میکند.
۲- سرعت اجرای پایینی دارد.
۳- حجم برنامه به مراتب بالاتر میباشد.
۴- از منابع سخت افزاری بیشتری استفاده میکند.
اگر شما هم میخواهید از فریم ورکها برای طراحی اپلیکیشن استفاده کنید در اینجا میتوانید اطلاعات بیشتری را درباره هر کدام از فریم ورکها ببینید. هر کدام از این فریم ورکها دارای مزایا معایب و همچنین رزومه کاری خوب میباشند. بیشتر فریم ورکهای بالا در رندر آخر، همان کد native را تولید میکنند؛ مثلا اگر برنامهای را با react native بنویسید، میتوانید همان برنامه را بر روی اندروید استودیو و کد native بالا بیارید. توضیحات بالا مقایسه فریم ورکهای Cross Platform با زبانهای native بود. اما در این مقاله قصد آشنایی با تکنولوژی جدیدی را داریم که شما را قادر میسازد یک وب اپلیکیشن را با آن طراحی کنید.
pwa مخفف Progressive Web Apps است و یک تکنولوژی برای طراحی وب اپلیکیشنهای تحت مرورگر میباشد. شما با pwa میتوانید اپلیکیشن خود را بر روی پلتفرمهای مختلفی اجرا کنید، طوری که کاربران متوجه نشوند با یک صفحهی وبی دارند کار میکنند. شاید شما هم فکر کنید این کار را هم میتوان با html ,css و responsive کردن صفحه انجام داد! ولی اگر بخواهید کاربر متوجه استفادهی از یک صفحهی وبی نشود، باید حتما از pwa استفاده کنید! برای مثال یک صفحهی وبی معمولی حتما باید در بستر اینترنت اجرا شود، ولی با pwa با یکبار وصل شدن به اینترنت و کش کردن دادهها، برای بار دوم دیگر نیازی به اینترنت ندارد و میتواند به صورت offline کار کند. شما حتی با pwa میتوانید اپلیکیشن را در background اجرا کنید و notification ارسال کنید.
۱- یکی از مزیتهای مهم pwa، حالت offline آن میباشد که حتی با قطع اینترنت، شما میتوانید همچنان با اپلیکیشن کار کنید.
۲- با توجه به اینکه شما در حقیقت با یک صفحهی وبی کار میکنید، دیگر نیازی به دانلود و نصب ندارید.
۳- امکان بهروز رسانی کردن، بدون اعلام کردن نسخه جدید.
۴- از سرعت بسیار زیادی برخوردار است.
۵- چون pwa از پروتکل https استفاده میکند، دارای امنیت بالایی میباشد.
۱- محدود به مرورگر میباشد.
۲- هرچند امروزه اکثر مرورگرها pwa را پشتیبانی میکنند، ولی در بعضی از مرورگرها و مرورگرهای با ورژن پایین، pwa پشتیبانی نمیشود.
3- نمیتوان یک برنامهی مبتنی بر os را نوشت و محدود به مرورگر میباشد
1- برای pwa لزومی ندارد حتما از فریم ورکهای spa استفاده کنید. شما از هر فریم ورک Client Side ای میتوانید استفاده کنید.
۲- چون صفحات شما در پلتفرمهای مختلف و با صفحه نمایشهای مختلفی اجرا میشود، باید صفحات به صورت کاملا responsive شده طراحی شوند.
۳- باید از پروتکل https استفاده کنید.
ما در این مقاله از فریم ورک angular استفاده خواهیم کرد.
قبل از شروع، با شیوه کار pwa آشنا خواهیم شد. یکی از قسمتهای مهم Service Worker ،pwa میباشد که از جمله کش کردن، notification فرستادن و اجرای پردازشها در پس زمینه را بر عهده دارد.
چند نکته در رابطه با Service Worker
- نباید برای نگهداری داده global از Service Worker استفاد کرد. برای استفاده از دادههای Global میتوان از Local Storage یا IndexedDB استفاده کرد.
- service worker به dom دسترسی ندارند.
قبل از شروع، سناریوی پروژه را تشریح خواهیم کرد. رکن اصلی یک برنامهی وب، UI آن میباشد و قصد داریم کاربران را متوجه کار با یک صفحهی وبی نکنیم. قالبی که ما در این مثال در نظر گرفتیم خیلی شبیه به یک اپلیکیشن پلتفرم اندرویدی میباشد. یک کاربر با کشیدن منوی کشویی میتواند گزینههای خود را انتخاب نمایند. اولین گزینهای که قصد پیاده سازی آن را داریم، ثبت کاربران میباشد. بعد از ثبت کاربران در یک Component جدا، کاربران را در یک جدول نمایش خواهیم داد.
1- سرویس crud را به صورت کامل در پروژه قرار خواهیم داد، ولی چون از حوصلهی مقاله خارج است، فقط ثبت کاربران و نمایش کاربران را پیاده سازی خواهیم کرد.
شروع به کار
پیش نیازهای یک پروژهی انگیولاری را بر روی سیستم خود فراهم کنید. ما در این مثال از یک template آماده انگیولاری استفاده خواهیم کرد. پس برای اینکه با جزئیات و طراحی ui درگیر نشویم، از لینک github پروژه را دریافت کنید.
سپس وارد root پروژه شوید و با دستور زیر پکیجهای پروژه را نصب کنید:
npm install
قبل از اینکه کاری را انجام دهید، چند بار صفحه را refresh کنید! صفحه بدون هیچ تغییری refresh میشود. اینبار گزینهی offline را فعال کنید و مجددا صفحه را refresh کنید.
نصب pwa بر روی پروژه
برای اضافه کردن pwa به پروژه وارد ریشهی پروژه شوید و دستور زیر را وارد کنید:
ng add @angular/PWA
Manifest.json : اگر محتویات فایل را مشاهده کرده باشید، شامل تنظیمات فنی وب اپلیکیشن میباشد؛ از جمله Home Screen Icon و نام وب اپلیکیشن و سایر تنظیمات دیگر.
Nsgw-config.json : این فایل نسبت به فایل manifest فنیتر میباشد و بیشتر به کانفیگ مد آفلاین و کش کردن مرتبط میشود. در ادامه با این فایل بیشتر کار داریم.
اجرا کردن وب اپلیکیشن
برای اجرا کردن و نمایش خروجی از وب اپلیکیشن، ابتدا باید از پروژه build گرفت. با استفاده از دستور زیر از پروژه خود build بگیرید:
ng build -- prod
cd dist/Web Application Pwa
Http-server -o
npm i http-server
برسی فایل Nsgw-config.json
وارد فایل Nsgw-config.json شوید:
"$schema": "./node_modules/@angular/service-worker/config/schema.json", "index": "/index.html", "assetGroups": [ { "name": "app", "installMode": "prefetch", "resources": { "files": [ "/favicon.ico", "/index.html", "/*.css", "/*.js" ] } }, { "name": "assets", "installMode": "lazy", "updateMode": "prefetch", "resources": { "files": [ "/assets/**", "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)" ] } } ], "dataGroups": [ { "name": "api-performance", "urls": [ "https://api/**" ], "cacheConfig": { "strategy": "performance", "maxSize": 100, "maxAge": "3d" } } ] }
۱- assetGroups : کش کردن اطلاعات مربوط به اپلیکیشن
۲- Index : کش کردن فایل مربوط به index.html
۳- assetGroups : کش کردن فایلهای مربوط به asset، شامل فایلهای js، css و غیره
۴- dataGroups : این object مربوط به وقتی است که برنامه در حال اجرا است. میتوان دادههای در حال اجرای اپلیکیشن را کش کرد. دادهی در حال اجرا میتواند شامل فراخوانی apiها باشد. برای مثال فرض کنید شما در حالت کار کردن online با اپلیکیشن، لیستی از اسامی کاربران را از api گرفته و نمایش میدهید. وقتی دفعهی بعد در حالت offline اپلیکیشن را باز کنیم، اگر api را کش کرده باشیم، اپلیکیشن دادهها را از کش فراخوانی میکند. این عمل درباره post کردن دادهها هم صدق میکند.
خود dataGroups شامل چند object زیر میباشد:
۱- name : یک نام انتخابی برای Groups میباشد.
۲- urls : شامل آرایهای از آدرسها میباشد. میتوان آدرس یک دومین را همراه با کل apiها به صورت زیر کش کرد:
"https://api/**"
۱- maxSize : حداکثر تعداد کشهای مربوط به Groups .
۲- maxAge : حداکثر lifetime مربوط به کش.
۳- strategy : که میتواند یکی از مقادیر freshness به معنی Network-First یا performance به معنی Cache-First باشد.
پیاده سازی پیغام نمایش بهروزرسانی جدید وب اپلیکیشن
در اپلیکیشنهای native وقتی بهروزرسانی جدیدی برای app اعلام میشود، در فروشگاهای اینترنتی پیغامی مبنی بر بهروزرسانی جدید app برای کاربران ارسال میشود که کاربران میتوانند app خود را بهروزرسانی کنند. ولی در pwa تنها با یک رفرش صفحه میتوان اپلیکیشن را به جدیدترین امکانات بهروزرسانی کرد! برای اینکه بتوانیم با هر تغییر، پیغامی را جهت بهروزرسانی نسخه یا هر پیغامی دیگری را نمایش دهیم، از کد زیر استفاده میکنم:
if(this.swUpdate.isEnabled) { this.swUpdate.available.subscribe(()=> { if(confirm("New Version available.Load New Version?")){ window.location.reload(); } }) }
در منوی سمت چپ، بر روی database کلیک کنید و یک دیتابیس را در حالت test mode ایجاد نماید. سپس یک collection را به نام user ایجاد کرده و فیلدهای زیر را به آن اضافه کنید:
Age :number Fullname :string Mobile : string
npm install --save firebase @angular/fire
@NgModule({ declarations: [AppComponent, RegisterComponent,AboutComponent, UserListComponent], imports: [ BrowserModule, FormsModule, HttpClientModule, HttpClientModule, SharedModule, AppRoutingModule, AngularFireModule.initializeApp(environmentFirebase.firebase), AngularFireDatabaseModule, ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }), ], providers: [FirebaseService], bootstrap: [AppComponent] }) export class AppModule {}
export const environment ={ production: true, firebase: { apiKey: "AIz×××××××××××××××××××××××××××××××××8", authDomain: "pwaangular-6c041.firebaseapp.com", databaseURL: "https://pwaangular-6c041.firebaseio.com", projectId: "pwaangular-6c041", storageBucket: "pwaangular-6c041.appspot.com", messagingSenderId: "545522081966" } }
FirebaseService در قسمت providers مربوط به سرویس crud میباشد. اگر وارد فایل مربوطه شوید، چند عمل اصلی به صورت زیر در آن پیاده سازی شده است:
import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import * as firebase from 'firebase'; import { AngularFireDatabase } from '@angular/fire/database'; import { HttpClient } from '@angular/common/http'; import { map } from 'rxjs/operators'; import { ThrowStmt } from '@angular/compiler'; @Injectable({ providedIn: 'root' }) export class FirebaseService { ref = firebase.firestore().collection('users'); constructor(public db: AngularFireDatabase,public _http:HttpClient) { } getUsers(): Observable<any> { return new Observable((observer) => { this.ref.onSnapshot((querySnapshot) => { let User = []; querySnapshot.forEach((doc) => { let data = doc.data(); User.push({ key: doc.id, fullname: data.fullname, age: data.age, mobile: data.mobile }); }); observer.next(User); }); }); } getUser(id: string): Observable<any> { return new Observable((observer) => { this.ref.doc(id).get().then((doc) => { let data = doc.data(); observer.next({ key: doc.id, title: data.title, description: data.description, author: data.author }); }); }); } postUser(user): Observable<any> { return new Observable((observer) => { this.ref.add(user).then((doc) => { observer.next({ key: doc.id, }); }); }); } updateUser(id: string, data): Observable<any> { return new Observable((observer) => { this.ref.doc(id).set(data).then(() => { observer.next(); }); }); } deleteUser(id: string): Observable<{}> { return new Observable((observer) => { this.ref.doc(id).delete().then(() => { observer.next(); }); }); } getDataOnApi(){ return this._http.get('https://site.com/api/General/Getprovince') .pipe( map((res: Response) => { return res; }) ); } getOnApi(){ return this._http.get("https://site.com/api/General/Getprovince",).pipe( map((response:any) => { return response } ) ); } }
با دستورات زیر میتوانید مجددا پروژه را اجرا کنید:
ng build --prod cd dist/Pwa-WepApp Http-server -o
تست وب اپلیکیشن بر روی پلتفرمهای مختلف
برای اینکه بتوانیم خروجی وب اپلیکیشن را بر روی پلتفرمهای مختلفی تست کنیم، میتوانیم آن را آپلود کرده و مثل یک سایت اینترنتی، با وارد کردن دومین، وارده پروژه شد. ولی کم هزینهترین راه، استفاده از ابزار ngrok میباشد. میتوانید توسط این مقاله پروژه خودتان در لوکال بر روی https سوار کنید.
نکته! توجه کنید apiهای مربوط به firebase را نمیتوان کش کرد.
کدهای مربوط به این قسمت را میتوانید از این repository دریافت کنید .
بررسی سلامت SQL Server از طریق PowerShell
NOSQL قسمت اول
با فراگیر شدن اینترنت در سالهای اخیر و افزایش کاربران ، سیستمهای RDBMS جوابگوی نیازهای برنامهنویسان در حوزهی وب نبودند زیرا نیاز به نگهداری دادهها با حجم بالا و سرعت خواندن و نوشتن بالا از جمله نقط ضعف سیستمهای RDBMS میباشد ، چرا که با افزایش شدید کاربران دادهها اصولا به صورت منطقی ساختار یکدست خود را جهت نگهداری از دست میدهند و به این ترتیب عملیات نرمال سازی منجر به ساخت جداول زیادی میشود که نتیجه آن برای هر کوئری عملیات Joinهای متعدد میباشد که سرعت خواندن و نوشتن را به خصوص برای برنامههای با گسترهی وب پایین میآورد و مشکلات دیگری در سیستمهای RDBMS که ویژگیهای سیستمهای NoSql مشخص کننده آن مشکلات است که در ادامه به آن میپردازیم.
طبق تعریف کلی پایگاه داده NOSql عبارت است از:
نسل بعدی پایگاه داده (نسل از بعد RDBMS ) که اصولا دارای چند ویژگی زیر باشد:
۱- دادهها در این سیستم به صورت رابطهای (جدولی) نمیباشند
۲-دادهها به صورت توزیع شده نگهداری میشوند.
۳-سیستم نرمافزاری متن باز میباشد.
۴-پایگاه داده مقیاس پذیر به صورت افقی میباشد(در مطالب بعدی توضیح داده خواهد شد.)
همانگونه که گفته شد این نوع پایگاه داده به منظور رفع نیازهای برنامههای با حجم ورود و خروج داده بسیار بالا (برنامههای مدرن وب فعلی) ایجاد شدند.
شروع کار پیادهسازی این سیستمها در اوایل سال ۲۰۰۹ شکل گرفت و با سرعت زیادی رشد کرد و همچنین ویژگیهای کلی دیگری نیز به این نوع سیستم اضافه شد.
که این ویژگیها عبارتند از:
- Schema-free
: بدون شَما ! ، با توجه به برنامههای وبی فعلی ممکن است شمای نگهداری
دادهها ( ساختار کلی ) مرتبا و یا گهگاهی تغییر کند. لذا در این سیستمها
اصولا دادهها بدون شمای اولیه طراحی و ذخیره میشوند. ( به عنوان مثال
میتوان در یک سیستم که مشخصات کاربران وارد سیستم میشود برای یک کاربر یک
سری اطلاعات اضافی و برای کاربری دیگر از ورود اطلاعات اضافی صرفنظر کرد ،
و در مقایسه با RDBMS به این ترتیب از ورود مقادیر Null و یا پیوندهای
بیمورد جلوگیری کرد.
کنترل اطلاعات الزامی توسط لایه سرویس برنامه انجام میشود. ( در زبان جاوا توسط jsr-303 و یاBean Validation ها) - easy replication support : در این سیستم ، نحوهی گرفتن نسخههای پشتیبان و sync بودن نسخههای مختلف بسیار ساده و سر راست میباشد و سرور پایگاه داده به محض عدم توانایی خواندن و یا نوشتن از روی دیسک سراغ نسخهی پشتیبان میرود و آن نسخه را به عنوان نسخهی اصلی در نظر میگیرد.
- Simple API : به دلیل متنباز بودن و فعال بودن Community این سیستمها APIهای ساده و بهینهای برای اکثر زبانهای برنامهنویس محبوب ایجاد شده است که در پستهای بعدی با ارائه مثال آنها را بررسی خواهیم کرد.
- eventually consistent : در سیستمهای RDBMS که دادهها خاصیت ACID را ( در قالب Transaction) پیاده میکنند ، در این سیستمهای دادهها در وضعیت BASE قرار دارند که سرنام کلمات Basically Available ، Soft State ، Eventual Consistency میباشد.
- huge amount of data: این سیستمها به منظور کار با دادههای با حجم بالا ایجاد شدهاند ، یک تعریف کلی میگوید اگر مقدار دادههای نگهداری شده در پایگاههای داده برنامه شما ظرفیتی کمتر از یک ترابایت داده دارد از پایگاه داده RDBMS استفاده کنید واگر ظرفیت آن از واحد ترابایت فراتر میرود از سیستمهای NOSql استفاده کنید.
جهت پیاده سازی پایگاه داده با این سیستمها تا حدودی نگرش کلی به دادهها و نحوهی چیدمان آنها تغییر میکند ، به صورت کلی مباحث مربوط به normalization و de-normalization و تصور دادهها به صورت جدولی کنار میرود.
سیستم NoSql به جهت دستهبندی نحوهی ذخیرهسازی دادهها و ارتباط بین آنها به ۴ دسته کلی تقسیم میشود که معرفی کلی آن دستهبندیها موضوع مطلب بعدی میباشد.
نحوه برقراری ارتباطات بین صفحات، سیستم راهبری و ViewModelها در قالب پروژه WPF Framework
1) ایجاد صفحه تغییر مشخصات کاربر
کلیه Viewهای برنامه، در پروژه ریشه، ذیل پوشه Views اضافه خواهند شد. همچنین چون در آینده تعداد این فایلها افزایش خواهند یافت، بهتر است جهت مدیریت آنها، به ازای هر گروه از قابلیتها، یک پوشه جدید را ذیل پوشه Views اضافه کرد.
همانطور که ملاحظه میکنید در اینجا پوشه UserInfo به همراه یک فایل جدید XAML به نام ChangeProfile.xaml، ذیل پوشه Views پروژه ریشه اصلی اضافه شدهاند.
ChangeProfile.xaml از نوع Page است؛ از این جهت که اگر به فایل MainWindow.xaml که سیستم راهبری برنامه در آن تعبیه شده است مراجعه کنید، یک چنین تعریفی را ملاحظه خواهید نمود:
<CustomControls:FrameFactory x:Name="ActiveScreen" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" NavigationUIVisibility="Hidden" Grid.Column="1" Margin="0" />
مرحله بعد، تعریف محتویات فایل ChangeProfile.xaml است. در این فایل اطلاعات انقیاد دادهها از ViewModel مرتبط که در ادامه توضیح داده خواهد شد دریافت میگردد. مثلا مقدار خاصیت ChangeProfileData.Password، از ViewModel به صورت خودکار تغذیه خواهد شد.
در این فایل یک سری DynamicResource را هم برای تعریف دکمههای سبک مترو ملاحظه میکنید. کلیدهای متناظر با آن در فایل Icons.xaml که در فایل App.xaml برای کل برنامه ثبت شده است، تامین میگردد.
2) تنظیم اعتبارسنجی صفحه اضافه شده
پس از اینکه صفحه جدید اضافه شد، نیاز است وضعیت دسترسی به آن مشخص شود:
/// <summary> /// تغییر مشخصات کاربر جاری /// </summary> [PageAuthorization(AuthorizationType.FreeForAuthenticatedUsers)] public partial class ChangeProfile
در مورد نحوه تعیین نقشهای متفاوت در صورت نیاز، در قسمت قبل بحث گردید.
3) تغییر منوی برنامه جهت اشاره به صفحه جدید
خوب، ما تا اینجا یک صفحه جدید را تهیه کردهایم. در مرحله بعد باید مدخلی را در منوی برنامه جهت اشاره به آن تهیه کنیم.
منوی برنامه در فایل MainMenu.xaml قرار دارد. اطلاعات متناظر با دکمه ورود به صفحه تغییر مشخصات کاربری نیز به شکل ذیل تعریف شده است:
<Button Style="{DynamicResource MetroCircleButtonStyle}" Height="55" Width="55" Command="{Binding DoNavigate}" CommandParameter="\Views\UserInfo\ChangeProfile.xaml" Margin="2"> <Rectangle Width="28" Height="17.25"> <Rectangle.Fill> <VisualBrush Stretch="Fill" Visual="{StaticResource appbar_user_tie}" /> </Rectangle.Fill> </Rectangle> </Button>
در این ViewModel تعاریف DoNavigate و پردازش پارامتر دریافتی به صورت خودکار صورت خواهد گرفت و سورس کامل آن در اختیار شما است. بنابراین تنها کافی است CommandParameter را مشخص کنید، DoNavigate کار هدایت به آنرا انجام خواهد داد.
4) ایجاد ViewModel متناظر با صفحه
مرحله آخر افزودن یک صفحه، تعیین ViewModel متناظر با آن است. عنوان شد که اطلاعات مورد نیاز جهت عملیات Binding در این فایل قرار میگیرند و اگر به فایل ChangeProfileViewModel.cs مراجعه کنید (نام آن مطابق قرارداد، یک کلمه ViewModel را نسبت به نام View متناظر بیشتر دارد)، چنین خاصیت عمومی را در آن خواهید یافت.
مطابق قراردادهای توکار قالب تهیه شده:
- نیاز است ViewModel تعریف شده از کلاس پایه BaseViewModel مشتق شود تا علاوه بر تامین یک سری کدهای تکراری مانند:
public abstract class BaseViewModel : DataErrorInfoBase, INotifyPropertyChanged, IViewModel
- پس از اضافه شدن کلاس پایه BaseViewModel نیاز است تکلیف خاصیت public override bool ViewModelContextHasChanges را نیز مشخص کنید. در اینجا به سیستم راهبری اعلام میکنید که آیا در ViewModel جاری تغییرات ذخیره نشدهای وجود دارند؟ فقط باید true یا false را بازگردانید. برای مثال خاصیت uow.ContextHasChanges برای این منظور بسیار مناسب است و از طریق پیاده سازی الگوی واحد کار به صورت خودکار چنین اطلاعاتی را در اختیار برنامه قرار میدهد.
در ViewModelها هرجایی که نیاز به اطلاعات کاربر وارد شده به سیستم داشتید، از اینترفیس IAppContextService در سازنده کلاس ViewModel جاری استفاده کنید. اینترفیس IUnitOfWork امکانات ذخیره سازی اطلاعات و همچنین مشخص سازی وضعیت Context جاری را در اختیار شما قرار میدهد.
کلیه کدهای کار کردن با یک موجودیت باید در کلاس سرویس متناظر با آن قرار گیرند و این کلاس سرویس توسط اینترفیس آن مانند IUsersService در اینجا باید توسط سازنده کلاس در اختیار ViewModel قرار گیرد.
تزریق وابستگیها در اینجا خودکار بوده و تنظیمات آن در فایل IocConfig.cs پروژه Infrastructure قرار دارد. این کلاس آنچنان نیازی به تغییر ندارد؛ اگر پیش فرضهای نامگذاری آنرا مانند کلاسهای Test و اینترفیسهای ITest، در لایه سرویس برنامه رعایت شوند.
DNTViewer
برای استفاده از آن نیاز به نصب دات نت فریم ورک 4 بر روی سیستم شما است. همچنین فایل XML بانک اطلاعاتی سایت را پس از دریافت نیاز است در کنار فایل EXE برنامه کپی کنید.
روشهای متفاوت ایجاد توالی (sequence) در Rx
الف) استفاده از متدهای Factory
1) Observable.Create
نمونهای از استفاده از آنرا در مطلب «معرفی Reactive extensions» مشاهده کردید.
var query = Enumerable.Range(1, 5).Select(number => number); var observableQuery = query.ToObservable(); var observer = Observer.Create<int>(onNext: number => Console.WriteLine(number)); observableQuery.Subscribe(observer);
البته در این مثال فقط delegate مربوط به onNext را ملاحظه میکند. توسط سایر overloadهای آن امکان ذکر delegateهای OnError/OnCompleted نیز وجود دارد.
2) Observable.Return
برای ایجاد یک خروجی Observable از یک مقدار مشخص، میتوان از متد جنریک Observable.Return استفاده کرد. برای مثال:
var observableValue1 = Observable.Return("Value"); var observableValue2 = Observable.Return(2);
public static IObservable<T> Return<T>(T value) { return Observable.Create<T>(o => { o.OnNext(value); o.OnCompleted(); return Disposable.Empty; }); }
var observableValue1 = Observable.Return<string>("Value"); var observableValue2 = Observable.Return<int>(2);
3) Observable.Empty
برای بازگشت یک توالی خالی که تنها کار اطلاع رسانی onCompleted را انجام میدهد.
var emptyObservable = Observable.Empty<string>();
public static IObservable<T> Empty<T>() { return Observable.Create<T>(o => { o.OnCompleted(); return Disposable.Empty; }); }
4) Observable.Never
برای بازگشت یک توالی بدون قابلیت اطلاع رسانی و notification
var neverObservable = Observable.Never<string>();
public static IObservable<T> Never<T>() { return Observable.Create<T>(o => { return Disposable.Empty; }); }
5) Observable.Throw
برای ایجاد یک توالی که صرفا کار اطلاع رسانی OnError را توسط استثنای معرفی شده به آن انجام میدهد.
var throwObservable = Observable.Throw<string>(new Exception());
public static IObservable<T> Throws<T>(Exception exception) { return Observable.Create<T>(o => { o.OnError(exception); return Disposable.Empty; }); }
6) توسط Observable.Range
به سادگی میتوان بازهی Observable ایی را ایجاد کرد:
var range = Observable.Range(10, 15); range.Subscribe(Console.WriteLine, () => Console.WriteLine("Completed"));
7) Observable.Generate
اگر بخواهیم عملیات Observable.Range را پیاده سازی کنیم، میتوان از متد Observable.Generate استفاده کرد:
public static IObservable<int> Range(int start, int count) { var max = start + count; return Observable.Generate( initialState: start, condition: value => value < max, iterate: value => value + 1, resultSelector: value => value); }
8) Observable.Interval
عموما از انواع و اقسام تایمرهای موجود در دات نت مانند System.Timers.Timer ، System.Threading.Timer و System.Windows.Threading.DispatcherTimer برای ایجاد یک توالی از رخدادها استفاده میشود. تمام اینها را به سادگی میتوان توسط متد Observable.Interval، که قابل انتقال به تمام پلتفرمهایی است که Rx برای آنها تهیه شدهاست، جایگزین کرد:
var interval = Observable.Interval(period: TimeSpan.FromMilliseconds(250)); interval.Subscribe(Console.WriteLine, () => Console.WriteLine("completed"));
Overload دوم این متد، امکان معرفی scheduler و اجرای بر روی تردی دیگر را نیز میسر میکند.
9) Observable.Timer
تفاوت Observable.Timer با Observable.Interval در مفهوم پارامتر ارسالی به آنها است:
var timer = Observable.Timer(dueTime: TimeSpan.FromSeconds(1)); timer.Subscribe(Console.WriteLine, () => Console.WriteLine("completed"));
خروجی Observable.Interval مثال زده شده به نحو زیر است و خاتمهای ندارد:
0
1
2
3
4
5
اما خروجی Observable.Timer به نحو ذیل بوده و پس از یک ثانیه، خاتمه مییابد:
0
completed
متد Observable.Timer دارای هفت overload متفاوت است که توسط آنها dueTime (مدت زمان صبر کردن تا تولید اولین خروجی)، period (کار Observable.Timer را به صورت متوالی در بازهی زمانی مشخص شده تکرار میکند) و scheduler (تعیین ترد اجرایی عملیات) قابل مقدار دهی هستند.
اگر میخواهید Observable.Timer بلافاصله شروع به کار کند، مقدار dueTime آنرا مساوی TimeSpan.Zero قرار دهید. به این ترتیب یک Observable.Interval را به وجود آوردهاید که بلافاصله شروع به کار کرده است و تا مدت زمان مشخص شدهای جهت اجرای اولین callback خود صبر نمیکند.
ب) تبدیلگرهایی که خروجی IObservable ایجاد میکنند
برای تبدیل مدلهای برنامه نویسی Async قدیمی دات نت مانند APM، رخدادها و امثال آن به معادلهای Rx، متدهای الحاقی خاصی تهیه شدهاند.
1) تبدیل delegates به معادل Observable
متد Observable.Start، امکان تبدیل یک Func یا Action زمانبر را به یک توالی observable میسر میکند. در این حالت به صورت پیش فرض، پردازش عملیات بر روی یکی از تردهای ThreadPool انجام میشود.
static void StartAction() { var start = Observable.Start(() => { Console.Write("Observable.Start"); for (int i = 0; i < 10; i++) { Thread.Sleep(100); Console.Write("."); } }); start.Subscribe( onNext: unit => Console.WriteLine("published"), onCompleted: () => Console.WriteLine("completed")); } static void StartFunc() { var start = Observable.Start(() => { Console.Write("Observable.Start"); for (int i = 0; i < 10; i++) { Thread.Sleep(100); Console.Write("."); } return "value"; }); start.Subscribe( onNext: Console.WriteLine, onCompleted: () => Console.WriteLine("completed")); }
زمانیکه از Func استفاده میشود، تابع یک خروجی را ارائه داده و سپس توالی خاتمه مییابد. اگر از Action استفاده شود، نوع Observable بازگشت داده شده از نوع Unit است که در برنامه نویسی functional معادل void است و هدف از آن مشخص سازی پایان عملیات Action میباشد. Unit دارای مقداری نبوده و صرفا سبب اجرای اطلاع رسانی OnNext میشود.
تفاوت مهم Observable.Start و Observable.Return در این است که Observable.Start مقدار تابع را به صورت تنبل (lazily) پردازش میکند، اما Observable.Return پردازش حریصانهای (eagrly) را به همراه خواهد داشت. به این ترتیب Observable.Start بسیار شبیه به یک Task (پردازشهای غیرهمزمان) عمل میکند.
در اینجا شاید این سؤال مطرح شود که استفاده از قابلیتهای Async سیشارپ 5 برای اینگونه کارها مناسب است یا Rx؟ قابلیتهای Async بیشتر به اعمال مخصوص IO bound مانند کار با شبکه، دریافت فایل از اینترنت، کار با یک بانک اطلاعاتی خارج از مرزهای سیستم، مرتبط میشوند؛ اما اعمال CPU bound مانند محاسبات سنگین حاصل از توالیهای observable را به خوبی میتوان توسط Rx مدیریت کرد.
2) تبدیل Events به معادل Observable
دات نت از روزهای اول خود به همراه یک event driven programming model بودهاست. Rx متدهایی را برای دریافت یک رخداد و تبدیل آن به یک توالی Observable ارائه دادهاست. برای نمونه ObservableCollection زیر را درنظر بگیرید
var items = new System.Collections.ObjectModel.ObservableCollection<string> { "Item1", "Item2", "Item3" };
items.CollectionChanged += (sender, ea) => { if (ea.Action == NotifyCollectionChangedAction.Remove) { foreach (var oldItem in ea.OldItems.Cast<string>()) { Console.WriteLine("Removed {0}", oldItem); } } };
var removals = Observable.FromEventPattern<NotifyCollectionChangedEventHandler, NotifyCollectionChangedEventArgs> ( addHandler: handler => items.CollectionChanged += handler, removeHandler: handler => items.CollectionChanged -= handler ) .Where(e => e.EventArgs.Action == NotifyCollectionChangedAction.Remove) .SelectMany(c => c.EventArgs.OldItems.Cast<string>()); var disposable = removals.Subscribe(onNext: item => Console.WriteLine("Removed {0}", item));
items.Remove("Item1");
3) تبدیل Task به معادل Observable
متد ToObservable واقع در فضای نام System.Reactive.Threading.Tasks را بر روی یک Task نیز میتوان فراخوانی کرد:
var task = Task.Factory.StartNew(() => "Test"); var source = task.ToObservable(); source.Subscribe(Console.WriteLine, () => Console.WriteLine("completed"));
4) تبدیل IEnumerable به معادل Observable
با این مورد تاکنون آشنا شدهاید. فقط کافی است متد ToObservable را بر روی یک IEnumerable، جهت تهیه خروجی Observable فراخوانی کرد.
5) تبدیل APM به معادل Observable
APM یا Asynchronous programming model، همان روش کار با متدهای Async با نامهای BeginXXX و EndXXX است که از نگارشهای آغازین دات نت به همراه آن بودهاند. کار کردن با آن مشکل است و مدیریت آن به همراه پراکندگیهای بسیاری جهت کار با callbacks آن است. برای تبدیل این نوع روش برنامه نویسی به روش Rx نیز متدهایی پیش بینی شدهاست؛ مانند Observable.FromAsyncPattern.
یک نکته
کتابخانهای به نام Rxx بسیاری از این محصور کنندهها را تهیه کردهاست:
http://Rxx.codeplex.com
ابتدا بستهی نیوگت آنرا نصب کنید:
PM> Install-Package Rxx
using (new FileStream("file.txt", FileMode.Open) .ReadToEndObservable() .Subscribe(x => Console.WriteLine(x.Length))) { Console.ReadKey(); }