Kendo UI DataSource جهت تامین دادههای سمت کلاینت ویجتهای مختلف KendoUI طراحی شدهاست و به عنوان یک اینترفیس استاندارد قابل استفاده توسط تمام کنترلهای دادهای Kendo UI کاربرد دارد. Kendo UI DataSource امکان کار با منابع داده محلی، مانند اشیاء و آرایههای جاوا اسکریپتی و همچنین منابع تامین شده از راه دور، مانند JSON، JSONP و XML را دارد. به علاوه توسط آن میتوان اعمال ثبت، ویرایش و حذف اطلاعات، به همراه صفحه بندی، گروه بندی و مرتب سازی دادهها را کنترل کرد.
استفاده از منابع داده محلی
در ادامه مثالی را از نحوهی استفاده از یک منبع داده محلی جاوا اسکریپتی، مشاهده میکنید:
در اینجا cars آرایهای از اشیاء جاوا اسکریپتی بیانگر ساختار یک خودرو است. سپس برای معرفی آن به Kendo UI، کار با مقدار دهی خاصیت data مربوط به new kendo.data.DataSource شروع میشود.
ذکر new kendo.data.DataSource به تنهایی به معنای مقدار دهی اولیه است و در این حالت منبع داده مورد نظر، استفاده نخواهد شد. برای مثال اگر متد total آنرا جهت یافتن تعداد عناصر موجود در آن فراخوانی کنید، صفر را بازگشت میدهد. برای شروع به کار با آن، نیاز است ابتدا متد read را بر روی این منبع داده مقدار دهی شده، فراخوانی کرد.
استفاده از منابع داده راه دور
در برنامههای کاربردی، عموما نیاز است تا منبع داده را از یک وب سرور تامین کرد. در اینجا نحوهی خواندن اطلاعات JSON بازگشت داده شده از جستجوی توئیتر را مشاهده میکنید:
در قسمت transport، جزئیات تبادل اطلاعات با سرور راه دور مشخص میشود؛ برای مثال url ارائه دهندهی سرویس، dataType بیانگر نوع داده مورد انتظار و data کار مقدار دهی پارامتر مورد انتظار توسط سرویس توئیتر را انجام میدهد. در اینجا چون صرفا عملیات خواندن اطلاعات صورت میگیرد، خاصیت read مقدار دهی شدهاست.
در قسمت schema مشخص میکنیم که اطلاعات JSON بازگشت داده شده توسط توئیتر، در فیلد results آن قرار دارد.
کار با منابع داده OData
علاوه بر فرمتهای یاد شده، Kendo UI DataSource امکان کار با اطلاعاتی از نوع OData را نیز دارا است که تنظیمات ابتدایی آن به صورت ذیل است:
همانطور که ملاحظه میکنید، تنظیمات ابتدایی آن اندکی با حالت remote data پیشین متفاوت است. در اینجا ابتدا نوع دادهی بازگشتی مشخص میشود و در قسمت transport، خاصیت read آن، آدرس سرویس را دریافت میکند.
یک مثال: دریافت اطلاعات از ASP.NET Web API
یک پروژهی جدید ASP.NET را آغاز کنید. تفاوتی نمیکند که Web forms باشد یا MVC؛ از این جهت که مباحث Web API در هر دو یکسان است.
سپس یک کنترلر جدید Web API را به نام ProductsController با محتوای زیر ایجاد کنید:
در این مثال، هدف صرفا ارائه یک خروجی ساده JSON از طرف سرور است.
در ادامه نیاز است تعریف مسیریابی ذیل نیز به فایل Global.asax.cs برنامه اضافه شود تا بتوان به آدرس api/products در سایت، دسترسی یافت:
در ادامه فایلی را به نام Index.html (یا در یک View و یا یک فایل aspx دلخواه)، محتوای ذیل را اضافه کنید:
- ابتدا فایلهای اسکریپت و CSS مورد نیاز Kendo UI اضافه شدهاند.
- گرید صفحه، در محل div ایی با id مساوی report-grid تشکیل خواهد شد.
- سپس DataSource ایی که به آدرس api/products اشاره میکند، تعریف شده و در آخر productsDataSource را توسط یک kendoGrid نمایش دادهایم.
- نحوهی تعریف productsDataSource، در قسمت استفاده از منابع داده راه دور ابتدای بحث توضیح داده شد. در اینجا فقط دو خاصیت pageSize و sort نیز به آن اضافه شدهاند. این دو خاصیت بر روی نحوهی نمایش گرید نهایی تاثیر گذار هستند. pageSize تعداد رکورد هر صفحه را مشخص میکند و sort نحوهی مرتب سازی را بر اساس فیلد Id و در حالت نزولی قرار میدهد.
- در ادامه، ابتداییترین حالت کار با kendoGrid را ملاحظه میکنید.
- تنظیم dataSource و autoBind: true (حالت پیش فرض)، سبب خواهند شد تا به صورت خودکار، اطلاعات JSON از مسیر api/products خوانده شوند.
- سه خاصیت بعدی صفحه بندی و مرتب سازی خودکار ستونها را فعال میکنند.
- در آخر هم دو ستون گرید، بر اساس نامهای خواص کلاس Product تعریف شدهاند.
سورس کامل این قسمت را از اینجا میتوانید دریافت کنید:
KendoUI02.zip
استفاده از منابع داده محلی
در ادامه مثالی را از نحوهی استفاده از یک منبع داده محلی جاوا اسکریپتی، مشاهده میکنید:
<script type="text/javascript"> $(function () { var cars = [ { "Year": 2000, "Make": "Hyundai", "Model": "Elantra" }, { "Year": 2001, "Make": "Hyundai", "Model": "Sonata" }, { "Year": 2002, "Make": "Toyota", "Model": "Corolla" }, { "Year": 2003, "Make": "Toyota", "Model": "Yaris" }, { "Year": 2004, "Make": "Honda", "Model": "CRV" }, { "Year": 2005, "Make": "Honda", "Model": "Accord" }, { "Year": 2000, "Make": "Honda", "Model": "Accord" }, { "Year": 2002, "Make": "Kia", "Model": "Sedona" }, { "Year": 2004, "Make": "Fiat", "Model": "One" }, { "Year": 2005, "Make": "BMW", "Model": "M3" }, { "Year": 2008, "Make": "BMW", "Model": "X5" } ]; var carsDataSource = new kendo.data.DataSource({ data: cars }); carsDataSource.read(); alert(carsDataSource.total()); }); </script>
ذکر new kendo.data.DataSource به تنهایی به معنای مقدار دهی اولیه است و در این حالت منبع داده مورد نظر، استفاده نخواهد شد. برای مثال اگر متد total آنرا جهت یافتن تعداد عناصر موجود در آن فراخوانی کنید، صفر را بازگشت میدهد. برای شروع به کار با آن، نیاز است ابتدا متد read را بر روی این منبع داده مقدار دهی شده، فراخوانی کرد.
استفاده از منابع داده راه دور
در برنامههای کاربردی، عموما نیاز است تا منبع داده را از یک وب سرور تامین کرد. در اینجا نحوهی خواندن اطلاعات JSON بازگشت داده شده از جستجوی توئیتر را مشاهده میکنید:
<script type="text/javascript"> $(function () { var twitterDataSource = new kendo.data.DataSource({ transport: { read: { url: "http://search.twitter.com/search.json", dataType: "jsonp", contentType: 'application/json; charset=utf-8', type: 'GET', data: { q: "#kendoui" } }, schema: { data: "results" } }, error: function (e) { alert(e.errorThrown.stack); } }); }); </script>
در قسمت schema مشخص میکنیم که اطلاعات JSON بازگشت داده شده توسط توئیتر، در فیلد results آن قرار دارد.
کار با منابع داده OData
علاوه بر فرمتهای یاد شده، Kendo UI DataSource امکان کار با اطلاعاتی از نوع OData را نیز دارا است که تنظیمات ابتدایی آن به صورت ذیل است:
<script type="text/javascript"> var moviesDataSource = new kendo.data.DataSource({ type: "odata", transport: { read: "http://demos.kendoui.com/service/Northwind.svc/Orders" }, error: function (e) { alert(e.errorThrown.stack); } }); }); </script>
یک مثال: دریافت اطلاعات از ASP.NET Web API
یک پروژهی جدید ASP.NET را آغاز کنید. تفاوتی نمیکند که Web forms باشد یا MVC؛ از این جهت که مباحث Web API در هر دو یکسان است.
سپس یک کنترلر جدید Web API را به نام ProductsController با محتوای زیر ایجاد کنید:
using System.Collections.Generic; using System.Web.Http; namespace KendoUI02 { public class Product { public int Id { set; get; } public string Name { set; get; } } public class ProductsController : ApiController { public IEnumerable<Product> Get() { var products = new List<Product>(); for (var i = 1; i <= 100; i++) { products.Add(new Product { Id = i, Name = "Product " + i }); } return products; } } }
در ادامه نیاز است تعریف مسیریابی ذیل نیز به فایل Global.asax.cs برنامه اضافه شود تا بتوان به آدرس api/products در سایت، دسترسی یافت:
using System; using System.Web.Http; using System.Web.Routing; namespace KendoUI02 { public class Global : System.Web.HttpApplication { protected void Application_Start(object sender, EventArgs e) { RouteTable.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); } } }
در ادامه فایلی را به نام Index.html (یا در یک View و یا یک فایل aspx دلخواه)، محتوای ذیل را اضافه کنید:
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta charset="utf-8" /> <title>Kendo UI: Implemeting the Grid</title> <link href="styles/kendo.common.min.css" rel="stylesheet" type="text/css" /> <link href="styles/kendo.default.min.css" rel="stylesheet" type="text/css" /> <script src="js/jquery.min.js" type="text/javascript"></script> <script src="js/kendo.all.min.js" type="text/javascript"></script> </head> <body> <div id="report-grid"></div> <script type="text/javascript"> $(function () { var productsDataSource = new kendo.data.DataSource({ transport: { read: { url: "api/products", dataType: "json", contentType: 'application/json; charset=utf-8', type: 'GET' } }, error: function (e) { alert(e.errorThrown.stack); }, pageSize: 5, sort: { field: "Id", dir: "desc" } }); $("#report-grid").kendoGrid({ dataSource: productsDataSource, autoBind: true, scrollable: false, pageable: true, sortable: true, columns: [ { field: "Id", title: "#" }, { field: "Name", title: "Product" } ] }); }); </script> </body> </html>
- گرید صفحه، در محل div ایی با id مساوی report-grid تشکیل خواهد شد.
- سپس DataSource ایی که به آدرس api/products اشاره میکند، تعریف شده و در آخر productsDataSource را توسط یک kendoGrid نمایش دادهایم.
- نحوهی تعریف productsDataSource، در قسمت استفاده از منابع داده راه دور ابتدای بحث توضیح داده شد. در اینجا فقط دو خاصیت pageSize و sort نیز به آن اضافه شدهاند. این دو خاصیت بر روی نحوهی نمایش گرید نهایی تاثیر گذار هستند. pageSize تعداد رکورد هر صفحه را مشخص میکند و sort نحوهی مرتب سازی را بر اساس فیلد Id و در حالت نزولی قرار میدهد.
- در ادامه، ابتداییترین حالت کار با kendoGrid را ملاحظه میکنید.
- تنظیم dataSource و autoBind: true (حالت پیش فرض)، سبب خواهند شد تا به صورت خودکار، اطلاعات JSON از مسیر api/products خوانده شوند.
- سه خاصیت بعدی صفحه بندی و مرتب سازی خودکار ستونها را فعال میکنند.
- در آخر هم دو ستون گرید، بر اساس نامهای خواص کلاس Product تعریف شدهاند.
سورس کامل این قسمت را از اینجا میتوانید دریافت کنید:
KendoUI02.zip
در ادامه مطلب قبلی آموزش (jQuery) جی کوئری 4# به ادامه بحث میپردازیم.
در پست قبل به بررسی انتخاب عناصر بر اساس موقعیت پرداختیم، در این پست به بحث "استفاده از انتخاب کنندههای سفارشی jQuery" خواهیم پرداخت.
4-1- استفاده از انتخاب کنندههای سفارشی jQuery
در پستهای قبلی (^ و ^ ) تعدادی از انتخاب کنندههای CSS که هر کدامشان موجب قدرت و انعطاف پذیری انتخاب اشیا موجود در صفحه میشوند را بررسی کردیم. با این وجود فیلترهای انتخاب کننده قدرتمندتری وجود دارند که توانایی ما را برای انتخاب بیشتر میکنند.
به عنوان مثال اگر بخواهید از میان تمام چک باکس ها، گزینه هایی را که تیک خورده اند انتخاب نمایید، از آنجا که تلاش برای مطابقت حالتهای اولیه کنترلهای HTML را بررسی میکنیم، jQuery انتخابگر سفارشی checked: را پیشنهاد میکند، که مجموعه از عناصر را که خاصیت checked آنها فعال باشد را برای ما برمی گرداند. براس مثال انتخاب کننده input تمامی المانهای <input> را انتخاب میکند، و انتخاب کننده input:checked تمامی inputهایی را انتخاب میکند که checked هستند. انتخاب کننده سفارشی checked:یک انتخاب کننده خصوصیت CSS عمل میکند (مانند [foo=bar]). ترکیب این انتخاب کنندهها میتواند قدرت بیشتری به ما بدهد، انتخاب کننده هایی مانند radio:checked: و checkbox:checked: .
همانطور هم که قبلا بیان شد، jQuery علاوه بر پشتیبانی از انتخاب کنندههای CSS تعدادی انتخاب کننده سفارشی را نیز شامل میشود که در جدول 3-2 شرح داده شده است.
بسیاری از انتخاب کنندههای سفارشی jQuery بررسی شده برای انتخاب عناصر فرم ورود اطلاعات کاربر استفاده میشوند. این فیلترها قابلیت ادغام را دارند، برای مثال در زیر دستوری را به منظور انتخاب آن دسته از گزینههای Checkbox که تیک خورده اند و فعال هستند را مشاهده میکنید:
این فیلترها و انتخاب کنندهها کاربردهای وسیعی در صفحات اینترنتی دارند، آیا آنها حالت معکوسی نیز دارند؟
استفاده از فیلتر not:
برای آنکه نتیجه انتخاب کنندهها را معکوس کنیم میتوانیم از این فیلتر استفاده کنیم. برای مثال دستور زیر تمام عناصری را که checkBox نیستند را انتخاب میکند:
اما استفاده از این فیلتر دقت زیادی را میطلبد زیرا به سادگی ممکن است با نتیجه ای غیر منتظره مواجه شویم.
استفاده از فیلتر has:
در اینجا دیدیم که CSS انتخاب کننده قدرتمندی را ارایه کرده است که فرزندران یک عنصر را در هر سطحی که باشند (حتی اگر فرزند مستقیم هم نباشند) انتخاب میکند. برای مثال دستور زیر تمام عناصر span را که در div معرفی شده باشند را انتخاب میکند:
اما اگر بخواهیم انتخابی برعکس این انتخاب داشته باشیم، باید چه کنیم؟ برای این کار باید تمام divهایی که دارای عنصر span میباشد را انتخاب کرد. برای چنین انتخابی از فیلتر has: استفاده میکنیم. به دستور زیر توجه نمایید، این دستور تمام عناصر div را که در آنها عنصر span معرفی شده است را انتخاب میکند:
برای برخی انتخابهای پیچیده و مشکل، این فیلتر و مکانیزم بسیار کارا میباشد و به سادگی ما را به هدف دلخواه میرساند. فرض کنید میخواهیم آن خانه از جدول که دارای یک عنصر عکس خاص میباشد را پیدا کنیم. با توجه به این نکته که آن عکس از طریق مقدار src قابل تشخیص میباشد، با استفاده از فیلتر has: دستوری مانند زیر مینویسیم:
این دستور هر خانه از جدول را که این عکس در آن قرار گرفته باشد را انتخاب میکند.
همانگونه که دیدیم jQuery گزینههای بسیار متعددی را به منظور انتخاب عناصر موجود در صفحه برای ما مهیا کرده است که میتوانیم هر عنصری از صفحه را انتخاب و سپس تغییر دهیم که تغییر این عناصر در پستهای آینده بحث خواهد شد.
موفق و موید باشید.
در پست قبل به بررسی انتخاب عناصر بر اساس موقعیت پرداختیم، در این پست به بحث "استفاده از انتخاب کنندههای سفارشی jQuery" خواهیم پرداخت.
4-1- استفاده از انتخاب کنندههای سفارشی jQuery
در پستهای قبلی (^ و ^ ) تعدادی از انتخاب کنندههای CSS که هر کدامشان موجب قدرت و انعطاف پذیری انتخاب اشیا موجود در صفحه میشوند را بررسی کردیم. با این وجود فیلترهای انتخاب کننده قدرتمندتری وجود دارند که توانایی ما را برای انتخاب بیشتر میکنند.
به عنوان مثال اگر بخواهید از میان تمام چک باکس ها، گزینه هایی را که تیک خورده اند انتخاب نمایید، از آنجا که تلاش برای مطابقت حالتهای اولیه کنترلهای HTML را بررسی میکنیم، jQuery انتخابگر سفارشی checked: را پیشنهاد میکند، که مجموعه از عناصر را که خاصیت checked آنها فعال باشد را برای ما برمی گرداند. براس مثال انتخاب کننده input تمامی المانهای <input> را انتخاب میکند، و انتخاب کننده input:checked تمامی inputهایی را انتخاب میکند که checked هستند. انتخاب کننده سفارشی checked:یک انتخاب کننده خصوصیت CSS عمل میکند (مانند [foo=bar]). ترکیب این انتخاب کنندهها میتواند قدرت بیشتری به ما بدهد، انتخاب کننده هایی مانند radio:checked: و checkbox:checked: .
همانطور هم که قبلا بیان شد، jQuery علاوه بر پشتیبانی از انتخاب کنندههای CSS تعدادی انتخاب کننده سفارشی را نیز شامل میشود که در جدول 3-2 شرح داده شده است.
جدول 3-2: انتخاب کنندههای سفارشی jQuery
توضیح | انتخاب کننده |
عناصری را انتخاب میکند که تحت کنترل انیمیشن میباشند. در پستهای بعدی انیمیشنها توضیح داده میشوند. | animated: |
عناصر دکمه را انتخاب میکند، عناصری مانند (input[type=submit]، input[type=reset]، input[type=button]، یا button) | button: |
عناصر Checkbox را انتخاب میکند، مانند ([input[type=checkbox). | checkbox: |
عناصر checkboxها یا دکمههای رادیویی را انتخاب میکند که در حالت انتخاب باشند. | checked: |
عناصری ر انتخاب میکند که دارای عبارت foo باشند. | contains(foo) //c: |
عناصر در حالت disabled را انتخاب میکند. | disabled: |
عناصر در حالت enabledرا انتخاب میکند. | enabled: |
عناصر فایل را انتخاب میکند، مانند ([input[type=file). | file: |
عناصر هدر مانند h1 تا h6 را انتخاب میکند. | header: |
عناصر مخفی شده را انتهاب میکند. | hidden: |
عناصر تصویر را انتخاب میکند، مانند ([input[type=image). | image: |
عناصر فرم مانند input ، select، textarea، button را انتخاب میکند. | input: |
انتخاب کنندهها را برعکس میکند. | not(filter)//c: |
عناصری که فرزندی دارند را انتخاب میکند. | parent: |
عناصر password را انتخاب میکند، مانند ([input[type=password). | password: |
عناصر radio را انتخاب میکند، مانند ([input[type=radio). | radio: |
دکمههای reset را انتخاب میکند، مانند ([input[type=reset یا [button[type=reset). | raset: |
عناصری (عناصر option) را انتخاب میکند که در وضعیت selected قراردارند. | selected: |
دکمههای submit را انتخاب میکند، مانند ([input[type=submit یا [button[type=submit). | submit: |
عناصر text را انتخاب میکند، مانند ([input[type=text). | text: |
عناصری را که در وضعیت visibleباشند انتخاب میکند. | visible: |
:checkbox:checked:enabled
این فیلترها و انتخاب کنندهها کاربردهای وسیعی در صفحات اینترنتی دارند، آیا آنها حالت معکوسی نیز دارند؟
استفاده از فیلتر not:
برای آنکه نتیجه انتخاب کنندهها را معکوس کنیم میتوانیم از این فیلتر استفاده کنیم. برای مثال دستور زیر تمام عناصری را که checkBox نیستند را انتخاب میکند:
input:not(:checkbox)
استفاده از فیلتر has:
در اینجا دیدیم که CSS انتخاب کننده قدرتمندی را ارایه کرده است که فرزندران یک عنصر را در هر سطحی که باشند (حتی اگر فرزند مستقیم هم نباشند) انتخاب میکند. برای مثال دستور زیر تمام عناصر span را که در div معرفی شده باشند را انتخاب میکند:
div span
اما اگر بخواهیم انتخابی برعکس این انتخاب داشته باشیم، باید چه کنیم؟ برای این کار باید تمام divهایی که دارای عنصر span میباشد را انتخاب کرد. برای چنین انتخابی از فیلتر has: استفاده میکنیم. به دستور زیر توجه نمایید، این دستور تمام عناصر div را که در آنها عنصر span معرفی شده است را انتخاب میکند:
div:has(span)
برای برخی انتخابهای پیچیده و مشکل، این فیلتر و مکانیزم بسیار کارا میباشد و به سادگی ما را به هدف دلخواه میرساند. فرض کنید میخواهیم آن خانه از جدول که دارای یک عنصر عکس خاص میباشد را پیدا کنیم. با توجه به این نکته که آن عکس از طریق مقدار src قابل تشخیص میباشد، با استفاده از فیلتر has: دستوری مانند زیر مینویسیم:
$('tr:has(img[src$="foo.png"])')
این دستور هر خانه از جدول را که این عکس در آن قرار گرفته باشد را انتخاب میکند.
همانگونه که دیدیم jQuery گزینههای بسیار متعددی را به منظور انتخاب عناصر موجود در صفحه برای ما مهیا کرده است که میتوانیم هر عنصری از صفحه را انتخاب و سپس تغییر دهیم که تغییر این عناصر در پستهای آینده بحث خواهد شد.
موفق و موید باشید.
پس از تهیه ساختار اولیهی بلاگی مبتنی بر ember.js در قسمت قبل، در ادامه قصد داریم امکانات تعاملی را به آن اضافه کنیم. بنابراین کار را با تعریف کنترلرها که تعیین کنندهی رفتار برنامه هستند، ادامه میدهیم.
اضافه کردن دکمهی More info به صفحهی About و مدیریت کلیک بر روی آن
فایل Scripts\Templates\about.hbs را گشوده و سپس محتوای فعلی آن را به نحو ذیل تکمیل کنید:
در ember.js اگر قصد مدیریت عملی را که قرار است توسط کلیک بر روی المانی رخ دهد، داشته باشیم، میتوان از handlebar helper ایی به نام action استفاده کرد. سپس برای تهیه کدهای مرتبط با آن، این اکشن را باید در کنترلر متناظر با route جاری (مسیریابی about) اضافه کنیم.
به همین جهت فایل جدید Scripts\Controllers\about.js را در پوشهی کنترلرهای سمت کاربر اضافه کنید (نام آن با نام مسیریابی یکی است)؛ با این محتوا:
کنترلرها به صورت یک خاصیت جدید به شیء Application برنامه اضافه میشوند. مطابق اصول نامگذاری ember.js، نام خاصیت کنترلر با حروف بزرگ متناظر با route آن شروع میشود و به نام Controller ختم خواهد شد. به این ترتیب ember.js هرگاه قصد پردازش مسیریابی about را داشته باشد، میداند که باید از کدام شیء جهت پردازش اعمال کاربر استفاده کند.
در ادامه این خاصیت را با تهیه یک زیرکلاس از کلاس پایه Controller تهیه شده توسط ember.js مقدار دهی میکنیم. به این ترتیب به کلیه امکانات این کلاس پایه دسترسی خواهیم داشت؛ به علاوه میتوان ویژگیهای سفارشی را نیز به آن افزود. برای مثال در اینجا در قسمت actions آن، دقیقا مطابق نام اکشنی که در فایل about.hbs تعریف کردهایم، یک متد جدید اضافه شدهاست.
پس از تعریف کنترلر about.js نیاز است مدخل متناظر با آنرا به فایل index.html برنامه نیز در انتهای تعاریف موجود، اضافه کرد:
اکنون یکبار برنامه را اجرا کرده و در صفحهی about بر روی دکمهی more info کلیک کنید.
اضافه کردن دکمهی ارسال پیام خصوصی به صفحهی Contact و مدیریت کلیک بر روی آن
در ادامه به قالب فعلی Scripts\Templates\contact.hbs یک دکمه را جهت ارسال پیام خصوصی اضافه میکنیم.
سپس برای مدیریت اکشن جدید sendMessage نیاز است کنترلر آنرا نیز تعریف کنیم. با توجه به نام مسیریابی جاری، نام این کنترلر نیز contact خواهد بود. برای این منظور ابتدا فایل جدید Scripts\Controllers\contact.js را اضافه نمائید؛ با این محتوا:
همچنین مدخل متناظر با فایل contact.js نیز باید به صفحهی index.html اضافه شود:
نمایش تصویری تعاملی در صفحهی about
تا اینجا با نحوهی تعریف اکشنها در قالبها و مدیریت آنها توسط کنترلرهای متناظر آشنا شدیم. در ادامه قصد داریم با اصول binding اطلاعات در ember.js آشنا شویم. برای مثال فرض کنید میخواهیم دکمهای را در صفحهی about قرار داده و با کلیک بر روی آن، لوگوی ember.js را که به صورت یک تصویر مخفی در صفحه قرار دارد، نمایان کنیم. برای اینکار نیاز است خاصیتی را در کنترلر متناظر، تعریف کرده و سپس آنرا به template جاری bind کرد.
برای این منظور فایل Scripts\Templates\about.hbs را گشوده و تعاریف موجود آنرا به نحو ذیل تکمیل کنید:
در اینجا بر اساس مقدار خاصیت isAuthorShowing تصمیم گیری خواهد شد که آیا تصویر لوگوی ember.js نمایش داده شود یا خیر. همچنین دو اکشن نمایش و مخفی کردن تصویر نیز اضافه شدهاند که با کلیک بر روی هر کدام، سبب تغییر وضعیت خاصیت isAuthorShowing خواهیم شد.
کنترلر about (فایل Scripts\Controllers\about.js) جهت مدیریت این خاصیت جدید، به همراه دو اکشن تعریف شده، اینبار به نحو ذیل تغییر خواهد یافت:
ابتدا خاصیت isAuthorShowing به کنترلر اضافه شدهاست. از این خاصیت بار اولی که مسیر http://localhost:25918/#/about توسط کاربر درخواست میشود، استفاده خواهد شد.
سپس در دو متد showAuthor و hideAuthor که به اکشنهای دو دکمهی جدید تعریف شده در قالب about متصل خواهند شد، نحوهی تغییر مقدار خاصیت isAuthorShowing را توسط متد set ملاحظه میکنید.
این قسمت مهمترین تفاوت ember.js با jQuery است. در jQuery مستقیما المانهای صفحه در همانجا تغییر داده میشوند. در ember.js منطق مدیریت کنندهی رابط کاربری و کدهای قالب متناظر با آن از هم جدا شدهاند تا بتوان یک برنامهی بزرگ را بهتر مدیریت کرد. همچنین در اینجا مشخص است که هر قسمت و هر فایل، چه ارتباطی با سایر اجزای تعریف شده دارد و چگونه به هم متصل شدهاند و اینبار شاهد انبوهی از کدهای جاوا اسکریپتی مخلوط بین المانهای HTML صفحه نیستیم.
نمایش پیامی به کاربر پس از ارسال پیام خصوصی در صفحهی تماس با ما
قصد داریم ویژگی مشابهی را به صفحهی contact نیز اضافه کنیم. اگر کاربر بر روی دکمهی ارسال پیام کلیک کرد، پیام تشکری به همراه عددی ویژه به او نمایش خواهیم داد.
برای اینکار قالب Scripts\Templates\contact.hbs را به نحو ذیل تکمیل کنید:
در آن شرط بررسی if messageSent اضافه شدهاست؛ به همراه نمایش confirmationNumber در انتهای پیام تشکر.
برای تعریف منطق مرتبط با این خواص، به کنترلر contact واقع در فایل Scripts\Controllers\contact.js مراجعه کرده و آنرا به نحو ذیل تغییر میدهیم:
همانطور که مشاهده میکنید، مقدار اولیه خاصیت messageSent مساوی false است. بنابراین در قالب contact.hbs قسمت else شرط نمایش داده میشود. اگر کاربر پیامی را وارد کند، خاصیت confirmationNumber به یک عدد اتفاقی و خاصیت messageSent به true تنظیم خواهد شد. به این ترتیب اینبار به صورت خودکار پیام تشکر به همراه عددی اتفاقی، به کاربر نمایش داده میشود.
بنابراین به صورت خلاصه، کار کنترلر، مدیریت منطق نمایشی برنامه است و برای اینکار حداقل دو مکانیزم را ارائه میدهد: اکشنها و خواص. اکشنها بیانگر نوعی رفتار هستند؛ برای مثال نمایش یک popup و یا تغییر مقدار یک خاصیت. مقدار خواص را میتوان مستقیما در صفحه نمایش داد و یا از آنها جهت پردازش عبارات شرطی و نمایش قسمت خاصی از قالب جاری نیز میتوان کمک گرفت.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید:
EmberJS03_02.zip
اضافه کردن دکمهی More info به صفحهی About و مدیریت کلیک بر روی آن
فایل Scripts\Templates\about.hbs را گشوده و سپس محتوای فعلی آن را به نحو ذیل تکمیل کنید:
<h2>About Ember Blog</h2> <p>Bla bla bla!</p> <button class="btn btn-primary" {{action 'showRealName' }}>more info</button>
به همین جهت فایل جدید Scripts\Controllers\about.js را در پوشهی کنترلرهای سمت کاربر اضافه کنید (نام آن با نام مسیریابی یکی است)؛ با این محتوا:
Blogger.AboutController = Ember.Controller.extend({ actions: { showRealName: function () { alert("You clicked at showRealName of AboutController."); } } });
در ادامه این خاصیت را با تهیه یک زیرکلاس از کلاس پایه Controller تهیه شده توسط ember.js مقدار دهی میکنیم. به این ترتیب به کلیه امکانات این کلاس پایه دسترسی خواهیم داشت؛ به علاوه میتوان ویژگیهای سفارشی را نیز به آن افزود. برای مثال در اینجا در قسمت actions آن، دقیقا مطابق نام اکشنی که در فایل about.hbs تعریف کردهایم، یک متد جدید اضافه شدهاست.
پس از تعریف کنترلر about.js نیاز است مدخل متناظر با آنرا به فایل index.html برنامه نیز در انتهای تعاریف موجود، اضافه کرد:
<script src="Scripts/Controllers/about.js" type="text/javascript"></script>
اکنون یکبار برنامه را اجرا کرده و در صفحهی about بر روی دکمهی more info کلیک کنید.
اضافه کردن دکمهی ارسال پیام خصوصی به صفحهی Contact و مدیریت کلیک بر روی آن
در ادامه به قالب فعلی Scripts\Templates\contact.hbs یک دکمه را جهت ارسال پیام خصوصی اضافه میکنیم.
<h1>Contact</h1> <div class="row"> <div class="col-md-6"> <p> Want to get in touch? <ul> <li>{{#link-to 'phone'}}Phone{{/link-to}}</li> <li>{{#link-to 'email'}}Email{{/link-to}}</li> </ul> </p> <p> Or, click here to send a secret message: </p> <button class="btn btn-primary" {{action 'sendMessage' }}>Send message</button> </div> <div class="col-md-6"> {{outlet}} </div> </div>
Blogger.ContactController = Ember.Controller.extend({ actions: { sendMessage: function () { var message = prompt('Type your message here:'); } } });
<script src="Scripts/Controllers/contact.js" type="text/javascript"></script>
نمایش تصویری تعاملی در صفحهی about
تا اینجا با نحوهی تعریف اکشنها در قالبها و مدیریت آنها توسط کنترلرهای متناظر آشنا شدیم. در ادامه قصد داریم با اصول binding اطلاعات در ember.js آشنا شویم. برای مثال فرض کنید میخواهیم دکمهای را در صفحهی about قرار داده و با کلیک بر روی آن، لوگوی ember.js را که به صورت یک تصویر مخفی در صفحه قرار دارد، نمایان کنیم. برای اینکار نیاز است خاصیتی را در کنترلر متناظر، تعریف کرده و سپس آنرا به template جاری bind کرد.
برای این منظور فایل Scripts\Templates\about.hbs را گشوده و تعاریف موجود آنرا به نحو ذیل تکمیل کنید:
<h2>About Ember Blog</h2> <p>Bla bla bla!</p> <button class="btn btn-primary" {{action 'showRealName' }}>more info</button> {{#if isAuthorShowing}} <button class="btn btn-warning" {{action 'hideAuthor' }}>Hide Image</button> <p><img src="Content/images/ember-productivity-sm.png"></p> {{else}} <button class="btn btn-info" {{action 'showAuthor' }}>Show Image</button> {{/if}}
کنترلر about (فایل Scripts\Controllers\about.js) جهت مدیریت این خاصیت جدید، به همراه دو اکشن تعریف شده، اینبار به نحو ذیل تغییر خواهد یافت:
Blogger.AboutController = Ember.Controller.extend({ isAuthorShowing: false, actions: { showRealName: function () { alert("You clicked at showRealName of AboutController."); }, showAuthor: function () { this.set('isAuthorShowing', true); }, hideAuthor: function () { this.set('isAuthorShowing', false); } } });
سپس در دو متد showAuthor و hideAuthor که به اکشنهای دو دکمهی جدید تعریف شده در قالب about متصل خواهند شد، نحوهی تغییر مقدار خاصیت isAuthorShowing را توسط متد set ملاحظه میکنید.
این قسمت مهمترین تفاوت ember.js با jQuery است. در jQuery مستقیما المانهای صفحه در همانجا تغییر داده میشوند. در ember.js منطق مدیریت کنندهی رابط کاربری و کدهای قالب متناظر با آن از هم جدا شدهاند تا بتوان یک برنامهی بزرگ را بهتر مدیریت کرد. همچنین در اینجا مشخص است که هر قسمت و هر فایل، چه ارتباطی با سایر اجزای تعریف شده دارد و چگونه به هم متصل شدهاند و اینبار شاهد انبوهی از کدهای جاوا اسکریپتی مخلوط بین المانهای HTML صفحه نیستیم.
نمایش پیامی به کاربر پس از ارسال پیام خصوصی در صفحهی تماس با ما
قصد داریم ویژگی مشابهی را به صفحهی contact نیز اضافه کنیم. اگر کاربر بر روی دکمهی ارسال پیام کلیک کرد، پیام تشکری به همراه عددی ویژه به او نمایش خواهیم داد.
برای اینکار قالب Scripts\Templates\contact.hbs را به نحو ذیل تکمیل کنید:
<h1>Contact</h1> <div class="row"> <div class="col-md-6"> <p> Want to get in touch? <ul> <li>{{#link-to 'phone'}}Phone{{/link-to}}</li> <li>{{#link-to 'email'}}Email{{/link-to}}</li> </ul> </p> {{#if messageSent}} <p> Thank you. Your message has been sent. Your confirmation number is {{confirmationNumber}}. </p> {{else}} <p> Or, click here to send a secret message: </p> <button class="btn btn-primary" {{action 'sendMessage' }}>Send message</button> {{/if}} </div> <div class="col-md-6"> {{outlet}} </div> </div>
برای تعریف منطق مرتبط با این خواص، به کنترلر contact واقع در فایل Scripts\Controllers\contact.js مراجعه کرده و آنرا به نحو ذیل تغییر میدهیم:
Blogger.ContactController = Ember.Controller.extend({ messageSent: false, actions: { sendMessage: function () { var message = prompt('Type your message here:'); if (message) { this.set('confirmationNumber', Math.round(Math.random() * 100000)); this.set('messageSent', true); } } } });
بنابراین به صورت خلاصه، کار کنترلر، مدیریت منطق نمایشی برنامه است و برای اینکار حداقل دو مکانیزم را ارائه میدهد: اکشنها و خواص. اکشنها بیانگر نوعی رفتار هستند؛ برای مثال نمایش یک popup و یا تغییر مقدار یک خاصیت. مقدار خواص را میتوان مستقیما در صفحه نمایش داد و یا از آنها جهت پردازش عبارات شرطی و نمایش قسمت خاصی از قالب جاری نیز میتوان کمک گرفت.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید:
EmberJS03_02.zip
در این پست قصد داریم مثال قسمت قبل را توسعه داده و پیاده سازی Commandها را در آن در طی یک مثال بررسی کنیم. از این جهت دکمهای، جهت حذف آیتم انتخاب شده در دیتا گرید، به فرم BookShell اضافه مینماییم. به صورت زیر:
از طرفی نیاز به خاصیتی داریم که به آیتم جاری در دیتاگرید اشاره کند.
همان طور که در پست قبلی توضیح داده شد پیاده سازیها تعاریف ViewModel در Controller انجام میگیرد برای همین منظور باید تعریف DelegateCommand که یک پیاده سازی خاص از ICommand است در کنترلر انجام شود. :
تغییرات:
»خاصیتی به نام RemoveItemCommand که از نوع DelegateCommand است تعریف شده است؛
»متدی به نام Initialize اضافه شد که متدهای Execute و CanExecute برای Commandها را در این قسمت رجیستر میکنیم.
»در نهایت Command تعریف شده در کنترلر به Command مربوطه در ViewModel انتساب داده شد.
حال کافیست خاصیت SelectedItem دیتاگرید BookShell به خاصیت CurrentItem موجود در ViewModel مقید شود:
اگر پروژه را اجرا نمایید، بعد از انتخاب سطر مورد نظر و کلیک بر روی دکمه RemoveItem مورد زیر قابل مشاهده است:
<Button Content="RemoveItem" Command="{Binding RemoveItemCommand}" HorizontalAlignment="Left" VerticalAlignment="Top" Width="75"/>Command تعریف شده در Button مورد نظر به خاصیتی به نام RemoveItemCommand در BookViewModel که نوع آن ICommand است اشاره میکند. پس باید تغییرات زیر را در ViewModel اعمال کنیم:
public ICommand RemoveItemCommand { get; set; }
public Book CurrentItem { get { return currentItem; } set { if(currentItem != value) { currentItem = value; RaisePropertyChanged("CurrentItem"); } } } private Book currentItem;
همان طور که در پست قبلی توضیح داده شد پیاده سازیها تعاریف ViewModel در Controller انجام میگیرد برای همین منظور باید تعریف DelegateCommand که یک پیاده سازی خاص از ICommand است در کنترلر انجام شود. :
[Export] public class BookController { [ImportingConstructor] public BookController(BookViewModel viewModel) { ViewModelCore = viewModel; } public BookViewModel ViewModelCore { get; private set; } public DelegateCommand RemoveItemCommand { get; private set; } private void ExecuteRemoveItemCommand() { ViewModelCore.Books.Remove(ViewModelCore.CurrentItem); } private void Initialize() { RemoveItemCommand = new DelegateCommand(ExecuteRemoveItemCommand); ViewModelCore.RemoveItemCommand = RemoveItemCommand; } public void Run() { var result = new List<Book>(); result.Add(new Book { Code = 1, Title = "Book1" }); result.Add(new Book { Code = 2, Title = "Book2" }); result.Add(new Book { Code = 3, Title = "Book3" }); Initialize(); ViewModelCore.Books = new ObservableCollection<Models.Book>(result); (ViewModelCore.View as IBookView).Show(); } }
تغییرات:
»خاصیتی به نام RemoveItemCommand که از نوع DelegateCommand است تعریف شده است؛
»متدی به نام Initialize اضافه شد که متدهای Execute و CanExecute برای Commandها را در این قسمت رجیستر میکنیم.
»در نهایت Command تعریف شده در کنترلر به Command مربوطه در ViewModel انتساب داده شد.
حال کافیست خاصیت SelectedItem دیتاگرید BookShell به خاصیت CurrentItem موجود در ViewModel مقید شود:
<DataGrid ItemsSource="{Binding Books}" SelectedItem="{Binding CurrentItem ,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="400" Height="200"> <DataGrid.Columns> <DataGridTextColumn Header="Code" Binding="{Binding Code}" Width="100"></DataGridTextColumn> <DataGridTextColumn Header="Title" Binding="{Binding Title}" Width="300"></DataGridTextColumn> </DataGrid.Columns> </DataGrid>
از virtual pc که یک ویندوز اکس پی بر روی آن نصب کردهام برای اتصال به VPN استفاده میکنم. (متاسفانه آخرین نگارش vmware برای اینکار جواب نداد)
زمان اتصال به VPN کل سیستم وارد شبکه مورد نظر خواهد شد و این مورد شاید بهدلایلی برای مثال قطع اینترنت و یا اعمال پالیسیهای شبکه بر روی کامپیوتر کاری جالب نباشد (استفاده از VPN برای اتصال به یک شبکه دومین ویندوزی). اما با نصب ماشین مجازی و اجرای یک سیستم عامل دیگر به موازات سیستم عامل اصلی، کار اتصال به VPN از داخل ماشین مجازی صورت خواهد گرفت و تمام این اعمال هم از سیستم عامل مادر مجزا و ایزوله خواهند بود.
یک ویندوز اکس پی با اختصاص 200 مگ رم هم کار میکند و عملا باری را بر روی سیستم عامل مادر تحمیل نخواهد کرد. همچنین حتما از منوی action گزینه install or update virtual machine additions را انتخاب کنید تا کارآیی سیستم عامل مجازی را بهبود بخشید. حداقل فایده آن این است که اشارهگر ماوس را به سادگی میتوان از ماشین مجازی خارج کرد و هر بار نیازی به فشردن دکمه ALT سمت راست نخواهد بود!
اولین مشکلی که هنگام کار با یک ماشین مجازی خود نمایی میکند بحث انتقال فایل بین سیستم عامل مادر (ویندوزی که شما ماشین مجازی را روی آن نصب کردهاید) و ماشین مجازی است. عمومیترین راه، ایجاد یک فایل iso از فایلهای مورد نظر است و سپس انتخاب منوی CD و گزینه capture iso image . این روش بر روی vmware هم جواب میدهد (معرفی فایل iso بعنوان CD-ROM آن). خوشبختانه عمل drag & drop (از سیستم عامل مادر به ماشین مجازی) که شاید در وحله اول به ذهن نرسد اینجا بخوبی کار خواهد کرد و مشکل ساخت فایلهای iso را برطرف میکند. (البته vmware کمی پیشرفتهتر است و حتی copy و Paste را نیز پشتیبانی میکند. اما خوب، رایگان نیست!)
مشکل بعدی با ms virtual pc افزایش تدریجی حجم آن است. روز اول 2 گیگ، روز سوم 4 گیگ، هفته بعد میشود 6 گیگ! برای فشرده سازی آن میتوان به روش زیر عمل کرد:
به مسیر زیر مراجعه کنید: (اگر پیشفرضهای نصب را پذیرفتهاید)
C:\Program Files\Microsoft Virtual PC\Virtual Machine Additions
فایل Virtual Disk Precompactor.iso را از طریق منوی CD و گزینه capture iso image باز کنید. برنامهای به صورت خودکار اجرا خواهد شد که سیستم عامل مجازی را آماده فشرده سازی میکند. پس از پایان کار، سیستم عامل مجازی را خاموش کنید. سپس به منوی file گزینه virtual disk wizard مراجعه نمائید. در صفحه بعدی گزینه ویرایش یک ماشین مجازی موجود را انتخاب کرده و فایل ماشین مجازی مورد نظر را که پیشتر برای فشرده سازی آماده کردیم به آن معرفی کنید. در صفحه بعد گزینه compact it را انتخاب کرده و در ادامه میتوانید مسیر جدیدی را مشخص کنید یا انتخاب کنید که فایل نهایی فشرده شده جایگزین فایل موجود شود.
با اینکار یک ماشین مجازی 6 گیگابایتی به 3 گیگ کاهش حجم یافت که قابل توجه است.
برای استفاده از اینترنت سیستم عامل مادر در ms virtual pc میشود از منوی edit ، گزینه setting و انتخاب networking در صفحه ظاهر شده، تنظیم اولین adapter شبکه را بر روی shared networking NAT قرار داد و همه چیز به خوبی کار خواهد کرد. (البته برای استفاده از اینترنت در vmware باید روی کانکشن اینترنت خود در سیستم عامل مادر کلیک راست کرد و سپس انتخاب گزینه advanced و فعال سازی internet connection sharing بر روی کارت شبکه مجازی نصب شده آن ضروری خواهد بود)
در مطلب معرفی خواص init-only، با روش معرفی خواص immutable آشنا شدیم. نوع جدیدی که به C# 9.0 به نام record اضافه شدهاست، قسمتی از آن بر اساس همان خواص init-only کار میکند. به همین جهت مطالعهی آن مطلب، پیش از ادامهی بحث جاری، ضروری است.
چرا در C# 9.0 تا این اندازه بر روی سادگی ایجاد اشیاء Immutable تمرکز شدهاست؟
به شیءای Immutable گفته میشود که پس از وهله سازی ابتدایی آن، وضعیت آن دیگر قابل تغییر نباشد. همچنین به کلاسی Immutable گفته میشود که تمام وهلههای ساخته شدهی از آن نیز Immutable باشند. نمونهی یک چنین شیءای را از نگارش 1 دات نت در حال استفاده هستیم: رشتهها. رشتهها در دات نت غیرقابل تغییر هستند و هرگونه تغییری بر روی آنها، سبب ایجاد یک رشتهی جدید (یک شیء جدید) میشود. نوع جدید record نیز به همین صورت عمل میکند.
مزایای وجود Immutability:
- اشیاء Immutable یا غیرقابل تغییر، thread-safe هستند که در نتیجه، برنامه نویسی همزمان و موازی را بسیار ساده میکنند؛ چون چندین thread میتوانند با شیءای کار کنند که دسترسی به آن، تنها read-only است.
- اشیاء Immutable از اثرات جانبی، مانند تغییرات آنها در متدهای مختلف در امان هستند. میتوانید آنها را به هر متدی ارسال کنید و مطمئن باشید که پس از پایان کار، این شیء تغییری نکردهاست.
- کار با اشیاء Immutable، امکان بهینه سازی حافظه را میسر میکنند. برای مثال NET runtime.، هش رشتههای تعریف شدهی در برنامه را در پشت صحنه نگهداری میکند تا مطمئن شود که تخصیص حافظهی اضافی، برای رشتههای تکراری صورت نمیگیرد. نمونهی دیگر آن نمایش حرف "a" در یک ادیتور یا نمایشگر است. زمانیکه یک شیء Immutable حاوی اطلاعات حرف "a"، ایجاد شود، به سادگی میتوان این تک وهله را جهت نمایش هزاران حرف "a" مورد استفادهی مجدد قرار داد، بدون اینکه نگران مصرف حافظهی بالای برنامه باشیم.
- کار با اشیاء Immutable به باگهای کمتری ختم میشود؛ چون همواره امکان تغییر حالت درونی یک شیء، توسط قسمتهای مختلف برنامه، میتواند به باگهای ناخواستهای منتهی شوند.
- Hash listها که در جهت بهبود کارآیی برنامهها بسیار مورد استفاده قرار میگیرند، بر اساس کلیدهایی Immutable قابل تشکیل هستند.
روش تعریف نوعهای جدید record
کلاس سادهی زیر را در نظر بگیرید:
برای تبدیل آن به یک نوع جدید record فقط کافی است واژهی کلیدی class آنرا با record جایگزین کنیم (به آن nominal record هم میگویند):
نحوهی کار با آن و وهله سازی آن نیز دقیقا مانند کلاسها است:
و ... در اینجا امکان انتساب مقداری به خاصیت Name وجود دارد؛ یعنی این خاصیت به صورت پیشفرض Immutable نیست.
روش تعریف دومی نیز در اینجا میسر است (به آن positional record هم میگویند):
با اینکار، به صورت خودکار یک record جدید تشکیل میشود که به همراه خاصیت Name است؛ چیزی شبیه به record قبلی که تعریف کردیم (به همین جهت نیاز است نام آنرا شروع شدهی با حروف بزرگ درنظر بگیریم). با این تفاوت که این record، اینبار دارای سازنده است و همچنین خاصیت Name آن از نوع init-only است. در این حالت است که کل record به صورت immutable معرفی میشود؛ وگرنه روش تعریف یک خاصیت معمولی که از نوع init-only نیست (مانند مثال اول)، سبب بروز Immutability نخواهد شد.
برای کار با رکورد دومی که تعریف کردیم باید سازندهی این record را مقدار دهی کرد:
و همانطور که ملاحظه میکنید، چون خاصیت Name از نوع init-only است و در سازندهی record تعریف شده مقدار دهی شدهاست، دیگر نمیتوان آنرا مقدار دهی مجدد کرد. همچنین در اینجا امکان استفادهی از object initializers مانند new User { Name = "User 1" } نیز وجود ندارد؛ چون به همراه یک سازندهی به صورت خودکار تولید شدهاست که خاصیتی init-only را مقدار دهی کردهاست.
نوع جدید record چه اطلاعاتی را به صورت خودکار تولید میکند؟
روش دوم تعریف recordها اگر در نظر بگیریم:
و در این حالت برنامه را کامپایل کنیم، به کدهای زیر که حاصل از دیکامپایل است، میرسیم:
این خروجی به صورت خودکار تولید شدهی توسط کامپایلر، چنین نکاتی را به همراه دارد:
- recordها هنوز هم در اصل همان classهای استاندارد #C هستند (یعنی در اصل reference type هستند).
- این کلاس به همراه یک سازنده و یک خاصیت init-only است (بر اساس تعاریف ما).
- متد ToString آن بازنویسی شدهاست تا اگر آنرا بر روی شیء حاصل، فراخوانی کردیم، به صورت خودکار نمایش زیبایی را از محتوای آن ارائه دهد.
- این کلاس از نوع <IEquatable<User است که امکان مقایسهی اشیاء record را به سادگی میسر میکند. برای این منظور متدهای GetHashCode و Equals آن به صورت خودکار بازنویسی و تکمیل شدهاند (یعنی مقایسهی آن شبیه به value-type است).
- این کلاس امکان clone کردن اطلاعات جاری را مهیا میکند.
- همچنین به همراه یک متد Deconstruct هم هست که جهت انتساب خواص تعریف شدهی در آن، به یک tuple مفید است.
بنابراین یک رکورد به همراه قابلیتهایی است که سالها در زبان #C وجود داشتهاند و شاید ما به سادگی حاضر به تشکیل و تکمیل آنها نمیشدیم؛ اما اکنون کامپایلر زحمت کدنویسی خودکار آنها را متقبل میشود!
ساخت یک وهلهی جدید از یک record با clone کردن آن
اگر به کدهای حاصل از دیکامپایل فوق دقت کنید، یک قسمت جدید clone هم با syntax خاصی در آن ظاهر شدهاست:
زمانیکه یک شیء Immutable است، دیگر نمیتوان مقادیر خواص آنرا در ادامه تغییر داد. اما اگر نیاز به اینکار وجود داشت، باید چکار کنیم؟ در C# 9.0 برای ایجاد وهلهی جدید معادلی از یک record، واژهی کلیدی جدیدی را به نام with، اضافه کردهاند. برای نمونه اگر record زیر را در نظر بگیریم که دارای دو خاصیت نام و سن است:
وهله سازی متداول آن به صورت زیر خواهد بود:
اما اگر خواستیم خاصیت سن آنرا تغییر دهیم، میتوان با استفاده از واژهی کلیدی with، به صورت زیر عمل کرد:
کاری که در اصل در اینجا انجام میشود، ابتدا clone کردن شیء user1 است (یعنی دقیقا یک وهلهی جدید از user1 را با تمام اطلاعات قبلی آن در اختیار ما قرار میدهد که این وهله، ارجاعی را به شیء قبلی ندارد و از آن منقطع است). بنابراین نام user2، دقیقا همان "User 1" است که پیشتر تنظیم کردیم؛ با این تفاوت که اینبار مقدار سن آن متفاوت است. با استفاده از cloning، هنوز شیء user1 که immutable است، دست نخورده باقی ماندهاست و توسط with میتوان خواص آنرا تغییر داد و حاصل کار، یک شیء کاملا جدید است که مکان آن در حافظه، با مکان شیء user1 در حافظه، یکی نیست.
مقایسهی نوعهای record
در کدهای حاصل از دیکامپایل فوق، قسمت عمدهای از آن به تکمیل اینترفیس <IEquatable<User پرداخته شده بود. به همین جهت اکنون دو رکورد با مقادیر خواص یکسانی را ایجاد میکنیم:
سپس یکبار آنها را از طریق عملگر == و بار دیگر به کمک متد Equals، مقایسه میکنیم:
خروجی هر دو حالت، True است:
این مورد، یکی از مهمترین تفاوتهای recordها با classها هستند.
- زمانیکه عملگر == را بر روی شیء user1 و user2 اعمال میکنیم، اگر User، از نوع کلاس معمولی باشد، حاصل آن false خواهد بود؛ چون این دو، به یک مکان از حافظه اشاره نمیکنند، حتی با اینکه مقادیر خواص هر دو شیء یکی است.
- اما اگر به قطعه کد دیکامپایل شده دقت کنید، در یک رکورد که هر چند در اصل یک کلاس است، حتی عملگر == نیز بازنویسی شدهاست تا در پشت صحنه همان متد Equals را فراخوانی کند و این متد با توجه به پیاده سازی اینترفیس <IEquatable<User، اینبار دقیقا مقادیر خواص رکورد را یک به یک مقایسه کرده و نتیجهی حاصل را باز میگرداند:
این متدی است که به صورت خودکار توسط کامپایلر جهت مقایسهی مقادیر خواص رکورد جدید تعریف شده، تشکیل شدهاست. به عبارتی recordها از لحاظ مقایسه، شبیه به value objects عمل میکنند؛ هرچند در اصل یک کلاس هستند.
یک نکته: بازنویسی عملگر == در SDK نگارش rc2 فعلی رخدادهاست و در نگارشهای قبلی preview، اینگونه نبود.
امکان ارثبری در recordها
دو رکورد زیر را در نظر بگیرید که اولی به همراه Name است و نمونهی مشتق شدهی از آن، خاصیت init-only سن را نیز به همراه دارد:
در اینجا روش دیگر تعریف recordها را ملاحظه میکنید که شبیه به کلاسها است و خواص آن init-only هستند. در این حالت اگر مقایسهی زیر را انجام دهیم:
به خروجی زیر خواهیم رسید:
علت آن را هم پیشتر بررسی کردیم. تساوی رکوردها بر اساس مقایسهی مقدار تک تک خواص آنها صورت میگیرد و چون user1 به همراه سن نیست، مقایسهی این دو، false را بر میگرداند.
امکان تعریف ارثبری رکوردها به صورت زیر نیز وجود دارد و الزاما نیازی به روش تعریف کلاس مانند آنها، مانند مثال فوق نیست:
رکوردها متد ToString را بازنویسی میکنند
در مثال قبلی اگر یک ToString را بر روی اشیاء تشکیل شده فراخوانی کنیم:
به این خروجیها میرسیم:
که حاصل بازنویسی خودکار متد ToString در پشت صحنه است.
امکان استفادهی از Deconstruct در رکوردها
دو روش برای تعریف رکوردها وجود دارند؛ یکی شبیه به تعریف کلاسها است و دیگری تعریف یک سطری، که positional record نیز نامیده میشود:
فقط در حالت تعریف یک سطری positional record فوق است که خروجی خودکار نهایی تولیدی، به همراه public void Deconstruct نیز خواهد بود:
در این حالت میتوان از tuples نیز برای کار با آن استفاده کرد:
واژهی «positional» نیز دقیقا به همین قابلیت اشاره میکند که بر اساس موقعیت خواص تعریف شدهی در رکورد، امکان Deconstruct آنها به متغیرهای یک tuple وجود دارد. حالت تعریف کلاس مانند رکوردها، nominal نام دارد.
امکان استفادهی از نوعهای record در ASP.NET Core 5x
سیستم model binding در ASP.NET Core 5x، از نوعهای record نیز پشتیبانی میکند؛ یک مثال:
پرسش و پاسخ
آیا نوعهای record به صورت value type معرفی میشوند؟
پاسخ: خیر. رکوردها در اصل reference type هستند؛ اما از لحاظ مقایسه، شبیه به value types عمل میکنند.
آیا میتوان در یک کلاس، خاصیتی از نوع رکورد را تعریف کرد؟
پاسخ: بله. از این لحاظ محدودیتی وجود ندارد.
آیا میتوان در رکوردها، از struct و یا کلاسها جهت تعریف خواص استفاده کرد؟
پاسخ: بله. از این لحاظ محدودیتی وجود ندارد.
آیا میتوان از واژهی کلیدی with با کلاسها و یا structها استفاده کرد؟
پاسخ: خیر. این واژهی کلیدی در C# 9.0 مختص به رکوردها است.
آیا رکوردها به صورت پیشفرض Immutable هستند؟
پاسخ: اگر آنها را به صورت positional records تعریف کنید، بله. چون در این حالت خواص تشکیل شدهی توسط آنها از نوع init-only هستند. در غیراینصورت، میتوان خواص غیر init-only را نیز به تعریف رکوردها اضافه کرد.
چرا در C# 9.0 تا این اندازه بر روی سادگی ایجاد اشیاء Immutable تمرکز شدهاست؟
به شیءای Immutable گفته میشود که پس از وهله سازی ابتدایی آن، وضعیت آن دیگر قابل تغییر نباشد. همچنین به کلاسی Immutable گفته میشود که تمام وهلههای ساخته شدهی از آن نیز Immutable باشند. نمونهی یک چنین شیءای را از نگارش 1 دات نت در حال استفاده هستیم: رشتهها. رشتهها در دات نت غیرقابل تغییر هستند و هرگونه تغییری بر روی آنها، سبب ایجاد یک رشتهی جدید (یک شیء جدید) میشود. نوع جدید record نیز به همین صورت عمل میکند.
مزایای وجود Immutability:
- اشیاء Immutable یا غیرقابل تغییر، thread-safe هستند که در نتیجه، برنامه نویسی همزمان و موازی را بسیار ساده میکنند؛ چون چندین thread میتوانند با شیءای کار کنند که دسترسی به آن، تنها read-only است.
- اشیاء Immutable از اثرات جانبی، مانند تغییرات آنها در متدهای مختلف در امان هستند. میتوانید آنها را به هر متدی ارسال کنید و مطمئن باشید که پس از پایان کار، این شیء تغییری نکردهاست.
- کار با اشیاء Immutable، امکان بهینه سازی حافظه را میسر میکنند. برای مثال NET runtime.، هش رشتههای تعریف شدهی در برنامه را در پشت صحنه نگهداری میکند تا مطمئن شود که تخصیص حافظهی اضافی، برای رشتههای تکراری صورت نمیگیرد. نمونهی دیگر آن نمایش حرف "a" در یک ادیتور یا نمایشگر است. زمانیکه یک شیء Immutable حاوی اطلاعات حرف "a"، ایجاد شود، به سادگی میتوان این تک وهله را جهت نمایش هزاران حرف "a" مورد استفادهی مجدد قرار داد، بدون اینکه نگران مصرف حافظهی بالای برنامه باشیم.
- کار با اشیاء Immutable به باگهای کمتری ختم میشود؛ چون همواره امکان تغییر حالت درونی یک شیء، توسط قسمتهای مختلف برنامه، میتواند به باگهای ناخواستهای منتهی شوند.
- Hash listها که در جهت بهبود کارآیی برنامهها بسیار مورد استفاده قرار میگیرند، بر اساس کلیدهایی Immutable قابل تشکیل هستند.
روش تعریف نوعهای جدید record
کلاس سادهی زیر را در نظر بگیرید:
public class User { public string Name { set; get; } }
public record User { public string Name { set; get; } }
var user = new User(); user.Name = "User 1";
روش تعریف دومی نیز در اینجا میسر است (به آن positional record هم میگویند):
public record User(string Name);
برای کار با رکورد دومی که تعریف کردیم باید سازندهی این record را مقدار دهی کرد:
var user = new User("User 1"); // Error: Init-only property or indexer 'User.Name' can only be assigned // in an object initializer, or on 'this' or 'base' in an instance constructor // or an 'init' accessor. [CS9Features]csharp(CS8852) user.Name = "User 1";
نوع جدید record چه اطلاعاتی را به صورت خودکار تولید میکند؟
روش دوم تعریف recordها اگر در نظر بگیریم:
public record User(string Name);
using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Text; using CS9Features; public class User : IEquatable<User> { protected virtual Type EqualityContract { [System.Runtime.CompilerServices.NullableContext(1)] [CompilerGenerated] get { return typeof(User); } } public string Name { get; set/*init*/; } public User(string Name) { this.Name = Name; base..ctor(); } public override string ToString() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("User"); stringBuilder.Append(" { "); if (PrintMembers(stringBuilder)) { stringBuilder.Append(" "); } stringBuilder.Append("}"); return stringBuilder.ToString(); } protected virtual bool PrintMembers(StringBuilder builder) { builder.Append("Name"); builder.Append(" = "); builder.Append((object?)Name); return true; } [System.Runtime.CompilerServices.NullableContext(2)] public static bool operator !=(User? r1, User? r2) { return !(r1 == r2); } [System.Runtime.CompilerServices.NullableContext(2)] public static bool operator ==(User? r1, User? r2) { return (object)r1 == r2 || (r1?.Equals(r2) ?? false); } public override int GetHashCode() { return EqualityComparer<Type>.Default.GetHashCode(EqualityContract) * -1521134295 + EqualityComparer<string>.Default.GetHashCode(Name); } public override bool Equals(object? obj) { return Equals(obj as User); } public virtual bool Equals(User? other) { return (object)other != null && EqualityContract == other!.EqualityContract && EqualityComparer<string>.Default.Equals(Name, other!.Name); } public virtual User <Clone>$() { return new User(this); } protected User(User original) { Name = original.Name; } public void Deconstruct(out string Name) { Name = this.Name; } }
- recordها هنوز هم در اصل همان classهای استاندارد #C هستند (یعنی در اصل reference type هستند).
- این کلاس به همراه یک سازنده و یک خاصیت init-only است (بر اساس تعاریف ما).
- متد ToString آن بازنویسی شدهاست تا اگر آنرا بر روی شیء حاصل، فراخوانی کردیم، به صورت خودکار نمایش زیبایی را از محتوای آن ارائه دهد.
- این کلاس از نوع <IEquatable<User است که امکان مقایسهی اشیاء record را به سادگی میسر میکند. برای این منظور متدهای GetHashCode و Equals آن به صورت خودکار بازنویسی و تکمیل شدهاند (یعنی مقایسهی آن شبیه به value-type است).
- این کلاس امکان clone کردن اطلاعات جاری را مهیا میکند.
- همچنین به همراه یک متد Deconstruct هم هست که جهت انتساب خواص تعریف شدهی در آن، به یک tuple مفید است.
بنابراین یک رکورد به همراه قابلیتهایی است که سالها در زبان #C وجود داشتهاند و شاید ما به سادگی حاضر به تشکیل و تکمیل آنها نمیشدیم؛ اما اکنون کامپایلر زحمت کدنویسی خودکار آنها را متقبل میشود!
ساخت یک وهلهی جدید از یک record با clone کردن آن
اگر به کدهای حاصل از دیکامپایل فوق دقت کنید، یک قسمت جدید clone هم با syntax خاصی در آن ظاهر شدهاست:
public virtual User <Clone>$() { return new User(this); }
public record User(string Name, int Age);
var user1 = new User("User 1", 21);
var user2 = user1 with { Age = 31 };
مقایسهی نوعهای record
در کدهای حاصل از دیکامپایل فوق، قسمت عمدهای از آن به تکمیل اینترفیس <IEquatable<User پرداخته شده بود. به همین جهت اکنون دو رکورد با مقادیر خواص یکسانی را ایجاد میکنیم:
var user1 = new User("User 1", 21); var user2 = new User("User 1", 21);
Console.WriteLine("user1.Equals(user2) -> {0}", user1.Equals(user2)); Console.WriteLine("user1 == user2 -> {0}", user1 == user2);
user1.Equals(user2) -> True user1 == user2 -> True
- زمانیکه عملگر == را بر روی شیء user1 و user2 اعمال میکنیم، اگر User، از نوع کلاس معمولی باشد، حاصل آن false خواهد بود؛ چون این دو، به یک مکان از حافظه اشاره نمیکنند، حتی با اینکه مقادیر خواص هر دو شیء یکی است.
- اما اگر به قطعه کد دیکامپایل شده دقت کنید، در یک رکورد که هر چند در اصل یک کلاس است، حتی عملگر == نیز بازنویسی شدهاست تا در پشت صحنه همان متد Equals را فراخوانی کند و این متد با توجه به پیاده سازی اینترفیس <IEquatable<User، اینبار دقیقا مقادیر خواص رکورد را یک به یک مقایسه کرده و نتیجهی حاصل را باز میگرداند:
public virtual bool Equals(User? other) { return (object)other != null && EqualityContract == other!.EqualityContract && EqualityComparer<string>.Default.Equals(Name, other!.Name) && EqualityComparer<int>.Default.Equals(Age, other!.Age); }
یک نکته: بازنویسی عملگر == در SDK نگارش rc2 فعلی رخدادهاست و در نگارشهای قبلی preview، اینگونه نبود.
امکان ارثبری در recordها
دو رکورد زیر را در نظر بگیرید که اولی به همراه Name است و نمونهی مشتق شدهی از آن، خاصیت init-only سن را نیز به همراه دارد:
public record User { public string Name { get; init; } public User(string name) { Name = name; } } public record UserWithAge : User { public int Age { get; init; } public UserWithAge(string name, int age) : base(name) { Age = age; } }
var user1 = new User("User 1"); var user2 = new UserWithAge("User 1", 21); Console.WriteLine("user1.Equals(user2) -> {0}", user1.Equals(user2)); Console.WriteLine("user1 == user2 -> {0}", user1 == user2);
user1.Equals(user2) -> False user1 == user2 -> False
امکان تعریف ارثبری رکوردها به صورت زیر نیز وجود دارد و الزاما نیازی به روش تعریف کلاس مانند آنها، مانند مثال فوق نیست:
public abstract record Food(int Calories); public record Milk(int C, double FatPercentage) : Food(C);
رکوردها متد ToString را بازنویسی میکنند
در مثال قبلی اگر یک ToString را بر روی اشیاء تشکیل شده فراخوانی کنیم:
Console.WriteLine(user1.ToString()); Console.WriteLine(user2.ToString());
User { Name = User 1 } UserWithAge { Name = User 1, Age = 21 }
امکان استفادهی از Deconstruct در رکوردها
دو روش برای تعریف رکوردها وجود دارند؛ یکی شبیه به تعریف کلاسها است و دیگری تعریف یک سطری، که positional record نیز نامیده میشود:
public record Person(string Name, int Age);
public void Deconstruct(out string Name, out int Age) { Name = this.Name; Age = this.Age; }
var (name, age) = new Person("User 1", 21);
امکان استفادهی از نوعهای record در ASP.NET Core 5x
سیستم model binding در ASP.NET Core 5x، از نوعهای record نیز پشتیبانی میکند؛ یک مثال:
public record Person([Required] string Name, [Range(0, 150)] int Age); public class PersonController { public IActionResult Index() => View(); [HttpPost] public IActionResult Index(Person person) { // ... } }
پرسش و پاسخ
آیا نوعهای record به صورت value type معرفی میشوند؟
پاسخ: خیر. رکوردها در اصل reference type هستند؛ اما از لحاظ مقایسه، شبیه به value types عمل میکنند.
آیا میتوان در یک کلاس، خاصیتی از نوع رکورد را تعریف کرد؟
پاسخ: بله. از این لحاظ محدودیتی وجود ندارد.
آیا میتوان در رکوردها، از struct و یا کلاسها جهت تعریف خواص استفاده کرد؟
پاسخ: بله. از این لحاظ محدودیتی وجود ندارد.
آیا میتوان از واژهی کلیدی with با کلاسها و یا structها استفاده کرد؟
پاسخ: خیر. این واژهی کلیدی در C# 9.0 مختص به رکوردها است.
آیا رکوردها به صورت پیشفرض Immutable هستند؟
پاسخ: اگر آنها را به صورت positional records تعریف کنید، بله. چون در این حالت خواص تشکیل شدهی توسط آنها از نوع init-only هستند. در غیراینصورت، میتوان خواص غیر init-only را نیز به تعریف رکوردها اضافه کرد.
با سلام؛ زمانی که سایت بر روی هاست روی اینترنت قرار داره باید چه کار کرد که صفحه در حالت Https قابل مشاهده باشه و هشداری به کاربر ارسال نشه؟ آیا این تنظیمات باید در کنترل پنل هاست انجام بشه یا باید تغییرات در اپلیکشین خودمون اعمال کنیم؟
برای اینکار نیاز است تا style تعریف شده را کمی تغییر داد. به صورت زیر:
styles.LoadTagStyle(HtmlTags.BODY, HtmlTags.ALIGN, HtmlTags.ALIGN_LEFT)
styles.LoadTagStyle(HtmlTags.BODY, HtmlTags.ALIGN, HtmlTags.ALIGN_LEFT)
اشتراکها
مجموعه ای از کتابخانه های jQuery
پیش نمایش چند نمونه از کتابخانه ها