در ابتدا مثالهای زیر را در نظر بگیرید:
using System;
using System.Collections.Generic;
using System.Linq;
namespace testWinForms87
{
public class Data
{
public int id { get; set; }
public string name { get; set; }
}
class CLinqTests
{
public static int TestGetListMin1()
{
var lst = new List<Data>
{
new Data{ id=1, name="id1"},
new Data{ id=2, name="id2"},
new Data{ id=3, name="name3"}
};
return (from c in lst
where c.name.Contains("id")
select c.id).Min();
}
public static int TestGetListMin2()
{
var lst = new List<Data>();
return (from c in lst
where c.name.Contains("id")
select c.id).Min();
}
}
}
محاسبات آن کار میکند و مشکلی هم ندارد. اما همیشه در دنیای واقعی همه چیز قرار نیست به این خوبی پیش برود. ممکن است همانند متد TestGetListMin2 ، لیست ما خالی باشد (برای مثال از دیتابیس، رکوردی مطابق شرایط کوئریهای قبلی بازگشت داده نشده باشد). در این حالت هنگام فراخوانی متد Min ، استثنای Sequence contains no elements رخ خواهد داد و همانطور که در مباحث defensive programming عنوان شد، وظیفهی ما این نیست که خودرو را به دیوار کوبیده (یا منتظر شویم تا کوبیده شود) و سپس به فکر چاره بیفتیم که خوب، عجب! مشکلی رخ داده است!
اکنون چه باید کرد؟ حداقل یک مرحله بررسی اینکه آیا کوئری ما حاوی رکوردی میباشد یا خیر باید به این متد اضافه شود (به صورت زیر):
public static int TestGetListMin3()
{
var lst = new List<Data>();
var query = from c in lst
where c.name.Contains("id")
select c.id;
if (query.Any())
return query.Min();
else
return -1;
}
شبیه به این مورد در هنگام استفاده از تابع Single مربوط به LINQ نیز ممکن است رخ دهد (تولید استثنای ذکر شده) اما در اینجا مایکروسافت تابع SingleOrDefault را نیز پیش بینی کرده است. در این حالت اگر کوئری ما رکوردی را برنگرداند، SingleOrDefault مقدار نال را برگشت داده و استثنایی رخ نخواهد داد (نمونهی دیگر آن متدهای First و FirstOrDefault هستند).
در مورد متدهای Min و Max ، متدهای MinOrDefault یا MaxOrDefault در دات نت فریم ورک وجود ندارند. میتوان این نقیصه را با استفاده از extension methods برطرف کرد.
using System;
using System.Collections.Generic;
using System.Linq;
public static class LinqExtensions
{
public static T MinOrDefault<T>(this IEnumerable<T> source, T defaultValue)
{
if (source.Any<T>())
return source.Min<T>();
return defaultValue;
}
public static T MaxOrDefault<T>(this IEnumerable<T> source, T defaultValue)
{
if (source.Any<T>())
return source.Max<T>();
return defaultValue;
}
}
public static int TestGetListMin4()
{
var lst = new List<Data>();
return (from c in lst
where c.name.Contains("id")
select c.id).MinOrDefault(-1);
}
ECMAScript 5
در حالیکه ECMAScript 5 قابلیتهای فوق العادهای را برای کنترل کردن خصوصیات موجود در اشیاء، در اختیار شما قرار میدهد، اما هیچ راه کاری را برای خصوصیاتی که موجود نیستند، ندارد. شما میتوانید برای خواص موجود، از رونویسی (تنظیم writable برابر false) و یا حذف شدن (تنظیم configurable برابر false) جلوگیری کنید. شما میتوانید از اختصاص خصوصیات جدید به اشیاء با استفاده از ()Object.preventExtensions و یا تنظیم تمام خصوصیات به صورت فقط خواندنی و یا غیرقابل حذف ()Object.freeze جلوگیری کنید.
اگر شما نمیخواهید تمام خصوصیات را فقط خواندنی کنید میتوانید از ()Object.seal استفاده کنید. اینها مانع از اضافه کردن خصوصیات و یا حذف کردن خصوصیات موجود میشوند. اگر به یک شیء مهر و موم شده (sealed)، زمانی که از strict mode استفاده میکنید، یک خصوصیت جدید اضافه کنید باعث ایجاد خطا میشود:
"use strict"; var person = { name: "Vahid Mohammad Taheri" }; Object.seal(person); person.age = 27; // Error!
نجات با Proxyها
پروکسیها، دارای سابقه طولانی و پیچیده ای در ECMAScript 6 است. طرح اولیه آن توسط Firefox و Chrome قبل از تصمیم TC-39 به تغییر پروکسیها، اجرا شده است. این تغییرات، برای بهتر و روانتر شدن پروکسیها از طرح اولیه پروکسیها انجام گرفت.
بزرگترین تغییر صورت گرفته، معرفی شیء هدف که میخواست با پروکسی در تعامل باشد، بود. به جای تعریف دام برای نوعهای مختلفی از عملیات، با ایجاد پروکسی، مستقیما برای عملیاتی از قبل تعیین شده بر روی اشیاء هدف، این کار را انجام میدهیم.
این کار از طریق یک سری از روشهایی که به مخفی کردن عملیات در ECMAScript مطابقت دارند، انجام میشود. به عنوان مثال زمانیکه بر روی یک ویژگی از یک شیء، عمل خواندن انجام میشود، عملیات [[Get]]
در موتور جاوااسکریپت انجام میگیرد. نحوهی رفتار [[Get]]
را نمیتوان تغییر داد؛ با این حال، با استفاده از پروکسیها میتوان دامی برای زمان فراخوانی [[Get]]
قرار داد و عملیات خاص مورد نظر خود را اعمال کرد. به مثال زیر توجه کنید:
var proxy = new Proxy({ name: "Vahid" }, { get: function(target, property) { if (property in target) { return target[property]; } else { return 13; } } }); console.log(proxy.time); // 13 console.log(proxy.name); // "Vahid" console.log(proxy.title); // 13
[[Get]]
به دام افتاده و تابع تعریف شدهی ما اجرا میشود (باقی عملیات به صورت عادی اجرا میشوند). دامی که برای شیء مورد نظر تعریف کردهایم دو پارامتر دریافت میکند (اول شی هدف، دوم ویژگی مورد نظر). با استفاده از کد نوشته شده در آن ابتدا بررسی میشود که شیء مورد نظر دارای ویژگی ارسال شده است یا خیر؟ در صورتی که وجود داشته باشد، مقدار آن بازگشت داده میشود و در غیر اینصورت به صورت ثابت مقدار 13 برگشت داده میشود.برای ایجاد اشیاء دفاعی لازم است چگونگی رهگیری عملیات
[[Get]]
را درک کنید و هدف از این کار صدور خطا در زمان دستیابی به ویژگی ای از شیءایی که وجود ندارد است.function createDefensiveObject(target) { return new Proxy(target, { get: function(target, property) { if (property in target) { return target[property]; } else { throw new ReferenceError("Property \"" + property + "\" does not exist."); } } }); }
تابع
()createDefensiveObject
یک شیء را به عنوان هدف میپذیرد و یک شیء دفاعی برای آن ایجاد میکند. پروکسی یک دام به نام get دارد؛ برای زمانی که عمل خواندن انجام میشود. اگر ویژگی خوانده شده در شیء وجود داشت، مقدار آن برگشت داده میشود و از سوی دیگر، وقتی ویژگی خوانده شده در شیء وجود نداشته باشد، سبب بروز خطا میشود. به مثال زیر توجه کنید:var person = { name: "Vahid" }; var defensivePerson = createDefensiveObject(person); console.log(defensivePerson.name); // "Vahid" console.log(defensivePerson.age); // Error!
اشیاء دفاعی باعث میشوند تا بر روی ویژگیهایی که در شیء وجود دارند، بتوان عمل خواندن را انجام داد و در ویژگیهایی که موجود نیستند در هنگام خواندن، باعث صدور پیام خطا میشوند. با این حال هنوز هم شما میتوانید ویژگیهای جدید را بدون خطا اضافه کنید:
var person = { name: "Vahid" }; var defensivePerson = createDefensiveObject(person); console.log(defensivePerson.name); // "Vahid" defensivePerson.age = 13; console.log(defensivePerson.age); // 13
روشهای تشخیص ویژگیهای استاندارد هنوز هم به طور معمول و بدون خطا کار میکنند.
var person = { name: "Vahid" }; var defensivePerson = createDefensiveObject(person); console.log("name" in defensivePerson); // true console.log(defensivePerson.hasOwnProperty("name")); // true console.log("age" in defensivePerson); // false console.log(defensivePerson.hasOwnProperty("age")); // false
var person = { name: "Vahid" }; Object.preventExtensions(person); var defensivePerson = createDefensiveObject(person); defensivePerson.age = 27; // Error! console.log(defensivePerson.age); // Error!
defensivePerson
برای هر دو حالت خواندن و نوشتن ویژگیهایی که وجود ندارند، خطا صادر میکند.شاید مفیدترین زمان برای استفاده از اشیاء دفاعی، در هنگام تعریف یک سازنده باشد و شما میتوانید این کار را به عنوان یک قرارداد در نوشتن اشیاء حفظ کنید.
برای مثال:
function Person(name) { this.name = name; return createDefensiveObject(this); } var person = new Person("Vahid"); console.log(person.age); // Error!
()createDefensiveObject
درون سازنده، میتوانید اطمینان کامل داشته باشید که همهی نمونههای ساخته شدهی از شیء Person، دارای حالت دفاعی میباشند.معماری تمیز در asp.net core
ASP.NET Core provides better support for clean, testable, maintainable architectures than any of its predecessors. Learn the underlying principles that apply to project/solution organization, how best to arrange your projects to support clean code, and how to refactor existing solutions toward this architecture. Examples will use ASP.NET Core but the principles covered apply to all languages and platforms.
Kendo UI MVVM
- «استفاده از Kendo UI templates »
- «اعتبار سنجی ورودیهای کاربر در Kendo UI»
- «فعال سازی عملیات CRUD در Kendo UI Grid» جهت آشنایی با نحوهی تعریف DataSource ایی که میتواند اطلاعات را ثبت، حذف و یا ویرایش کند.
در این مطلب قصد داریم به یک چنین صفحهای برسیم که در آن در ابتدای نمایش، لیست ثبت نامهای موجود، از سرور دریافت و توسط یک Kendo UI template نمایش داده میشود. سپس امکان ویرایش و حذف هر ردیف، وجود خواهد داشت، به همراه امکان افزودن ردیفهای جدید. در این بین مدیریت نمایش لیست ثبت نامها توسط امکانات binding توکار فریم ورک MVVM مخصوص Kendo UI صورت خواهد گرفت. همچنین کلیه اعمال مرتبط با هر ردیف نیز توسط data binding دو طرفه مدیریت خواهد شد.
Kendo UI MVVM
الگوی MVVM یا Model-View-ViewModel که برای اولین بار جهت کاربردهای WPF و Silverlight معرفی شد، برای ساده سازی اتصال تغییرات کنترلهای برنامه به خواص ViewModel یک View کاربرد دارد. برای مثال با تغییر عنصر انتخابی یک DropDownList در یک View، بلافاصله خاصیت متصل به آن که در ViewModel برنامه تعریف شدهاست، مقدار دهی و به روز خواهد شد. هدف نهایی آن نیز جدا سازی منطق کدهای UI، از کدهای جاوا اسکریپتی سمت کاربر است. برای این منظور کتابخانههایی مانند Knockout.js به صورت اختصاصی برای این کار تهیه شدهاند؛ اما Kendo UI نیز جهت یکپارچگی هرچه تمامتر اجزای آن، دارای یک فریم ورک MVVM توکار نیز میباشد. طراحی آن نیز بسیار شبیه به Knockout.js است؛ اما با سازگاری 100 درصد با کل مجموعه.
پیاده سازی الگوی MVVM از 4 قسمت تشکیل میشود:
- Model که بیانگر خواص متناظر با اشیاء رابط کاربری است.
- View همان رابط کاربری است که به کاربر نمایش داده میشود.
- ViewModel واسطی است بین Model و View. کار آن انتقال دادهها و رویدادها از View به مدل است و در حالت binding دوطرفه، عکس آن نیز صحیح میباشد.
- Declarative data binding جهت رهایی برنامه نویسها از نوشتن کدهای هماهنگ سازی اطلاعات المانهای View و خواص ViewModel کاربرد دارد.
در ادامه این اجزا را با پیاده سازی مثالی که در ابتدای بحث مطرح شد، دنبال میکنیم.
تعریف Model و ViewModel
در سمت سرور، مدل ثبت نام برنامه چنین شکلی را دارد:
namespace KendoUI07.Models { public class Registration { public int Id { set; get; } public string UserName { set; get; } public string CourseName { set; get; } public int Credit { set; get; } public string Email { set; get; } public string Tel { set; get; } } }
<script type="text/javascript"> $(function () { var model = kendo.data.Model.define({ id: "Id", fields: { Id: { type: 'number' }, // leave this set to 0 or undefined, so Kendo knows it is new. UserName: { type: 'string' }, CourseName: { type: 'string' }, Credit: { type: 'number' }, Email: { type: 'string' }, Tel: { type: 'string' } } }); }); </script>
<script type="text/javascript"> $(function () { var viewModel = kendo.observable({ accepted: false, course: new model() }); }); </script>
اتصال ViewModel به View برنامه
تعریف فرم ثبت نام را در اینجا ملاحظه میکنید. فیلدهای مختلف آن بر اساس نکات اعتبارسنجی HTML 5 با ویژگیهای خاص آن، مزین شدهاند. جزئیات آنرا در مطلب «اعتبار سنجی ورودیهای کاربر در Kendo UI» پیشتر بررسی کردهایم.
اگر به تعریف هر فیلد دقت کنید، ویژگی data-bind جدیدی را هم ملاحظه خواهید کرد:
<div id="coursesSection" class="k-rtl k-header"> <div class="box-col"> <form id="myForm" data-role="validator" novalidate="novalidate"> <h3>ثبت نام</h3> <ul> <li> <label for="Id">Id</label> <span id="Id" data-bind="text:course.Id"></span> </li> <li> <label for="UserName">نام</label> <input type="text" id="UserName" name="UserName" class="k-textbox" data-bind="value:course.UserName" required /> </li> <li> <label for="CourseName">دوره</label> <input type="text" dir="ltr" id="CourseName" name="CourseName" required data-bind="value:course.CourseName" /> <span class="k-invalid-msg" data-for="CourseName"></span> </li> <li> <label for="Credit">مبلغ پرداختی</label> <input id="Credit" name="Credit" type="number" min="1000" max="6000" required data-max-msg="عددی بین 1000 و 6000" dir="ltr" data-bind="value:course.Credit" class="k-textbox k-input" /> <span class="k-invalid-msg" data-for="Credit"></span> </li> <li> <label for="Email">پست الکترونیک</label> <input type="email" id="Email" dir="ltr" name="Email" data-bind="value:course.Email" required class="k-textbox" /> </li> <li> <label for="Tel">تلفن</label> <input type="tel" id="Tel" name="Tel" dir="ltr" pattern="\d{8}" required class="k-textbox" data-bind="value:course.Tel" data-pattern-msg="8 رقم" /> </li> <li> <input type="checkbox" name="Accept" data-bind="checked:accepted" required /> شرایط دوره را قبول دارم. <span class="k-invalid-msg" data-for="Accept"></span> </li> <li> <button class="k-button" data-bind="enabled: accepted, click: doSave" type="submit"> ارسال </button> <button class="k-button" data-bind="click: resetModel">از نو</button> </li> </ul> <span id="doneMsg"></span> </form> </div>
<script type="text/javascript"> $(function () { var model = kendo.data.Model.define({ // ... }); var viewModel = kendo.observable({ // ... }); kendo.bind($("#coursesSection"), viewModel); }); </script>
<input type="text" id="UserName" name="UserName" class="k-textbox" data-bind="value:course.UserName" required />
بنابراین تا اینجا به صورت خلاصه، مدلی را توسط متد kendo.data.Model.define، معادل مدل سمت سرور خود ایجاد کردیم. سپس وهلهای از این مدل را به صورت یک خاصیت جدید دلخواهی در ViewModel تعریف شده توسط متد kendo.observable در معرض دید View برنامه قرار دادیم. در ادامه اتصال ViewModel و View، با فراخوانی متد kendo.bind انجام شد. اکنون برای دریافت تغییرات کنترلهای برنامه، تنها کافی است ویژگیهای data-bind ایی را به آنها اضافه کنیم.
در ناحیهی تعریف شده توسط متد kendo.bind، کلیه خواص ViewModel در دسترس هستند. برای مثال اگر به تعریف ViewModel دقت کنید، یک خاصیت دیگر به نام accepted با مقدار false نیز در آن تعریف شدهاست (این خاصیت چون صرفا کاربرد UI داشت، در model برنامه قرار نگرفت). از آن برای اتصال checkbox تعریف شده، به button ارسال اطلاعات، استفاده کردهایم:
<input type="checkbox" name="Accept" data-bind="checked:accepted" required /> <button class="k-button" data-bind="enabled: accepted, click: doSave" type="submit"> ارسال </button>
ارسال دادههای تغییر کردهی ViewModel به سرور
تا اینجا 4 جزء اصلی الگوی MVVM که در ابتدای بحث عنوان شد، تکمیل شدهاند. مدل اطلاعات فرم تعریف گردید. ViewModel ایی که این خواص را به المانهای فرم متصل میکند نیز در ادامه اضافه شدهاست. توسط ویژگیهای data-bind کار Declarative data binding انجام میشود.
در ادامه نیاز است تغییرات ViewModel را به سرور، جهت ثبت، به روز رسانی و حذف نهایی منتقل کرد.
<script type="text/javascript"> $(function () { var model = kendo.data.Model.define({ //... }); var dataSource = new kendo.data.DataSource({ type: 'json', transport: { read: { url: "api/registrations", dataType: "json", contentType: 'application/json; charset=utf-8', type: 'GET' }, create: { url: "api/registrations", contentType: 'application/json; charset=utf-8', type: "POST" }, update: { url: function (course) { return "api/registrations/" + course.Id; }, contentType: 'application/json; charset=utf-8', type: "PUT" }, destroy: { url: function (course) { return "api/registrations/" + course.Id; }, contentType: 'application/json; charset=utf-8', type: "DELETE" }, parameterMap: function (data, type) { // Convert to a JSON string. Without this step your content will be form encoded. return JSON.stringify(data); } }, schema: { model: model }, error: function (e) { alert(e.errorThrown); }, change: function (e) { // فراخوانی در زمان دریافت اطلاعات از سرور و یا تغییرات محلی viewModel.set("coursesDataSourceRows", new kendo.data.ObservableArray(this.view())); } }); var viewModel = kendo.observable({ //... }); kendo.bind($("#coursesSection"), viewModel); dataSource.read(); // دریافت لیست موجود از سرور در آغاز کار }); </script>
متصل کردن DataSource به ViewModel
تا اینجا DataSource ایی جهت کار با سرور تعریف شدهاست؛ اما مشخص نیست که اگر رکوردی اضافه شد، چگونه باید اطلاعات خودش را به روز کند. برای این منظور خواهیم داشت:
<script type="text/javascript"> $(function () { $("#coursesSection").kendoValidator({ // ... }); var model = kendo.data.Model.define({ // ... }); var dataSource = new kendo.data.DataSource({ // ... }); var viewModel = kendo.observable({ accepted: false, course: new model(), doSave: function (e) { e.preventDefault(); console.log("this", this.course); var validator = $("#coursesSection").data("kendoValidator"); if (validator.validate()) { if (this.course.Id == 0) { dataSource.add(this.course); } dataSource.sync(); // push to the server this.set("course", new model()); // reset controls } }, resetModel: function (e) { e.preventDefault(); this.set("course", new model()); } }); kendo.bind($("#coursesSection"), viewModel); dataSource.read(); // دریافت لیست موجود از سرور در آغاز کار }); </script>
در متد doSave، ابتدا بررسی میکنیم آیا اعتبارسنجی فرم با موفقیت انجام شدهاست یا خیر. اگر بله، توسط متد add منبع داده، اطلاعات فرم جاری را توسط شیء course که هم اکنون به تمامی فیلدهای آن متصل است، اضافه میکنیم. در اینجا بررسی شدهاست که آیا Id این اطلاعات صفر است یا خیر. از آنجائیکه از همین متد برای به روز رسانی نیز در ادامه استفاده خواهد شد، در حالت به روز رسانی، Id شیء ثبت شده، از طرف سرور دریافت میگردد. بنابراین غیر صفر بودن این Id به معنای عملیات به روز رسانی است و در این حالت نیازی نیست کار بیشتری را انجام داد؛ زیرا شیء متناظر با آن پیشتر به منبع داده اضافه شدهاست.
استفاده از متد add صرفا به معنای مطلع کردن منبع داده محلی از وجود رکوردی جدید است. برای ارسال این تغییرات به سرور، از متد sync آن میتوان استفاده کرد. متد sync بر اساس متد add یک درخواست POST، بر اساس شیءایی که Id غیر صفر دارد، یک درخواست PUT و با فراخوانی متد remove بر روی منبع داده، یک درخواست DELETE را به سمت سرور ارسال میکند.
متد دلخواه resetModel سبب مقدار دهی مجدد شیء course با یک وهلهی جدید از شیء model میشود. همینقدر برای پاک کردن تمامی کنترلهای صفحه کافی است.
تا اینجا دو متد جدید را در ViewModel برنامه تعریف کردهایم. در مورد نحوهی اتصال آنها به View، به کدهای دو دکمهی موجود در فرم دقت کنید:
<button class="k-button" data-bind="enabled: accepted, click: doSave" type="submit"> ارسال </button> <button class="k-button" data-bind="click: resetModel">از نو</button>
مدیریت سمت سرور ثبت، ویرایش و حذف اطلاعات
در حالت ثبت، متد Post توسط آدرس مشخص شده در قسمت create منبع داده، فراخوانی میگردد. نکتهی مهمی که در اینجا باید به آن دقت داشت، نحوهی بازگشت Id رکورد جدید ثبت شدهاست. اگر این تنظیم صورت نگیرد، Id رکورد جدید را در لیست، مساوی صفر مشاهده خواهید کرد و منبع داده این رکورد را همواره به عنوان یک رکورد جدید، مجددا به سرور ارسال میکند.
using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Web.Http; using KendoUI07.Models; namespace KendoUI07.Controllers { public class RegistrationsController : ApiController { public HttpResponseMessage Delete(int id) { var item = RegistrationsDataSource.LatestRegistrations.FirstOrDefault(x => x.Id == id); if (item == null) return Request.CreateResponse(HttpStatusCode.NotFound); RegistrationsDataSource.LatestRegistrations.Remove(item); return Request.CreateResponse(HttpStatusCode.OK, item); } public IEnumerable<Registration> Get() { return RegistrationsDataSource.LatestRegistrations; } public HttpResponseMessage Post(Registration registration) { if (!ModelState.IsValid) return Request.CreateResponse(HttpStatusCode.BadRequest); var id = 1; var lastItem = RegistrationsDataSource.LatestRegistrations.LastOrDefault(); if (lastItem != null) { id = lastItem.Id + 1; } registration.Id = id; RegistrationsDataSource.LatestRegistrations.Add(registration); // ارسال آی دی مهم است تا از ارسال رکوردهای تکراری جلوگیری شود return Request.CreateResponse(HttpStatusCode.Created, registration); } [HttpPut] // Add it to fix this error: The requested resource does not support http method 'PUT' public HttpResponseMessage Update(int id, Registration registration) { var item = RegistrationsDataSource.LatestRegistrations .Select( (prod, index) => new { Item = prod, Index = index }) .FirstOrDefault(x => x.Item.Id == id); if (item == null) return Request.CreateResponse(HttpStatusCode.NotFound); if (!ModelState.IsValid || id != registration.Id) return Request.CreateResponse(HttpStatusCode.BadRequest); RegistrationsDataSource.LatestRegistrations[item.Index] = registration; return Request.CreateResponse(HttpStatusCode.OK); } } }
نمایش آنی اطلاعات ثبت شده در یک لیست
ردیفهای اضافه شده به منبع داده را میتوان بلافاصله در همان سمت کلاینت توسط Kendo UI Template که قابلیت کار با ViewModelها را دارد، نمایش داد:
<div id="coursesSection" class="k-rtl k-header"> <div class="box-col"> <form id="myForm" data-role="validator" novalidate="novalidate"> <!--فرم بحث شده در ابتدای مطلب--> </form> </div> <div id="results"> <table class="metrotable"> <thead> <tr> <th>Id</th> <th>نام</th> <th>دوره</th> <th>هزینه</th> <th>ایمیل</th> <th>تلفن</th> <th></th> <th></th> </tr> </thead> <tbody data-template="row-template" data-bind="source: coursesDataSourceRows"></tbody> <tfoot data-template="footer-template" data-bind="source: this"></tfoot> </table> <script id="row-template" type="text/x-kendo-template"> <tr> <td data-bind="text: Id"></td> <td data-bind="text: UserName"></td> <td dir="ltr" data-bind="text: CourseName"></td> <td> #: kendo.toString(get("Credit"), "c0") # </td> <td data-bind="text: Email"></td> <td data-bind="text: Tel"></td> <td><button class="k-button" data-bind="click: deleteCourse">حذف</button></td> <td><button class="k-button" data-bind="click: editCourse">ویرایش</button></td> </tr> </script> <script id="footer-template" type="text/x-kendo-template"> <tr> <td colspan="3"></td> <td> جمع کل: #: kendo.toString(totalPrice(), "c0") # </td> <td colspan="2"></td> <td></td> <td></td> </tr> </script> </div> </div>
<script type="text/javascript"> $(function () { // ... var viewModel = kendo.observable({ accepted: false, course: new model(), coursesDataSourceRows: new kendo.data.ObservableArray([]), doSave: function (e) { // ... }, resetModel: function (e) { // ... }, totalPrice: function () { var sum = 0; $.each(this.get("coursesDataSourceRows"), function (index, item) { sum += item.Credit; }); return sum; }, deleteCourse: function (e) { // the current data item is passed as the "data" field of the event argument var course = e.data; dataSource.remove(course); dataSource.sync(); // push to the server }, editCourse: function(e) { // the current data item is passed as the "data" field of the event argument var course = e.data; this.set("course", course); } }); kendo.bind($("#coursesSection"), viewModel); dataSource.read(); // دریافت لیست موجود از سرور در آغاز کار }); </script>
- ابتدا خاصیت دلخواه coursesDataSourceRows به viewModel اضافه میشود تا در ناحیهی coursesSection در دسترس قرار گیرد.
- سپس اگر به انتهای تعریف DataSource دقت کنید، داریم:
<script type="text/javascript"> $(function () { var dataSource = new kendo.data.DataSource({ //... change: function (e) { // فراخوانی در زمان دریافت اطلاعات از سرور و یا تغییرات محلی viewModel.set("coursesDataSourceRows", new kendo.data.ObservableArray(this.view())); } }); }); </script>
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید:
KendoUI07.zip
توابع(Function)
let add x y = x + y let result = add 4 5 printfn "(add 4 5) = %i" result
let add x y = x + y let result1 = add 4 5 let result2 = add 6 7 let finalResult = add result1 result2
let add x y = x + y let result =add (add 4 5) (add 6 7)
خروجی توابع
کامپایلر #F آخرین مقداری که در تابع، تعریف و استفاده میشود را به عنوان مقدار بازگشتی و نوع آن را نوع بازگشتی میشناسد.
let cylinderVolume radius length : float = let pi = 3.14159 length * pi * radius * radius
یک مثال دیگر:
let sign num = if num > 0 then "positive" elif num < 0 then "negative" else "zero"
تعریف پارامترهای تابع با ذکر نوع به صورت صریح
اگر هنگام تعریف توابع مایل باشید که نوع پارامترها را به صورت صریح تعیین کنید از روش زیر استفاده میکنیم.
let replace(str: string) = str.Replace("A", "a")
let addu1 (x : uint32) y = x + y
Pipe-Forward Operator
در #F روشی دیگری برای تعریف توابع وجود دارد که به pipe-Forward معروف است. فقط کافیست از اپراتور (<|) به صورت زیر استفاده کنید.
let (|>) x f = f x
let result = 0.5 |> System.Math.Cos
let add x y = x + y let result = add 6 7 |> add 4 |> add 5
Partial Fucntion Or Application
partial function به این معنی است که در هنگام فراخوانی یک تابع نیاز نیست که به تمام آرگومانهای مورد نیاز مقدار اختصاص دهیم. برای نمونه در مثال بالا تابع add نیاز به 2 آرگومان ورودی داشت در حالی که فقط یک مقدار به آن پاس داده شد.
let result = add 6 7 |> add 4
let sub (a, b) = a - b let subFour = sub 4
prog.fs(15,19): error: FS0001: This expression has type int but is here used with type 'a * 'b
در مورد ماهیت توابع بازگشتی نیاز به توضیح نیست فقط در مورد نوع پیاده سازی اون در #F توضیح خواهم داد. برای تعریف توابع به صورت بازگشتی کافیست از کلمه rec بعد از let استفاده کنیم(زمانی که قصد فراخوانی تابع رو در خود تابع داشته باشیم). مثال پایین به خوبی مسئله را روشن خواهد کرد.(پیاده سازی تابع فیبو ناچی)
let rec fib x = match x with | 1 -> 1 | 2 -> 1 | x -> fib (x - 1) + fib (x - 2)
printfn "(fib 2) = %i" (fib 2) printfn "(fib 6) = %i" (fib 6) printfn "(fib 11) = %i" (fib 11)
خروجی برای مثال بالا به صورت خواهد شد.
(fib 2) = 1 (fib 6) = 8 (fib 11) = 89
گاهی اوقات توابع به صورت دوطرفه بازگشتی میشوند. یعنی فراخوانی توابع به صورت چرخشی انجام میشود. (فراخوانی یک تابع در تابع دیگر و بالعکس). به مثال زیر دقت کنید.
let rec Even x = if x = 0 then true else Odd (x - 1) and Odd x = if x = 1 then true else Even (x - 1)
ترکیب توابع
let firstFunction x = x + 1 let secondFunction x = x * 2 let newFunction = firstFunction >> secondFunction let result = newFunction 100
توابع تودرتو
در #F امکان تعریف توابع تودرتو وجود دارد. بعنی میتونیم یک تابع را در یک تابع دیگر تعریف کنیم. فقط نکته مهم در امر استفاده از توابع به این شکل این است که توابع تودرتو فقط در همون تابعی که تعریف میشوند قایل استفاده هستند و محدوده این توابع در خود همون تابع است.
let sumOfDivisors n = let rec loop current max acc =
//شروع تابع داخلی
if current > max then acc else if n % current = 0 then loop (current + 1) max (acc + current) else loop (current + 1) max acc
//ادامه بدنه تابع اصلی
let start = 2 let max = n / 2 (* largest factor, apart from n, cannot be > n / 2 *) let minSum = 1 + n (* 1 and n are already factors of n *) loop start max minSum printfn "%d" (sumOfDivisors 10)
if current > max then acc else if n % current = 0 then loop (current + 1) max (acc + current) else loop (current + 1) max acc
آیا میتوان توابع را Overload کرد؟
در #F امکان overloading برای یک تابع وجود ندارد. ولی متدها را میتوان overload کرد.(متدها در فصل شی گرایی توضیح داده میشود).
do keyword
زمانی که قصد اجرای یک کد را بدون تعریف یک تابع داشته باشیم باید از do استفاده کنیم. همچنین از do در انجام برخی عملیات پیش فرض در کلاسها زیاد استفاده میکنیم.(در فصل شی گرایی با این مورد آشنا خواهید شد).
open System open System.Windows.Forms let form1 = new Form() form1.Text <- "XYZ" [<STAThread>] do Application.Run(form1)
public interface IHaveCustomMapping { void CreateMappings(AutoMapper.Profile profile); }
public class PostDtoMapping : IHaveCustomMapping { public void CreateMappings(Profile profile) { profile.CreateMap<PostDto, Post>().ReverseMap(); } }
public class CustomMappingProfile : Profile { public CustomMappingProfile(IEnumerable<IHaveCustomMapping> haveCustomMappings) { foreach (var item in haveCustomMappings) item.CreateMappings(this); } }
- این کلاس از AutoMapper.Profile ارث بری کردهاست.
- درون سازندهی خود لیستی از اشیاء اینترفیس IHaveCustomMapping را دریافت کرده و بر روی آنها گردش میکند.
- و متد CreateMappings هرکدام را فراخوانی کرده و خودش (this : شی جاری) را (که از نوع Profile شده) به عنوان پارامتر ورودی پاس میدهد.
public static class AutoMapperConfiguration { public static void InitializeAutoMapper() { Mapper.Initialize(config => { config.AddCustomMappingProfile(); }); //Compile mapping after configuration to boost map speed Mapper.Configuration.CompileMappings(); } public static void AddCustomMappingProfile(this IMapperConfigurationExpression config) { config.AddCustomMappingProfile(Assembly.GetEntryAssembly()); } public static void AddCustomMappingProfile(this IMapperConfigurationExpression config, params Assembly[] assemblies) { var allTypes = assemblies.SelectMany(a => a.ExportedTypes); //Find all classes that implement IHaveCustomMapping inteface and create new instance of each var list = allTypes.Where(type => type.IsClass && !type.IsAbstract && type.GetInterfaces().Contains(typeof(IHaveCustomMapping))) .Select(type => (IHaveCustomMapping)Activator.CreateInstance(type)); //Create a new automapper Profile for this list to create mapping then add to the config var profile = new CustomMappingProfile(list); config.AddProfile(profile); } }
- توضیحات متد های InitializeAutoMapper و AddCustomMappingProfile، مشابه مطلب قبل است و لازم به ذکر مجدد نیست.
- متد AddCustomMappingProfile آرایهای از اسمبلیها را دریافت و سپس تمامی نوعهای قابل دسترس آنها را (ExportedTypes) واکشی میکند.
- سپس توسط شرط Where، نوعهایی که کلاس بوده، abstract نیستند و از اینترفیس IHaveCustomMapping مشتق شدهاند فیلتر میشوند.
- سپس توسط متد Activator.CreateInstance، وهلهای از آنها ایجاد و به نوع IHaveCustomMapping تبدیل میشوند و نهایتا لیستی از اشیاء وهله سازی شده را باز میگرداند.
- سپس وهلهای از نوع CustomMappingProfile (که مسئول اعمال Mappingهای اشیاء دریافتی است و قبلا بررسی کردیم) ایجاد میکنیم و لیست مذکور را به سازنده آن پاس میدهیم.
- نهایتا profile ساخته شده (حاوی تمامی Mappingهای اعمال شده) را توسط متد config.AddProfile به AutoMapper معرفی میکنیم (در این لحظه تمامی Mappingهای تعریف شده داخل profile، به AutoMapper اعمال میشوند).
public abstract class BaseDto<TDto, TEntity, TKey> : IHaveCustomMapping where TEntity : BaseEntity<TKey> { [Display(Name = "ردیف")] public TKey Id { get; set; } /// <summary> /// Maps this dto to a new entity object. /// </summary> public TEntity ToEntity() { return Mapper.Map<TEntity>(CastToDerivedClass(this)); } /// <summary> /// Maps this dto to an exist entity object. /// </summary> public TEntity ToEntity(TEntity entity) { return Mapper.Map(CastToDerivedClass(this), entity); } /// <summary> /// Maps the specified entity to a new dto object. /// </summary> public static TDto FromEntity(TEntity model) { return Mapper.Map<TDto>(model); } protected TDto CastToDerivedClass(BaseDto<TDto, TEntity, TKey> baseInstance) { return Mapper.Map<TDto>(baseInstance); } //Get automapper Profile then create mapping and ignore unmapped properties public void CreateMappings(Profile profile) { var mappingExpression = profile.CreateMap<TDto, TEntity>(); var dtoType = typeof(TDto); var entityType = typeof(TEntity); //Ignore mapping to any property of source (like Post.Categroy) that dose not contains in destination (like PostDto) //To prevent from wrong mapping. for example in mapping of "PostDto -> Post", automapper create a new instance for Category (with null catgeoryName) because we have CategoryName property that has null value foreach (var property in entityType.GetProperties()) { if (dtoType.GetProperty(property.Name) == null) mappingExpression.ForMember(property.Name, opt => opt.Ignore()); } //Pass mapping expressin to customize mapping in concrete class CustomMappings(mappingExpression.ReverseMap()); } //Concrete class can override this method to customize mapping public virtual void CustomMappings(IMappingExpression<TEntity, TDto> mapping) { } }
- کلاس جنریک BaseDto، متدCreateMappings اینترفیس IHaveCustomMapping را پیاده سازی میکند.
- درون این متد، Mapping بین دو نوع TDto و TEntity، توسط ()<profile.CreateMap<TDto, TEntity کانفیگ میشود.
- مانند مطلب قبل، خواصی را که نباید نگاشت شوند، توسط Reflection یافته و Ignore میکنیم.
- سپس Mapping برعکس را توسط ReverseMap اعمال کرده و به متد زیرین آن که virtual نیز است، پاس میدهیم.
public class PostDto : BaseDto<PostDto, Post, long> { public string Title { get; set; } public string Text { get; set; } public int CategoryId { get; set; } public string CategoryName { get; set; } //=> Category.Name public string FullTitle { get; set; } //=> custom mapping for "Title (Category.Name)" public override void CustomMappings(IMappingExpression<Post, PostDto> mapping) { mapping.ForMember( dest => dest.FullTitle, config => config.MapFrom(src => $"{src.Title} ({src.Category.Name})")); } }
- این کلاس، خاصیتی به نام FullTitle دارد که معادلی (خاصیت همنامی) در کلاس Post برای آن وجود ندارد و قرار است مقدار ترکیبی حاصل از Title و Category.Name را نمایش دهد.
- به همین جهت متد CustomMappings را باز نویسی کرده، شیء mapping را دریافت و سفارشی سازی لازم را روی آن انجام دادهایم.
- توسط متد ForMember مشخص کردهایم که مقدار خاصیت FullTitle باید حاصلی از ترکیب Title و Category.Name به نحو مشخص شده باشد ( توسط متد MapFrom).
List<PostDto> list = //ProjectTo method select only needed properties (of PostDto) not all properties //Also select only needed property of navigations (like Post.Category.Name) not all unlike Include //This ability called "Projection" await _applicationDbContext.Posts.ProjectTo<PostDto>() //We can also use Where on IQuerable<PostDto> .Where(p => p.Title.Contains("test") || p.CategoryName.Contains("test")) .ToListAsync();
- متد ProjectTo کوئری post را به IQueryable ای از postDto تبدیل میکند (این قابلیت Projection نامیده میشود).
- نگاشت خودکار خواص موجود در postDto توسط AutoMapper به صورت خودکار انجام میشود و فقط خواص لازم برای postDto واکشی میشوند (نه همه خواص در جدول post، که این به لحاظ کارآیی بهتر است).
- همچنین اگر خواصی را داخل Navigation Propertyها مانند CategoryName داشته باشیم، موقع کوئری گرفتن از دیتابیس، آنها نیز اعمال شده و فقط خواص لازم از Category واکشی میشوند (فقط خاصیت Name، بر خلاف Include که همه ستونها را واکشی میکند).
- همچنین میتوان بر روی خواص Dto شرط Where را قرار داد مانند p.CategoryName.Contains("test") و تماما به کوئری SQL معادل آن ترجمه و اجرا میشوند.
Reflector 10 منتشر شد
What's new in this release?
- Full C#7.0 support
- .NET Core support
Web Payments is an emerging web standard being developed by the W3C to simplify online payments and enable a broader set of players to participate easily in the payments ecosystem on the web. The standards are flexible; they work with various types of payment systems and are intended to work on any browser on any device, payment method, or payment service provider. This flexibility enables development simplicity, deployment consistency, and future compatibility with emerging payment technologies.