EF Code First #7
مدیریت روابط بین جداول در EF Code first به کمک Fluent API
EF Code first بجای اتلاف وقت شما با نوشتن فایلهای XML تهیه نگاشتها یا تنظیم آنها با کد، رویه Convention over configuration را پیشنهاد میدهد. همین رویه، جهت مدیریت روابط بین جداول نیز برقرار است. روابط one-to-one، one-to-many، many-to-many و موارد دیگر را بدون یک سطر تنظیم اضافی، صرفا بر اساس یک سری قراردادهای توکار میتواند تشخیص داده و اعمال کند. عموما زمانی نیاز به تنظیمات دستی وجود خواهد داشت که قراردادهای توکار رعایت نشوند و یا برای مثال قرار است با یک بانک اطلاعاتی قدیمی از پیش موجود کار کنیم.
مفاهیمی به نامهای Principal و Dependent
در EF Code first از یک سری واژههای خاص جهت بیان ابتدا و انتهای روابط استفاده شده است که عدم آشنایی با آنها درک خطاهای حاصل را مشکل میکند:
الف) Principal : طرفی از رابطه است که ابتدا در بانک اطلاعاتی ذخیره خواهد شد.
ب) Dependent : طرفی از رابطه است که پس از ثبت Principal در بانک اطلاعاتی ذخیره میشود.
Principal میتواند بدون نیاز به Dependent وجود داشته باشد. وجود Dependent بدون Principal ممکن نیست زیرا ارتباط بین این دو توسط یک کلید خارجی تعریف میشود.
کدهای مثال مدیریت روابط بین جداول
در دنیای واقعی، همهی مثالها به مدل بلاگ و مطالب آن ختم نمیشوند. به همین جهت نیاز است یک مدل نسبتا پیچیدهتر را در اینجا بررسی کنیم. در ادامه کدهای کامل مثال جاری را مشاهده خواهید کرد:
using System.Collections.Generic;
namespace EF_Sample35.Models
{
public class Customer
{
public int Id { set; get; }
public string FirstName { set; get; }
public string LastName { set; get; }
public virtual AlimentaryHabits AlimentaryHabits { set; get; }
public virtual ICollection<CustomerAlias> Aliases { get; set; }
public virtual ICollection<Role> Roles { get; set; }
public virtual Address Address { get; set; }
}
}
namespace EF_Sample35.Models
{
public class CustomerAlias
{
public int Id { get; set; }
public string Aka { get; set; }
public virtual Customer Customer { get; set; }
}
}
using System.Collections.Generic;
namespace EF_Sample35.Models
{
public class Role
{
public int Id { set; get; }
public string Name { set; get; }
public virtual ICollection<Customer> Customers { set; get; }
}
}
namespace EF_Sample35.Models
{
public class AlimentaryHabits
{
public int Id { get; set; }
public bool LikesPasta { get; set; }
public bool LikesPizza { get; set; }
public int AverageDailyCalories { get; set; }
public virtual Customer Customer { get; set; }
}
}
using System.Collections.Generic;
namespace EF_Sample35.Models
{
public class Address
{
public int Id { set; get; }
public string City { set; get; }
public string StreetAddress { set; get; }
public string PostalCode { set; get; }
public virtual ICollection<Customer> Customers { set; get; }
}
}
همچنین تعاریف نگاشتهای برنامه نیز مطابق کدهای زیر است:
using System.Data.Entity.ModelConfiguration;
using EF_Sample35.Models;
namespace EF_Sample35.Mappings
{
public class CustomerAliasConfig : EntityTypeConfiguration<CustomerAlias>
{
public CustomerAliasConfig()
{
// one-to-many
this.HasRequired(x => x.Customer)
.WithMany(x => x.Aliases)
.WillCascadeOnDelete();
}
}
}
using System.Data.Entity.ModelConfiguration;
using EF_Sample35.Models;
namespace EF_Sample35.Mappings
{
public class CustomerConfig : EntityTypeConfiguration<Customer>
{
public CustomerConfig()
{
// one-to-one
this.HasOptional(x => x.AlimentaryHabits)
.WithRequired(x => x.Customer)
.WillCascadeOnDelete();
// many-to-many
this.HasMany(p => p.Roles)
.WithMany(t => t.Customers)
.Map(mc =>
{
mc.ToTable("RolesJoinCustomers");
mc.MapLeftKey("RoleId");
mc.MapRightKey("CustomerId");
});
// many-to-one
this.HasOptional(x => x.Address)
.WithMany(x => x.Customers)
.WillCascadeOnDelete();
}
}
}
به همراه Context زیر:
using System.Data.Entity;
using System.Data.Entity.Migrations;
using EF_Sample35.Mappings;
using EF_Sample35.Models;
namespace EF_Sample35.DataLayer
{
public class Sample35Context : DbContext
{
public DbSet<AlimentaryHabits> AlimentaryHabits { set; get; }
public DbSet<Customer> Customers { set; get; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new CustomerConfig());
modelBuilder.Configurations.Add(new CustomerAliasConfig());
base.OnModelCreating(modelBuilder);
}
}
public class Configuration : DbMigrationsConfiguration<Sample35Context>
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
AutomaticMigrationDataLossAllowed = true;
}
protected override void Seed(Sample35Context context)
{
base.Seed(context);
}
}
}
که نهایتا منجر به تولید چنین ساختاری در بانک اطلاعاتی میگردد:
توضیحات کامل کدهای فوق:
تنظیمات روابط one-to-one و یا one-to-zero
زمانیکه رابطهای 0..1 و یا 1..1 است، مطابق قراردادهای توکار EF Code first تنها کافی است یک navigation property را که بیانگر ارجاعی است به شیء دیگر، تعریف کنیم (در هر دو طرف رابطه).
برای مثال در مدلهای فوق یک مشتری که در حین ثبت اطلاعات اصلی او، «ممکن است» اطلاعات جانبی دیگری (AlimentaryHabits) نیز از او تنها در طی یک رکورد، دریافت شود. قصد هم نداریم یک ComplexType را تعریف کنیم. نیاز است جدول AlimentaryHabits جداگانه وجود داشته باشد.
namespace EF_Sample35.Models
{
public class Customer
{
// ...
public virtual AlimentaryHabits AlimentaryHabits { set; get; }
}
}
namespace EF_Sample35.Models
{
public class AlimentaryHabits
{
// ...
public virtual Customer Customer { get; set; }
}
}
در اینجا خواص virtual تعریف شده در دو طرف رابطه، به EF خواهد گفت که رابطهای، 1:1 برقرار است. در این حالت اگر برنامه را اجرا کنیم، به خطای زیر برخواهیم خورد:
Unable to determine the principal end of an association between
the types 'EF_Sample35.Models.Customer' and 'EF_Sample35.Models.AlimentaryHabits'.
The principal end of this association must be explicitly configured using either
the relationship fluent API or data annotations.
EF تشخیص داده است که رابطه 1:1 برقرار است؛ اما با قاطعیت نمیتواند طرف Principal را تعیین کند. بنابراین باید اندکی به او کمک کرد:
using System.Data.Entity.ModelConfiguration;
using EF_Sample35.Models;
namespace EF_Sample35.Mappings
{
public class CustomerConfig : EntityTypeConfiguration<Customer>
{
public CustomerConfig()
{
// one-to-one
this.HasOptional(x => x.AlimentaryHabits)
.WithRequired(x => x.Customer)
.WillCascadeOnDelete();
}
}
}
همانطور که ملاحظه میکنید در اینجا توسط متد WithRequired طرف Principal و توسط متد HasOptional، طرف Dependent تعیین شده است. به این ترتیب EF میتوان یک رابطه 1:1 را تشکیل دهید.
توسط متد WillCascadeOnDelete هم مشخص میکنیم که اگر Principal حذف شد، لطفا Dependent را به صورت خودکار حذف کن.
توضیحات ساختار جداول تشکیل شده:
هر دو جدول با همان خواص اصلی که در دو کلاس وجود دارند، تشکیل شدهاند.
فیلد Id جدول AlimentaryHabits اینبار دیگر Identity نیست. اگر به تعریف قید FK_AlimentaryHabits_Customers_Id دقت کنیم، در اینجا مشخص است که فیلد Id جدول AlimentaryHabits، به فیلد Id جدول مشتریها متصل شده است (یعنی در آن واحد هم primary key است و هم foreign key). به همین جهت به این روش one-to-one association with shared primary key هم گفته میشود (کلید اصلی جدول مشتری با جدول AlimentaryHabits به اشتراک گذاشته شده است).
تنظیمات روابط one-to-many
برای مثال همان مشتری فوق را درنظر بگیرید که دارای تعدادی نام مستعار است:
using System.Collections.Generic;
namespace EF_Sample35.Models
{
public class Customer
{
// ...
public virtual ICollection<CustomerAlias> Aliases { get; set; }
}
}
namespace EF_Sample35.Models
{
public class CustomerAlias
{
// ...
public virtual Customer Customer { get; set; }
}
}
همین میزان تنظیم کفایت میکند و نیازی به استفاده از Fluent API برای معرفی روابط نیست.
در طرف Principal، یک مجموعه یا لیستی از Dependent وجود دارد. در Dependent هم یک navigation property معرف طرف Principal اضافه شده است.
جدول CustomerAlias اضافه شده، توسط یک کلید خارجی به جدول مشتری مرتبط میشود.
سؤال: اگر در اینجا نیز بخواهیم CascadeOnDelete را اعمال کنیم، چه باید کرد؟
پاسخ: جهت سفارشی سازی نحوه تعاریف روابط حتما نیاز به استفاده از Fluent API به نحو زیر میباشد:
using System.Data.Entity.ModelConfiguration;
using EF_Sample35.Models;
namespace EF_Sample35.Mappings
{
public class CustomerAliasConfig : EntityTypeConfiguration<CustomerAlias>
{
public CustomerAliasConfig()
{
// one-to-many
this.HasRequired(x => x.Customer)
.WithMany(x => x.Aliases)
.WillCascadeOnDelete();
}
}
}
اینکار را باید در کلاس تنظیمات CustomerAlias انجام داد تا بتوان Principal را توسط متد HasRequired به Customer و سپس dependent را به کمک متد WithMany مشخص کرد. در ادامه میتوان متد WillCascadeOnDelete یا هر تنظیم سفارشی دیگری را نیز اعمال نمود.
متد HasRequired سبب خواهد شد فیلد Customer_Id، به صورت not null در سمت بانک اطلاعاتی تعریف شود؛ متد HasOptional عکس آن است.
تنظیمات روابط many-to-many
برای تنظیم روابط many-to-many تنها کافی است دو سر رابطه ارجاعاتی را به یکدیگر توسط یک لیست یا مجموعه داشته باشند:
using System.Collections.Generic;
namespace EF_Sample35.Models
{
public class Role
{
// ...
public virtual ICollection<Customer> Customers { set; get; }
}
}
using System.Collections.Generic;
namespace EF_Sample35.Models
{
public class Customer
{
// ...
public virtual ICollection<Role> Roles { get; set; }
}
}
همانطور که مشاهده میکنید، یک مشتری میتواند چندین نقش داشته باشد و هر نقش میتواند به چندین مشتری منتسب شود.
اگر برنامه را به این ترتیب اجرا کنیم، به صورت خودکار یک رابطه many-to-many تشکیل خواهد شد (بدون نیاز به تنظیمات نگاشتهای آن). نکته جالب آن تشکیل خودکار جدول ارتباط دهنده واسط یا اصطلاحا join-table میباشد:
CREATE TABLE [dbo].[RolesJoinCustomers](
[RoleId] [int] NOT NULL,
[CustomerId] [int] NOT NULL,
)
سؤال: نامهای خودکار استفاده شده را میخواهیم تغییر دهیم. چکار باید کرد؟
پاسخ: اگر بانک اطلاعاتی برای بار اول است که توسط این روش تولید میشود شاید این پیش فرضها اهمیتی نداشته باشد و نسبتا هم مناسب هستند. اما اگر قرار باشد از یک بانک اطلاعاتی موجود که امکان تغییر نام فیلدها و جداول آن وجود ندارد استفاده کنیم، نیاز به سفارشی سازی تعاریف نگاشتها به کمک Fluent API خواهیم داشت:
using System.Data.Entity.ModelConfiguration;
using EF_Sample35.Models;
namespace EF_Sample35.Mappings
{
public class CustomerConfig : EntityTypeConfiguration<Customer>
{
public CustomerConfig()
{
// many-to-many
this.HasMany(p => p.Roles)
.WithMany(t => t.Customers)
.Map(mc =>
{
mc.ToTable("RolesJoinCustomers");
mc.MapLeftKey("RoleId");
mc.MapRightKey("CustomerId");
});
}
}
}
تنظیمات روابط many-to-one
در تکمیل مدلهای مثال جاری، به دو کلاس زیر خواهیم رسید. در اینجا تنها در کلاس مشتری است که ارجاعی به کلاس آدرس او وجود دارد. در کلاس آدرس، یک navigation property همانند حالت 1:1 تعریف نشده است:
namespace EF_Sample35.Models
{
public class Address
{
public int Id { set; get; }
public string City { set; get; }
public string StreetAddress { set; get; }
public string PostalCode { set; get; }
}
}
using System.Collections.Generic;
namespace EF_Sample35.Models
{
public class Customer
{
// …
public virtual Address Address { get; set; }
}
}
این رابطه توسط EF Code first به صورت خودکار به یک رابطه many-to-one تفسیر خواهد شد و نیازی به تنظیمات خاصی ندارد.
زمانیکه جداول برنامه تشکیل شوند، جدول Addresses موجودیتی مستقل خواهد داشت و جدول مشتری با یک فیلد به نام Address_Id به جدول آدرسها متصل میگردد. این فیلد نال پذیر است؛ به عبارتی ذکر آدرس مشتری الزامی نیست.
اگر نیاز بود این تعاریف نیز توسط Fluent API سفارشی شوند، باید خاصیت public virtual ICollection<Customer> Customers به کلاس Address نیز اضافه شود تا بتوان رابطه زیر را توسط کدهای برنامه تعریف کرد:
using System.Data.Entity.ModelConfiguration;
using EF_Sample35.Models;
namespace EF_Sample35.Mappings
{
public class CustomerConfig : EntityTypeConfiguration<Customer>
{
public CustomerConfig()
{
// many-to-one
this.HasOptional(x => x.Address)
.WithMany(x => x.Customers)
.WillCascadeOnDelete();
}
}
}
متد HasOptional سبب میشود تا فیلد Address_Id اضافه شده به جدول مشتریها، null پذیر شود.
نکات زیر، جالبترین نکات در خصوص شیوه حمله بوده که قابل توجه است:
- یک کاربر از یک وب سایت حاوی کدهای مخرب و آلوده که یک فایل را میزبانی میکند، مانند evil.html بازدید میکند.
- با توجه به یک بخش از آسیب پذیری، فایل evil.html دانلود میشود و کارت SD آن را بدون هشدار به کاربر، ذخیره میکند!
- با توجه به بخش دیگری از آسیب پذیری، به محض اینکه فایل ذخیره شد، میتوان کدهای جاوا اسکریپت مخرب را روی آن اجرا کرد!
- بدلیل بخش پایانی حمله روی این آسیب پذیری، کدهای جاوااسکریپت تحت شرایطی خاص روی سیستم محلی (local) اجرا میشوند! ابزار آلوده و برنامه نویسی شده، براحتی روی کارت SD ذخیره و مستقر شده و به وب سایت مهاجم برای ارسال اطلاعات قربانی دسترسی کامل دارد.
ممکن است بپرسید، "من فقط یک توسعه دهنده اندرویدی کوچکی هستم که قصد دارم برنامه خودم را در یک مارکت اندرویدی بفروشم و قیمت این برنامه خیلی پایین است. بنابراین آیا واقعا باید زمانی را برای انجام این کار امنیتی از قبل هدر دهم؟
و من با صدای رسا جواب میدهم : "بله! باید این کار را انجام دهید." این کار باعث میشود تا در حین گسترش اپلیکیشن کوچک خود، دیگر نگران مشکلات حملات مستقیم یا غیرمستقیم نباشید.
حملات مستقیم (Direct Attacks)
حملات مستقیم به طور قابلتوجهی متفاوت هستند و میتوانند شکلهای گوناگونی برای حمله داشته باشند. یک حمله مستقیم میتواند به عنوان فردی که مستقیما در برنامه شما هدف قرار داده میشود، طبقهبندی شود. بنابراین، مهاجم به دنبال آسیب پذیری برای نفوذ در طراحی برنامه شما برای جمعآوری اطلاعات حساس در مورد شما و کاربرانتان است. استفاده از کاربران برنامه و یا حمله به کارگزار، از مواردی است که یک مهاجم در اولویت کار خود قرار میدهد! یک مهاجم ممکن است به دنبال برنامههای کاربردی تلفن همراه باشد که متعلق به یک نهاد دولتی است مثل، یک بانک خاص که شما اپلیکیشن آنرا روی تلفن خود نصب کردهاید و آپدیتهای امنیتی راهم انجام ندادهاید! اگر طرح امنیتی روی برنامه ضعیف باشد و دادههای حساس و حیاتی کاربران در محلی امن نگه داری نشود یا حتی ارتباط بین برنامه و ssl و سرور بدرستی برقرار نباشد، مهاجم میتواند حملات خاصی را روی ssl انجام دهد و نقاط ضعف را شناسایی کرده و در کسری از زمان به سرور متصل شود! این حمله یک حمله مستقیم و بدون دخالت فایل یا ابزار خاصی است. این یک حمله مستقیم به یک برنامه خاص است.
Proxim و ذخیره داده
بیایید با یک مثال ساده با نام Proxim شروع کنیم: برای نوشتن یک برنامه که میتواند یک SMS را به افراد خاص و معین ارسال کند، قرار دادی بستهایم؛ با توجه به نزدیکی به مجموعهای از مختصات مکانی آنها روی GPS. برای مثال: کاربر در این برنامه میتواند شماره تماس همسر خود را ذخیره کرده و هر زمان که به فاصله 3 مایلی به خانه یا محل کار برسد، با او تماس میگیرد. بدین صورت همسر فرد مطلع میشود که او نزدیک به محل کار یا خانه است و با یک تماس تلفنی او را آگاه میسازد.
کدهای زیر بخشی از فایل (Save Routine, SaveController. java) هستند:
package net.zenconsult.android.controller; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import net.zenconsult.android.model.Contact; import net.zenconsult.android.model.Location; import android.content.Context; import android.os.Environment; import android.util.Log; public class SaveController { private static final String TAG = "SaveController"; public static void saveContact(Context context, Contact contact) { if (isReadWrite()) { try { File outputFile = new File(context.getExternalFilesDir(null), contact.getFirstName()); FileOutputStream outputStream = new FileOutputStream(outputFile); outputStream.write(contact.getBytes()); outputStream.close(); } catch (FileNotFoundException e) { Log.e(TAG, "File not found"); } catch (IOException e) { Log.e(TAG, "IO Exception"); } } else { Log.e(TAG, "Error opening media card in read/write mode!"); } } public static void saveLocation(Context context, Location location) { if (isReadWrite()) { try { File outputFile = new File(context.getExternalFilesDir(null), location.getIdentifier()); FileOutputStream outputStream = new FileOutputStream(outputFile); outputStream.write(location.getBytes()); outputStream.close(); } catch (FileNotFoundException e) { Log.e(TAG, "File not found"); } catch (IOException e) { Log.e(TAG, "IO Exception"); } } else { Log.e(TAG, "Error opening media card in read/write mode!"); } } private static boolean isReadOnly() { Log.e(TAG, Environment .getExternalStorageState()); return Environment.MEDIA_MOUNTED_READ_ONLY.equals(Environment .getExternalStorageState()); } private static boolean isReadWrite() { Log.e(TAG, Environment .getExternalStorageState()); return Environment.MEDIA_MOUNTED.equals(Environment .getExternalStorageState()); } }
کدهای فایل ( Location .java)
package net.zenconsult.android.model; publicclass Location { private String identifier; privatedouble latitude; privatedouble longitude; public Location() {} publicdouble getLatitude() { return latitude; } publicvoid setLatitude(double latitude) { this.latitude = latitude; } publicdouble getLongitude() { return longitude; } publicvoid setLongitude(double longitude) { this.longitude = longitude; } publicvoid setIdentifier(String identifier) { this.identifier = identifier; } public String getIdentifier() { return identifier; } public String toString() { StringBuilder ret = new StringBuilder(); ret.append(getIdentifier()); ret.append(String.valueOf(getLatitude())); ret.append(String.valueOf(getLongitude())); return ret.toString(); } publicbyte[] getBytes() { return toString().getBytes(); } }
کدهای فایل (Contact.java)
package net.zenconsult.android.model; publicclass Contact { private String firstName; private String lastName; private String address1; private String address2; private String email; private String phone; public Contact() {} public String getFirstName() { return firstName; } publicvoid setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } publicvoid setLastName(String lastName) { this.lastName = lastName; } public String getAddress1() { return address1; } publicvoid setAddress1(String address1) { this.address1 = address1; } public String getAddress2() { return address2; } publicvoid setAddress2(String address2) { this.address2 = address2; } public String getEmail() { return email; } publicvoid setEmail(String email) { this.email = email; } public String getPhone() { return phone; } publicvoid setPhone(String phone) { this.phone = phone; } public String toString() { StringBuilder ret = new StringBuilder(); ret.append(getFirstName() + "|"); ret.append(getLastName() + "|"); ret.append(getAddress1() + "|"); ret.append(getAddress2() + "|"); ret.append(getEmail() + "|"); ret.append(getPhone() + "|"); return ret.toString(); } publicbyte[] getBytes() { return toString().getBytes(); } }
final Contact contact = new Contact(); contact.setFirstName("User 1"); contact.setLastName("L1"); contact.setAddress1(""); contact.setAddress2(""); contact.setEmail("name@site.net"); contact.setPhone("12120031337");
تعدادی از فناوریهایی که توسط این کتابخانه پشتیبانی میشوند در زیر آمده است:
Web Service: این فناوری اجازهی ارسال و دریافت پیامهای تحت شبکه را به خصوص بر روی اینترنت، فراهم میکند و باعث ارتباط جامعتر بین برنامهها و فناوریهای مختلف میگردد. در انواع جدیدتر WCF و Web Api نیز به بازار ارائه شدهاند.
webform و MVC : فناوریهای تحت وب که باعث سهولت در ساخت وب سایتها میشوند که وب فرم رفته رفته به سمت منسوخ شدن پیش میرود و در صورتی که قصد دارید طراحی وب را آغاز کنید توصیه میکنم از همان اول به سمت MVC بروید.
Rich Windows GUI Application : برای سهولت در ایجاد برنامههای تحت وب حالا چه با فناوری WPF یا فناوری قدیمی و البته منسوخ شده Windows Form.
Windows Console Application: برای ایجاد برنامههای ساده و بدون رابط گرافیکی.
Windows Services: شما میتوانید یک یا چند سرویس تحت ویندوز را که توسط Service Control Manager یا به اختصار SCM کنترل میشوند، تولید کنید.
Database stored Procedure: نوشتن stored procedure بر روی دیتابیسهایی چون sql server و اوراکل و ... توسط فریم ورک دات نت مهیاست.
Component Libraray: ساخت اسمبلیهای واحدی که میتوانند با انواع مختلفی از موارد بالا ارتباط برقرار کنند.
Portable Class Libary : این نوع پروژهها شما را قادر میسازد تا کلاسهایی با قابلیت انتقال پذیری برای استفاده در سیلور لایت، ویندوز فون و ایکس باکس و فروشگاه ویندوز و ... تولید کنید.
ازآنجا که یک کتابخانه شامل زیادی نوع میگردد سعی شده است گروه بندیهای مختلفی از آن در قالبی به اسم فضای نام namespace تقسیم بندی گردند که شما آشنایی با آنها دارید. به همین جهت فقط تصویر زیر را که نمایشی از فضای نامهای اساسی و مشترک و پرکاربرد هستند، قرار میدهم.
در CLR مفهومی به نام Common Type System یا CTS وجود دارد که توضیح میدهد نوعها باید چگونه تعریف شوند و چگونه باید رفتار کنند که این قوانین از آنجایی که در ریشهی CLR نهفته است، بین تمامی زبانهای دات نت مشترک میباشد. تعدادی از مشخصات این CTS در زیر آورده شده است ولی در آینده بررسی بیشتری روی آنان خواهیم داشت:
- فیلد
- متد
- پراپرتی
- رویدادها
CTS همچنین شامل قوانین زیادی در مورد وضعیت کپسوله سازی برای اعضای یک نوع دارد:
- private
- public
- Family یا در زبانهایی مثل سی ++ و سی شارپ با نام protected شناخته میشود.
- family and assembly: این هم مثل بالایی است ولی کلاس مشتق شده باید در همان اسمبلی باشد. در زبانهایی چون سی شارپ و ویژوال بیسیک، چنین امکانی پیاده سازی نشدهاست و دسترسی به آن ممکن نیست ولی در IL Assembly چنین قابلیتی وجود دارد.
- Assembly یا در بعضی زبانها به نام internal شناخته میشود.
- Family Or Assembly: که در سی شارپ با نوع Protected internal شناخته میشود. در این وضعیت هر عضوی در هر اسمبلی قابل ارث بری است و یک عضو فقط میتواند در همان اسمبلی مورد استفاده قرار بگیرد.
موارد دیگری که تحت قوانین CTS هستند مفاهیم ارث بری، متدهای مجازی، عمر اشیاء و .. است.
یکی دیگر از ویژگیهای CTS این است که همهی نوعها از نوع شیء Object که در فضای نام system قرار دارد ارث بری کردهاند. به همین دلیل همهی نوعها حداقل قابلیتهایی را که یک نوع object ارئه میدهد، دارند که به شرح زیر هستند:
- مقایسهی دو شیء از لحاظ برابری.
- به دست آوردن هش کد برای هر نمونه از یک شیء
- ارائهای از وضعیت شیء به صورت رشته ای
- دریافت نوع شیء جاری
وجود COMها به دلیل ایجاد اشیاء در یک زبان متفاوت بود تا با زبان دیگر ارتباط برقرار کنند. در طرف دیگر CLR هم بین زبانهای برنامه نویسی یکپارچگی ایجاد کرده است. یکپارچگی زبانهای برنامه نویسی علل زیادی دارند. اول اینکه رسیدن به هدف یا یک الگوریتم خاص در زبان دیگر راحتتر از زبان پایه پروژه است. دوم در یک کار تیمی که افراد مختلف با دانش متفاوتی حضور دارند و ممکن است زیان هر یک متفاوت باشند.
برای ایجاد این یکپارچگی، مایکروسافت سیستم CLS یا Common Language Specification را راه اندازی کرد. این سیستم برای تولیدکنندگان کامپایلرها جزئیاتی را تعریف میکند که کامپایلر آنها را باید با حداقل ویژگیهای تعریف شدهی CLR، پشتیبانی کند.
CLR/CTS مجموعهای از ویژگیها را شامل میشود و گفتیم که هر زبانی بسیاری از این ویژگیها را پشتیبانی میکند ولی نه کامل. به عنوان مثال برنامه نویسی که قصد کرده از IL Assembly استفاده کند، قادر است از تمامی این ویژگیهایی که CLR/CTS ارائه میدهند، استفاده کند ولی تعدادی دیگر از زبانها مثل سی شارپ و فورترن و ویژوال بیسیک تنها بخشی از آن را استفاده میکنند و CLS حداقل ویژگی که بین همه این زبانها مشترک است را ارائه میکند.
شکل زیر را نگاه کنید:
یعنی
اگر شما دارید نوع جدیدی را در یک زبان ایجاد میکنید که قصد دارید در یک
زبان دیگر استفاده شود، نباید از امتیازات ویژهای که آن زبان در اختیار شما میگذارد و به بیان بهتر CLS آنها را پشتیبانی نمیکند، استفاده کنید؛ چرا
که کد شما ممکن است در زبان دیگر مورد استفاده قرار نگیرد.
using System; // Tell compiler to check for CLS compliance [assembly: CLSCompliant(true)] namespace SomeLibrary { // Warnings appear because the class is public public sealed class SomeLibraryType { // Warning: Return type of 'SomeLibrary.SomeLibraryType.Abc()' // is not CLScompliant public UInt32 Abc() { return 0; } // Warning: Identifier 'SomeLibrary.SomeLibraryType.abc()' // differing only in case is not CLScompliant public void abc() { } // No warning: this method is private private UInt32 ABC() { return 0; } } }
دومین اخطار اینکه دو متد یکسان وجود دارند که در حروف بزرگ و کوچک تفاوت دارند. ولی زبان هایی چون ویژوال بیسیک نمیتوانند تفاوتی بین دو متد abc و ABC بیابند.
نکتهی جالب اینکه اگر شما کلمه public را از جلوی نام کلاس بردارید تمامی این اخطارها لغو میشود. به این خاطر که اینها اشیای داخلی آن اسمبلی شناخته شده و قرار نیست از بیرون به آن دسترسی صورت بگیرد. عضو خصوصی کد بالا را ببینید؛ کامنت بالای آن میگوید که چون خصوصی است هشداری نمیگیرد، چون قرار نیست در زبان مقصد از آن به طور مستقیم استفاده کند.
برای دیدن قوانین CLS به این صفحه مراجعه فرمایید.
در بالا در مورد یکپارچگی و سازگاری کدهای مدیریت شده توسط CLS صحبت کردیم ولی در مورد ارتباط با کدهای مدیریت نشده چطور؟
مایکروسافت موقعیکه CLR را ارئه کرد، متوجه این قضیه بود که بسیاری از شرکتها توانایی اینکه کدهای خودشون را مجددا طراحی و پیاده سازی کنند، ندارند و خوب، سورسهای مدیریت نشدهی زیادی هم موجود هست که توسعه دهندگان علاقه زیادی به استفاده از آنها دارند. در نتیجه مایکروسافت طرحی را ریخت که CLR هر دو قسمت کدهای مدیریت شده و نشده را پشتیبانی کند. دو نمونه از این پشتیبانی را در زیر بیان میکنیم:
یک. کدهای مدیریت شده میتوانند توابع مدیریت شده را در قالب یک dll صدا زده و از آنها استفاده کنند.
دو. کدهای مدیریت شده میتوانند از کامپوننتهای COM استفاده کنند: بسیاری از شرکتها از قبل بسیاری از کامپوننتهای COM را ایجاد کرده بودند که کدهای مدیریت شده با راحتی با آنها ارتباط برقرار میکنند. ولی اگر دوست دارید روی آنها کنترل بیشتری داشته باشید و آن کدها را به معادل CLR تبدیل کنید؛ میتوانید از ابزار کمکی که مایکروسافت همراه فریم ورک دات نت ارائه کرده است استفاده کنید. نام این ابزار TLBIMP.exe میباشد که از Type Library Importer گرفته شده است.
سه. اگر کدهای مدیریت نشدهی زیادتری دارید شاید راحتتر باشد که برعکس کار کنید و کدهای مدیریت شده را در در یک برنامهی مدیریت نشده اجرا کنید. این کدها میتوانند برای مثال به یک Activex یا shell Extension تبدیل شده و مورد استفاده قرار گیرند. ابزارهای TLBEXP .exe و RegAsm .exe برای این منظور به همراه فریم ورک دات نت عرضه شده اند.
سورس کد Type Library Importer را میتوانید در کدپلکس بیابید.
در ویندوز 8 به بعد مایکروسافت API جدید را تحت عنوان WinsowsRuntime یا winRT ارائه کرده است . این api یک سیستم داخلی را از طریق کامپوننتهای com ایجاد کرده و به جای استفاده از فایلهای کتابخانهای، کامپوننتها api هایشان را از طریق متادیتاهایی بر اساس استاندارد ECMA که توسط تیم دات نت طراحی شده است معرفی میکنند.
زیبایی این روش اینست که کد نوشته شده در زبانهای دات نت میتواند به طور مداوم با apiهای winrt ارتباط برقرار کند. یعنی همهی کارها توسط CLR انجام میگیرد بدون اینکه لازم باشد از ابزار اضافی استفاده کنید. در آینده در مورد winRT بیشتر صحبت میکنیم.
سخن پایانی: ممنون از دوستان عزیز بابت پیگیری مطالب تا بدینجا. تا این قسمت فصل اول کتاب با عنوان اصول اولیه CLR بخش اول مدل اجرای CLR به پایان رسید.
ادامهی مطالب بعد از تکمیل هر بخش در دسترس دوستان قرار خواهد گرفت.
دریافت و نصب AvalonEdit
برای نصب AvalonEdit میتوان دستور ذیل را در کنسول پاورشل نیوگت صادر کرد:
PM> install-package AvalonEdit
استفادهی مقدماتی از AvalonEdit
برای استفاده از این ویرایشگر ابتدا نیاز است فضای نام xmlns:avalonEdit تعریف شود. سپس کنترل avalonEdit:TextEditor در دسترس خواهد بود:
<Window x:Class="SyntaxHighlighter.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:avalonEdit="http://icsharpcode.net/sharpdevelop/avalonedit" Title="MainWindow" Height="401" Width="617"> <Grid> <avalonEdit:TextEditor Name="txtCode" SyntaxHighlighting="C#" FontFamily="Consolas" FontSize="10pt"/> </Grid> </Window>
استفاده از AvalonEdit در برنامههای MVVM
خاصیت Text این ویرایشگر به صورت معمولی تعریف شده (DependencyProperty نیست) و امکان binding دو طرفه به آن وجود ندارد. به همین جهت نیاز است یک چنین DependencyProperty را به آن اضافه کرد:
using System; using System.Collections.Concurrent; using System.Reflection; using System.Windows; using System.Xml; using ICSharpCode.AvalonEdit; using ICSharpCode.AvalonEdit.Highlighting; using ICSharpCode.AvalonEdit.Highlighting.Xshd; namespace AvalonEditWpfTest.Controls { public class BindableAvalonTextEditor : TextEditor { public static readonly DependencyProperty BoundTextProperty = DependencyProperty.Register("BoundText", typeof(string), typeof(BindableAvalonTextEditor), new FrameworkPropertyMetadata(default(string), propertyChangedCallback)); public static string GetBoundText(DependencyObject obj) { return (string)obj.GetValue(BoundTextProperty); } public static void SetBoundText(DependencyObject obj, string value) { obj.SetValue(BoundTextProperty, value); } protected override void OnTextChanged(EventArgs e) { SetCurrentValue(BoundTextProperty, Text); base.OnTextChanged(e); } private static void propertyChangedCallback(DependencyObject obj, DependencyPropertyChangedEventArgs args) { var target = (BindableAvalonTextEditor)obj; var value = args.NewValue; if (value == null) return; if (string.IsNullOrWhiteSpace(target.Text) || !target.Text.Equals(args.NewValue.ToString())) { target.Text = args.NewValue.ToString(); } } } }
اکنون برای استفاده از این کنترل جدید که BindableAvalonTextEditor نام دارد، میتوان به نحو ذیل عمل کرد:
<Window x:Class="AvalonEditWpfTest.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:viewModels="clr-namespace:AvalonEditTests.ViewModels" xmlns:controls="clr-namespace:AvalonEditWpfTest.Controls" Title="MainWindow" Height="350" Width="525"> <Window.Resources> <viewModels:MainWindowViewModel x:Key="MainWindowViewModel"/> </Window.Resources> <Grid DataContext="{Binding Source={StaticResource MainWindowViewModel}}"> <controls:BindableAvalonTextEditor BoundText="{Binding SourceCode, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" WordWrap="True" ShowLineNumbers="True" LineNumbersForeground="MediumSlateBlue" FontFamily="Consolas" VerticalScrollBarVisibility="Auto" Margin="3" HorizontalScrollBarVisibility="Auto" FontSize="10pt"/> </Grid> </Window>
افزودن syntax highlighting زبانهایی که به صورت رسمی پشتیبانی نمیشوند
به خاصیت SyntaxHighlighting این کنترل صرفا مقادیری را میتوان نسبت داد که به صورت توکار پشتیبانی میشوند. برای مثال#XML، C و امثال آن.
فرض کنید نیاز است SyntaxHighlighting زبان SQL را فعال کنیم. برای اینکار نیاز به فایلهای ویژهای است، با پسوند xshd. برای نمونه فایل sql-ce.xshd را در اینجا میتوانید مطالعه کنید. در آن یک سری واژههای کلیدی و حروفی که باید با رنگی متفاوت نمایش داده شوند، مشخص میگردند.
برای استفاده از فایل sql-ce.xshd باید به نحو ذیل عمل کرد:
الف) فایل sql-ce.xshd را به پروژه اضافه کرده و سپس در برگهی خواص آن در VS.NET، مقدار build action آنرا به embedded resource تغییر دهید.
ب) با استفاده از متد ذیل، این فایل مدفون شده در اسمبلی را گشوده و به متد HighlightingLoader.Load ارسال میکنیم:
private static IHighlightingDefinition getHighlightingDefinition(string resourceName) { if (string.IsNullOrWhiteSpace(resourceName)) throw new NullReferenceException("Please specify SyntaxHighlightingResourceName."); using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName)) { if (stream == null) throw new NullReferenceException(string.Format("{0} resource is null.", resourceName)); using (var reader = new XmlTextReader(stream)) { return HighlightingLoader.Load(reader, HighlightingManager.Instance); } } }
txtCode.SyntaxHighlighting = getHighlightingDefinition(resourceName);
برای سهولت استفاده از این قابلیت شاید بهتر باشد یک DependencyProperty دیگر به نام SyntaxHighlightingResourceName را به کنترل جدید BindableAvalonTextEditor اضافه کنیم:
using System; using System.Collections.Concurrent; using System.Reflection; using System.Windows; using System.Xml; using ICSharpCode.AvalonEdit; using ICSharpCode.AvalonEdit.Highlighting; using ICSharpCode.AvalonEdit.Highlighting.Xshd; namespace AvalonEditWpfTest.Controls { public class BindableAvalonTextEditor : TextEditor { public static readonly DependencyProperty SyntaxHighlightingResourceNameProperty = DependencyProperty.Register("SyntaxHighlightingResourceName", typeof(string), typeof(BindableAvalonTextEditor), new FrameworkPropertyMetadata(default(string), resourceNamePropertyChangedCallback)); public static string GetSyntaxHighlightingResourceName(DependencyObject obj) { return (string)obj.GetValue(SyntaxHighlightingResourceNameProperty); } public static void SetSyntaxHighlightingResourceName(DependencyObject obj, string value) { obj.SetValue(SyntaxHighlightingResourceNameProperty, value); } private static void loadHighlighter(TextEditor @this, string resourceName) { if (@this.SyntaxHighlighting != null) return; @this.SyntaxHighlighting = getHighlightingDefinition(resourceName); } private static void resourceNamePropertyChangedCallback(DependencyObject obj, DependencyPropertyChangedEventArgs args) { var target = (BindableAvalonTextEditor)obj; var value = args.NewValue; if (value == null) return; loadHighlighter(target, value.ToString()); } } }
استفاده از آن نیز به شکل زیر است:
<controls:BindableAvalonTextEditor SyntaxHighlightingResourceName = "AvalonEditWpfTest.Controls.sql-ce.xshd" />
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید:
AvalonEditWpfTest.zip
- سرویس لاگر سفارشی مبتنی بر EF Core
- کنترلر نمایش اطلاعات آن
- View مرتبط
- ثبت آن در سیستم: ^ و ^
- کنترلری که خطاهای سیستم را لاگ میکند و هدایت خطاها به این کنترلر
نیاز به یک چنین قسمتی در برنامه دارید تا بتوانید از خطاهای لاگ شده گزارش بگیرید و آنها را بررسی کنید.
تهیه یک binder provider پردازش رشتهها
کار model binding، تطابق اطلاعات رسیدهی از درخواست جاری، با پارامترهای اکشن متد یک کنترلر است. هر مقدار رسیده، به یک binder متناسب ارسال میشود تا پردازش آن مدیریت گردد. به صورت پیش فرض در ASP.NET Core، تعدد 14 عدد binder providers که اینترفیس IModelBinderProvider را پیاده سازی میکنند، در این بین جهت یافتن یک binder مناسب، بررسی خواهند شد. برای مثال کار یک binder، پردازش نوعهای پیچیدهاست (complex types) و دیگری نوعهای ساده (simple types) مانند int و string را پردازش میکند.
public class CustomStringModelBinderProvider : IModelBinderProvider { public IModelBinder GetBinder(ModelBinderProviderContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } if (context.Metadata.IsComplexType) { return null; } var fallbackBinder = new SimpleTypeModelBinder(context.Metadata.ModelType); if (context.Metadata.ModelType == typeof(string)) { return new CustomStringModelBinder(fallbackBinder); } return fallbackBinder; } }
سپس اگر نوع مدل جاری رشتهای بود، وهلهای از CustomStringModelBinder را بازگشت میدهیم (کلاسی است که آنرا در ادامه تهیه خواهیم کرد). درغیراینصورت همان SimpleTypeModelBinder توکار این فریمورک را بازگشت خواهیم داد.
تهیهی یک model binder سفارشی پردازش رشتهها
تا اینجا تامین کنندهای را که مشخص میکند چه model binder ایی قرار است بازگشت داده شود، تهیه کردیم. مرحلهی بعد، پیاده سازی CustomStringModelBinder با پیاده سازی اینترفیس IModelBinder است:
public class CustomStringModelBinder : IModelBinder { private readonly IModelBinder _fallbackBinder; public CustomStringModelBinder(IModelBinder fallbackBinder) { if (fallbackBinder == null) { throw new ArgumentNullException(nameof(fallbackBinder)); } _fallbackBinder = fallbackBinder; } public Task BindModelAsync(ModelBindingContext bindingContext) { if (bindingContext == null) { throw new ArgumentNullException(nameof(bindingContext)); } var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName); if (valueProviderResult != ValueProviderResult.None) { bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult); var valueAsString = valueProviderResult.FirstValue; if (string.IsNullOrWhiteSpace(valueAsString)) { return _fallbackBinder.BindModelAsync(bindingContext); } var model = valueAsString.Replace((char)1610, (char)1740).Replace((char)1603, (char)1705); bindingContext.Result = ModelBindingResult.Success(model); return Task.CompletedTask; } return _fallbackBinder.BindModelAsync(bindingContext); } }
در اینجا تمام مواردی را که نمیخواهیم پردازش کنیم، به همان SimpleTypeModelBinder که از طریق سازندهی کلاس دریافت میکنیم، واگذار خواهیم کرد.
معرفی به binder provider سفارشی به سیستم
مرحلهی آخر این عملیات، معرفی binder تهیه شده به سیستم است که روش آن را در ذیل مشاهده میکنید:
public static class CustomStringModelBinderExtensions { public static MvcOptions UseCustomStringModelBinder(this MvcOptions options) { if (options == null) { throw new ArgumentNullException(nameof(options)); } var simpleTypeModelBinder = options.ModelBinderProviders.FirstOrDefault(x => x.GetType() == typeof(SimpleTypeModelBinderProvider)); if (simpleTypeModelBinder == null) { return options; } var simpleTypeModelBinderIndex = options.ModelBinderProviders.IndexOf(simpleTypeModelBinder); options.ModelBinderProviders.Insert(simpleTypeModelBinderIndex, new CustomStringModelBinderProvider()); return options; } }
در آخر تنها کافی است در کلاس آغازین برنامه، متد الحاقی UseCustomStringModelBinder فوق را به تنظیمات Mvc اضافه کنیم:
public void ConfigureServices(IServiceCollection services) { services.AddMvc(options => { options.UseCustomStringModelBinder(); });
RIA Services و پشتیبانی از EntityFramework 5.0.0
آموزش سیلورلایت 4 - قسمتهای 21 تا 27
میشه جایگاه WCF RIA Services در MVVM رو بگین کجاست ، یعنی به عنوان Model بهش نگاه میکنیم؟
آشنایی با مدل برنامه نویسی TAP
دات نت فریم ورک، از زمان ارائه نگارش یک آن، از اعمال غیرهمزمان و API خاص آن پشتیبانی میکردهاست. همچنین این مورد یکی از ویژگیهای Win32 نیز میباشد. نوشتن کدهای همزمان متداول بسیار ساده است. در این نوع کدها هر عملیات خاص، پس از پایان عملیات قبلی انجام میشود.
public string TestNoneAsync() { var webClient = new WebClient(); return webClient.DownloadString("http://www.google.com"); }
این مورد همچنین در برنامههای سمت سرور نیز حائز اهمیت است. با قفل شدن تعداد زیادی ترد در حال اجرا، عملا قدرت پاسخدهی سرور نیز کاهش مییابد. بنابراین در این نوع موارد، برنامههای چند ریسمانی هرچند در سمت کلاینت ممکن است مفید واقع شوند و برای مثال ترد UI را آزاد کنند، اما اثر آنچنانی بر روی برنامههای سمت سرور ندارند. زیرا در آنها میتوان هزاران ترد را ایجاد کرد که همگی دارای کدهای اصطلاحا blocking باشند. برای حل این مساله استفاده از API غیرهمزمان توصیه میشود.
برای نمونه کلاس WebClient توکار دات نت، دارای متدی به نام DownloadStringAsync نیز میباشد. این متد به محض فراخوانی، ترد جاری را آزاد میکند. به این معنا که فراخوانی آن سبب توقف ترد جاری برای دریافت نتیجهی دریافت اطلاعات از وب نمیشود. به این نوع API، یک Asynchronous API گفته میشود؛ زیرا با سایر کدهای نوشته شده، هماهنگ و همزمان اجرا نمیشود.
هر چند این کد جدید مشکل عدم پاسخ دهی برنامه را برطرف میکند، اما مشکل دیگری را به همراه دارد؛ چگونه باید حاصل عملیات آنرا پس از پایان کار دریافت کرد؟ چگونه باید خطاها و مشکلات احتمالی را مدیریت کرد؟
برای مدیریت این مساله، رخدادی به نام DownloadStringCompleted تعریف شدهاست. روال رویدادگردان آن پس از پایان کار دریافت اطلاعات از وب، فراخوانی میگردد.
public void TestAsync() { var webClient = new WebClient(); webClient.DownloadStringAsync(new Uri("http://www.google.com")); webClient.DownloadStringCompleted += webClientDownloadStringCompleted; } void webClientDownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e) { // use e.Result }
مشکل! ما سادگی یک عملیات همزمان را از دست دادیم. متد TestNoneAsync از لحاظ پیاده سازی و همچنین خواندن و نگهداری آن در طول زمان، بسیار سادهتر است از نمونهی TestAsync نوشته شده. در کدهای غیرهمزمان فوق، یک متد ساده، به دو متد مجزا خرد شدهاست و نتیجهی نهایی، درون یک روال رخدادگردان بدست میآید.
به این مدل، EAP یا Event based asynchronous pattern نیز گفته میشود. EAP در دات نت 2 معرفی شد. روالهای رخدادگردان در این حالت، در ترد اصلی برنامه اجرا میشوند. اما اگر به حالت اصلی اعمال غیرهمزمان موجود از دات نت یک کوچ کنیم، اینطور نیست. در WinForms و WPF برای به روز رسانی رابط کاربری نیاز است اطلاعات دریافت شده در همان تردی که رابط کاربری ایجاد شده است، تحویل گرفته شده و استفاده شوند. در غیراینصورت استثنایی صادر شده و برنامه خاتمه مییابد.
آشنایی با Synchronization Context
ابتدا یک برنامهی WinForms ساده را آغاز کرده و یک دکمهی جدید را به نام btnGetInfo و یک تکست باکس را به نام txtResults، به آن اضافه کنید. سپس کدهای فرم اصلی آنرا به نحو ذیل تغییر دهید:
using System; using System.Linq; using System.Net; using System.Windows.Forms; namespace Async02 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void btnGetInfo_Click(object sender, EventArgs e) { var req = (HttpWebRequest)WebRequest.Create("http://www.google.com"); req.Method = "HEAD"; req.BeginGetResponse( asyncResult => { var resp = (HttpWebResponse)req.EndGetResponse(asyncResult); var headersText = formatHeaders(resp.Headers); txtResults.Text = headersText; }, null); } private string formatHeaders(WebHeaderCollection headers) { var headerString = headers.Keys.Cast<string>() .Select(header => string.Format("{0}:{1}", header, headers[header])); return string.Join(Environment.NewLine, headerString.ToArray()); } } }
همچنین در این مثال از متد غیرهمزمان BeginGetResponse نیز استفاده شدهاست. در این نوع API خاص، کار با BeginGetResponse آغاز شده و سپس در callback نهایی توسط EndGetResponse، نتیجهی عملیات به دست میآید.
اگر برنامه را اجرا کنید، با استثنای زیر مواجه خواهید شد:
An exception of type 'System.InvalidOperationException' occurred in System.Windows.Forms.dll but was not handled in user code Additional information: Cross-thread operation not valid: Control 'txtResults' accessed from a thread other than the thread it was created on.
private void btnGetInfo_Click(object sender, EventArgs e) { var sync = SynchronizationContext.Current; var req = (HttpWebRequest)WebRequest.Create("http://www.google.com"); req.Method = "HEAD"; req.BeginGetResponse( asyncResult => { var resp = (HttpWebResponse)req.EndGetResponse(asyncResult); var headersText = formatHeaders(resp.Headers); sync.Post(delegate { txtResults.Text = headersText; }, null); }, null); }
برای درک بهتر آن، سه break point را پیش از متد BeginGetResponse، داخل Async calback و داخل delegate متد Post قرار دهید. پس از اجرای برنامه، از منوی دیباگ در VS.NET گزینهی Windows و سپس Threads را انتخاب کنید.
در اینجا همانطور که مشخص است، کد داخل delegate تعریف شده، در ترد اصلی برنامه اجرا میشود و نه یکی از Worker threadهای ثانویه.
هر چند استفاده از متدهای تو در تو و lambda syntax، نیاز به تعریف چندین متد جداگانه را برطرف کردهاست، اما باز هم کد سادهای به نظر نمیرسد. در سی شارپ 5، برای مدیریت بهتر تمام مشکلات یاد شده، پشتیبانی توکاری از اعمال غیرهمزمان، به هستهی زبان اضافه شدهاست.
Syntax ابتدایی یک متد Async
در ابتدا کلاس و متد Async زیر را در نظر بگیرید:
using System; using System.Threading.Tasks; namespace Async01 { public class AsyncExample { public async Task DoWorkAsync(int parameter) { await Task.Delay(parameter); Console.WriteLine(parameter); } } }
- در مدل برنامه نویسی TAP، متدهای غیرهمزمان باید یک Task را بازگشت دهند؛ یا نمونهی جنریک آنرا. البته کامپایلر، async void را نیز پشتیبانی میکند ولی در قسمتهای بعدی بررسی خواهیم کرد که چرا استفاده از آن مشکلزا است و باید از آن پرهیز شود.
- همچنین مطابق TAP، اینگونه متدها باید به پسوند Async ختم شوند تا استفاده کننده در حین کار با Intellisense، بتواند آنها را از متدهای معمولی سریعتر تشخیص دهد.
- از واژهی کلیدی async نیز استفاده میگردد تا کامپایلر از وجود اعمال غیر همزمان مطلع گردد.
- await به کامپایلر میگوید، عبارت پس از من، یک وظیفهی غیرهمزمان است و ادامهی کدهای نوشته شده، تنها زمانی باید اجرا شوند که عملیات غیرهمزمان معرفی شده، تکمیل گردد.
در متد DoWorkAsync، ابتدا به اندازهای مشخص توقف حاصل شده و سپس سطر بعدی یعنی Console.WriteLine اجرا میشود.
یک اشتباه عمومی! استفاده از واژههای کلیدی async و await متد شما را async نمیکنند.
برخلاف تصور ابتدایی از بکارگیری واژههای کلیدی async و await، این کلمات نحوهی اجرای متد شما را async نمیکنند. این کلمات صرفا برای تشکیل متدهایی که هم اکنون غیرهمزمان هستند، مفید میباشند. برای توضیح بیشتر آن به مثال ذیل دقت کنید:
public async Task<double> GetNumberAsync() { var generator = new Random(); await Task.Delay(generator.Next(1000)); return generator.NextDouble(); }
در ادامه برای استفاده از آن خواهیم داشت:
public async Task<double> GetSumAsync() { var leftOperand = await GetNumberAsync(); var rightOperand = await GetNumberAsync(); return leftOperand + rightOperand; }
در کدهای همزمان متداول، سطر اول ابتدا انجام میشود و بعد سطر دوم و الی آخر. با استفاده از واژهی کلیدی await یک چنین عملکردی را با اعمال غیرهمزمان خواهیم داشت. پیش از این برای مدیریت اینگونه اعمال از یک سری callback و یا رخداد استفاده میشد. برای مثال ابتدا عملیات همزمانی شروع شده و سپس نتیجهی آن در یک روال رخداد گردان جایی در کدهای برنامه دریافت میشد (مانند مثال ابتدای بحث). اکنون تصور کنید که قصد داشتید جمع نهایی حاصل دو عملیات غیرهمزمان را از دو روال رخدادگردان جدا از هم، جمع آوری کرده و بازگشت دهید. هرچند اینکار غیرممکن نیست، اما حاصل کار به طور قطع آنچنان زیبا نبوده و قابلیت نگهداری پایینی دارد. واژهی کلیدی await، انجام اینگونه امور غیرهمزمان را طبیعی و همزمان جلوه میدهد. به این ترتیب بهتر میتوان بر روی منطق و الگوریتمهای مورد استفاده تمرکز داشت، تا اینکه مدام درگیر مکانیک اعمال غیرهمزمان بود.
امکان استفاده از واژهی کلیدی await در هر جایی از کدها وجود دارد. برای نمونه در مثال زیر، برای ترکیب دو عملیات غیرهمزمان، از await در حین تشکیل عملیات ضرب نهایی، دقیقا در جایی که مقدار متد باید بازگشت داده شود، استفاده شدهاست:
public async Task<double> GetProductOfSumAsync() { var leftOperand = GetSumAsync(); var rightOperand = GetSumAsync(); return await leftOperand * await rightOperand; }
Operator '*' cannot be applied to operands of type 'System.Threading.Tasks.Task<double>' and 'System.Threading.Tasks.Task<double>'
اگر متد DownloadString همزمان ابتدای بحث را نیز بخواهیم تبدیل به نمونهی async سیشارپ 5 کنیم، میتوان از متد الحاقی جدید آن به نام DownloadStringTaskAsync کمک گرفت:
public async Task<string> DownloadAsync() { var webClient = new WebClient(); return await webClient.DownloadStringTaskAsync("http://www.google.com"); }
سؤال: آیا استفاده از await نیز ترد جاری را قفل میکند؟
اگر به کدها دقت کنید، استفاده از await به معنای صبر کردن تا پایان عملیات async است. پس اینطور به نظر میرسد که در اینجا نیز ترد اصلی، همانند قبل قفل شدهاست.
public void TestDownloadAsync() { Debug.WriteLine("Before DownloadAsync"); DownloadAsync(); Debug.WriteLine("After DownloadAsync"); }
Before DownloadAsync After DownloadAsync
برنامههای Async و نگارشهای مختلف دات نت
شاید در ابتدا به نظر برسد که قابلیتهای جدید async و await صرفا متعلق هستند به دات نت 4.5 به بعد؛ اما خیر. اگر کامپایلری را داشته باشید که از این واژههای کلیدی را پشتیبانی کند، امکان استفاده از آنها را با دات نت 4 نیز خواهید داشت. برای این منظور تنها کافی است از VS 2012 به بعد استفاده نمائید. سپس در کنسول پاورشل نیوگت دستور ذیل را اجرا نمائید (فقط برای برنامههای دات نت 4 البته):
PM> Install-Package Microsoft.Bcl.Async