آشنایی با TransactionScope
گزارش در سیلورلایت
- یک مثال در مورد نحوه استفاده از PdfReport در سیلورلایت به همراه سورسهای برنامه موجود است. لطفا مراجعه کنید به پوشه Samples\SlPdf. برای استفاده، از WCF Service استفاده کرده.
هرچند بسیاری از قابلیتهای C# 8.0 در خود کامپایلر #C پیاده سازی شدهاند، اما برای مثال قابلیتی مانند «پیاده سازی پیشفرض اینترفیسها» نیاز به یک runtime جدید دارد که به همراه NET Core 3.0. ارائه میشود. بنابراین NET Full 4x. شاهد پیاده سازی C# 8.0 نخواهد بود. همچنین یک سری از قابلیتهای C# 8.0 وابستهی به NET Standard 2.1. و netcoreapp3.0 هستند؛ مانند نوعهای جدید System.IAsyncDisposable و یا System.Range. به همین جهت است که برای کار با C# 8.0، حتما نیاز به نصب NET Core 3.0. نیز میباشد و به روز رسانی کامپایلر #C کافی نیست.
چه نگارشهایی از Visual Studio از NET Core 3.0. پشتیبانی میکنند؟
مطابق مستندات رسمی موجود، یک چنین جدولی در مورد نگارشهای مختلف NET Core. و نگارشهای ویژوال استودیوهایی از که از آنها پشتیبانی میکنند، وجود دارد:
.NET Core SDK | .NET Core Runtime | Compatible Visual Studio | MSBuild | Notes |
---|---|---|---|---|
2.1.5nn | 2.1 | 2017 | 15 | Installed as part of VS 2017 version 15.9 |
2.1.6nn | 2.1 | 2019 | 16 | Installed as part of VS 2019 |
2.2.1nn | 2.2 | 2017 | 15 | Installed manually |
2.2.2nn | 2.2 | 2019 | 16 | Installed as part of VS 2019 |
3.0.1nn | 3.0 (Preview) | 2019 | 16 | Installed manually |
بنابراین فقط VS 2019 است که قابلیت پشتیبانی از NET Core 3.0. را دارد. به همین جهت اگر قصد دارید با ویژوال استودیو کار کنید، نصب VS 2019 برای کار با C# 8.0 الزامی است.
فعالسازی C# 8.0 در ویژوال استودیو 2019
در زمان نگارش این مطلب، NET Core 3.0. در حالت پیشنمایش، ارائه شدهاست. به همین جهت جزء یکپارچهی VS 2019 محسوب نشده و باید جداگانه نصب شود:
- برای این منظور ابتدا نیاز است آخرین نگارش NET Core 3.0 SDK. را دریافت و نصب کنید.
- سپس از منوی Tools | Options، گزینهی Projects and Solutions را انتخاب و در ادامه گزینهی Use previews of the .NET Core SDK را انتخاب کنید.
- پس از آن، این SDK جدید NET Core. به صورت زیر قابل انتخاب خواهد بود:
البته انتخاب شماره SDK صحیح به تنهایی برای کار با C# 8.0 کافی نیست؛ بلکه باید شمارهی زبان مورد استفاده را نیز صریحا انتخاب کرد:
برای اینکار بر روی پروژه کلیک راست کرده و گزینهی Properties آنرا انتخاب کنید. سپس در اینجا در برگهی Build، بر روی دکمهی Advanced کلیک کنید تا بتوان شماره نگارش زبان را مطابق تصویر فوق انتخاب کرد. در اینجا بجای C# 8.0 (beta)، گزینهی unsupported preview را نیز میتوانید انتخاب کنید.
یک نکته: خلاصهی تمام این مراحل، منوها و تصاویر، همان تنظیمات فایل csproj است که در ادامه بررسی میکنیم.
فعالسازی C# 8.0 در VSCode
مدتها است که برای کار با NET Core. نیازی به استفادهی از نگارش کامل ویژوال استودیو نیست. همینقدر که VSCode را به همراه افزونهی #C آن نصب کرده باشید، میتوانید برنامههای مبتنی بر NET Core. را بر روی سیستم عاملهای مختلفی که NET Core SDK. بر روی آنها نصب شدهاست، توسعه دهید.
پشتیبانی ابتدایی از C# 8.0، با نگارش v1.18.0 افزونهی #C مخصوص VSCode ارائه شد. بنابراین هم اکنون اگر آخرین نگارش آنرا نصب کرده باشید، امکان کار با پروژههای NET Core 3.0 و C# 8.0 را نیز دارید.
بنابراین در اینجا به صورت خلاصه:
- ابتدا باید NET Core 3.0 SDK. را به صورت جداگانهای دریافت و نصب کنید.
- سپس آخرین نگارش افزونهی #C مخصوص VSCode را نیز نصب کنید.
- در آخر، یک پوشهی جدید را ایجاد کرده و در خط فرمان دستور dotnet new console را صادر کنید. این دستور بر اساس آخرین شماره نگارش SDK نصب شده، یک پروژهی جدید کنسول را ایجاد میکند که ساختار فایل csproj آن به صورت زیر است:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>netcoreapp3.0</TargetFramework> </PropertyGroup> </Project>
- یا معادل همان گزینهی unsupported preview در تصویر سوم این مطلب:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>netcoreapp3.0</TargetFramework> <LangVersion>preview</LangVersion> </PropertyGroup> </Project>
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>netcoreapp3.0</TargetFramework> <LangVersion>8.0</LangVersion> </PropertyGroup> </Project>
یک نکته: در اینجا نمیتوان LangVersion را به latest تنظیم کرد؛ چون C# 8.0 هنوز در مرحلهی بتا است. زمانیکه از مرحلهی بتا خارج شد، مقدار پیشفرض آن دقیقا latest خواهد بود و ذکر صریح آن غیر ضروری است. انتخاب latest در اینجا به latest minor version یا همان نگارش C# 7.3 فعلی (آخرین نگارش پایدار زبان #C در زمان نگارش این مطلب) اشاره میکند.
Rider و پشتیبانی از C# 8.0
Rider 2019.1 نیز به همراه پشتیبانی از C# 8.0 ارائه شدهاست و میتواند گزینهی مطلوب دیگری برای توسعهی برنامههای مبتنی بر NET Core. باشد.
نصب NET Core 3.0 SDK. و عدم اجرای برنامههای پیشین
یکی از مزایای کار با NET Core.، امکان نصب چندین نوع مختلف SDK آن، به موازت هم است؛ بدون اینکه بر روی یکدیگر تاثیری بگذارند. البته این نکته را باید درنظر داشت که برنامههای NET Core. بدون وجود فایل مخصوص global.json در پوشهی ریشهی آنها، همواره از آخرین نگارش SDK نصب شده، برای اجرا استفاده خواهند کرد. اگر این مورد بر روی کار شما تاثیرگذار است، میتوانید شماره SDK مورد استفادهی برنامهی خود را قفل کنید، تا SDKهای جدید نصب شده، به عنوان SDK پیشفرض برنامههای پیشین، انتخاب نشوند. بنابراین ابتدا لیست SDKهای نصب شده را با دستور زیر پیدا کنید:
> dotnet --list-sdks
> dotnet new globaljson --sdk-version 2.2.300 > type global.json
برای انتقال دادهها از طریق WCF بین سیستمهای مختلف باید دادههای مورد نظر حتما سریالایز شوند که مثال هایی از این دست رو در همین سایت میتونید مطالعه کنید:
(^ ) و (^ ) و (^ )
با توجه به این که دادهها سریالایز میشوند، در نتیجه امکان انقال داده هایی که از نوع object هستند در WCF وجود ندارد. بلکه نوع داده باید صراحتا ذکر شود و این نوع باید قابیلت سریالایز شدن را دارا باشد.برای مثال شما نمیتونید متدی داشته باشید که پارامتر ورودی آن از نوع delegate باشد یا کلاسی باشد که صفت [Serializable] در بالای اون قرار نداشته باشد یا کلاسی باشد که صفت DataContract برای خود کلاس و صفت DataMember برای خاصیتهای اون تعریف نشده باشد. حالا سوال مهم این است اگر متدی داشته باشیم که پارامتر ورودی آن حتما باید از نوع delegate باشد چه باید کرد؟
برای تشریح بهتر مسئله یک مثال میزنم؟
سرویسی داریم برای اطلاعات کتاب ها. قصد داریم متدی بنوسیم که پارامتر
ورودی آن از نوع Lambda Expression است تا Query مورد نظر کاربر از سمت
کلاینت به سمت سرور دریافت کند و خروجی مورد نظر را با توجه به Query ورودی
به کلاینت برگشت دهد.( متدی متداول در اکثر پروژه ها). به صورت زیر عمل میکنیم.
*ابتدا یک Blank Solution ایجاد کنید.
*یک ClassLibrary به نام Model ایجاد کنید و کلاسی به نام Book در آن بسازید .(همانطور که میبینید کلاس مورد نظر سریالایز شده است):
[DataContract] public class Book { [DataMember] public int Code { get; set; } [DataMember] public string Title { get; set; } }
یک Contract برای ارتباط بین سرور و کلاینت میسازیم:
using System; using System.Collections.Generic; using System.Linq.Expressions; using System.ServiceModel; namespace WcfLambdaExpression { [ServiceContract] public interface IBookService { [OperationContract] IEnumerable<Book> GetByExpression( Expression<Func<Book, bool>> expression ); } }
using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; namespace WcfLambdaExpression { public class BookService : IBookService { public BookService() { ListOfBook = new List<Book>(); } public List<Book> ListOfBook { get; private set; } public IEnumerable<Book> GetByExpression( Expression<Func<Book, bool>> expression ) { ListOfBook.AddRange( new Book[] { new Book(){Code = 1 , Title = "Book1"}, new Book(){Code = 2 , Title = "Book2"}, new Book(){Code = 3 , Title = "Book3"}, new Book(){Code = 4 , Title = "Book4"}, new Book(){Code = 5 , Title = "Book5"}, } ); return ListOfBook.AsQueryable().Where( expression ); } } }
به طور حتم با خطا روبرو خواهید شد. دلیل آن هم این است که امکان سریالایز کردن برای پارامتر ورودی expression میسر نیست.
خطای مربوطه به شکل زیر خواهد بود:
Type 'System.Linq.Expressions.Expression`1[System.Func`2[WcfLambdaExpression.Book,System.Boolean]]' cannot be serialized. Consider marking it with the DataContractAttribute attribute, and marking all of its members you want serialized with the DataMemberAttribute attribute. If the type is a collection, consider marking it with the CollectionDataContractAttribute. See the Microsoft .NET Framework documentation for other supported types
روشهای زیادی برای بر طرف کردن این محدودیت وجود دارد. اما در این پست روشی رو که خودم از اون استفاده میکنم رو براتون شرح میدهم.
در این روش باید از XElement استفاده شود که در فضای نام System.Linq.Xml قرار دارد. یعنی آرگومان ورودی سمت کلاینت باید به فرمت Xml سریالایز شود و سمت سرور دوباره دی سریالایز شده و تبدیل به یک Lambda Expression شود. اما سریالایز کردن Lambda Expression واقعا کاری سخت و طاقت فرساست . با توجه به این که در اکثر پروژهها این متدها به صورت Generic نوشته میشوند. برای حل این مسئله بعد از مدتی جستجو، کلاسی رو پیدا کردم که این کار رو برام انجام میداد. بعد از مطالعه دقیق و مشاهده روش کار کلاس، تغییرات مورد نظرم رو اعمال کردم و الان در اکثر پروژه هام دارم از این کلاس استفاده میکنم.
یک مثال از روش استفاده :
برای اینکه از این کلاس در هر دو پروژه (سرور و کلاینت) استفاده میکنیم باید یک Class Library جدید به نام Common بسازید و یک ارجاع از اون رو به هر دو پروژه سمت سرور و کلاینت بدید.
سرویس و Contract بالا رو به صورت زیر باز نویسی کنید.
[ServiceContract] public interface IBookService { [OperationContract] IEnumerable<Book> GetByExpression( XElement expression ); }
using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Xml.Linq; namespace WcfLambdaExpression { public class BookService : IBookService { public BookService() { ListOfBook = new List<Book>(); } public List<Book> ListOfBook { get; private set; } public IEnumerable<Book> GetByExpression( XElement expression ) { ListOfBook.AddRange( new Book[] { new Book(){Code = 1 , Title = "Book1"}, new Book(){Code = 2 , Title = "Book2"}, new Book(){Code = 3 , Title = "Book3"}, new Book(){Code = 4 , Title = "Book4"}, new Book(){Code = 5 , Title = "Book5"}, } ); Common.ExpressionSerializer serializer = new Common.ExpressionSerializer(); return ListOfBook.AsQueryable().Where( serializer.Deserialize( expression ) as Expression<Func<Book, bool>> ); } }
using System; using System.Linq.Expressions; using TestExpression.MyBookService; namespace TestExpression { class Program { static void Main( string[] args ) { BookServiceClient bookService = new BookServiceClient(); Expression<Func<Book, bool>> expression = x => x.Code > 2 && x.Code < 5; Common.ExpressionSerializer serializer = new Common.ExpressionSerializer(); bookService.GetByExpression( serializer.Serialize( expression ) ); } } }
خروجی هم به صورت زیر خواهد بود:
دریافت سورس کامل Expression-Serialization
- کش کرنل به صورت پیش فرض بر روی صفحات ایستا فعال شده است؛ نه برای صفحاتی با محتوای پویا که البته این مورد قابل تغییر است که نحوه این تغییر را پایینتر توضیح خواهیم داد.
- اگر آدرس درخواستی شامل کوئری باشد صفحه کش نخواهد شد: http://www.site.info/postarchive.htm?id=25
- برای پاسخ ازمکانیزمهای فشرده سازی پویا استفاده شده باشد مثل gzip کش نخواهد شد
- صفحه درخواست شده صفحه اصلی سایت باشد کش نخواهد شد : http://www.dotnettip.info ولی اگر درخواست بدین صورت باشه http://www.domain.com/default.htm کش خواهد کرد.
- درخواست به صورت ناشناس anonymous نباشد و نیاز به authentication داشته باشد کش نخواهد شد (یعنی در هدر شامل گزینه authorization میباشد).
- درخواست باید از نوع نسخه http1 به بعد باشد.
- اگر درخواست شامل Entity-body باشد کش نخواهد کرد.
- درخواست شامل If-Range/Range header باشد کش نمیشود.
- کل حجم response بییشتر از اندازه تعیین شده باشد کش نخواهد گردید، این اندازه در کلید ریجستری UriMaxUriBytes قرار دارد. اطلاعات بیشتر
- اندازه هدر بیشتر از اندازه تعیین شده باشد که عموما اندازه تعیین شده یک کیلو بایت است.
- کش پر باشد، کش انجام نخواهد گرفت.
برای تعیین مقدار سایز کش response که در بالا اشاره کردیم میتوانید در همان پنجره، گزینه edit feature settings را انتخاب کنید.
این قسمت از مطلب که به نقل از مقاله آقای Karol Jarkovsky در این آدرس است یک سری تست هایی با نرم افزار(Web Capacity Analysis Tool (WCAT گرفته است که به نتایج زیر دست پیدا کرده است:
انقیاد به خواص یا property binding
قابلیت property binding این امکان را فراهم میکند که یکی از خواص المانهای HTML را به مقادیر دریافتی از کلاس کامپوننت، متصل کنیم:
<img [src]='producr.imageUrl'>
در حین تعریف property binding، مقصد اتصال، داخل براکتها قرار میگیرد و خاصیت مدنظر المان را مشخص میکند. منبع اتصال همیشه داخل "" در سمت راست علامت مساوی قرار میگیرد.
اگر اینکار را بخواهیم با interpolation معرفی شدهی در قسمت قبل انجام دهیم، به کد ذیل خواهیم رسید:
<img src={{producr.imageUrl}}>
خوب، در یک چنین مواردی property binding بهتر است یا interpolation؟
توصیهی کلی ترجیح property binding به interpolation است. اما اگر در اینجا نیاز به انجام محاسباتی بر روی عبارت منبع وجود داشت، باید از interpolation استفاده کرد؛ مانند:
<img src='http://www.mysite.com/images/{{producr.imageUrl}}'>
تکمیل قالب کامپوننت لیست محصولات
اگر از قسمت قبل به خاطر داشته باشید، در فایل product-list.component.html، لیست پردازش شدهی توسط ngFor*، فاقد ستون نمایش تصاویر محصولات است. به همین جهت فایل یاد شده را گشوده و سپس با استفاده از property binding، دو خاصیت src و title تصویر را به منبع دادهی آن متصل میکنیم:
<tbody> <tr *ngFor='#product of products'> <td> <img [src]='product.imageUrl' [title]='product.productName'> </td> <td>{{ product.productName }}</td> <td>{{ product.productCode }}</td> <td>{{ product.releaseDate }}</td> <td>{{ product.price }}</td> <td>{{ product.starRating }}</td> </tr> </tbody>
هرچند اینبار تصاویر محصولات نمایش داده شدهاند، اما اندکی بزرگ هستند. بنابراین در ادامه با استفاده از property binding، خواص style آنرا تنظیم خواهیم کرد. برای این منظور فایل product-list.component.ts را گشوده و به کلاس ProductListComponent، دو خاصیت imageWidth و imageMargin را اضافه میکنیم:
export class ProductListComponent { pageTitle: string = 'Product List'; imageWidth: number = 50; imageMargin: number = 2; products: any[] = [ // as before... ]; }
پس از تعریف این خواص، امکان دسترسی به آنها در قالب کامپوننت وجود خواهد داشت:
<tbody> <tr *ngFor='#product of products'> <td> <img [src]='product.imageUrl' [title]='product.productName' [style.width.px]='imageWidth' [style.margin.px]='imageMargin'> </td>
همچنین در اینجا نحوهی style binding را نیز مشاهده میکنید. مقصد اتصال همیشه با [] مشخص میشود و سپس کار با ذکر .style شروع شده و پس از آن نام خاصیت مدنظر عنوان خواهد شد. اگر نیاز به ذکر واحدی وجود داشت، پس از درج نام خاصیت، قید خواهد شد. برای مثال [style.fontSize.em] و یا [%.style.fontSize]
یک نکته:
اگر مثال را قدم به قدم دنبال کرده باشید، با افزودن style binding و بارگذاری مجدد صفحه، احتمالا تغییراتی را مشاهده نخواهید کرد. این مورد به علت کش شدن قالب قبلی و یا فایل جاوا اسکریپتی متناظر با آن است (فایلی که خواص عرض و حاشیهی تصویر به آن اضافه شدهاند).
یک روش سادهی حذف کش آن، بازکردن آدرس http://localhost:2222/app/products/product-list.component.js در مرورگر به صورت مجزا و سپس فشردن دکمههای ctrl+f5 بر روی آن است.
پاسخ دادن به رخدادها و یا event binding
تا اینجا تمام data bindingهای تعریف شدهی ما یک طرفه بودند؛ از خواص کلاس کامپوننت به اجزای قالب متناظر با آن. اما گاهی از اوقات نیاز است تا با کلیک کاربر بر روی دکمهای، عملی خاص صورت گیرد و در این حالت، جهت ارسال اطلاعات، از قالب کامپوننت، به متدها و خواص کلاس متناظر با آن خواهند بود. کامپوننت به اعمال کاربر از طریق event binding گوش فرا میدهد:
<button (click)='toggleImage()'>
در این حالت اگر کاربر روی دکمهی تعریف شده کلیک کند، متد toggleImage موجود در کلاس متناظر، فراخوانی خواهد شد.
چه رخدادهایی را در اینجا میتوان ذکر کرد؟ پاسخ آنرا در آدرس ذیل میتوانید مشاهده کنید:
https://developer.mozilla.org/en-US/docs/Web/Events
این syntax جدید AngularJS 2.0 سطح API آنرا کاهش داده است. دیگر در اینجا نیازی نیست تا به ازای هر رخداد ویژهای، یک دایرکتیو و یا syntax خاص آنرا در مستندات آن
جستجو کرد. فقط کافی است syntax جدید (نام رخداد) را مدنظر داشته باشید.
تکمیل مثال نمایش لیست محصولات با فعال سازی دکمهی Show Image آن
در اینجا قصد داریم با کلیک بر روی دکمهی Show image، تصاویر موجود در ستون تصاویر، مخفی و یا نمایان شوند. برای این منظور خاصیت جدیدی را به نام showImage به کلاس ProductListComponent اضافه میکنیم. بنابراین فایل product-list.component.ts را گشوده و تغییر ذیل را به آن اعمال کنید:
export class ProductListComponent { pageTitle: string = 'Product List'; imageWidth: number = 50; imageMargin: number = 2; showImage: boolean = false;
سپس به انتهای کلاس، پس از تعاریف خواص، متد جدید toggleImage را اضافه میکنیم:
export class ProductListComponent { // as before ... toggleImage(): void { this.showImage = !this.showImage; } }
پس از این تغییرات، اکنون میتوان به قالب این کامپوننت یا فایل product-list.component.html مراجعه و event binding را تنظیم کرد:
<button class='btn btn-primary' (click)='toggleImage()'> Show Image </button>
خوب، تا اینجا اگر کاربر بر روی دکمهی show image کلیک کند، مقدار خاصیت showImage کلاس ProductListComponent با توجه به کدهای متد toggleImage، معکوس خواهد شد.
مرحلهی بعد، استفاده از مقدار این خاصیت، جهت مخفی و یا نمایان ساختن المان تصویر جدول نمایش داده شدهاست. اگر از قسمت قبل به خاطر داشته باشید، کار ngIf*، حذف و یا افزودن المانهای DOM است. بنابراین ngIf* را به المان تصویر جدول اضافه میکنیم:
<tr *ngFor='#product of products'> <td> <img *ngIf='showImage' [src]='product.imageUrl' [title]='product.productName' [style.width.px]='imageWidth' [style.margin.px]='imageMargin'> </td>
اکنون برنامه را اجرا کنید. در اولین بار اجرای صفحه، تصاویر ستون اول جدول، نمایش داده نمیشود. پس از کلیک بر روی دکمهی Show image، این تصاویر نمایان شده و اگر بار دیگر بر روی این دکمه کلیک شود، این تصاویر مخفی خواهند شد.
یک مشکل! در هر دو حالت نمایش و مخفی سازی تصاویر، برچسب این دکمه Show image است. بهتر است زمانیکه قرار است تصاویر مخفی شوند، برچسب hide image نمایش داده شود و برعکس. برای حل این مساله از interpolation به نحو ذیل استفاده خواهیم کرد:
<button class='btn btn-primary' (click)='toggleImage()'> {{showImage ? 'Hide' : 'Show'}} Image </button>
بررسی انقیاد دو طرفه یا two-way binding
تا اینجا، اتصال مقدار یک خاصیت عمومی کلاس متناظر با قالبی، به اجزای مختلف آن، یک طرفه بودند. اما در ادامه نیاز است تا بتوان برای مثال در textbox قسمت filter by مثال جاری بتوان اطلاعاتی را وارد کرد و سپس بر اساس آن ردیفهای جدول نمایش داده شده را فیلتر نمود. این عملیات نیاز به انقیاد دو طرفه یا two-way data binding دارد.
برای تعریف انقیاد دو طرفه در AngularJS 2.0 از دایرکتیو توکاری به نام ngModel استفاده میشود:
<input [(ngModel)]='listFilter' >
سپس () تعریف شدهاست تا event binding را نیز گوشزد کند. کار آن انتقال تعاملات کاربر، با المان رابط کاربری جاری، به خاصیت عمومی کلاس یا همان listFilter است.
در اینجا ممکن است که فراموش کنید [()] صحیح است یا ([]) . به همین جهت به این syntax، نام banana in the box را دادهاند یا «موز درون جعبه»! موز همان event binding است که داخل جعبهی property binding قرار میگیرد!
خوب، برای اعمال انقیاد دو طرفه، به مثال جاری، فایل product-list.component.ts را گشوده و خاصیت رشتهای listFilter را به آن اضافه میکنیم:
export class ProductListComponent { pageTitle: string = 'Product List'; imageWidth: number = 50; imageMargin: number = 2; showImage: boolean = false; listFilter: string = 'cart';
<div class='panel-body'> <div class='row'> <div class='col-md-2'>Filter by:</div> <div class='col-md-4'> <input type='text' [(ngModel)]='listFilter' /> </div> </div> <div class='row'> <div class='col-md-6'> <h3>Filtered by: {{listFilter}}</h3> </div> </div>
پس از اجرای برنامه، تکست باکس تعریف شده، مقدار اولیهی cart را خواهد داشت و اگر آنرا تغییر دهیم، بلافاصله این مقدار تغییر یافته را در برچسب Filtered by میتوان مشاهده کرد. به این رخداد two-way binding میگویند.
البته هنوز کار فیلتر لیست محصولات در اینجا انجام نمیشود که آنرا در قسمت بعد تکمیل خواهیم کرد.
فرمت کردن اطلاعات نمایش داده شدهی در جدول با استفاده از Pipes
تا اینجا لیست محصولات نمایش داده شد، اما نیاز است برای مثال فرمت ستون نمایش قیمت آن بهبود یابد. برای این منظور، از ویژگی دیگری به نام pipes استفاده میشود و کار آنها تغییر دادهها، پیش از نمایش آنها است. AngularJS 2.0 به همراه تعدادی pipe توکار برای فرمت مقادیر است؛ مانند date، number، decimal، percent و غیره. همچنین امکان ساخت custom pipes نیز پیش بینی شدهاست.
در اینجا یک مثال سادهی pipes را مشاهده میکنید:
{{ product.productCode | lowercase }}
از pipes در property binding هم میتوان استفاده کرد:
[title]='product.productName | uppercase'
و یا میتوان pipes را به صورت زنجیرهای نیز تعریف کرد:
{{ product.price | currency | lowercase }}
بعضی از pipes، پارامتر هم قبول میکنند:
{{ product.price | currency:'USD':true:'1.2-2' }}
مبحث ایجاد custom pipes را در قسمت بعدی دنبال خواهیم کرد.
در ادامه برای ویرایش مثال جاری، فایل قالب product-list.component.html را گشوده و سطرهای جدول را به نحو ذیل تغییر دهید:
<td>{{ product.productName }}</td> <td>{{ product.productCode | lowercase }}</td> <td>{{ product.releaseDate }}</td> <td>{{ product.price | currency:'USD':true:'1.2-2'}}</td> <td>{{ product.starRating }}</td>
اینبار اگر برنامه را اجرا کنید، یک چنین خروجی را مشاهده خواهید کرد:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MVC5Angular2.part4.zip
خلاصهی بحث
data binding سبب سهولت نمایش مقادیر خواص کلاس یک کامپوننت، در قالب آن میشود. در AngularJS 2.0، چهار نوع binding وجود دارند:
interpolation، عبارت رشتهای محاسبه شده را در بین المانهای DOM درج میکند و یا میتواند خاصیت المانی را مقدار دهی نماید.
property binding سبب اتصال مقدار خاصیتی، به یکی از خواص المانی مشخص در DOM میشود.
event binding به رخدادها گوش فرا داده و سبب اجرای متدی در کلاس کامپوننت، در صورت بروز رخداد متناظری میشود.
حالت two-way binding، کار دریافت اطلاعات از کلاس و همچنین بازگشت مقادیر تغییر یافتهی توسط کاربر را به کلاس انجام میدهد.
اطلاعات نمایش داده شدهی توسط binding عموما فرمت مناسبی را ندارد. برای رفع این مشکل از pipes استفاده میشود.
مزیت آن این است که شما با Razor فقط یک سری فرمهای ابتدایی و اعتبارسنجی آنها را به خوبی میتوانید مدیریت کنید. یک مقدار کار UI که پیچیدهتر شد، به صفحهای میرسید که درون آن 100ها سطر کد جیکوئری با کدهای Razor مخلوط شدن و 2 ماه بعد حتی جرات نمیکنید به این صفحه دست بزنید و تغییری را در آن اعمال کنید. اینجا است که فریم ورکهای SPA ارزش خودشان را نشان میدهند.
برای فرمهای معمولی، Razor عالی است. برای UI پیچیده، ترکیب Razor و jQuery اصلا قابلیت نگهداری ندارد (صفحاتی پر از قطعات جاوا اسکریپت که نه فضای نامی دارند، به راحتی تداخل میکنند، عموما از TypeScript استفاده نمیکنند و مبتنی بر قابلیتهای جدید این زبان نیستند). در یک چنین حالتی قابلیت تست UI را هم از دست خواهید داد. همچنین مدام هم محدود به افزونههای جیکوئری خواهید بود، اما با فریم ورکهای SPA، خیلی از این افزونهها، کارهای معمولی آنها هستند. به علاوه زمانیکه back-end (سمت سرور) و front-end (مدیریت کدهای سمت کلاینت) را از هم جدا میکنید، بهتر میتوانید از مزیت کار با طراحان UI استفاده کنید. کسانیکه عموما قرار نیست با کدهای سمت سرور کار کنند.
آموزش TypeScript #1
مثال 1: تمام اطلاعات یک جدول را دریافت کنید.
هدف دریافت تمام اطلاعات جدول facilities است.
برای انجام اینکار فقط کافیاست بر روی DbSet متناظر با آن، متد ToList فراخوانی شود:
var facilities = context.Facilities.ToList();
یک نکته: به فراخوانی متد ToList، اصطلاحا materialization گفته میشود و هدف آن تبدیل یک IQueryable، به یک IEnumerable است. اطلاعات بیشتر
مثال 2: اطلاعات ستونهای خاصی از یک جدول را دریافت کنید.
میخواهیم لیست نام امکانات مجموعه را به همراه هزینهی مرتبط با آنها، نمایش دهیم:
var facilities = context.Facilities.Select(x => new { x.Name, x.MemberCost }).ToList();
مثال 3: نحوهی بازگشت ردیفها را کنترل کنید.
چگونه میتوان لیست امکاناتی را بازگشت داد که برای کاربران رایگان نیستند؟
var facilities = context.Facilities.Where(x => x.MemberCost > 0).ToList();
مثال 4: نحوهی بازگشت ردیفها را کنترل کنید؛ قسمت دوم.
چگونه میتوان لیست امکاناتی را بازگشت داد که برای کاربران رایگان نیستند و همچنین هزینهی آنها، 1/50 ام هزینهی نگهداری ماهیانهی آنها است؟ خروجی این کوئری باید تنها به همراه ستونهای FacId, Name, MemberCost, MonthlyMaintenance باشد.
var facilities = context.Facilities.Where(x => x.MemberCost > 0 && x.MemberCost < (x.MonthlyMaintenance / 50)) .Select(x => new { x.FacId, x.Name, x.MemberCost, x.MonthlyMaintenance }).ToList();
در این مثال نحوهی ترکیب چند شرط را با هم در قسمت Where، مشاهده میکنید و همچنین با استفاده از متد Select، تعداد ستونهای بازگشتی نیز کنترل شدهاند.
مثال 5: جستجوهای سادهی رشتهای
لیستی از امکاناتی را تهیه کنید که واژهی «Tennis» در نام آنها بکار رفتهاست.
var facilities = context.Facilities.Where(x => x.Name.Contains("Tennis")).ToList();
مثال 6: ردیفهایی را که با چندین مقدار ممکن تطابق دارند، بازگشت دهید.
چگونه میتوان امکانات دارای ID مساوی 1 و 5 را بازگشت داد؟ برای اینکار از ترکیب شرطها با استفاده از OR استفاده نکنید.
int[] ids = { 1, 5 }; var facilities = context.Facilities.Where(x => ids.Contains(x.FacId)).ToList();
مثال 7: نتایج بازگشت داده شده را طبقه بندی کنید.
گزارشی از امکانات را تهیه کنید که در آن اگر هزینهی نگهداری ماهیانهی امکاناتی بیشتر از 100 دلار بود، به صورت expensive و در غیراینصورت cheap، طبقه بندی شوند.
var facilities = context.Facilities .Select(x => new { x.Name, Cost = x.MonthlyMaintenance > 100 ? "expensive" : "cheap" }).ToList();
مثال 8: کار با تاریخ و زمان
لیست کاربرانی را بازگشت دهید که پس از September 2012 عضو این مجموعه شدهاند. این گزارش باید تنها به همراه ستونهای MemId, Surname, FirstName, JoinDate باشد.
var date = new DateTime(2012, 09, 01); var members = context.Members.Where(x => x.JoinDate >= date) .Select(x => new { x.MemId, x.Surname, x.FirstName, x.JoinDate }).ToList();
مثال 9: نتایج تکراری را از اطلاعات بازگشتی حذف کرده و آنها را مرتب کنید.
گزارشی را تهیه کنید که در آن تنها فیلد Surname مرتب شدهی کاربران وجود دارد. از لیست Surnameها، تنها 10 مورد غیر تکراری را بازگشت دهید.
var members = context.Members.OrderBy(x => x.Surname) .Select(x => new { x.Surname }) .Distinct() .Take(10) .ToList();
مثال 10: نتایج چند کوئری را با هم ترکیب کنید.
لیست نامهای امکانات و نامهای اشخاص را با هم ترکیب کنید.
var names = context.Members.Select(m => m.Surname).ToList() .Union(context.Facilities.Select(f => f.Name).ToList()) // For now we have to use `.ToList()` here .ToList();
SELECT surname FROM members UNION SELECT name FROM facilities;
یعنی در مثال فوق، دوبار رفت و برگشت به بانک اطلاعاتی صورت گرفته (به ازای هر ToList ذکر شده) و سپس نتیجهی حاصل، در سمت کلاینت با هم Union شدهاند و نه در سمت دیتابیس.
مثال 11: محاسبات تجمعی ابتدایی
زمان ثبت نام آخرین عضو مجموعه چیست؟
برای حل این مثال میتوان از روشهای مختلفی استفاده کرد:
الف) استفاده از متد تجمعی Max برای یافتن بزرگترین مقدار JoinDate
var latest = context.Members.Max(x => x.JoinDate);
متد Max برای خواص nullable میتواند null را بازگشت دهد و همچنین اگر این مجموعه دارای مقداری نباشد و آن خاصیت نیز nullable نباشد، استثنای Sequence contains no element را صادر میکند. میتوان این استثناء را به صورت زیر با استفاده از متد DefaultIfEmpty کنترل کرد:
var latest2 = context.Members.Select(m => m.JoinDate).DefaultIfEmpty().Max();
SELECT MAX([m].[JoinDate]) FROM (SELECT NULL AS [empty]) AS [empty] LEFT OUTER JOIN [Members] AS [m] ON 1 = 1;
var latest3 = context.Members.Max(m => (DateTime?)m.JoinDate) ?? DateTime.Now;
ب) بجای استفاده از متد Max میتوان ابتدا رکوردها را بر اساس JoinDate به صورت نزولی مرتب کرد و سپس اولین عضو حاصل را بازگشت داد؛ چون اکنون بر اساس مرتب سازی صورت گرفته، در بالای لیست قرار دارد:
var latest4 = context.Members.OrderByDescending(m => m.JoinDate).Select(m => m.JoinDate).FirstOrDefault();
مثال 12: مثالی دیگر از محاسبات تجمعی ابتدایی
در مثال قبلی، نام و نام خانوادگی آخرین شخص ثبت نام شده را نیز به گزارش اضافه کنید؛ یعنی Select انجام شده شامل x.FirstName, x.Surname, x.JoinDate باشد.
یک روش انجام اینکار، همان کوئری ب مثال قبلی است که اینبار فقط Select آن فرق میکند:
var lastMember = context.Members.OrderByDescending(m => m.JoinDate) .Select(x => new { x.FirstName, x.Surname, x.JoinDate }) .FirstOrDefault();
روش دیگر آن نوشتن یک sub-query در قسمت Where است:
var members = context.Members.Select(x => new { x.FirstName, x.Surname, x.JoinDate }) .Where(x => x.JoinDate == context.Members.Max(x => x.JoinDate)) .ToList();
کدهای کامل این قسمت را در اینجا میتوانید مشاهده کنید.