EF Code First #15
EF Code first و بانکهای اطلاعاتی متفاوت
در آخرین قسمت از سری EF Code first بد نیست نحوه استفاده از بانکهای اطلاعاتی دیگری را بجز SQL Server نیز بررسی کنیم. در اینجا کلاسهای مدل و کدهای مورد استفاده نیز همانند قسمت 14 است و تنها به ذکر تفاوتها و نکات مرتبط اکتفاء خواهد شد.
حالت کلی پشتیبانی از بانکهای اطلاعاتی مختلف توسط EF Code first
EF Code first با کلیه پروایدرهای تهیه شده برای ADO.NET 3.5 که پشتیبانی از EF را لحاظ کرده باشند، به خوبی کار میکند. پروایدرهای مخصوص ADO.NET 4.0، تنها سه گزینه DeleteDatabase/CreateDatabase/DatabaseExists را نسبت به نگارش قبلی بیشتر دارند و EF Code first ویژگیهای بیشتری را طلب نمیکند.
بنابراین اگر حین استفاده از پروایدر ADO.NET مخصوص بانک اطلاعاتی خاصی با پیغام «CreateDatabase is not supported by the provider» مواجه شدید، به این معنا است که این پروایدر برای دات نت 4 به روز نشده است. اما به این معنا نیست که با EF Code first کار نمیکند. فقط باید یک دیتابیس خالی از پیش تهیه شده را به برنامه معرفی کنید تا مباحث Database Migrations به خوبی کار کنند؛ یا اینکه کلا میتوانید Database Migrations را خاموش کرده (متد Database.SetInitializer را با پارامتر نال فراخوانی کنید) و فیلدها و جداول را دستی ایجاد کنید.
استفاده از EF Code first با SQLite
برای استفاده از SQLite در دات نت ابتدا نیاز به پروایدر ADO.NET آن است: «مکان دریافت درایورهای جدید SQLite مخصوص دات نت»
ضمن اینکه به نکته «استفاده از اسمبلیهای دات نت 2 در یک پروژه دات نت 4» نیز باید دقت داشت.
و یکی از بهترین management studio هایی که برای آن تهیه شده: «SQLite Manager»
پس از دریافت پروایدر آن، ارجاعی را به اسمبلی System.Data.SQLite.dll به برنامه اضافه کنید.
سپس فایل کانفیگ برنامه را به نحو زیر تغییر دهید:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=4.3.1.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
</configSections>
<startup useLegacyV2RuntimeActivationPolicy="true">
<supportedRuntime version="v4.0"/>
</startup>
<connectionStrings>
<clear/>
<add name="Sample09Context"
connectionString="Data Source=CodeFirst.db"
providerName="System.Data.SQLite"/>
</connectionStrings>
</configuration>
همانطور که ملاحظه میکنید، تفاوت آن با قبل، تغییر connectionString و providerName است.
اکنون اگر همان برنامه قسمت قبل را اجرا کنیم به خطای زیر برخواهیم خورد:
«The given key was not present in the dictionary»
در این مورد هم توضیح داده شد. سه گزینه DeleteDatabase/CreateDatabase/DatabaseExists در پروایدر جاری SQLite برای دات نت وجود ندارد. به همین جهت نیاز است فایل «CodeFirst.db» ذکر شده در کانکشن استرینگ را ابتدا دستی درست کرد.
برای مثال از افزونه SQLite Manager استفاده کنید. ابتدا یک بانک اطلاعاتی خالی را درست کرده و سپس دستورات زیر را بر روی بانک اطلاعاتی اجرا کنید تا دو جدول خالی را ایجاد کند (در برگه Execute sql افزونه SQLite Manager):
CREATE TABLE [Payees](
[Id] [integer] PRIMARY KEY AUTOINCREMENT NOT NULL,
[Name] [text] NULL,
[CreatedOn] [datetime] NOT NULL,
[CreatedBy] [text] NULL,
[ModifiedOn] [datetime] NOT NULL,
[ModifiedBy] [text] NULL
);
CREATE TABLE [Bills](
[Id] [integer] PRIMARY KEY AUTOINCREMENT NOT NULL,
[Amount] [float](18, 2) NOT NULL,
[Description] [text] NULL,
[CreatedOn] [datetime] NOT NULL,
[CreatedBy] [text] NULL,
[ModifiedOn] [datetime] NOT NULL,
[ModifiedBy] [text] NULL,
[Payee_Id] [integer] NULL
);
سپس سطر زیر را نیز به ابتدای برنامه اضافه کنید:
Database.SetInitializer<Sample09Context>(null);
به این ترتیب database migrations خاموش میشود و اکنون برنامه بدون مشکل کار خواهد کرد.
فقط باید به یک سری نکات مانند نوع دادهها در بانکهای اطلاعاتی مختلف دقت داشت. برای مثال integer در اینجا از نوع Int64 است؛ بنابراین در برنامه نیز باید به همین ترتیب تعریف شود تا نگاشتها به درستی انجام شوند.
در کل تنها مشکل پروایدر فعلی SQLite عدم پشتیبانی از مباحث database migrations است. این مورد را خاموش کرده و تغییرات ساختار بانک اطلاعاتی را به صورت دستی به بانک اطلاعاتی اعمال کنید. بدون مشکل کار خواهد کرد.
البته اگر به دنبال پروایدری تجاری با پشتیبانی از آخرین نگارش EF Code first هستید، گزینه زیر نیز مهیا است:
http://devart.com/dotconnect/sqlite/
برای مثال اگر علاقمند به استفاده از حالت تشکیل بانک اطلاعاتی SQLite در حافظه هستید (با رشته اتصالی ویژه Data Source=:memory:;Version=3;New=True;)، فعلا تنها گزینه مهیا استفاده از پروایدر تجاری فوق است؛ زیرا مبحث Database Migrations را به خوبی پشتیبانی میکند.
استفاده از EF Code first با SQL Server CE
قبلا در مورد «استفاده از SQL-CE به کمک NHibernate» مطلبی را در این سایت مطالعه کردهاید. سه مورد اول آن با EF Code first یکی است و تفاوتی نمیکند (یک سری بحث عمومی مشترک است). البته با یک تفاوت؛ در اینجا EF Code first قادر است یک بانک اطلاعاتی خالی SQL Server CE را به صورت خودکار ایجاد کند و نیازی نیست تا آنرا دستی ایجاد کرد. مباحث database migrations و به روز رسانی خودکار ساختار بانک اطلاعاتی نیز در اینجا پشتیبانی میشود.
برای استفاده از آن ابتدا ارجاعی را به اسمبلی System.Data.SqlServerCe.dll قرار گرفته در مسیر Program Files\Microsoft SQL Server Compact Edition\v4.0\Desktop اضافه کنید.
سپس رشته اتصالی به بانک اطلاعاتی و providerName را به نحو زیر تغییر دهید:
<connectionStrings>
<clear/>
<add name="Sample09Context"
connectionString="Data Source=mydb.sdf;Password=1234;Encrypt Database=True"
providerName="System.Data.SqlServerCE.4.0"/>
</connectionStrings>
بدون نیاز به هیچگونه تغییری در کدهای برنامه، همین مقدار تغییر در تنظیمات ابتدایی برنامه برای کار با SQL Server CE کافی است.
ضمنا مشکلی هم با فیلد Identity در آخرین نگارش EF Code first وجود ندارد؛ برخلاف حالت database first آن که پیشتر این اجازه را نمیداد و خطای «Server-generated keys and server-generated values are not supported by SQL Server Compact» را ظاهر میکرد.
استفاده از EF Code first با MySQL
برای استفاده از EF Code first با MySQL (نگارش 5 به بعد البته) ابتدا نیاز است پروایدر مخصوص ADO.NET آنرا دریافت کرد: (^)
که از EF نیز پشتیبانی میکند. پس از نصب آن، ارجاعی را به اسمبلی MySql.Data.dll قرار گرفته در مسیر Program Files\MySQL\MySQL Connector Net 6.5.4\Assemblies\v4.0 به پروژه اضافه نمائید.
سپس رشته اتصالی و providerName را به نحو زیر تغییر دهید:
<connectionStrings>
<clear/>
<add name="Sample09Context"
connectionString="Datasource=localhost; Database=testdb2; Uid=root; Pwd=123;"
providerName="MySql.Data.MySqlClient"/>
</connectionStrings>
<system.data>
<DbProviderFactories>
<remove invariant="MySql.Data.MySqlClient"/>
<add name="MySQL Data Provider"
invariant="MySql.Data.MySqlClient"
description=".Net Framework Data Provider for MySQL"
type="MySql.Data.MySqlClient.MySqlClientFactory, MySql.Data, Version=6.5.4.0, Culture=neutral, PublicKeyToken=c5687fc88969c44d" />
</DbProviderFactories>
</system.data>
همانطور که مشاهده میکنید در اینجا شماره نگارش دقیق پروایدر مورد استفاده نیز ذکر شده است. برای مثال اگر چندین پروایدر روی سیستم نصب است، با مقدار دهی DbProviderFactories میتوان از نگارش مخصوصی استفاده کرد.
با این تغییرات پس از اجرای برنامه قسمت قبل، به خطای زیر برخواهیم خورد:
The given key was not present in the dictionary
توضیحات این مورد با قسمت SQLite یکی است؛ به عبارتی نیاز است بانک اطلاعاتی testdb را دستی درست کرد. همچنین جداول و فیلدها را نیز باید دستی ایجاد کرد و database migrations را نیز باید خاموش کرد (پارامتر Database.SetInitializer را به نال مقدار دهی کنید).
برای این منظور یک دیتابیس خالی را ایجاد کرده و سپس دو جدول زیر را به آن اضافه کنید:
CREATE TABLE IF NOT EXISTS `bills` (
`Id` int(11) NOT NULL AUTO_INCREMENT,
`Amount` float DEFAULT NULL,
`Description` varchar(400) CHARACTER SET utf8 COLLATE utf8_persian_ci NOT NULL,
`CreatedOn` datetime NOT NULL,
`CreatedBy` varchar(400) CHARACTER SET utf8 COLLATE utf8_persian_ci NOT NULL,
`ModifiedOn` datetime NOT NULL,
`ModifiedBy` varchar(400) CHARACTER SET utf8 COLLATE utf8_persian_ci NOT NULL,
`Payee_Id` int(11) NOT NULL,
PRIMARY KEY (`Id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_persian_ci AUTO_INCREMENT=1 ;
CREATE TABLE IF NOT EXISTS `payees` (
`Id` int(11) NOT NULL AUTO_INCREMENT,
`Name` varchar(400) CHARACTER SET utf8 COLLATE utf8_persian_ci NOT NULL,
`CreatedOn` datetime NOT NULL,
`CreatedBy` varchar(400) CHARACTER SET utf8 COLLATE utf8_persian_ci NOT NULL,
`ModifiedOn` datetime NOT NULL,
`ModifiedBy` varchar(400) CHARACTER SET utf8 COLLATE utf8_persian_ci NOT NULL,
PRIMARY KEY (`Id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_persian_ci AUTO_INCREMENT=1 ;
پس از این تغییرات، برنامه بدون مشکل اجرا خواهد شد (ایجاد بانک اطلاعاتی خالی به همراه ایجاد ساختار جداول و خاموش کردن database migrations که توسط این پروایدر پشتیبانی نمیشود).
به علاوه پروایدر تجاری دیگری هم در سایت devart.com برای MySQL و EF Code first مهیا است که مباحث database migrations را به خوبی مدیریت میکند.
مشکل!
اگر به همین نحو برنامه را اجرا کنیم، فیلدهای یونیکد فارسی ثبت شده در MySQL با «??????? ?? ????» مقدار دهی خواهند شد و تنظیم CHARACTER SET utf8 COLLATE utf8_persian_ci نیز کافی نبوده است (این مورد با SQLite یا نگارشهای مختلف SQL Server بدون مشکل کار میکند و نیاز به تنظیم اضافهتری ندارد):
ALTER TABLE `bills` DEFAULT CHARACTER SET utf8 COLLATE utf8_persian_ci
برای رفع این مشکل توصیه شده است که CharSet=UTF8 را به رشته اتصالی به بانک اطلاعاتی اضافه کنیم. اما در این حالت خطای زیر ظاهر میشود:
The provider did not return a ProviderManifestToken string
این مورد فقط به اشتباه بودن تعاریف رشته اتصالی بر میگردد؛ یا عدم پشتیبانی از تنظیم اضافهای که در رشته اتصالی ذکر شده است.
مقدار صحیح آن دقیقا مساوی CHARSET=utf8 است (با همین نگارش و رعایت کوچکی و بزرگی حروف؛ مهم!):
<connectionStrings>
<clear/>
<add name="Sample09Context"
connectionString="Datasource=localhost; Database=testdb; Uid=root; Pwd=123;CHARSET=utf8"
providerName="MySql.Data.MySqlClient"/>
</connectionStrings>
به این ترتیب، مشکل ثبت عبارات یونیکد فارسی برطرف میشود (البته جدول هم بهتر است به DEFAULT CHARACTER SET utf8 COLLATE utf8_persian_ci تغییر پیدا کند؛ مطابق دستور Alter ایی که در بالا ذکر شد).
ASP.NET MVC #12
+ و یا همچنین layout، مدل محتوای خودش را به ارث میبرد. یعنی مدلی که در View تنظیم میشود، همان مدلی است که layout به آن دسترسی خواهد داشت. به همین جهت مثلا میتونید توسط ViewBag، عنوان صفحه را که در layout تعریف شده، مقدار دهی کنید.
اگر میخواهید Strongly typed کار کنید، روش Html.RenderAction یک راه حل است و روش دوم به صورت زیر است:
یک کلاس پایه abstract تعریف کنید:
public abstract class BaseViewModel { public string Name { get; set; } }
public class HomeViewModel : BaseViewModel { public int Data1 { set; get;} // ... }
@model BaseViewModel <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>Test</title> </head> <body> <header> Hello @Model.Name </header> <div> @this.RenderBody() </div> </body> </html>
Webgrid گرید توکار asp.net
mvc 3 است که در سری آموزشهای mvc جناب نصیری به خوبی بررسی شده است . WebGrid از طریق مجموعه ای از خواص امکان استایل دهی
به ستونها و ردیفها را به توسعه دهنده میدهد . اما در این بخش مشکلی وجود دارد که
در ادامه به آن خواهم پرداخت . کدهای زیر را در نظر بگیرید
مدلها :
public class Customer { public int Id { get; set; } public string Name { get; set; } public string Email { get; set; } public string Website { get; set; } public string Phone { get; set; } } public class Customers { public IList<Customer> GetList() { return new List<Customer>() { new Customer() { Id=1, Name="mohsen.d", Email="email@domain.com", Website="domain.com", Phone="213214321" } }; } public IList<Customer> GetEmptyList() { return new List<Customer>(); } }
public class HomeController : Controller { public ActionResult List() { var model = new Customers().GetList(); return View(model); } public ActionResult EmptyList() { var model = new Customers().GetEmptyList(); return View("list", model); } }
تابع کمکی برای ایجاد گرید :
@helper GenerateList(IEnumerable<object> items, List<WebGridColumn> columns) { var grid = new WebGrid(items); <div> @grid.GetHtml( tableStyle: "list", headerStyle: "list-header", footerStyle: "list-footer", alternatingRowStyle: "list-alt", selectedRowStyle: "list-selected", rowStyle: "list-row", htmlAttributes: new { id = "listItems" }, mode: WebGridPagerModes.All, columns: columns ) </div> }
@model IEnumerable<WebGridHeaderStyle.Models.Customer> @{ ViewBag.Title = "List"; } <h2>List</h2> @_List.GenerateList( Model, new List<WebGridColumn>() { new WebGridColumn(){ ColumnName="Id", Header="Id", Style="list-small-field" }, new WebGridColumn(){ ColumnName="Name", Header="Name", Style="list-long-field" }, new WebGridColumn(){ ColumnName="Email", Header="Email", Style="list-mid-field" }, new WebGridColumn(){ ColumnName="Website", Header="Website", Style="list-mid-field" }, new WebGridColumn(){ ColumnName="Phone", Header="Phone", Style="list-mid-field" } } )
خوب چندان بد نیست . با استفاده از استایلهای تعریف شده برای فیلدها و ردیفها ، لیست ساختار مناسبی دارد . اما حالا به Home/EmptyList می رویم :
همانطور که میبینید استایل هایی که برای هر ستون تعریف کرده بودیم اعمال نشده اند. مشکل هم همین
جاست . WebGrid استایل تعریف شده را تنها به ستونهای درون tbody
اعمال میکند و thead از این تنظیمات بی نصیب میماند ( WebGrid از table برای ساختن لیست استفاده میکند ) و در زمانی که رکوردی وجود نداشته باشد فرمت طراحی شده اعمال نمیشود .
در وب ترفندهایی را برای این مشکل پیدا
کردم که اصلا جالب نبودند . در نهایت راه حل زیر به نظرم رسید :
در زمان ساختن
گرید ، استایلهای تعریف شده را در یک فیلد hidden ذخیره و سپس با
استفاده از jquery این استایلها را به ستونهای header اعمال میکنیم .
تابع ساختن فیلد hidden :
@helper SetHeaderColumnsStyle(IEnumerable<WebGridColumn> columns) { var styles = new List<string>(); foreach(var col in columns) { styles.Add(col.Style); } <input id="styles" type="hidden" value="@string.Join("#",styles)" /> }
@SetHeaderColumnsStyle(columns)
<script> $(document).ready(function () { var styles = $("#styles").attr("value").split('#'); var $cols = $("#listItems th"); $cols.each(function (i) { $(this).addClass(styles[i]); }); }); </script>
در قسمت قبلی بروز رسانی موجودیتهای منفصل با WCF را بررسی کردیم. در این قسمت خواهیم دید چگونه میتوان تغییرات موجودیتها را تشخیص داد و عملیات CRUD را روی یک Object Graph اجرا کرد.
تشخیص تغییرات با Web API
فرض کنید میخواهیم از سرویسهای Web API برای انجام عملیات CRUD استفاده کنیم، اما بدون آنکه برای هر موجودیت متدهایی مجزا تعریف کنیم. به بیان دیگر میخواهیم عملیات مذکور را روی یک Object Graph انجام دهیم. مدیریت دادهها هم با مدل Code-First پیاده سازی میشود. در مثال جاری یک اپلیکیشن کنسول خواهیم داشت که بعنوان یک کلاینت سرویس را فراخوانی میکند. هر پروژه نیز در Solution مجزایی قرار دارد، تا یک محیط n-Tier را شبیه سازی کنیم.
مدل زیر را در نظر بگیرید.
همانطور که میبینید مدل ما آژانسهای مسافرتی و رزرواسیون آنها را ارائه میکند. میخواهیم مدل و کد دسترسی دادهها را در یک سرویس Web API پیاده سازی کنیم تا هر کلاینتی که به HTTP دسترسی دارد بتواند عملیات CRUD را انجام دهد. برای ساختن سرویس مورد نظر مراحل زیر را دنبال کنید:
- در ویژوال استودیو پروژه جدیدی از نوع ASP.NET Web Application بسازید و قالب پروژه را Web API انتخاب کنید. نام پروژه را به Recipe3.Service تغییر دهید.
- کنترلر جدیدی بنام TravelAgentController به پروژه اضافه کنید.
- دو کلاس جدید با نامهای TravelAgent و Booking بسازید و کد آنها را مطابق لیست زیر تغییر دهید.
public class TravelAgent { public TravelAgent() { this.Bookings = new HashSet<Booking>(); } public int AgentId { get; set; } public string Name { get; set; } public virtual ICollection<Booking> Bookings { get; set; } } public class Booking { public int BookingId { get; set; } public int AgentId { get; set; } public string Customer { get; set; } public DateTime BookingDate { get; set; } public bool Paid { get; set; } public virtual TravelAgent TravelAgent { get; set; } }
- با استفاده از NuGet Package Manager کتابخانه Entity Framework 6 را به پروژه اضافه کنید.
- کلاس جدیدی بنام Recipe3Context بسازید و کد آن را مطابق لیست زیر تغییر دهید.
public class Recipe3Context : DbContext { public Recipe3Context() : base("Recipe3ConnectionString") { } public DbSet<TravelAgent> TravelAgents { get; set; } public DbSet<Booking> Bookings { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity<TravelAgent>().HasKey(x => x.AgentId); modelBuilder.Entity<TravelAgent>().ToTable("TravelAgents"); modelBuilder.Entity<Booking>().ToTable("Bookings"); } }
- فایل Web.config پروژه را باز کنید و رشته اتصال زیر را به قسمت ConnectionStrings اضافه کنید.
<connectionStrings> <add name="Recipe3ConnectionString" connectionString="Data Source=.; Initial Catalog=EFRecipes; Integrated Security=True; MultipleActiveResultSets=True" providerName="System.Data.SqlClient" /> </connectionStrings>
- فایل Global.asax را باز کنید و کد زیر را به متد Application_Start اضافه نمایید. این کد بررسی Model Compatibility در EF را غیرفعال میکند. همچنین به JSON serializer میگوییم که self-referencing loop خاصیتهای پیمایشی را نادیده بگیرد. این حلقه بدلیل ارتباط bidirectional بین موجودیتها بوجود میآید.
protected void Application_Start() { // Disable Entity Framework Model Compatibilty Database.SetInitializer<Recipe1Context>(null); // The bidirectional navigation properties between related entities // create a self-referencing loop that breaks Web API's effort to // serialize the objects as JSON. By default, Json.NET is configured // to error when a reference loop is detected. To resolve problem, // simply configure JSON serializer to ignore self-referencing loops. GlobalConfiguration.Configuration.Formatters.JsonFormatter .SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore; ... }
- فایل RouteConfig.cs را باز کنید و قوانین مسیریابی را مانند لیست زیر تغییر دهید.
public static void Register(HttpConfiguration config) { config.Routes.MapHttpRoute( name: "ActionMethodSave", routeTemplate: "api/{controller}/{action}/{id}", defaults: new { id = RouteParameter.Optional }); }
- در آخر کنترلر TravelAgent را باز کنید و کد آن را مطابق لیست زیر بروز رسانی کنید.
public class TravelAgentController : ApiController { // GET api/travelagent [HttpGet] public IEnumerable<TravelAgent> Retrieve() { using (var context = new Recipe3Context()) { return context.TravelAgents.Include(x => x.Bookings).ToList(); } } /// <summary> /// Update changes to TravelAgent, implementing Action-Based Routing in Web API /// </summary> public HttpResponseMessage Update(TravelAgent travelAgent) { using (var context = new Recipe3Context()) { var newParentEntity = true; // adding the object graph makes the context aware of entire // object graph (parent and child entities) and assigns a state // of added to each entity. context.TravelAgents.Add(travelAgent); if (travelAgent.AgentId > 0) { // as the Id property has a value greater than 0, we assume // that travel agent already exists and set entity state to // be updated. context.Entry(travelAgent).State = EntityState.Modified; newParentEntity = false; } // iterate through child entities, assigning correct state. foreach (var booking in travelAgent.Bookings) { if (booking.BookingId > 0) // assume booking already exists if ID is greater than zero. // set entity to be updated. context.Entry(booking).State = EntityState.Modified; } context.SaveChanges(); HttpResponseMessage response; // set Http Status code based on operation type response = Request.CreateResponse(newParentEntity ? HttpStatusCode.Created : HttpStatusCode.OK, travelAgent); return response; } } [HttpDelete] public HttpResponseMessage Cleanup() { using (var context = new Recipe3Context()) { context.Database.ExecuteSqlCommand("delete from [bookings]"); context.Database.ExecuteSqlCommand("delete from [travelagents]"); } return Request.CreateResponse(HttpStatusCode.OK); } }
- در ویژوال استودیو پروژه جدیدی از نوع Console application بسازید و نام آن را به Recipe3.Client تغییر دهید.
- فایل program.cs را باز کنید و کد آن را مطابق لیست زیر بروز رسانی کنید.
internal class Program { private HttpClient _client; private TravelAgent _agent1, _agent2; private Booking _booking1, _booking2, _booking3; private HttpResponseMessage _response; private static void Main() { Task t = Run(); t.Wait(); Console.WriteLine("\nPress <enter> to continue..."); Console.ReadLine(); } private static async Task Run() { var program = new Program(); program.ServiceSetup(); // do not proceed until clean-up is completed await program.CleanupAsync(); program.CreateFirstAgent(); // do not proceed until agent is created await program.AddAgentAsync(); program.CreateSecondAgent(); // do not proceed until agent is created await program.AddSecondAgentAsync(); program.ModifyAgent(); // do not proceed until agent is updated await program.UpdateAgentAsync(); // do not proceed until agents are fetched await program.FetchAgentsAsync(); } private void ServiceSetup() { // set up infrastructure for Web API call _client = new HttpClient {BaseAddress = new Uri("http://localhost:6687/")}; // add Accept Header to request Web API content negotiation to return resource in JSON format _client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); } private async Task CleanupAsync() { // call cleanup method in service _response = await _client.DeleteAsync("api/travelagent/cleanup/"); } private void CreateFirstAgent() { // create new Travel Agent and booking _agent1 = new TravelAgent {Name = "John Tate"}; _booking1 = new Booking { Customer = "Karen Stevens", Paid = false, BookingDate = DateTime.Parse("2/2/2010") }; _booking2 = new Booking { Customer = "Dolly Parton", Paid = true, BookingDate = DateTime.Parse("3/10/2010") }; _agent1.Bookings.Add(_booking1); _agent1.Bookings.Add(_booking2); } private async Task AddAgentAsync() { // call generic update method in Web API service to add agent and bookings _response = await _client.PostAsync("api/travelagent/update/", _agent1, new JsonMediaTypeFormatter()); if (_response.IsSuccessStatusCode) { // capture newly created travel agent from service, which will include // database-generated Ids for each entity _agent1 = await _response.Content.ReadAsAsync<TravelAgent>(); _booking1 = _agent1.Bookings.FirstOrDefault(x => x.Customer == "Karen Stevens"); _booking2 = _agent1.Bookings.FirstOrDefault(x => x.Customer == "Dolly Parton"); Console.WriteLine("Successfully created Travel Agent {0} and {1} Booking(s)", _agent1.Name, _agent1.Bookings.Count); } else Console.WriteLine("{0} ({1})", (int) _response.StatusCode, _response.ReasonPhrase); } private void CreateSecondAgent() { // add new agent and booking _agent2 = new TravelAgent {Name = "Perry Como"}; _booking3 = new Booking { Customer = "Loretta Lynn", Paid = true, BookingDate = DateTime.Parse("3/15/2010")}; _agent2.Bookings.Add(_booking3); } private async Task AddSecondAgentAsync() { // call generic update method in Web API service to add agent and booking _response = await _client.PostAsync("api/travelagent/update/", _agent2, new JsonMediaTypeFormatter()); if (_response.IsSuccessStatusCode) { // capture newly created travel agent from service _agent2 = await _response.Content.ReadAsAsync<TravelAgent>(); _booking3 = _agent2.Bookings.FirstOrDefault(x => x.Customer == "Loretta Lynn"); Console.WriteLine("Successfully created Travel Agent {0} and {1} Booking(s)", _agent2.Name, _agent2.Bookings.Count); } else Console.WriteLine("{0} ({1})", (int) _response.StatusCode, _response.ReasonPhrase); } private void ModifyAgent() { // modify agent 2 by changing agent name and assigning booking 1 to him from agent 1 _agent2.Name = "Perry Como, Jr."; _agent2.Bookings.Add(_booking1); } private async Task UpdateAgentAsync() { // call generic update method in Web API service to update agent 2 _response = await _client.PostAsync("api/travelagent/update/", _agent2, new JsonMediaTypeFormatter()); if (_response.IsSuccessStatusCode) { // capture newly created travel agent from service, which will include Ids _agent1 = _response.Content.ReadAsAsync<TravelAgent>().Result; Console.WriteLine("Successfully updated Travel Agent {0} and {1} Booking(s)", _agent1.Name, _agent1.Bookings.Count); } else Console.WriteLine("{0} ({1})", (int) _response.StatusCode, _response.ReasonPhrase); } private async Task FetchAgentsAsync() { // call Get method on service to fetch all Travel Agents and Bookings _response = _client.GetAsync("api/travelagent/retrieve").Result; if (_response.IsSuccessStatusCode) { // capture newly created travel agent from service, which will include Ids var agents = await _response.Content.ReadAsAsync<IEnumerable<TravelAgent>>(); foreach (var agent in agents) { Console.WriteLine("Travel Agent {0} has {1} Booking(s)", agent.Name, agent.Bookings.Count()); } } else Console.WriteLine("{0} ({1})", (int) _response.StatusCode, _response.ReasonPhrase); } }
- در آخر کلاسهای TravelAgent و Booking را به پروژه کلاینت اضافه کنید. اینگونه کدها بهتر است در لایه مجزایی قرار گیرند و بین پروژهها به اشتراک گذاشته شوند.
اگر اپلیکیشن کنسول (کلاینت) را اجرا کنید با خروجی زیر مواجه خواهید شد.
(Successfully created Travel Agent John Tate and 2 Booking(s
(Successfully created Travel Agent Perry Como and 1 Booking(s
(Successfully updated Travel Agent Perry Como, Jr. and 2 Booking(s
(Travel Agent John Tate has 1 Booking(s
(Travel Agent Perry Como, Jr. has 2 Booking(s
شرح مثال جاری
با اجرای اپلیکیشن Web API شروع کنید. این اپلیکیشن یک کنترلر MVC Web Controller دارد که پس از اجرا شما را به صفحه خانه هدایت میکند. در این مرحله سایت در حال اجرا است و سرویسها قابل دسترسی هستند.
سپس اپلیکیشن کنسول را باز کنید، روی خط اول کد فایل program.cs یک breakpoint قرار دهید و آن را اجرا کنید. ابتدا آدرس سرویس Web API را نگاشت میکنیم و با تنظیم مقدار خاصیت Accept Header از سرویس درخواست میکنیم که اطلاعات را با فرمت JSON بازگرداند.
بعد از آن با استفاده از آبجکت HttpClient متد DeleteAsync را فراخوانی میکنیم که روی کنترلر TravelAgent تعریف شده است. این متد تمام دادههای پیشین را حذف میکند.
در قدم بعدی سه آبجکت جدید میسازیم: یک آژانس مسافرتی و دو رزرواسیون. سپس این آبجکتها را با فراخوانی متد PostAsync روی آبجکت HttpClient به سرویس ارسال میکنیم. اگر به متد Update در کنترلر TravelAgent یک breakpoint اضافه کنید، خواهید دید که این متد آبجکت آژانس مسافرتی را بعنوان یک پارامتر دریافت میکند و آن را به موجودیت TravelAgents در Context جاری اضافه مینماید. این کار آبجکت آژانس مسافرتی و تمام آبجکتهای فرزند آن را در حالت Added اضافه میکند و باعث میشود که context جاری شروع به ردیابی (tracking) آنها کند.
نکته: قابل ذکر است که اگر موجودیتهای متعددی با مقداری یکسان در خاصیت کلید اصلی (Primary-key value) دارید باید مجموعه آبجکتهای خود را Add کنید و نه Attach. در مثال جاری چند آبجکت Booking داریم که مقدار کلید اصلی آنها صفر است (Bookings with Id = 0). اگر از Attach استفاده کنید EF پیغام خطایی صادر میکند چرا که چند موجودیت با مقادیر کلید اصلی یکسان به context جاری اضافه کرده اید.
بعد از آن بر اساس مقدار خاصیت Id مشخص میکنیم که موجودیتها باید بروز رسانی شوند یا خیر. اگر مقدار این فیلد بزرگتر از صفر باشد، فرض بر این است که این موجودیت در دیتابیس وجود دارد بنابراین خاصیت EntityState را به Modified تغییر میدهیم. علاوه بر این فیلدی هم با نام newParentEntity تعریف کرده ایم که توسط آن بتوانیم کد وضعیت مناسبی به کلاینت بازگردانیم. در صورتی که مقدار فیلد Id در موجودیت TravelAgent برابر با یک باشد، مقدار خاصیت EntityState را به همان Added رها میکنیم.
سپس تمام آبجکتهای فرزند آژانس مسافرتی (رزرواسیون ها) را بررسی میکنیم و همین منطق را روی آنها اعمال میکنیم. یعنی در صورتی که مقدار فیلد Id آنها بزرگتر از 0 باشد وضعیت EntityState را به Modified تغییر میدهیم. در نهایت متد SaveChanges را فراخوانی میکنیم. در این مرحله برای موجودیتهای جدید اسکریپتهای Insert و برای موجودیتهای تغییر کرده اسکریپتهای Update تولید میشود. سپس کد وضعیت مناسب را به کلاینت بر میگردانیم. برای موجودیتهای اضافه شده کد وضعیت 201 (Created) و برای موجودیتهای بروز رسانی شده کد وضعیت 200 (OK) باز میگردد. کد 201 به کلاینت اطلاع میدهد که رکورد جدید با موفقیت ثبت شده است، و کد 200 از بروز رسانی موفقیت آمیز خبر میدهد. هنگام تولید سرویسهای REST-based بهتر است همیشه کد وضعیت مناسبی تولید کنید.
پس از این مراحل، آژانس مسافرتی و رزرواسیون جدیدی میسازیم و آنها را به سرویس ارسال میکنیم. سپس نام آژانس مسافرتی دوم را تغییر میدهیم، و یکی از رزرواسیونها را از آژانس اولی به آژانس دومی منتقل میکنیم. اینبار هنگام فراخوانی متد Update تمام موجودیتها شناسه ای بزرگتر از 1 دارند، بنابراین وضعیت EntityState آنها را به Modified تغییر میدهیم تا هنگام ثبت تغییرات دستورات بروز رسانی مناسب تولید و اجرا شوند.
در آخر کلاینت ما متد Retreive را روی سرویس فراخوانی میکند. این فراخوانی با کمک متد GetAsync انجام میشود که روی آبجکت HttpClient تعریف شده است. فراخوانی این متد تمام آژانسهای مسافرتی بهمراه رزرواسیونهای متناظرشان را دریافت میکند. در اینجا با استفاده از متد Include تمام رکوردهای فرزند را بهمراه تمام خاصیت هایشان (properties) بارگذاری میکنیم.
دقت کنید که مرتب کننده JSON تمام خواص عمومی (public properties) را باز میگرداند، حتی اگر در کد خود تعداد مشخصی از آنها را انتخاب کرده باشید.
نکته دیگر آنکه در مثال جاری از قراردادهای توکار Web API برای نگاشت درخواستهای HTTP به اکشن متدها استفاده نکرده ایم. مثلا بصورت پیش فرض درخواستهای POST به متدهایی نگاشت میشوند که نام آنها با "Post" شروع میشود. در مثال جاری قواعد مسیریابی را تغییر داده ایم و رویکرد مسیریابی RPC-based را در پیش گرفته ایم. در اپلیکیشنهای واقعی بهتر است از قواعد پیش فرض استفاده کنید چرا که هدف Web API ارائه سرویسهای REST-based است. بنابراین بعنوان یک قاعده کلی بهتر است متدهای سرویس شما به درخواستهای متناظر HTTP نگاشت شوند. و در آخر آنکه بهتر است لایه مجزایی برای میزبانی کدهای دسترسی داده ایجاد کنید و آنها را از سرویس Web API تفکیک نمایید.
ساختار مورد نیاز یک Kendo UI Tree View
فرض کنید قصد دارید نظرات تو در توی مطلبی را توسط Kendo UI Tree View نمایش دهید. مدل خود ارجاع دهندهی آن میتواند چنین شکلی را داشته باشد:
namespace KendoUI11.Models { public class BlogComment { public int Id { set; get; } public string Body { set; get; } public int? ParentId { get; set; } // مخصوص کندو یو آی هستند public bool HasChildren { get; set; } public string imageUrl { get; set; } } }
چند خاصیت بعدی مانند HasChildren و imageUrl مخصوص Kendo UI هستند. از imageUrl اختیاری میتوان جهت نمایش آیکنی در کنار یک آیتم استفاده کرد و HasChildren به این معنا است که آیا گره جاری دارای عناصر فرزندی میباشد یا خیر.
تهیه یک منبع داده نمونه
شکل ابتدای مطلب، از طریق منبع داده ذیل تهیه شدهاست:
using System.Collections.Generic; namespace KendoUI11.Models { /// <summary> /// منبع داده فرضی جهت سهولت دموی برنامه /// </summary> public static class BlogCommentsDataSource { private static readonly IList<BlogComment> _cachedItems; static BlogCommentsDataSource() { _cachedItems = createBlogCommentsDataSource(); } public static IList<BlogComment> LatestComments { get { return _cachedItems; } } /// <summary> /// هدف صرفا تهیه یک منبع داده آزمایشی ساده تشکیل شده در حافظه است /// </summary> private static IList<BlogComment> createBlogCommentsDataSource() { var list = new List<BlogComment>(); var comment1 = new BlogComment { Id = 1, Body = "نظر من این است که", HasChildren = true, ParentId = null }; list.Add(comment1); var comment12 = new BlogComment { Id = 2, Body = "پاسخی به نظر اول", HasChildren = true, ParentId = 1 }; list.Add(comment12); var comment12A = new BlogComment { Id = 3, Body = "پاسخی دیگری به نظر اول", HasChildren = false, ParentId = 1 }; list.Add(comment12A); var comment121 = new BlogComment { Id = 4, Body = "پاسخی به پاسخ به نظر اول", HasChildren = false, ParentId = 2 }; list.Add(comment121); var comment2 = new BlogComment { Id = 5, Body = "نظر 2", HasChildren = true, ParentId = null, imageUrl= "images/search.png" }; list.Add(comment2); var comment21 = new BlogComment { Id = 6, Body = "پاسخ به نظر 2", HasChildren = false, ParentId = 5 }; list.Add(comment21); return list; } } }
در این لیست دو رکورد، دارای ParentId مساوی null هستند. از این null بودنها جهت کوئری گرفتن و نمایش ریشههای TreeView در ادامه استفاده خواهیم کرد.
بازگشت نظرات با فرمت JSON به سمت کلاینت
در ادامه یک کنترلر ASP.NET MVC را مشاهده میکنید که توسط اکشن متد GetBlogComments، رکوردهای مورد نظر را با فرمت JSON به سمت کلاینت ارسال میکند:
using System.Linq; using System.Web.Mvc; using KendoUI11.Models; namespace KendoUI11.Controllers { public class HomeController : Controller { public ActionResult Index() { return View(); // shows the page. } [HttpGet] public ActionResult GetBlogComments(int? id) { if (id == null) { //دریافت ریشهها return Json( BlogCommentsDataSource.LatestComments .Where(x => x.ParentId == null) // ریشهها .ToList(), JsonRequestBehavior.AllowGet); } else { //دریافت فرزندهای یک ریشه return Json( BlogCommentsDataSource.LatestComments .Where(x => x.ParentId == id) .ToList(), JsonRequestBehavior.AllowGet); } } } }
اگر مقدار id به سمت سرور ارسال شود، یعنی کاربر گره و نودی را گشودهاست. بر این اساس، تمامی فرزندان این گره را یافته و بازگشت میدهیم.
کدهای سمت کاربر نمایش Kendo UI Tree View
برای کار با Kendo UI TreeView نیاز است از منبع داده خاصی به نام HierarchicalDataSource به نحو ذیل استفاده کنیم. در قسمت transport آن مشخص میکنیم که اطلاعات باید از چه آدرسی خوانده شوند که در اینجا به آدرس اکشن متد GetBlogComments اشاره میکند.
همچنین نیاز است مشخص کنیم کدامیک از خواص مدل بازگردانده شده، همان hasChildren است که در مثال فوق دقیقا به همین نام نیز تنظیم شدهاست.
<!--نحوهی راست به چپ سازی --> <div class="k-rtl k-header demo-section"> <div id="my-treeview"></div> </div> @section JavaScript { <script type="text/javascript"> $(function () { var dataSource = new kendo.data.HierarchicalDataSource({ transport: { read: { url: "@Url.Action("GetBlogComments", "Home")", dataType: "json", contentType: 'application/json; charset=utf-8', type: 'GET' } }, schema: { model: { id: "Id", hasChildren: "HasChildren" } } }); $("#my-treeview").kendoTreeView({ //استفاده از قالب در صورت نیاز template: kendo.template($("#treeview-template").html()), checkboxes: { checkChildren: false }, dataSource: dataSource, dataTextField: "Body", //رخدادها select: function (e) { console.log("Selecting: " + this.text(e.node)); }, check: function (e) { console.log("Checkbox changed :: " + this.text(e.node)); }, change: function (e) { console.log("Selection changed"); }, collapse: function (e) { console.log("Collapsing " + this.text(e.node)); }, expand: function (e) { console.log("Expanding " + this.text(e.node)); } }); }); </script> <script id="treeview-template" type="text/kendo-ui-template"> <strong> #: item.Body # </strong> </script> <style scoped> .demo-section { width: 100%; height: 300px; } </style> }
- در ابتدا به ازای هر ردیف این TreeView، از یک قالب استفاده شدهاست. تعریف این مورد اختیاری است. اگر نیاز به سفارشی سازی نحوهی نمایش هر آیتم را داشتید، میتوان از قالبها استفاده کرد.
- قسمت checkboxes مشخص میکند که آیا نیاز است در کنار هر آیتم یک checkbox نیز نمایش داده شود یا خیر.
- dataSource را به HierarchicalDataSource تنظیم کردهایم.
- dataTextField مشخص میکند که کدام فیلد دربرگیرندهی متن هر آیتم TreeView است.
- تعدادی رخداد منتسب به TreeView نیز تنظیم شدهاند که خروجی آنها را در console تصویر ابتدای بحث مشاهده میکنید.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید.
var ctx = new Entities(); var Fields = ctx.ENTITIES_FEILDS.ToList(); var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly( name: new AssemblyName("Demo"), access: AssemblyBuilderAccess.Run); var moduleBuilder = assemblyBuilder.DefineDynamicModule(name: "Module"); var typeBuilder = moduleBuilder.DefineType(name: Fields.First(c => c.FEILD_ID == 1).ENTITIES.ENTITY_NAME, attr: TypeAttributes.Public); foreach (var item in Fields) { switch (item.FEILD_TYPE) { case 0://int { var intField = typeBuilder.DefineField(fieldName: string.Format("_{0}", item.FEILD_NAME), type: typeof(string), attributes: FieldAttributes.Private); var intProperty = typeBuilder.DefineProperty( name: item.FEILD_NAME, attributes: PropertyAttributes.HasDefault, returnType: typeof(string), parameterTypes: null); // خاصیت پارامتر ورودی ندارد //تعریف گت var intpropertyGetMethod = typeBuilder.DefineMethod( name: string.Format("get_{0}", item.FEILD_NAME), attributes: MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, returnType: typeof(string), parameterTypes: Type.EmptyTypes); // اتصال گت متد به خاصیت عددی intProperty.SetGetMethod(intpropertyGetMethod); //تعریف ست var propertySetMethod = typeBuilder.DefineMethod(name: string.Format("set_{0}", item.FEILD_NAME), attributes: MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, returnType: typeof(void), parameterTypes: new[] { typeof(string) }); //اتصال ست متد intProperty.SetSetMethod(propertySetMethod); // بدنه گت متد در اینجا تعریف خواهد شد var propertyGetMethodIL = intpropertyGetMethod.GetILGenerator(); propertyGetMethodIL.Emit(OpCodes.Ldarg_0); // بارگذاری اشارهگری به وهلهای از کلاس جاری در پشته propertyGetMethodIL.Emit(OpCodes.Ldfld, intField); // بارگذاری فیلد نام propertyGetMethodIL.Emit(OpCodes.Ret); //بدنه ست متد در اینجا تعریف شده است var propertySetIL = propertySetMethod.GetILGenerator(); propertySetIL.Emit(OpCodes.Ldarg_0); propertySetIL.Emit(OpCodes.Ldarg_1); propertySetIL.Emit(OpCodes.Stfld, intField); propertySetIL.Emit(OpCodes.Ret); } break; case 1://string { } break; } } var t = typeBuilder.CreateType(); var instance = Activator.CreateInstance(t); var type = instance.GetType(); //تغییر مقدار یک خاصیت var setNameMethod = type.GetMethod("set_CoOrder"); setNameMethod.Invoke(obj: instance, parameters: new[] {"1"}); // دسترسی به خاصیت نام var nProperty = t.GetProperty("CoOrder"); // و دریافت مقدار آن برای نمایش var result = nProperty.GetValue(instance, null); Console.WriteLine(result); Console.ReadKey();
مدتها از کلاس DelegateCommand معرفی شده در این آدرس استفاده میکردم. این کلاس یک مشکل جزئی دارد و آن هم عدم بررسی مجدد قسمت canExecute به صورت خودکار هست.
خلاصهای برای کسانی که بار اول هست با این مباحث برخورد میکنند؛ یا MVVM به زبان بسیار ساده:
در برنامه نویسی متداول سیستم مایکروسافتی، در هر سیستمی که ایجاد کرده و در هر فناوری که ارائه داده از زمان VB6 تا امروز، شما روی یک دکمه مثلا دوبار کلیک میکنید و در فایل اصطلاحا code behind این فرم و در روال رخدادگردان آن شروع به کد نویسی خواهید کرد. این مورد تقریبا در همه جا صادق است؛ از WinForms تا WPF تا Silverlight تا حتی ASP.NET Webforms . به عمد هم این طراحی صورت گرفته تا برنامه نویسها در این محیطها زیاد احساس غریبی نکنند. اما این روش یک مشکل مهم دارد و آن هم «توهم» جداسازی رابط کاربر از کدهای برنامه است. به ظاهر یک فایل فرم وجود دارد و یک فایل جدای code behind ؛ اما در عمل هر دوی اینها یک partial class یا به عبارتی «یک کلاس» بیشتر نیستند. «فکر میکنیم» که از هم جدا شدند اما واقعا یکی هستند. شما در code behind صفحه به صورت مستقیم با عناصر رابط کاربری سروکار دارید و کدهای شما به این عناصر گره خوردهاند.
شاید بپرسید که چه اهمیتی دارد؟
مشکل اول: امکان نوشتن آزمونها واحد برای این متدها وجود ندارد یا بسیار سخت است. این متدها فقط با وجود فرم و رابط کاربری متناظر با آنها هست که معنا پیدا میکنند و تک تک عناصر آنها وهله سازی میشوند.
مشکل دوم: کد نوشته فقط برای همین فرم جاری آن قابل استفاده است؛ چون به صورت صریح به عناصر موجود در فرم اشاره میکند. نمیتونید این فایل code behind رو بردارید بدون هیچ تغییری برای فرم دیگری استفاده کنید.
مشکل سوم: نمیتونید طراحی فرم رو بدید به یک نفر، کد نویسی اون رو به شخصی دیگر. چون ایندو لازم و ملزوم یکدیگرند.
این سیستم کد نویسی دهه 90 است.
چند سالی است که طراحان سعی کردهاند این سیستم رو دور بزنند و روشهایی رو ارائه بدن که در آنها فرمهای برنامه و فایلهای پیاده سازی کنندهی منطق آن هیچگونه ارتباط مستقیمی باهم نداشته باشند؛ به هم گره نخورده باشند؛ ارجاعی به هیچیک از عناصر بصری فرم را در خود نداشته باشند. به همین دلیل ASP.NET MVC به وجود آمده و در همان سالها مثلا MVVM .
سؤال:
الان که رابط کاربری از فایل پیاده سازی کننده منطق آن جدا شده و دیگر Code behind هم نیست (همان partial class های متداول)، این فایلها چطور متوجه میشوند که مثلا روی یک فرم، شیءایی قرار گرفته؟ از کجا متوجه خواهند شد که روی دکمهای کلیک شده؟ اینها که ارجاعی از فرم را در درون خود ندارند.
در الگوی MVVM این سیم کشی توسط امکانات قوی Binding موجود در WPF میسر میشود. در ASP.NET MVC چیزی شبیه به آن به نام Model binder و همان مکانیزمهای استاندارد HTTP این کار رو میکنه. در MVVM شما بجای code behind خواهید داشت ViewModel (اسم جدید آن). در ASP.NET MVC این اسم شده Controller. بنابراین اگر این اسامی رو شنیدید زیاد تعجب نکنید. اینها همان Code behind قدیمی هستند اما ... بدون داشتن ارجاعی از رابط کاربری در خود که ... اطلاعات موجود در فرم به نحوی به آنها Bind و ارسال میشوند.
این سیم کشیها هم نامرئی هستند. یعنی فایل ViewModel یا فایل Controller نمیدونند که دقیقا از چه کنترلی در چه فرمی این اطلاعات دریافت شده.
این ایده هم جدید نیست. شاید بد نباشه به دوران طلایی Win32 برگردیم. همان توابع معروف PostMessage و SendMessage را به خاطر دارید؟ شما در یک ترد میتونید با مثلا PostMessage شیءایی رو به یک فرم که در حال گوش فرا دادن به تغییرات است ارسال کنید (این سیم کشی هم نامرئی است). بنابراین پیاده سازی این الگوها حتی در Win32 و کلیه فریم ورکهای ساخته شده بر پایه آنها مانند VCL ، VB6 ، WinForms و غیره ... «از روز اول» وجود داشته و میتونستند بعد از 10 سال نیان بگن که اون روشهای RAD ایی رو که ما پیشنهاد دادیم، میشد خیلی بهتر از همان ابتدا، طور دیگری پیاده سازی بشه.
ادامه بحث!
این سیم کشی یا اصطلاحا Binding ، در مورد رخدادها هم در WPF وجود داره و اینبار به نام Commands معرفی شدهاست. به این معنا که بجای اینکه بنویسید:
<Button Click="btnClick_Event">Last</Button>
بنویسید:
<Button Command="{Binding GoLast}">Last</Button>
حالا باید مکانیزمی وجود داشته باشه تا این پیغام رو به ViewModel برنامه برساند. اینکار با پیاده سازی اینترفیس ICommand قابل انجام است که معرفی یک کلاس عمومی از پیاده سازی آنرا در ابتدای بحث مشاهده نمودید.
در یک DelegateCommand، توسط متد منتسب به executeAction، مشخص خواهیم کرد که اگر این سیم کشی برقرار شد (که ما دقیقا نمیدانیم و نمیخواهیم که بدانیم از کجا و کدام فرم دقیقا)، لطفا این اعمال را انجام بده و توسط متد منتسب به canExecute به سیستم Binding خواهیم گفت که آیا مجاز هستی این اعمال را انجام دهی یا خیر. اگر این متد false برگرداند، مثلا دکمه یاد شده به صورت خودکار غیرفعال میشود.
اما مشکل کلاس DelegateCommand ذکر شده هم دقیقا همینجا است. این دکمه تا ابد غیرفعال خواهد ماند. در WPF کلاسی وجود دارد به نام CommandManager که حاوی متدی استاتیکی است به نام InvalidateRequerySuggested. اگر این متد به صورت دستی فراخوانی شود، یکبار دیگر کلیه متدهای منتسب به تمام canExecute های تعریف شده، به صورت خودکار اجرا میشوند و اینجا است که میتوان دکمهای را که باید مجددا بر اساس شرایط جاری تغییر وضعیت پیدا کند، فعال کرد. بنابراین فراخوانی متد InvalidateRequerySuggested یک راه حل کلی رفع نقیصهی ذکر شده است.
راه حل دومی هم برای حل این مشکل وجود دارد. میتوان از رخدادگردان CommandManager.RequerySuggested استفاده کرد. روال منتسب به این رخدادگردان هر زمانی که احساس کند تغییری در UI رخ داده، فراخوانی میشود. بنابراین پیاده سازی بهبود یافته کلاس DelegateCommand به صورت زیر خواهد بود:
using System;
using System.Windows.Input;
namespace MvvmHelpers
{
// Ref.
// - http://johnpapa.net/silverlight/5-simple-steps-to-commanding-in-silverlight/
// - http://joshsmithonwpf.wordpress.com/2008/06/17/allowing-commandmanager-to-query-your-icommand-objects/
public class DelegateCommand<T> : ICommand
{
readonly Func<T, bool> _canExecute;
bool _canExecuteCache;
readonly Action<T> _executeAction;
public DelegateCommand(Action<T> executeAction, Func<T, bool> canExecute = null)
{
if (executeAction == null)
throw new ArgumentNullException("executeAction");
_executeAction = executeAction;
_canExecute = canExecute;
}
public event EventHandler CanExecuteChanged
{
add { if (_canExecute != null) CommandManager.RequerySuggested += value; }
remove { if (_canExecute != null) CommandManager.RequerySuggested -= value; }
}
public bool CanExecute(object parameter)
{
return _canExecute == null ? true : _canExecute((T)parameter);
}
public void Execute(object parameter)
{
_executeAction((T)parameter);
}
}
}
استفاده از آن هم در ViewModel ساده است. یکبار خاصیتی به این نام تعریف میشود. سپس در سازنده کلاس مقدار دهی شده و متدهای متناظر آن تعریف خواهند شد:
public DelegateCommand<string> GoLast { set; get; }
//in ctor
GoLast = new DelegateCommand<string>(goLast, canGoLast);
private bool canGoLast(string data)
{
//ex.
return ListViewGuiData.CurrentPage != ListViewGuiData.TotalPage - 1;
}
private void goLast(string data)
{
//do something
}
مزیت کلاس DelegateCommand جدید هم این است که مثلا متد canGoLast فوق، به صورت خودکار با به روز رسانی UI ، فراخوانی و تعیین اعتبار مجدد میشود.
بررسی متد های یک طرفه در WCF
» ابتدا یک Extension برای OperationContext تعریف میکنیم(با فرض اینکه IDatabaseContext نماینده کلاس DbContext پروژه است):
private class OperationContainerExtension : IExtension<OperationContext> { public OperationContainerExtension( IDatabaseContext dbContext, string contextKey ) { this.CurrentDbContext = dbContext; this.ContextKey = contextKey; } public IDatabaseContext CurrentDbContext { get; private set; } public string ContextKey { get; private set; } public void Attach( OperationContext owner ) { } public void Detach( OperationContext owner ) { } }
if ( OperationContext.Current != null ) { OperationContext.Current.Extensions.Add( new OperationContainerExtension( dbContext , CONTEXTKEY ) ); OperationContext.Current.OperationCompleted += CurrentOperationContext_OperationCompleted; OperationContext.Current.Channel.Faulted += Channel_Faulted; }
void Channel_Faulted( object sender, EventArgs e ) { IDatabaseContext dbContext = GetDbContext(); if ( dbContext != null ) { dbContext.Dispose(); GC.Collect(); } } private void CurrentOperationContext_OperationCompleted( object sender, EventArgs e ) { IDatabaseContext dbContext = GetDbContext(); if ( dbContext != null ) { dbContext.Dispose(); GC.Collect(); } }
protected override IDatabaseContext GetDbContext() { if ( OperationContext.Current != null ) { var operationContainerExtension = OperationContext.Current.Extensions.OfType<OperationContainerExtension>().FirstOrDefault( e => e.ContextKey == CONTEXTKEY ); if ( operationContainerExtension != null ) { return operationContainerExtension.CurrentDbContext; } return staticDbContext; } else return staticDbContext; }
private string CONTEXTKEY = Guid.NewGuid().ToString();