public class Employee { public string Name { get; set; } public string Title { get; set; } public static Employee GetEmployee() { var emp = new Employee { Name = "Mani", Title = "CEO" }; return emp; } }
<Grid> <StackPanel Name="Display"> <StackPanel Orientation="Horizontal"> <TextBlock>First Name :</TextBlock> <TextBlock Margin="5,0,0,0" Text="{Binding Name}"/> </StackPanel> <StackPanel Orientation="Horizontal"> <TextBlock>Title :</TextBlock> <TextBlock Margin="5,0,0,0" Text="{Binding Title}"></TextBlock> </StackPanel> </StackPanel> </Grid>
public MainWindow() { InitializeComponent(); DataContext = Employee.GetEmployee(); }
public class Employee :INotifyPropertyChanged { public string Name { get; set; } public string Title { get; set; } public static Employee GetEmployee() { var emp = new Employee { Name = "Mani", Title = "CEO" }; return emp; } public event PropertyChangedEventHandler PropertyChanged; [NotifyPropertyChangedInvocator] protected virtual void OnPropertyChanged ([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }
private string _name; private string _title; public string Name { get { return _name; } set { _name = value; OnPropertyChanged(); } } public string Title { get { return _title; } set { _title = value; OnPropertyChanged(); } }
private Employee emp; public MainWindow() { InitializeComponent(); emp = new Employee() { Name = "Mani", Title = "CEO" }; DataContext = emp; }
<StackPanel Orientation="Horizontal"> <Button Click="btnClick" Width="70" Height="30" Content="Change"/> </StackPanel>
private void btnClick(object sender, RoutedEventArgs e) { emp.Name = "Amir"; emp.Title = "Manager"; }
<Grid> <StackPanel Name="Display" > <StackPanel Orientation="Horizontal"> <TextBlock Margin="5,0,0,0">Name :</TextBlock> <TextBox Margin="5,0,0,0" Text="{Binding Name, Mode=TwoWay}"/> </StackPanel> <StackPanel Orientation="Horizontal"> <TextBlock Margin="5,0,0,0">Title :</TextBlock> <TextBox Margin="5,0,0,0" Text="{Binding Title,Mode=TwoWay}"/> </StackPanel> <StackPanel Orientation="Horizontal"> <TextBlock Margin="5,0,0,0">Name :</TextBlock> <TextBlock Margin="5,0,0,0" Text="{Binding Name}"/> </StackPanel> <StackPanel Orientation="Horizontal"> <TextBlock Margin="5,0,0,0">Title :</TextBlock> <TextBlock Margin="5,0,0,0" Text="{Binding Title}"/> </StackPanel> </StackPanel> </Grid>
در این سیستم، کلیه اتصالاتی که Mode آنها به OneTime تنظیم نشده است، به صورت اجباری دارای یک valueChangedHandlers متصل توسط سیستم PropertyDescriptor خواهند بود و در حافظه زنده نگه داشته میشوند؛ تا بتوان در صورت نیاز، توسط سیستم binding اطلاعات آنها را به روز کرد.
همین مساله سبب میشود تا اگر قرار نیست خاصیتی برای نمونه توسط مکانیزم INotifyPropertyChanged اطلاعات UI را به روز کند (یک خاصیت معمولی دات نتی است) و همچنین حالت اتصال آن به OneTime نیز تنظیم نشده، سبب مصرف حافظه بیش از حد برنامه شود.
اطلاعات بیشتر
A memory leak may occur when you use data binding in Windows Presentation Foundation
راه حل آن هم ساده است. برای اینکه valueChangedHandler ایی به خاصیت سادهای که قرار نیست بعدها UI را به روز کند، متصل نشود، حالت اتصال آنرا باید به OneTime تنظیم کرد.
سؤال: در یک برنامه بزرگ که هم اکنون مشغول به کار است، چطور میتوان این مسایل را ردیابی کرد؟
برای دستیابی به اطلاعات کش Binding در WPF، باید به Reflection متوسل شد. به این ترتیب در برنامه جاری، در کلاس PropertyDescriptor به دنبال یک کلاس خصوصی تو در توی دیگری به نام ReflectTypeDescriptionProvider خواهیم گشت (این اطلاعات از طریق مراجعه به سورس دات نت و یا حتی برنامههای ILSpy و Reflector قابل استخراج است) و سپس در این کلاس خصوصی داخلی، فیلد خصوصی propertyCache آنرا که از نوع HashTable است استخراج میکنیم:
var reflectTypeDescriptionProvider = typeof(PropertyDescriptor).Module.GetType("System.ComponentModel.ReflectTypeDescriptionProvider"); var propertyCacheField = reflectTypeDescriptionProvider.GetField("_propertyCache", BindingFlags.Static | BindingFlags.NonPublic);
اکنون به لیست داخلی Binding نگهداری شونده توسط WPF دسترسی پیدا کردهایم. در این لیست به دنبال مواردی خواهیم گشت که فیلد valueChangedHandlers به آنها متصل شده است و در حال گوش فرا دادن به سیستم binding هستند (سورس کامل و طولانی این مبحث را در پروژه پیوست شده میتوانید ملاحظه کنید).
یک مثال: تعریف یک کلاس ساده، اتصال آن و سپس بررسی اطلاعات درونی سیستم Binding
فرض کنید یک کلاس مدل ساده به نحو ذیل تعریف شده است:
namespace WpfOneTime.Models { public class User { public string Name { set; get; } } }
using WpfOneTime.Models; using System.Collections.Generic; namespace WpfOneTime.ViewModels { public class MainWindowViewModel { public IList<User> Users { set; get; } public MainWindowViewModel() { Users = new List<User>(); for (int i = 0; i < 1000; i++) { Users.Add(new User { Name = "name " + i }); } } } }
<Window x:Class="WpfOneTime.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:ViewModels="clr-namespace:WpfOneTime.ViewModels" Title="MainWindow" Height="350" Width="525"> <Window.Resources> <ViewModels:MainWindowViewModel x:Key="vmMainWindowViewModel" /> </Window.Resources> <Grid DataContext="{Binding Source={StaticResource vmMainWindowViewModel}}"> <ListBox ItemsSource="{Binding Users}"> <ListBox.ItemTemplate> <DataTemplate> <TextBlock Text="{Binding Name}" /> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Grid> </Window>
خوب؛ اکنون اگر اطلاعات HashTable داخلی سیستم Binding را در مورد View فوق بررسی کنیم به شکل زیر خواهیم رسید:
بله. تعداد زیادی خاصیت Name زنده و موجود در حافظه باقی هستند که تحت ردیابی سیستم Binding میباشند.
در ادامه، نکتهی ابتدای بحث را جهت تعیین حالت Binding به OneTime، به View فوق اعمال میکنیم (یک سطر ذیل باید تغییر کند):
<TextBlock Text="{Binding Name, Mode=OneTime}" />
به این ترتیب میتوان در لیستهای طولانی، به مصرف حافظه کمتری در برنامه WPF خود رسید.
بدیهی است این نکته را تنها در مواردی میتوان اعمال کرد که نیاز به بهروز رسانیهای ثانویه اطلاعات UI در کدهای برنامه وجود ندارند.
چطور از این نکته برای پروفایل یک برنامه موجود استفاده کنیم؟
کدهای برنامه را از انتهای بحث دریافت کنید. سپس دو فایل ReflectPropertyDescriptorWindow.xaml و ReflectPropertyDescriptorWindow.xaml.cs آنرا به پروژه خود اضافه نمائید و در سازنده پنجره اصلی برنامه، کد ذیل را فراخوانی نمائید:
new ReflectPropertyDescriptorWindow().Show();
دریافت کدهای کامل پروژه این مطلب
WpfOneTime.zip
نگاهی به نحوهی عملکرد سرویسها و تزریق وابستگیها در AngularJS 2.0
فرض کنید کلاس سرویسی، به نحو ذیل تعریف شدهاست:
export class MyService {}
let svc = new MyService();
همچنین در این حالت، mocking این سرویس برای نوشتن unit tests نیز مشکل میباشد.
راه بهتر و توصیه شدهی در اینجا، ثبت و معرفی این سرویسها به AngularJS 2.0 است. سپس AngularJS 2.0 به ازای هر کلاس سرویس معرفی شدهی به آن، یک وهله/نمونه را ایجاد میکند. بنابراین طول عمر سرویسهای ایجاد شدهی در این حالت، singleton است (یکبار ایجاد شده و تا پایان طول عمر برنامه زنده نگه داشته میشوند).
پس از آن میتوان از تزریق کنندههای توکار AngularJS 2.0، جهت تزریق وهلههای این سرویسها استفاده کرد.
اکنون اگر کلاسی، نیاز به این سرویس داشته باشد، نیاز خود را به صورت یک وابستگی تعریف شدهی در سازندهی کلاس اعلام میکند:
constructor(private _myService: MyService){}
به این فرآیند اصطلاحا dependency injection و یا تزریق وابستگیها میگویند. در فرآیند تزریق وابستگیها، یک کلاس، وهلههای کلاسهای دیگر مورد نیاز خودش را بجای وهله سازی مستقیم، از یک تزریق کننده دریافت میکند. بنابراین بجای نوشتن newها در کلاس جاری، آنها را به صورت وابستگیهایی در سازندهی کلاس تعریف میکنیم تا توسط AngularJS 2.0 تامین شوند.
با توجه به اینکه طول عمر این وابستگیها singleton است و این طول عمر توسط AngularJS 2.0 مدیریت میشود، اطلاعات وهلههای سرویسهای مختلف و تغییرات صورت گرفتهی در آنها، بین تمام کامپوننتها به صورت یکسانی به اشتراک گذاشته میشوند.
به علاوه اکنون امکان mocking سرویسها با توجه به عدم وهله سازی آنها در داخل کلاسها به صورت مستقیم، سادهتر از قبل میسر است.
مراحل ساخت یک سرویس در AngularJS 2.0
ساخت یک سرویس در AngularJS 2.0، با ایجاد یک کلاس جدید شروع میشود. سپس متادیتای آن افزوده شده و در آخر موارد مورد نیاز آن import خواهند شد. با این موارد پیشتر در حین ساختن یک کامپوننت جدید و یا یک Pipe جدید آشنا شدهاید و این طراحی یک دست را در سراسر AngularJS 2.0 میتوان مشاهده کرد.
اولین سرویس خود را با افزودن فایل جدید product.service.ts به پوشهی app\products آغاز میکنیم؛ با این محتوا:
import { Injectable } from 'angular2/core'; import { IProduct } from './product'; @Injectable() export class ProductService { getProducts(): IProduct[] { return [ { "productId": 2, "productName": "Garden Cart", "productCode": "GDN-0023", "releaseDate": "March 18, 2016", "description": "15 gallon capacity rolling garden cart", "price": 32.99, "starRating": 4.2, "imageUrl": "app/assets/images/garden_cart.png" }, { "productId": 5, "productName": "Hammer", "productCode": "TBX-0048", "releaseDate": "May 21, 2016", "description": "Curved claw steel hammer", "price": 8.9, "starRating": 4.8, "imageUrl": "app/assets/images/rejon_Hammer.png" } ]; } }
همانند سایر ماژولهای تعریف شده، در اینجا نیز باید کلاس تعریف شده export شود تا در قسمتهای دیگر قابل استفاده و دسترسی گردد.
سپس در این سرویس، یک متد برای بازگشت لیست محصولات ایجاد شدهاست.
در ادامه یک decorator جدید به نام ()Injectable@ به بالای این کلاس اضافه شدهاست. این متادیتا است که مشخص میکند کلاس جاری، یک سرویس AngularJS 2.0 است.
البته باید دقت داشت که این مزین کننده تنها زمانی نیاز است حتما قید شود که کلاس تعریف شده، دارای وابستگیهای تزریق شدهای باشد. اما توصیه شدهاست که بهتر است هر کلاس سرویسی (حتی اگر دارای وابستگیهای تزریق شدهای هم نبود) به این decorator ویژه، مزین شود تا بتوان طراحی یک دستی را در سراسر برنامه شاهد بود.
در آخر هم موارد مورد نیاز، import میشوند. برای مثال Injectable در ماژول angular2/core تعریف شدهاست.
هدف از تعریف این سرویس، دور کردن وظیفهی تامین داده، از کلاس کامپوننت لیست محصولات است؛ جهت رسیدن به یک طراحی SOLID.
در قسمت بعدی این سری، این لیست را بجای یک آرایهی از پیش تعریف شده، از یک سرور HTTP دریافت خواهیم کرد.
ثبت و معرفی سرویس جدید ProductService به AngularJS 2.0 Injector
مرحلهی اول استفاده از سرویسهای تعریف شده، ثبت و معرفی آنها به AngularJS 2.0 Injector است. سپس این Injector است که تک وهلهی سرویس ثبت شدهی در آنرا در اختیار هر کامپوننتی که آنرا درخواست کند، قرار میدهد.
مرحلهی ثبت این سرویس، معرفی نام این کلاس، به خاصیتی آرایهای، به نام providers است که یکی از خواص decorator ویژهی Component است. بدیهی است هر کامپوننتی که در برنامه وجود داشته باشد، توانایی ثبت این سرویس را نیز دارد؛ اما باید از کدامیک استفاده کرد؟
اگر سرویس خود را در کامپوننت لیست محصولات رجیستر کنیم، تک وهلهی این سرویس تنها در این کامپوننت و زیر کامپوننتهای آن در دسترس خواهند بود و اگر این سرویس را در بیش از یک کامپوننت ثبت کنیم، آنگاه دیگر هدف اصلی طول عمر singleton یک سرویس مفهومی نداشته و برنامه هم اکنون دارای چندین وهله از سرویس تعریف شدهی ما میگردد و دیگر نمیتوان اطلاعات یکسانی را بین کامپوننتها به اشتراک گذاشت.
بنابراین توصیه شدهاست که از خاصیت providers کامپوننتهای غیر ریشهای، صرفنظر کرده و سرویسهای خود را تنها در بالاترین سطح کامپوننتهای تعریف شده، یعنی در فایل app.component.ts ثبت و معرفی کنید. به این ترتیب تک وهلهی ایجاد شدهی در اینجا، در این کامپوننت ریشهای و تمام زیر کامپوننتهای آن (یعنی تمام کامپوننتهای دیگر برنامه) به صورت یکسانی در دسترس قرار میگیرد.
به همین جهت فایل app.component.ts را گشوده و تغییرات ذیل را به آن اعمال کنید:
import { Component } from 'angular2/core'; import { ProductListComponent } from './products/product-list.component'; import { ProductService } from './products/product.service'; @Component({ selector: 'pm-app', template:` <div><h1>{{pageTitle}}</h1> <pm-products></pm-products> </div> `, directives: [ProductListComponent], providers: [ProductService] }) export class AppComponent { pageTitle: string = "DNT AngularJS 2.0 APP"; }
الف) خاصیت providers که آرایهای از سرویسها را قبول میکند، با ProductService مقدار دهی شدهاست.
ب) در ابتدای فایل، ProductService، از ماژول آن import گردیدهاست.
تزریق سرویسها به کامپوننتها
تا اینجا یک سرویس جدید را ایجاد کردیم و سپس آنرا به AngularJS 2.0 Injector معرفی نمودیم. اکنون نوبت به استفاده و تزریق آن، به کلاسی است که به این وابستگی نیاز دارد. در TypeScript، تزریق وابستگیها در سازندهی یک کلاس صورت میگیرند. هر کلاس، دارای متد سازندهای است که در زمان وهله سازی آن، اجرا میشود. اگر نیاز به تزریق وابستگیها باشد، تعریف این سازنده به صورت صریح، ضروری است. باید دقت داشت که هدف اصلی از متد سازنده، آغاز و مقدار دهی متغیرها و وابستگیهای مورد نیاز یک کلاس است و باید تا حد امکان از منطقهای طولانی عاری باشد.
در ادامه فایل product-list.component.ts را گشوده و سپس سازندهی ذیل را به آن اضافه کنید:
import { ProductService } from './product.service'; export class ProductListComponent implements OnInit { pageTitle: string = 'Product List'; imageWidth: number = 50; imageMargin: number = 2; showImage: boolean = false; listFilter: string = 'cart'; constructor(private _productService: ProductService) { }
روش خلاصه شدهای که در اینجا جهت تعریف سازندهی کلاس و متغیر تعریف شدهی در آن بکار گرفته شده، معادل قطعه کد متداول ذیل است و هر دو حالت ذکر شده، در TypeScript یکی میباشند:
private _productService: ProductService; constructor(productService: ProductService) { _productService = productService; }
این وابستگی در اولین باری که کلاس کامپوننت، توسط AngularJS 2.0 وهله سازی میشود، از لیست providers ثبت شدهی در کامپوننت ریشهی سایت، تامین خواهد شد.
اکنون نوبت به استفادهی از این سرویس تزریق شدهاست. به همین جهت ابتدا لیست عناصر آرایهی خاصیت products را حذف میکنیم (برای اینکه قرار است این سرویس، کار تامین اطلاعات را انجام دهد و نه کلاس کامپوننت).
products: IProduct[];
this.products = _productService.getProducts();
به همین جهت روش صحیح انجام این مقدار دهی، با پیاده سازی life cycle hook ویژهای به نام OnInit است که در قسمت پنجم آنرا معرفی کردیم:
export class ProductListComponent implements OnInit { products: IProduct[]; constructor(private _productService: ProductService) { } ngOnInit(): void { //console.log('In OnInit'); this.products = this._productService.getProducts(); }
در اینجا اکنون خاصیت products عاری است از ذکر صریح عناصر تشکیل دهندهی آن. سپس وابستگی مورد نیاز، در سازندهی کلاس تزریق شدهاست و در آخر، در رویداد چرخهی حیات ngOnInit، با استفاده از این وابستگی تزریقی، لیست محصولات دریافت و به خاصیت عمومی products نسبت داده شدهاست.
در ادامه برنامه را اجرا کنید. باید هنوز هم مطابق قبل، لیست محصولات قابل مشاهده باشد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MVC5Angular2.part7.zip
خلاصهی بحث
فرآیند کلی تعریف یک سرویس AngularJS 2.0، تفاوتی با ساخت یک کامپوننت یا Pipe سفارشی ندارد. پس از تعریف کلاسی که نام آن ختم شدهی به Service است، آنرا مزین به ()Injectable@ میکنیم. سپس این سرویس را در بالاترین سطح کامپوننتهای موجود یا همان کامپوننت ریشهی سایت، ثبت و معرفی میکنیم؛ تا تنها یک وهله از آن توسط AngularJS 2.0 Injector ایجاد شده و در اختیار تمام کامپوننتهای برنامه قرار گیرد. البته اگر این سرویس تنها در یک کامپوننت استفاده میشود و قصد به اشتراک گذاری اطلاعات آنرا نداریم، میتوان سطح سلسله مراتب دسترسی به آنرا نیز کاهش داد. برای مثال این سرویس را در لیست providers همان کامپوننت ویژه، ثبت و معرفی کرد. به این ترتیب تنها این کامپوننت خاص و فرزندان آن دسترسی به امکانات سرویس مدنظر را مییابند و نه تمام کامپوننتهای دیگر تعریف شدهی در برنامه.
در ادامه هر کلاسی که به این سرویس نیاز دارد (با توجه به سلسه مراتب دسترسی ذکر شده)، تنها کافی است در سازندهی خود، این وابستگی را اعلام کند تا توسط AngularJS 2.0 Injector تامین گردد.
در پروژهی iOS، در فایل AppDelegate.cs، بعد از Forms.Init، کد زیر را کپی کنید:
SfListViewRenderer.Init();
همین کد را در MainPage.xaml.cs در پروژه UWP، قبل از LoadApplication قرار دهید. نیازی به انجام کاری در Android نیست.
سپس Product Key این محصول را به دست آورده و در پروژه XamApp، فولدر Views در فایل SyncfusionLicense قرار دهید.
حال برای نمایش لیستی از محصولات، ابتدا کلاس Product را ایجاد میکنیم. چه در زمانیکه یک Rest api را در سمت سرور فراخوانی میکنیم و چه زمانیکه با دیتابیس بر روی گوشی یعنی Sqlite کار میکنیم، در نهایت لیستی از یک کلاس را داریم (در اینجا Product).
public class Product : Bindable { public int Id { get; set; } public string Name { get; set; } public bool IsActive { get; set; } public decimal Price { get; set; } }
در یک View Model جدید با نام ProductsViewModel، در OnNavigatedToAsync، دیتا را از سرور یا دیتابیس، بر روی گوشی دریافت میکنیم؛ اما در این مثال، برای راحتی بیشتر یک List را New میکنیم:
public class ProductsViewModel : BitViewModelBase { public List<Product> Products { get; set; } public async override Task OnNavigatedToAsync(INavigationParameters parameters) { Products = new List<Product> // getting products from server or sqlite database { new Product { Id = 1, IsActive = true, Name = "Product1" , Price = 12.2m /* m => decimal */ }, new Product { Id = 2, IsActive = false, Name = "Product2" , Price = 14 }, new Product { Id = 3, IsActive = true, Name = "Product3" , Price = 11 }, }; await base.OnNavigatedToAsync(parameters); } }
حال نوبت به دادن یک Template میرسد. مثلا فرض کنید میخواهیم نام را درون یک Label نمایش دهیم و بر اساس فعال یا غیر فعال بودن Product، یک Checkbox را تغییر داده، تیک بزنیم یا نزنیم و در نهایت نمایش قیمت را در یک Label دیگر خواهیم داشت.
<sfListView:SfListView ItemsSource="{Binding Products}"> <sfListView:SfListView.ItemTemplate> <DataTemplate> <FlexLayout x:DataType="model:Product" Direction="Row"> <Label FlexLayout.Basis="50%" Text="{Binding Name}" VerticalTextAlignment="Center" /> <bitControls:BitCheckbox InputTransparent="True" FlexLayout.Basis="25%" IsChecked="{Binding IsActive}" /> <Label FlexLayout.Basis="25%" Text="{Binding Price}" VerticalTextAlignment="Center" /> </FlexLayout> </DataTemplate> </sfListView:SfListView.ItemTemplate> </sfListView:SfListView>
همانطور که میبینید، در DataTemplate از Flex Layout استفاده شده است. Flex Layout در کنار Grid, Stack, Relative, Absolute و سایر Layoutهای Xamarin Forms در پروژه قابلیت استفاده دارد و مزیتهای خاص خود را دارد.
این Data Template توسط List View، حداکثر سه بار ساخته میشود؛ چون View Model در لیست مثال خود، سه Product دارد. خود List View تکنیکهای Virtualization و Cell Reuse را بدون نیاز به هیچ کد اضافهای هندل میکند و Performance خوبی دارد. در View مربوطه یعنی ProductsView.xaml، هر Binding ای (مثل Binding Products) به View Model اشاره میکند، اما درون Data Template، هر Binding به Product ای اشاره میکند که آن ردیف List View، دارد نمایشاش میدهد. برای همین x:DataType را روی Flex Layout درون Data Template به Product وصل کردهایم. در این صورت اگر بنویسیم Binding N_ame، به ما خطا داده میشود که کلاس Product هیچ Property با نام N_ame ندارد که خطای درستی است.
روی BitCheckbox مقدار InputTransparent را برابر با True دادهایم که باعث میشود کلیک روی Checkbox عملا در نظر گرفته نشود. این منطقی است، زیرا عوض کردن مقدار Checkbox در این مثال ما ذخیره نمیشود و کاربرد نمایشی دارد و فقط باعث گیج شدن کاربر میشود.
کنترل BitCheckbox از مجموعه کنترلهای Bit است که اخیرا با BitDatePicker آن آشنا شدهاید. برای آشنایی با نحوه افزودن این کنترلها به یک پروژه، به مستندات Bit Framework مراجعه کنید. خود Syncfusion نیز Checkbox دارد.
حال فرض کنید که قرار است دکمهای برای هر ردیف List View داشته باشیم که با زدن روی آن، اطلاعات Product به سرور ارسال شود و جزئیات بیشتری دریافت و در قالب یک Alert نمایش داده شود. برای این کار، ابتدا به Data Template که Flex Layout است، یک دکمه اضافه میکنیم. سپس Command آن دکمه را به View Model بایند میکنیم. در آن Command البته احتیاج داریم بدانیم درخواست نمایش جزئیات بیشتر، برای کدام Product داده شده. این مهم با Command Parameter شدنی است.
برای پیاده سازی این مثال، در سمت View Model داریم:
public BitDelegateCommand<Product> ShowProductDetailsCommand { get; set; }public IUserDialogs UserDialogs { get; set; } async Task ShowProductDetails(Product product) { string productDetail = $"Product: {product.Name}'s more info: ..."; // get more info from server. await UserDialogs.AlertAsync(productDetail, "Product Detail"); }
کامند ShowProductDetailCommand یک پارامتر را از جنس Product میگیرد و آن Product ای است که روی دکمه آن کلیک شدهاست. با Clone کردن آخرین نسخه XamApp و درخواست نمایش صفحهی Products در App.xaml.cs به صورت زیر و اجرای برنامه، میتوانید درک بهتری از عملکرد آن داشته باشید:
await NavigationService.NavigateAsync("/Nav/Products", animated: false);
سپس در View مربوطه داریم:
...<Button Command="{Binding ShowProductDetailsCommand}" CommandParameter="{Binding .}" Text="Detail..." /> </FlexLayout> </DataTemplate>
CommandParameter اگر برابر با Binding Id میبود، به Command در سمت View Model، بجای کل Product، فقط Id آن ارسال میشد. ولی Show Product Detail Command منتظر یک Product کامل است، نه فقط Id آن. با نوشتن
CommandParameter="{Binding .}"
کل Product با کلیک روی دکمه به Command ارسال میشود.
اکنون اگر پروژه را Build کنید، خطایی را از x:DataType خواهید گرفت که منطقی است. اگر Binding Name و Binding Price دو Property با نامهای Name و Price را از کلاس Product جستجو میکنند، پس قاعدتا ShowProductDetailCommand نیز در همان کلاس مدل، یعنی Product جستجو میشود! ولی میدانیم که این Command در View Model ما یعنی ProductsViewModel است. برای حل این مشکل، به جای Binding از bit:ViewModelBinding استفاده میکنیم:
Command="{bit:ViewModelBinding ShowProductDetailsCommand}"
در این صورت، بجای جستجو کردن ShowProductDetailCommand در کلاس Product، این را در ProductsViewModel جستجو میکند که منجر به خروجی درست میشود.
این List View دارای امکاناتی چون Infinite loading، Pull to refresh و Grouping-Sorting-Filtering و ... است که میتوانید از روی مستندات خوب Syncfusion، آنها را راه اندازی کنید و اگر به مشکلی برخوردید نیز اینجا بپرسید. همچنین نگاهی به لیست 129 کنترل دیگر بیاندازید و ببینید که در برنامههای خود از کدام یک از آنها میتوانید استفاده کنید.
<InputText @bind-Value="employee.FirstName" />
[Parameter] public string Value { set; get; }
[Parameter] public EventCallback<string> ValueChanged { get; set; }
[Parameter] public Action<string> ValueChanged { get; set; }
[Parameter] public Expression<Func<string>> ValueExpression { get; set; }
مرحلهی آخر این طراحی، فراخوانی پارامتر ValueChanged است تا به کامپوننت والد این تغییرات را اطلاع رسانی کنیم. روش استاندارد آن به صورت زیر است:
private string _value; [Parameter] public string Value { get => _value; set { var hasChanged = string.Equals(_value, value, StringComparison.Ordinal); if (hasChanged) { _value = value; if (ValueChanged.HasDelegate) { _ = ValueChanged.InvokeAsync(value); } } } }
در این قطعه کد، بررسی ValueChanged.HasDelegate را هم مشاهده میکنید. زمانیکه پارامتر Value ای با طی سه مرحلهی فوق تعریف شد، قرار نیست حتما توسط bind-Value@ مورد استفاده قرار گیرد. میتوان Value را به صورت یک طرفه هم مورد استفاده قرار داد. در این حالت دو پارامتر ب و ج دیگر توسط Blazor ایجاد و مقدار دهی نشده و رهگیری نخواهند شد. یعنی تعریف bind-Value@ در سمت والد، معادل سیم کشی خودکار به ValueChanged و ValueExpression از طرف Blazor است و تعریف دستی آنها ضرورتی ندارد. اما میتوان bind-Value@ را هم تعریف نکرد و فقط نوشت Value. در این حالت از تنظیمات ب و ج صرفنظر میشود. بنابراین ضروری است که بررسی کنیم آیا پارامتر ValueChanged واقعا متصل به روال رویدادگردانی شدهاست یا خیر. اگر خیر، نیازی به اطلاع رسانی و فراخوانی متد ValueChanged.InvokeAsync نیست.
مقدمهای بر RxJS
اگر به پیشنیازهای نصب AngularJS 2.0 در قسمت اول این سری دقت کرده باشید، یکی از موارد آن، RxJS است:
"dependencies": { "rxjs": "5.0.0-beta.2" },
قرار است Observableها به ES 2016 یا نگارش پس از ES 6 اضافه شوند و یکی از پیشنهادات آن هستند. اما هم اکنون AngularJS 2.0 از این امکان، توسط یک کتابخانهی ثالث، به نام reactive extensions یا Rx، استفاده میکند. از RxJS در سرویس HTTP و همچنین مدیریت سیستم رخدادهای AngularJS 2.0 استفاده میشود. Observableها امکانی را فراهم میکنند تا به ازای دریافت هر اطلاعات async از سرور، بتوان توسط رخدادهایی از وقوع آنها مطلع شد.
در نگارش قبلی AngularJS از Promises برای مدیریت اعمال غیرهمزمان استفاده میشد. Observableها تفاوتهای قابل ملاحظهای با Promises دارند:
- یک Promise تنها یک مقدار، یا خطا را بر میگرداند؛ اما یک Observable چندین مقدار را در طول یک بازهی زمانی باز میگرداند.
- برخلاف Promises، میتوان عملیات یک Observable را لغو کرد.
- Observableها از عملگرهایی مانند map، reduce، filter و غیره نیز پشتیبانی میکنند.
البته باید عنوان کرد که هنوز هم میتوان از Promises در صورت تمایل در AngularJS 2.0 نیز استفاده کرد.
تنظیمات اولیهی کار با RxJS در AngularJS 2.0
برای استفاده از RxJS در AngularJS 2.0، مراحلی مانند افزودن مدخل اسکریپت http.dev.js، ثبت پروایدر HTTP و importهای لازم، باید طی شوند که در ادامه آنها را بررسی خواهیم کرد:
الف) سرویس HTTP جزئی از angular2/core نیست. به همین جهت مدخل اسکریپت متناظر با آن، باید به صفحهی اصلی سایت اضافه شود که این مورد، در قسمت اول بررسی پیشنیازهای نصب AngularJS 2.0 صورت گرفتهاست:
<!-- Required for http --> <script src="~/node_modules/angular2/bundles/http.dev.js"></script>
ب) اکنون فایل app.component.ts را گشوده و سرویس HTTP را به آن اضافه میکنیم. با نحوهی ثبت سرویسها در قسمت قبل آشنا شدیم:
import { Component } from 'angular2/core'; import { HTTP_PROVIDERS } from 'angular2/http'; import 'rxjs/Rx'; // Load all features import { ProductListComponent } from './products/product-list.component'; import { ProductService } from './products/product.service'; @Component({ selector: 'pm-app', template:` <div><h1>{{pageTitle}}</h1> <pm-products></pm-products> </div> `, directives: [ProductListComponent], providers: [ ProductService, HTTP_PROVIDERS ] }) export class AppComponent { pageTitle: string = "DNT AngularJS 2.0 APP"; }
ج) پس از آن نیاز است importهای متناظر نیز به ابتدای ماژول فعلی، جهت شناسایی این سرویس و همچنین امکانات rx.js اضافه شوند.
تعریف 'import 'rxjs/Rx به این شکل، به module loader اعلام میکند که این کتابخانه را بارگذاری کن، اما چیزی را import نکن. هنگامیکه این کتابخانه بارگذاری میشود، کدهای جاوا اسکریپتی آن اجرا شده و سبب میشوند که عملگرهای ویژهی Observable آن مانند map و filter نیز در دسترس برنامه قرار گیرند.
ساخت یک سرویس سمت سرور بازگشت لیست محصولات به صورت JSON
چون در ادامه میخواهیم لیست محصولات را از سرور دریافت کنیم، برنامهی ASP.NET MVC فعلی را اندکی تغییر میدهیم تا این لیست را به صورت JSON بازگشت دهد.
بنابراین ابتدا کلاس مدل محصولات را به نحو ذیل به پوشهی Models اضافه کرده:
namespace MVC5Angular2.Models { public class Product { public int ProductId { set; get; } public string ProductName { set; get; } public string ProductCode { set; get; } public string ReleaseDate { set; get; } public decimal Price { set; get; } public string Description { set; get; } public double StarRating { set; get; } public string ImageUrl { set; get; } } }
using System.Collections.Generic; using System.Text; using System.Web.Mvc; using MVC5Angular2.Models; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; namespace MVC5Angular2.Controllers { public class HomeController : Controller { // GET: Home public ActionResult Index() { return View(); } public ActionResult Products() { var products = new List<Product> { new Product { ProductId= 2, ProductName= "Garden Cart", ProductCode= "GDN-0023", ReleaseDate= "March 18, 2016", Description= "15 gallon capacity rolling garden cart", Price= (decimal) 32.99, StarRating= 4.2, ImageUrl= "app/assets/images/garden_cart.png" }, new Product { ProductId= 5, ProductName= "Hammer", ProductCode= "TBX-0048", ReleaseDate= "May 21, 2016", Description= "Curved claw steel hammer", Price= (decimal) 8.9, StarRating= 4.8, ImageUrl= "app/assets/images/rejon_Hammer.png" } }; return new ContentResult { Content = JsonConvert.SerializeObject(products, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }), ContentType = "application/json", ContentEncoding = Encoding.UTF8 }; } } }
برای مطالعهی بیشتر:
«استفاده از JSON.NET در ASP.NET MVC»
«تنظیمات و نکات کاربردی کتابخانهی JSON.NET»
به این ترتیب، آدرس http://localhost:2222/home/products، خروجی JSON سرویس لیست محصولات را در مثال جاری، ارائه میدهد.
ارسال یک درخواست HTTP به سرور توسط AngularJS 2.0
اکنون پس از تنظیمات ثبت و معرفی سرویس HTTP و همچنین برپایی یک سرویس سمت سرور ارائهی لیست محصولات، میخواهیم سرویس ProductService را که در قسمت قبل ایجاد کردیم (فایل product.service.ts)، جهت دریافت لیست محصولات از سمت سرور، تغییر دهیم:
import { Injectable } from 'angular2/core'; import { IProduct } from './product'; import { Http, Response } from 'angular2/http'; import { Observable } from 'rxjs/Observable'; @Injectable() export class ProductService { private _productUrl = '/home/products'; constructor(private _http: Http) { } getProducts(): Observable<IProduct[]> { return this._http.get(this._productUrl) .map((response: Response) => <IProduct[]>response.json()) .do(data => console.log("All: " + JSON.stringify(data))) .catch(this.handleError); } private handleError(error: Response) { console.error(error); return Observable.throw(error.json().error || 'Server error'); } }
در سازندهی کلاس ProductService، کار تزریق وابستگی سرویس Http انجام شدهاست. به این ترتیب با استفاده از متغیر خصوصی http_، میتوان در کلاس جاری به امکانات این سرویس دسترسی یافت (همان «تزریق سرویسها به کامپوننتها» در قسمت قبل).
سپس متد get آن، یک درخواست HTTP از نوع GET را به آدرس مشخص شدهی در متغیر productUrl_ ارسال میکند (یا همان سرویس سمت سرور برنامه).
سرویس Http و همچنین شیء Response آن در ماژولهای Http و Response قرار دارند که در ابتدای صفحه import شدهاند.
متد http get یک Observable را بازگشت میدهد که در نهایت خروجی این متد نیز به همان <[]Observable<IProduct، تنظیم شدهاست. Observable یک شیء جنریک است و در اینجا نوع آن، آرایهای از محصولات درنظر گرفته شدهاست.
اکنون که امضای این متد تغییر یافته است (پیش از این صرفا یک آرایهی ساده از محصولات بود)، استفاده کننده (در کلاس ProductListComponent) باید به تغییرات آن از طریق متد subscribe گوش فرا دهد.
فعلا در کلاس جاری، پس از پایان کار دریافت اطلاعات از سرور، اطلاعات نهایی در متد map در دسترس قرار میگیرد (که یکی از عملگرهای RxJs است). کار متد map، اصطلاحا projection است. این متد، هر عضو دریافتی از خروجی سرور را به فرمتی جدید نگاشت میکند.
هر درخواست HTTP، در اصل یک عملیات async است. یعنی در اینجا توالی که در اختیار Observable ما قرار میگیرد، تنها یک المان دارد که همان شیء HTTP Response است.
بنابراین کار متد map فوق، تبدیل شیء خروجی از سرور، به آرایهای از محصولات است.
در اینجا یک سری کدهای مدیریت استثناءها را نیز در صورت بروز مشکلی میتوان تعریف کرد. برای مثال در اینجا متد catch، کار پردازش خطاهای رخ داده را انجام میدهد.
از متد do جهت لاگ کردن عملیات رخ داده و دادههای دریافتی در کنسول developer tools مرورگرها استفاده شدهاست.
یک نکته:
اگر خروجی JSON از سرور، برای مثال داخل خاصیتی به نام data محصور شده بود، بجای ()response.json میبایستی از response.json().data استفاده میشد.
گوش فرا دادن به Observable دریافتی از سرور
تا اینجا یک درخواست HTTP GET را به سمت سرور ارسال کردیم و خروجی آن به صورت Observable در اختیار ما است. اکنون نیاز است کدهای ProductListComponent را جهت گوش فرا دادن به این Observable تغییر دهیم. برای این منظور فایل product-list.component.ts را گشوده و تغییرات ذیل را به آن اعمال کنید:
errorMessage: string; ngOnInit(): void { //console.log('In OnInit'); this._productService.getProducts() .subscribe( products => this.products = products, error => this.errorMessage = <any>error); }
اولین پارامتر متد subscribe، کار دریافت نتایج حاصل را به عهده دارد. برای مثال اگر حاصل عملیات در طی سه مرحله صورت گیرد، سه بار نتیجهی دریافتی را میتوان در اینجا پردازش کرد. البته همانطور که عنوان شد، یک عملیات غیرهمزمان HTTP، تنها در طی یک مرحله، HTTP Response را دریافت میکند؛ بنابراین، پارامتر اول متد subscribe نیز تنها یکبار اجرا میشود. در اینجا فرصت خواهیم داشت تا آرایهی دریافتی حاصل از متد map قسمت قبل را به خاصیت عمومی products کلاس جاری نسبت دهیم.
پارامتر دوم متد subscribe در صورت شکست عملیات فراخوانی میشود. در اینجا حاصل آن به خاصیت جدید errorMessage نسبت داده شدهاست.
اکنون برنامه را مجددا اجرا کنید، هنوز باید لیست محصولات، مانند قبل نمایش داده شود.
یک نکته
اگر برنامه را اجرا کردید و خروجی مشاهده نشد، به کنسول developer tools مرورگر مراجعه کنید؛ احتمالا خطای ذیل در آن درج شدهاست:
EXCEPTION: No provider for Http!
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MVC5Angular2.part8.zip
خلاصهی بحث
برای کار با سرور و ارسال درخواستهای HTTP، ابتدا نیاز است مدخل تعریف http.dev.js به index.html اضافه شود و سپس HTTP_PROVIDERS را در بالاترین سطح کامپوننتهای تعریف شده، ثبت و معرفی کرد. پس از آن نیاز است RxJs را نیز import کرد. در ادامه، سرویس دریافت لیست محصولات، وابستگی سرویس HTTP را توسط سازندهی خود دریافت کرده و از آن برای صدور یک فرمان HTTP GET استفاده میکند. سپس با استفاده از متد map، کار نگاشت شیء Response دریافتی، به فرمت مناسب مدنظر، صورت میگیرد.
در ادامه هر کلاسی که نیاز دارد با این کلاس سرویس دریافت اطلاعات کار کند، متد subscribe را فراخوانی کرده و نتیجهی عملیات را پردازش میکند.
آموزش Knockout.Js #1
در یک جمله Knockout.Js یک فریم ورک جاوا اسکریپ است که امکان پیاده سازی الگوی MVVM و مکانیزم data-binding را در پروژههای تحت وب به راحتی میسر میکند. به عبارت دیگر عناصر DOM را به data-model و آبجکتهای data-model را به عناصر DOM مقید میکند، به طوری که با هر تغییر در مقدار یا وضعیت این عناصر یا آبجکت ها، تغییرات به موارد مقید شده نیز اعمال میگردد. به تصاویر زیر دقت کنید!
به روز رسانی data-model بدون استفاده از KO
به روز رسانی data-model با استفاده از KO
ویژگیهای مهم KO
»ارائه یک راه حل بسیار ساده و واضح برای اتصال بخشهای مختلف UI به data-model
»به روز رسانی خودکار عناصر و بخشهای مختلف UI بر اساس تغییرات صورت گرفته در data-model
»به صورت کامل با کتابخانه و توابع javascript پیاده سازی شده است.
»حجم بسیار کم(سیزده کیلو بایت) بعد از فشرده سازی
»سازگار با تمام مروگرهای جدید(... ,IE 6+, Firefox 2+, Chrome, Safari )
»امکان استفاده راحت بدون اعمال تغییرات اساسی در معماری پروژه هایی که در فاز توسعه هستند و بخشی از مسیر توسعه را طی کرده اند
»و...
آیا KO برای تکمیل JQuery در نظر گرفته شده است یا جایگزین؟
»به دست آوردن تعداد divهای موجود با استفاده از یک کلاس مشخص css؛
» یا حتی به دست آوردن تعداد آیتمهای نمایشی در span هایی مشخص.
و البته سایر راه حل ها...
حال فرض کنید دکمههای دیگر نظیر Delete نیز مد نظر باشد که مراحل بالا تکرار خواهند شد. اما با استفاده از KO به راحتی میتوانیم تعداد آیتمهای موجود در یک آرایه را به یک عنصر مشخص bind کنیم به طور با هر تغییر در این مقدار، عنصر مورد نظر نیز به روز میشود یا به بیانی دیگر همواره تغییرات observe خواهند شد. برای مثال:
Number of items :<span data-bind="text: myList().count"></span>
در پست بعد، شروع به کار با KO آموزش داده خواهد شد.
ادامه دارد...
پروژه Prerender.io
در مورد تنظیمات سمت سرور AngularJS 2.0 در ASP.NET Core 1.0، در قسمت «معرفی بستهی نیوگت Microsoft.AspNetCore.SpaServices» بیشتر بحث شدهاست.