<%@ Page Title="Home Page" Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="WebAppSingleSubmit._Default" %> <asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeadContent"> <script type="text/javascript"> $(document).ready(function () { $("input[type=submit]").click(function (e) { if ((typeof (Page_ClientValidate) == 'function') && (Page_ClientValidate() == false)) { return false; } if (!confirm("آیا مطمئن هستید؟")) { return false; } this.disabled = true; this.value = 'در حال پردازش اطلاعات ...'; __doPostBack($(this).attr('name'), ''); }); }); </script> </asp:Content> <asp:Content ID="BodyContent" runat="server" ContentPlaceHolderID="MainContent"> <p> <asp:TextBox ID="TextBox1" runat="server"></asp:TextBox> <asp:RequiredFieldValidator ID="RequiredFieldValidator1" runat="server" ControlToValidate="TextBox1" ErrorMessage="RequiredFieldValidator"></asp:RequiredFieldValidator> <br /> <asp:Button ID="Button1" runat="server" Text="Button" OnClick="Button1_Click" /> </p> </asp:Content>
مطالب
Tuple در دات نت 4
نوع جدیدی در دات نت 4 به نام Tuple اضافه شده است که در این مطلب به بررسی آن خواهیم پرداخت.
در ریاضیات، Tuple به معنای لیست مرتبی از اعضاء با تعداد مشخص است. Tuple در زبانهای برنامه نویسی Dynamic مانند اف شارپ، Perl ، LISP و بسیاری موارد دیگر مطلب جدیدی نیست. در زبانهای dynamic برنامه نویسها میتوانند متغیرها را بدون معرفی نوع آنها تعریف کنند. اما در زبانهای Static مانند سی شارپ، برنامه نویسها موظفند نوع متغیرها را پیش از کامپایل آنها معرفی کنند که هر چند کار کد نویسی را اندکی بیشتر میکند اما به این صورت شاهد خطاهای کمتری نیز خواهیم بود (البته سی شارپ 4 این مورد را با معرفی واژهی کلیدی dynamic تغییر داده است).
برای مثال در اف شارپ داریم:
let data = (“John Doe”, 42)
که سبب ایجاد یک tuple که المان اول آن یک رشته و المان دوم آن یک عدد صحیح است میشود. اگر data را بخواهیم نمایش دهیم خروجی آن به صورت زیر خواهد بود:
printf “%A” data
// Output: (“John Doe”,42)
در دات نت 4 فضای نام جدیدی به نام System.Tuple معرفی شده است که در حقیقت ارائه دهندهی نوعی جنریک میباشد که توانایی در برگیری انواع مختلفی را دارا است :
public class Tuple<T1>
up to:
public class Tuple<T1, T2, T3, T4, T5, T6, T7, TRest>
همانند آرایهها، اندازهی Tuples نیز پس از تعریف قابل تغییر نیستند (immutable). اما تفاوت مهم آن با یک آرایه در این است که اعضای آن میتوانند نوعهای کاملا متفاوتی داشته باشند. همچنین تفاوت مهم آن با یک ArrayList یا آرایهای از نوع Object، مشخص بودن نوع هر یک از اعضاء آن است که type safety بیشتری را به همراه خواهد داشت و کامپایلر میتواند در حین کامپایل دقیقا مشخص نماید که اطلاعات دریافتی از نوع صحیحی هستند یا خیر.
یک مثال کامل از Tuples را در کلاس زیر ملاحظه خواهید نمود:
using System;
using System.Linq;
using System.Collections.Generic;
namespace TupleTest
{
class TupleCS4
{
#region Methods (4)
// Public Methods (4)
public static Tuple<string, string> GetFNameLName(string name)
{
if (string.IsNullOrWhiteSpace(name))
throw new NullReferenceException("name is empty.");
var nameParts = name.Split(',');
if (nameParts.Length != 2)
throw new FormatException("name must contain ','");
return Tuple.Create(nameParts[0], nameParts[1]);
}
public static void PrintSelectedTuple()
{
var list = new List<Tuple<string, int>>
{
new Tuple<string, int>("A", 1),
new Tuple<string, int>("B", 2),
new Tuple<string, int>("C", 3)
};
var item = list.Where(x => x.Item2 == 2).SingleOrDefault();
if (item != null)
Console.WriteLine("Selected Item1: {0}, Item2: {1}",
item.Item1, item.Item2);
}
public static void PrintTuples()
{
var tuple1 = new Tuple<int>(12);
Console.WriteLine("tuple1 contains: item1:{0}", tuple1.Item1);
var tuple2 = Tuple.Create("Item1", 12);
Console.WriteLine("tuple2 contains: item1:{0}, item2:{1}",
tuple2.Item1, tuple2.Item2);
var tuple3 = Tuple.Create(new DateTime(2010, 5, 6), "Item2", 20);
Console.WriteLine("tuple3 contains: item1:{0}, item2:{1}, item3:{2}",
tuple3.Item1, tuple3.Item2, tuple3.Item3);
}
public static void Tuple8()
{
var tup =
new Tuple<int, int, int, int, int, int, int, Tuple<int, int>>
(1, 2, 3, 4, 5, 6, 7, new Tuple<int, int>(8, 9));
Console.WriteLine("tup.Rest Item1: {0}, Item2: {1}",
tup.Rest.Item1,tup.Rest.Item2);
}
#endregion Methods
}
}
using System;
namespace TupleTest
{
class Program
{
static void Main()
{
var data = TupleCS4.GetFNameLName("Vahid, Nasiri");
Console.WriteLine("Data Item1:{0} & Item2:{1}",
data.Item1, data.Item2);
TupleCS4.PrintTuples();
TupleCS4.PrintSelectedTuple();
TupleCS4.Tuple8();
Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}
توضیحات :
- روشهای متفاوت ایجاد Tuples را در متد PrintTuples میتوانید ملاحظه نمائید. همچنین نحوهی دسترسی به مقادیر هر کدام از اعضاء نیز مشخص شده است.
- کاربرد مهم Tuples در متد GetFNameLName نمایش داده شده است؛ زمانیکه نیاز است تا چندین خروجی از یک تابع داشته باشیم. به این صورت دیگر نیازی به تعریف آرگومانهایی به همراه واژه کلیدی out نخواهد بود یا دیگر نیازی نیست تا یک شیء جدید را ایجاد کرده و خروجی را به آن نسبت دهیم. به همان سادگی زبانهای dynamic در اینجا نیز میتوان یک tuple را ایجاد و استفاده کرد.
- بدیهی است از Tuples در یک لیست جنریک و یا حالات دیگر نیز میتوان استفاده کرد. مثالی از این دست را در متد PrintSelectedTuple ملاحظه خواهید نمود. ابتدا یک لیست جنریک از Tuple ایی با دو عضو تشکیل شده است. سپس با استفاده از امکانات LINQ ، عضوی که آیتم دوم آن مساوی 2 است یافت شده و سپس المانهای آن نمایش داده میشود.
- نکتهی دیگری را که حین کار با Tuples میتوان در نظر داشت این است که اعضای آن حداکثر شامل 8 عضو میتوانند باشند که عضو آخر باید یک Tuple تعریف گردد و بدیهی است این Tuple نیز میتواند شامل 8 عضو دیگر باشد و الی آخر که نمونهای از آن را در متد Tuple8 میتوان مشاهده کرد.
در پست قبلی با نوشتن یک تست ساده، با مفهوم TDD بیشتر آشنا شدیم .در این پست قصد بر این است که به وسیله Mvc.Net شروع به نوشتن تستهای جدیتر کرده و از مزایای آن بهره ببریم .
در کد بالا مثل تست قبل، یک وهله از Controller می سازیم و سپس نام view بازگشتی را با string.Empty مقایسه میکنیم به این معنی که view خروجی Action ما نباید نامی داشته باشد و براساس قرار دادها باید هم نام اکشن باشد.
حال نوبت به پیاده سازی اکشن رسید.:
در تست بعدی میخواهیم عملیات اضافه شدن یک Idea را به لیست بررسی کنیم:
تست بالا نیز مانند دو تست قبل است با این تفاوت که مخواهیم ریدارکت شدن به یک Action خاص را نیز تست کنیم.برای همین مقدار خروجی را به RedirectToRouteResult تبدیل میکنیم.در ادامه یک Idea جدید ساخته و به لست اضافه میکنیم سپس از وجود داشتن آن در لیست Ideas اطمینان حاصل میکنیم.در خط آخر نیز نام Action که انتظار داریم بعد از اضافه شدن یک Idea ,کاربر به آن هدایت شود را ست میکنیم.
برای شروع یک پروژه Mvc.Net ساخته و Nunit را در آن نصب میکنیم.
مدل زیر را در پوشه مدلها میسازیم:
public class Idea { public static List<Idea> Ideas = new List<Idea> { new Idea{Content="سایتی که در ایده به اشتراک گذاشته شود",Title = "سایت ایده ها"}, new Idea{Content="عینک گوگل را فارسی کنم",Title = "عینک گوگل"}, }; public string Content { get; set;} public string Title { get; set; } }
[TestFixture] public class IdeaTest { [Test] public void ShouldDisplayListOfIdea() { var viewResult = new IdeaController().Index() as ViewResult; Assert.AreEqual(Idea.Ideas, viewResult.Model) Assert.IsNotNull(viewResult.Model); } }
کد بالا شامل مقایسه مقدار خروجی Action با لیستی از مدل Idea و همچنین اطمینان از خالی نبودن مدل ارسالی به view میباشد.در خط اول یک وهله از Controller می سازیم و Action مورد نظر را به شی از جنس ViewResult تبدیل(Cast) میکنیم پس از آن به وسیله viewResult.Model به مدلی که به سمت view پاس داده میشود دسترسی خواهیم داشت.اکنون اگر تست را اجرا کنیم با خطای کامپایل مواجه میشویم.حال Controller و Action مورد نظر را به صورتی که تست ما پاس شود پیاده سازی میکنیم.
public class IdeaController : Controller { public ActionResult Index() { return View(Idea.Ideas); } }
کد بالا مقدار Ideas را به view برمیگرداند.
در این دروره ما به تست کردن ویوها نخواهیم پرداخت.
تست بعدی تست ساده ای است که فقط میخواهیم از از وجود داشتن یک Action و نام view بازگشتی اطمینان حاصل کنیم.
[Test] public void ShouldLoadCreateIdeaView() { var viewResult = new IdeaController().Create() as ViewResult; Assert.AreEqual(string.Empty, viewResult.ViewName); }
حال نوبت به پیاده سازی اکشن رسید.:
public ActionResult Create() { return View(); }
[Test] public void ShouldAddIdeaItem() { var idea = new Idea { Title = "شبکه اجتماعی ", Content = " شبکه اجتماعی سینمایی" }; var redirectToRouteResult = new IdeaController().Create(idea) as RedirectToRouteResult; Assert.Contains(idea, Idea.Ideas); Assert.AreEqual("Index",redirectToRouteResult.RouteValues["action"]); }
پیاده سازی Action به شکل زیر است:
public ActionResult Create(Idea idea) { Idea.Ideas.Add(idea); return RedirectToAction("Index"); }
در این پست شما با مدل تست نویسی برایMvc.Net آشنا شدید.در مطلب بعدی شما با تست حذف و اصلاح Ideas آشنا خواهید شد.
در زبان #C، زمانیکه از کلاسی ارثبری میشود، امکان بازنویسی متدهای کلاس پایه، در صورت معرفی آنها به صورت virtual و یا abstract، وجود دارد؛ اما در این بازنویسیها، تغییر نوع خروجی این متدها مجاز نیست. این محدودیت در C# 9.0 با معرفی Covariant returns برداشته شدهاست؛ با یک شرط: نوع جدید این خروجی، باید covariant نوع اصلی متدی باشد که از کلاس پایهی آن ارثبری شدهاست.
وضعیت خروجی متدهای بازنویسی شده تا پیش از C# 9.0
برای توضیح بهتر Covariant returns، نیاز است مثال زیر را بررسی کنیم:
در اینجا یک کلاس abstract و پایهی Product وجود دارد که میتوان متد abstract سفارش دهی آنرا در کلاسهای مشتق شدهی از آن، مانند Book، بازنویسی کرد.
همانطور که مشاهده میکنید، در کلاس Book، تنها خروجی که برای متد Order بازنویسی شده میتوان درنظر گرفت، همانی است که در کلاس پایهی Product تعریف شدهاست و قابل تغییر نیست؛ یعنی همان ProductOrder.
همچنین در حین استفادهی از این کلاسها، تبدیل خروجی متد Order، به BookOrder ضروری است:
امکان تغییر خروجی متدهای بازنویسی شده در C# 9.0
در C# 9.0 با مجاز اعلام شدن خروجیهای covariant، میتوان تغییرات زیر را به کدهای فوق اعمال کرد:
چون کلاس BookOrder از ProductOrder تعریف شدهی در کلاس پایه مشتق شدهاست (مفهوم covariant بودن خروجی متد)، میتوان در C# 9.0 آنرا به عنوان خروجی متد Order بازنویسی شدهی در کلاس Book، تنظیم کرد.
مزایای این ویژگی:
- داشتن یک خروجی مختص و متناسب با کلاس کتاب، مانند BookOrder؛ بجای ارائهی یک خروجی بسیار عمومی ProductOrder.
- نیاز به کار با Generics را برای اینگونه اختصاصی سازیها منتفی میکند.
- با این تغییر، دیگر نیازی به تبدیل نوع خروجی متد Order یک کتاب نیست و سطر سفارش دهی را میتوان به صورت زیر خلاصه کرد:
وضعیت خروجی متدهای بازنویسی شده تا پیش از C# 9.0
برای توضیح بهتر Covariant returns، نیاز است مثال زیر را بررسی کنیم:
public abstract class Product { public string Name { get; set; } public abstract ProductOrder Order(int quantity); } public class Book : Product { public string ISBN { get; set; } public override ProductOrder Order(int quantity) => new BookOrder { Quantity = quantity, Product = this }; } public class ProductOrder { public int Quantity { get; set; } } public class BookOrder : ProductOrder { public Book Product { get; set; } }
همانطور که مشاهده میکنید، در کلاس Book، تنها خروجی که برای متد Order بازنویسی شده میتوان درنظر گرفت، همانی است که در کلاس پایهی Product تعریف شدهاست و قابل تغییر نیست؛ یعنی همان ProductOrder.
همچنین در حین استفادهی از این کلاسها، تبدیل خروجی متد Order، به BookOrder ضروری است:
var book = new Book { Name = "My book", ISBN = "11-1-12-22-0" }; BookOrder orderBook = (BookOrder)book.Order(1);
امکان تغییر خروجی متدهای بازنویسی شده در C# 9.0
در C# 9.0 با مجاز اعلام شدن خروجیهای covariant، میتوان تغییرات زیر را به کدهای فوق اعمال کرد:
public class Book : Product { public string ISBN { get; set; } public override BookOrder Order(int quantity) => new BookOrder { Quantity = quantity, Product = this }; }
مزایای این ویژگی:
- داشتن یک خروجی مختص و متناسب با کلاس کتاب، مانند BookOrder؛ بجای ارائهی یک خروجی بسیار عمومی ProductOrder.
- نیاز به کار با Generics را برای اینگونه اختصاصی سازیها منتفی میکند.
- با این تغییر، دیگر نیازی به تبدیل نوع خروجی متد Order یک کتاب نیست و سطر سفارش دهی را میتوان به صورت زیر خلاصه کرد:
BookOrder orderBook = book.Order(1);
در نگارش 1.6، قالب سلول جدیدی به نام MonthCalendar اضافه شده است که امکان نمایش تقویم ماهیانه شمسی و میلادی را فراهم میکند. در ادامه نحوه استفاده از آنرا بررسی خواهیم کرد. کدهای کامل این مثال را از اینجا نیز میتوانید دریافت کنید: (^)
فرض کنید اطلاعات حضور و غیاب کارمندان را به نحو زیر در اختیار دارید:
و برای نمونه منبع داده فرضی ما نیز به صورت زیر است (تعدادی روز، به همراه ساعات کارکرد):
در این منبع داده فرضی، متن Description ذیل شماره روز، در تقویم ماهیانه نمایش داده خواهد شد.
سلولی که قرار است قالب MonthCalendar را نمایش دهد نیاز به شیءایی از نوع PdfRpt.Calendar.CalendarData دارد که به نحو زیر تعریف شده است:
این ساختار بر اساس اطلاعات یک ماه و روزهای آن است. متن Description در صورت false بودن ShowDescriptionInFooter ذیل شماره روز نمایش داده خواهد شد، در غیراینصورت در پایان ماه به شکل یک سطر جدید نمایش داده میشود. در اینجا روزهای ماه و سال بر اساس نوع تقویم معنا خواهند شد.
اکنون نیاز است تا اطلاعات منبع داده خود را به CalendarData نگاشت کنیم تا بتوان از آن در قالب سلول جدید MonthCalendar استفاده کرد. انجام اینکار با استفاده از امکانات LINQ به نحو زیر است:
UserMonthCalendar، شامل ستونهایی است که قرار است در گزارش ما ظاهر شوند:
ستون اول، شماره شخص، ستون دوم شامل نام شخص و ستون سوم، شامل اطلاعات یک ماه شخص است.
برای نمایش این اطلاعات توسط PdfReport، دو ستون اول یاد شده نکته خاصی ندارند، اما نحوه تعریف ستون تقویم ماهیانه آن به صورت زیر خواهد بود:
توسط CalendarAttributes میتوان یک سری از خواص تقویم نمایش داده شده را تغییر داد. برای مثال CalendarType مشخص میکند که نوع تقویم شمسی است یا میلادی؛ UseLongDayNamesOfWeek برای نمایش نام روزها به صورت کامل «شنبه» یا «ش» (نام کوتاه شده آن) بکار میرود. SplitRows مشخص میکند که اگر تقویم در یک صفحه جا نشد، به صفحه بعد منتقل شود یا تا جایی که ممکن است در صفحه جاری اطلاعات آن نمایش داده شده و سپس مابقی را در صفحه بعد ترسیم کند (مقدار true آن). به علاوه توسط CellsCustomizer میتوان فرمت کردن شرطی اطلاعات را انجام داد. برای مثال در اینجا اگر روز مورد نظر، روز اول سال 91 باشد، رنگ زمینه سلول و رنگ متن عدد آن تغییر خواهد کرد.
فرض کنید اطلاعات حضور و غیاب کارمندان را به نحو زیر در اختیار دارید:
namespace PdfReportSamples.Models { public class UserWorkedHours { public int Id { set; get; } public string Name { set; get; } public int DayNumber { set; get; } public int Month { set; get; } public int Year { set; get; } public string Description { set; get; } } }
private static List<UserWorkedHours> createUsersWorkedHours() { var usersWorkedHours = new List<UserWorkedHours>(); for (int i = 1; i < 11; i++) { for (int j = 1; j < 28; j++) { usersWorkedHours.Add(new UserWorkedHours { Id = i, Name = "کارمند " + i, Year = 1391, // سال و ماه بر اساس نوع تقویم انتخابی مشخص میشود Month = i, DayNumber = j, Description = i % 2 == 0 ? "05:00" : "08:00" }); } } return usersWorkedHours; }
سلولی که قرار است قالب MonthCalendar را نمایش دهد نیاز به شیءایی از نوع PdfRpt.Calendar.CalendarData دارد که به نحو زیر تعریف شده است:
using System.Collections.Generic; namespace PdfRpt.Calendar { public class CalendarData { public int Month { set; get; } public int Year { set; get; } public IList<DayInfo> MonthDaysInfo { set; get; } } }
namespace PdfRpt.Calendar { public class DayInfo { public int DayNumber { set; get; } public int Month { set; get; } public int Year { set; get; } public string Description { set; get; } public bool ShowDescriptionInFooter { set; get; } } }
اکنون نیاز است تا اطلاعات منبع داده خود را به CalendarData نگاشت کنیم تا بتوان از آن در قالب سلول جدید MonthCalendar استفاده کرد. انجام اینکار با استفاده از امکانات LINQ به نحو زیر است:
public static IList<UserMonthCalendar> CreateDataSource() { var usersWorkedHours = createUsersWorkedHours(); // Mapping a list of normal Users WorkedHours to a list of Users + CalendarData return usersWorkedHours .GroupBy(x => new { Id = x.Id, Name = x.Name }) .Select( x => new UserMonthCalendar { Id = x.Key.Id, Name = x.Key.Name, // Calendar's cell data type should be PdfRpt.Calendar.CalendarData MonthCalendarData = new CalendarData { Year = x.First().Year, Month = x.First().Month, MonthDaysInfo = x.ToList().Select(y => new DayInfo { Description = y.Description, ShowDescriptionInFooter = false, DayNumber = y.DayNumber }).ToList() } }).ToList(); }
using PdfRpt.Calendar; namespace PdfReportSamples.Models { public class UserMonthCalendar { public int Id { set; get; } public string Name { set; get; } // Calendar's cell data type should be CalendarData public CalendarData MonthCalendarData { set; get; } } }
برای نمایش این اطلاعات توسط PdfReport، دو ستون اول یاد شده نکته خاصی ندارند، اما نحوه تعریف ستون تقویم ماهیانه آن به صورت زیر خواهد بود:
columns.AddColumn(column => { // Calendar's cell data type should be PdfRpt.Calendar.CalendarData column.PropertyName<UserMonthCalendar>(x => x.MonthCalendarData); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(3); column.Width(3); column.HeaderCell("تقویم ماهیانه"); column.ColumnItemsTemplate(itemsTemplate => { itemsTemplate.MonthCalendar(new CalendarAttributes { CalendarType = CalendarType.PersianCalendar, UseLongDayNamesOfWeek = true, Padding = 3, DescriptionHorizontalAlignment = HorizontalAlignment.Center, SplitRows = true, CellsCustomizer = info => { if (info.Year == 1391 && info.Month == 1 && info.DayNumber == 1) { info.NumberCell.BackgroundColor = new BaseColor(System.Drawing.Color.LimeGreen); var phrase = info.NumberCell.Phrase; foreach (var chunk in phrase.Chunks) chunk.Font.Color = new BaseColor(System.Drawing.Color.Yellow); } } }); }); });
Our new React-based web app is built from more than a hundred components, many of which are themselves created using simpler building block components.
نوعهای ارجاعی (Reference Types) در #C، همیشه نالپذیر بودهاند؛ در مقابل نوعهای مقداری (value types) مانند DateTime که برای نالپذیر کردن آنها باید یک علامت سؤال را در حین تعریف نوع آنها ذکر کرد تا تبدیل به یک نوع نالپذیر شود (DateTime? Created). بنابراین عنوانی مانند «نوعهای ارجاعی نالنپذیر» شاید آنچنان مفهوم نباشد.
خالق Null در زبانهای برنامه نویسی، آنرا یک اشتباه چند میلیارد دلاری میداند! و به عنوان یک توسعه دهندهی دات نت، غیرممکن است که در حین اجرای برنامههای خود تابحال به null reference exception برخورد نکرده باشید. هدف از ارائهی قابلیت جدید «نوعهای ارجاعی نالنپذیر» در C# 8.0، مقابلهی با یک چنین مشکلاتی است و خصوصا غنی سازی IDEها برای ارائهی اخطارهایی پیش از کامپایل برنامه، در مورد قسمتهایی از کد که ممکن است سبب بروز null reference exception شوند.
فعالسازی «نوعهای ارجاعی نالنپذیر»
قابلیت «نوعهای ارجاعی نالنپذیر» به صورت پیشفرض غیرفعال است. برای فعالسازی آن میتوان فایل csproj را به صورت زیر، با افزودن خاصیت NullableContextOptions، ویرایش کرد:
یک نکته: در نگارشهای بعدی NET Core SDK. و همچنین ویژوال استودیو (از نگارش 16.2.0 به بعد)، خاصیت NullableContextOptions به صرفا Nullable تغییر نام یافته و ساده شدهاست. بنابراین اگر در این نگارشها به خطاهای ذیل برخوردید:
صرفا به معنای استفادهی از نام قدیمی این ویژگی است که باید به Nullable تغییر پیدا کند:
اما در زمان نگارش این مطلب که 3.0.100-preview5-011568 در دسترس است، فعلا همان نام قدیمی NullableContextOptions کار میکند.
تغییر ماهیت نوعهای ارجاعی #C با فعالسازی NullableContextOptions
در #C ای که ما میشناسیم، رشتهها قابلیت پذیرش نال را دارند و همچنین ذکر آنها به صورت nullable بیمعنا است. اما پس از فعالسازی ویژگی نوعهای ارجاعی نالنپذیر، اکنون عکس آن رخ میدهد. رشتهها نالنپذیر میشوند؛ اما میتوان در صورت نیاز، آنها را nullable نیز تعریف کرد.
یک مثال: بررسی تاثیر فعالسازی NullableContextOptions بر روی یک پروژه
کلاس زیر را در نظر بگیرید:
با فعالسازی خاصیت NullableContextOptions، بلافاصله اخطار زیر در IDE ظاهر میشود (اگر ظاهر نشد، یکبار پروژه را بسته و مجددا بارگذاری کنید):
در این کلاس، دو سازنده وجود دارند که یکی MiddleName را دریافت میکند و دیگری خیر. در اینجا کامپایلر تشخیص دادهاست که چون در سازندهی اولی که MiddleName را دریافت نمیکند، مقدار پیشفرض خاصیت MiddleName، نال خواهد بود و همچنین ما NullableContextOptions را نیز فعال کردهایم، بنابراین این خاصیت دیگر به صورت معمول و متداول یک نوع ارجاعی نالپذیر عمل نمیکند و دیگر نمیتوان نال را به عنوان مقدار پیشفرض آن، به آن نسبت داد. به همین جهت اخطار فوق ظاهر شدهاست.
برای رفع این مشکل:
به کامپایلر اعلام میکنیم: «میدانیم که MiddleName میتواند نال هم باشد» و آنرا در این زمینه راهنمایی میکنیم:
پس از این تغییر، اخطار فوق که ذیل سازندهی اول کلاس Person ظاهر شده بود، محو میشود. اما اکنون مجددا کامپایلر، در جائیکه میخواهیم از آن استفاده کنیم:
اخطارهایی را صادر میکند:
در اینجا در متد محلی (local function) تعریف شده، سعی در دسترسی به خاصیت MiddleName وجود دارد و اکنون با تغییر جدیدی که اعمال کردیم، به صورت نالپذیر تعریف شدهاست.
همچنین در سطر بعدی آن نیز نتیجهی نهایی middleName، مورد استفاده قرار گرفتهاست که آن نیز مشکلدار تشخیص داده شدهاست.
مشکل اولین سطر را به این صورت میتوانیم برطرف کنیم:
در اینجا بجای ذکر صریح نوع string، از var استفاده شدهاست. پیشتر با ذکر صریح نوع string، آنرا یک رشتهی نالنپذیر تعریف کرده بودیم. اما اکنون چون person.MiddleName نالپذیر تعریف شدهاست، var نیز به صورت خودکار به این رشتهی نالپذیر اشاره میکند.
اما مشکل سطر دوم هنوز باقی است:
علت اینجا است که متغیر middleName نیز اکنون ممکن است مقدار نال را داشته باشد. برای رفع این مشکل میتوان از اپراتور .? استفاده کرد و سپس اگر مقدار نهایی این عبارت نال بود، مقدار صفر را بازگشت میدهیم:
هدف از این قابلیت و ویژگی کامپایلر، کمک کردن به توسعه دهندهها جهت نوشتن کدهایی امنتر و مقاومتر به null reference exceptionها است.
امکان خاموش و روشن کردن ویژگی نوعهای ارجاعی نالنپذیر به صورت موضعی
زمانیکه خاصیت NullableContextOptions را فعال میکنیم، بر روی کل پروژه تاثیر میگذارد. برای مثال اگر یک چنین قابلیتی را بر روی پروژههای قدیمی خود فعال کنید، با صدها اخطار مواجه خواهید شد. به همین جهت است که این ویژگی حتی با فعالسازی C# 8.0 و انتخاب آن، به صورت پیشفرض غیرفعال است. بنابراین برای اینکه بتوان پروژههای قدیمی را قدم به قدم و سر فرصت، «مقاومتر» کرد، میتوان تعیین کرد که کدام قسمت، تحت تاثیر این ویژگی قرار بگیرد و کدام قسمتها خیر:
در اینجا میتوان با استفاده از compiler directive جدید nullable# به کامپایلر اعلام کرد که از این قسمت صرفنظر کن. مقدار آن میتواند disable و یا enable باشد.
مجبور ساختن خود به «مقاوم سازی» برنامه
اگر NullableContextOptions را فعال کنید، کامپایلر صرفا یکسری اخطار را در مورد مشکلات احتمالی صادر میکند؛ اما برنامه هنوز کامپایل میشود. برای اینکه خود را مقید به «مقاوم سازی» برنامه کنیم، میتوانیم با فعالسازی ویژگی TreatWarningsAsErrors در فایل csprj، این اخطارها را تبدیل به خطای کامپایلر کرده و از کامپایل برنامه جلوگیری کنیم:
البته TreatWarningsAsErrors تمام اخطارهای برنامه را تبدیل به خطا میکند. اگر میخواهید انتخابیتر عمل کنید، میتوان از خاصیت WarningsAsErrors استفاده کرد:
آیا اگر برنامهای با C# 7.0 کامپایل شود، کتابخانههای تهیه شدهی با C# 8.0 را میتواند استفاده کند؟
پاسخ: بله. از دیدگاه برنامههای قدیمی، کتابخانههای تهیه شدهی با C# 8.0، تفاوتی با سایر کتابخانه ندارند. آنها نوعهای نالپذیر جدید را مانند ?string مشاهده نمیکنند؛ آنها فقط string را مشاهده میکنند و روش کار کردن با آنها نیز همانند قبل است. بدیهی است در این حالت از مزایای کامپایلر C# 8.0 در تشخیص زود هنگام مشکلات برنامه محروم خواهند بود؛ اما عملکرد برنامه تفاوتی نمیکند.
وضعیت برنامهی C# 8.0 ای که از کتابخانههای C# 7.0 و یا قبل از آن استفاده میکند، چگونه خواهد بود؟
چون کتابخانههای قدیمیتر از مزایای کامپایلر C# 8.0 استفاده نمیکنند، خروجیهای آن بدون بروز خطایی توسط کامپایلر C# 8.0 استفاده میشوند؛ چون حجم اخطارهای صادر شدهی در این حالت بیش از حد خواهد بود. یعنی این بررسیهای کامپایلر صرفا برای کتابخانههای جدید فعال هستند و نه برای کتابخانههای قدیمی.
مهارتهای مواجه شدن با اخطارهای ناشی از فعالسازی NullableContextOptions
در مثالی که بررسی شد، یک نمونه از روشهای مواجه شدن با اخطارهای ناشی از فعالسازی ویژگی نوعهای ارجاعی نالنپذیر را بررسی کردیم. در ادامه روشهای تکمیلی دیگری را بررسی میکنیم.
1- هرجائیکه قرار است متغیر ارجاعی نالپذیر باشد، آنرا صراحتا اعلام کنید.
این مثال را پیشتر بررسی کردیم. با فعالسازی ویژگی نوعهای ارجاعی نالنپذیر، ماهیت آنها نیز تغییر میکند و دیگر نمیتوان به آنها null را انتساب داد. اگر نیاز است حتما اینکار صورت گیرد، آنها را توسط ? به صورت nullable تعریف کنید.
نمونهی دیگر آن مثال زیر است:
در اینجا Address یک نوع ارجاعی نالپذیر است. بنابراین حاصل Address?.Country میتواند نال باشد و به Country نالنپذیر قابل انتساب نیست. برای رفع این مشکل کافی است دقیقا مشخص کنیم که این رشته نیز نالپذیر است:
البته در این حالت باید به مثال زیر دقت داشت:
چون node در اینجا توسط var تعریف شدهاست، دقیقا نوع this را که non-nullable است، پیدا میکند. بنابراین بعدها نمیتوان به آن null را انتساب داد. اگر چنین موردی نیاز بود، باید صریحا نوع آنرا بدو امر، nullable تعریف کرد؛ چون هنوز امکان تعریف ?var میسر نیست:
2- نوعهای خود را مقدار دهی اولیه کنید.
در مثال زیر:
در این حالت چون خاصیت Name، در سازندهی کلاس مقدار دهی اولیه نشدهاست، یک اخطار صادر میشود که بیانگر احتمال نال بودن آن است. یک روش مواجه شدن با این مشکل، تعریف آن به صورت یک خاصیت نالپذیر است:
یا یک استثناء را صادر کنید:
به این ترتیب کامپایلر میداند که اگر نام دریافتی نال بود، دقیقا باید چگونه رفتار کند.
البته در این حالت برای مقدار دهی اولیهی Name، حتما نیاز به تعریف یک سازندهاست و در این حالت کدهایی را که از سازندهی پیشفرض استفاده کرده بودند (مانند new Person { Name = "Vahid" })، باید تغییر دهید.
راهحل دیگر، مقدار دهی اولیهی این خواص بدون تعریف یک سازندهی اضافی است:
برای مثال میتوان از مقادیر خالی زیر برای مقدار دهی اولیهی رشتهها، آرایهها و مجموعهها استفاده کرد:
یا حتی میتوان اشیاء دیگر را نیز به صورت زیر مقدار دهی اولیه کرد:
البته در این حالت باید مفهوم فلسفی «خالی بودن» را پیش خودتان تفسیر و تعریف کنید که دقیقا مقصود از یک آدرس خالی چیست؟ به همین جهت شاید تعریف این شیء به صورت nullable بهتر باشد.
خالق Null در زبانهای برنامه نویسی، آنرا یک اشتباه چند میلیارد دلاری میداند! و به عنوان یک توسعه دهندهی دات نت، غیرممکن است که در حین اجرای برنامههای خود تابحال به null reference exception برخورد نکرده باشید. هدف از ارائهی قابلیت جدید «نوعهای ارجاعی نالنپذیر» در C# 8.0، مقابلهی با یک چنین مشکلاتی است و خصوصا غنی سازی IDEها برای ارائهی اخطارهایی پیش از کامپایل برنامه، در مورد قسمتهایی از کد که ممکن است سبب بروز null reference exception شوند.
فعالسازی «نوعهای ارجاعی نالنپذیر»
قابلیت «نوعهای ارجاعی نالنپذیر» به صورت پیشفرض غیرفعال است. برای فعالسازی آن میتوان فایل csproj را به صورت زیر، با افزودن خاصیت NullableContextOptions، ویرایش کرد:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>netcoreapp3.0</TargetFramework> <LangVersion>8.0</LangVersion> <NullableContextOptions>enable</NullableContextOptions> </PropertyGroup> </Project>
CS8632: The annotation for nullable reference types should only be used in code within a ‘#nullable’ context. CS8627: A nullable type parameter must be known to be a value-type or non-nullable reference type. Consider adding a ‘class’, ‘struct’ or type constraint.
<PropertyGroup> <LangVersion>preview</LangVersion> <Nullable>enable</Nullable> </PropertyGroup>
تغییر ماهیت نوعهای ارجاعی #C با فعالسازی NullableContextOptions
در #C ای که ما میشناسیم، رشتهها قابلیت پذیرش نال را دارند و همچنین ذکر آنها به صورت nullable بیمعنا است. اما پس از فعالسازی ویژگی نوعهای ارجاعی نالنپذیر، اکنون عکس آن رخ میدهد. رشتهها نالنپذیر میشوند؛ اما میتوان در صورت نیاز، آنها را nullable نیز تعریف کرد.
یک مثال: بررسی تاثیر فعالسازی NullableContextOptions بر روی یک پروژه
کلاس زیر را در نظر بگیرید:
public class Person { public string FirstName { get; set; } public string MiddleName { get; set; } public string LastName { get; set; } public Person(string first, string last) => (FirstName, LastName) = (first, last); public Person(string first, string middle, string last) => (FirstName, MiddleName, LastName) = (first, middle, last); public override string ToString() => $"{FirstName} {MiddleName} {LastName}"; }
در این کلاس، دو سازنده وجود دارند که یکی MiddleName را دریافت میکند و دیگری خیر. در اینجا کامپایلر تشخیص دادهاست که چون در سازندهی اولی که MiddleName را دریافت نمیکند، مقدار پیشفرض خاصیت MiddleName، نال خواهد بود و همچنین ما NullableContextOptions را نیز فعال کردهایم، بنابراین این خاصیت دیگر به صورت معمول و متداول یک نوع ارجاعی نالپذیر عمل نمیکند و دیگر نمیتوان نال را به عنوان مقدار پیشفرض آن، به آن نسبت داد. به همین جهت اخطار فوق ظاهر شدهاست.
برای رفع این مشکل:
به کامپایلر اعلام میکنیم: «میدانیم که MiddleName میتواند نال هم باشد» و آنرا در این زمینه راهنمایی میکنیم:
public string? MiddleName { get; set; }
public static class NullableReferenceTypes { //#nullable enable // Toggle to enable public static string Exemplify() { var vahid = new Person("Vahid", "N"); var length = GetLengthOfMiddleName(vahid); return $"{vahid.FirstName}'s middle name has {length} characters in it."; static int GetLengthOfMiddleName(Person person) { string middleName = person.MiddleName; return middleName.Length; } } }
در اینجا در متد محلی (local function) تعریف شده، سعی در دسترسی به خاصیت MiddleName وجود دارد و اکنون با تغییر جدیدی که اعمال کردیم، به صورت نالپذیر تعریف شدهاست.
همچنین در سطر بعدی آن نیز نتیجهی نهایی middleName، مورد استفاده قرار گرفتهاست که آن نیز مشکلدار تشخیص داده شدهاست.
مشکل اولین سطر را به این صورت میتوانیم برطرف کنیم:
var middleName = person.MiddleName;
اما مشکل سطر دوم هنوز باقی است:
علت اینجا است که متغیر middleName نیز اکنون ممکن است مقدار نال را داشته باشد. برای رفع این مشکل میتوان از اپراتور .? استفاده کرد و سپس اگر مقدار نهایی این عبارت نال بود، مقدار صفر را بازگشت میدهیم:
static int GetLengthOfMiddleName(Person person) { var middleName = person.MiddleName; return middleName?.Length ?? 0; }
امکان خاموش و روشن کردن ویژگی نوعهای ارجاعی نالنپذیر به صورت موضعی
زمانیکه خاصیت NullableContextOptions را فعال میکنیم، بر روی کل پروژه تاثیر میگذارد. برای مثال اگر یک چنین قابلیتی را بر روی پروژههای قدیمی خود فعال کنید، با صدها اخطار مواجه خواهید شد. به همین جهت است که این ویژگی حتی با فعالسازی C# 8.0 و انتخاب آن، به صورت پیشفرض غیرفعال است. بنابراین برای اینکه بتوان پروژههای قدیمی را قدم به قدم و سر فرصت، «مقاومتر» کرد، میتوان تعیین کرد که کدام قسمت، تحت تاثیر این ویژگی قرار بگیرد و کدام قسمتها خیر:
public static class NullableReferenceTypes { #nullable disable // Toggle to enable
مجبور ساختن خود به «مقاوم سازی» برنامه
اگر NullableContextOptions را فعال کنید، کامپایلر صرفا یکسری اخطار را در مورد مشکلات احتمالی صادر میکند؛ اما برنامه هنوز کامپایل میشود. برای اینکه خود را مقید به «مقاوم سازی» برنامه کنیم، میتوانیم با فعالسازی ویژگی TreatWarningsAsErrors در فایل csprj، این اخطارها را تبدیل به خطای کامپایلر کرده و از کامپایل برنامه جلوگیری کنیم:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>netcoreapp3.0</TargetFramework> <LangVersion>8.0</LangVersion> <NullableContextOptions>enable</NullableContextOptions> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> </PropertyGroup> </Project>
<WarningsAsErrors>CS8600;CS8602;CS8603</WarningsAsErrors>
آیا اگر برنامهای با C# 7.0 کامپایل شود، کتابخانههای تهیه شدهی با C# 8.0 را میتواند استفاده کند؟
پاسخ: بله. از دیدگاه برنامههای قدیمی، کتابخانههای تهیه شدهی با C# 8.0، تفاوتی با سایر کتابخانه ندارند. آنها نوعهای نالپذیر جدید را مانند ?string مشاهده نمیکنند؛ آنها فقط string را مشاهده میکنند و روش کار کردن با آنها نیز همانند قبل است. بدیهی است در این حالت از مزایای کامپایلر C# 8.0 در تشخیص زود هنگام مشکلات برنامه محروم خواهند بود؛ اما عملکرد برنامه تفاوتی نمیکند.
وضعیت برنامهی C# 8.0 ای که از کتابخانههای C# 7.0 و یا قبل از آن استفاده میکند، چگونه خواهد بود؟
چون کتابخانههای قدیمیتر از مزایای کامپایلر C# 8.0 استفاده نمیکنند، خروجیهای آن بدون بروز خطایی توسط کامپایلر C# 8.0 استفاده میشوند؛ چون حجم اخطارهای صادر شدهی در این حالت بیش از حد خواهد بود. یعنی این بررسیهای کامپایلر صرفا برای کتابخانههای جدید فعال هستند و نه برای کتابخانههای قدیمی.
مهارتهای مواجه شدن با اخطارهای ناشی از فعالسازی NullableContextOptions
در مثالی که بررسی شد، یک نمونه از روشهای مواجه شدن با اخطارهای ناشی از فعالسازی ویژگی نوعهای ارجاعی نالنپذیر را بررسی کردیم. در ادامه روشهای تکمیلی دیگری را بررسی میکنیم.
1- هرجائیکه قرار است متغیر ارجاعی نالپذیر باشد، آنرا صراحتا اعلام کنید.
string name = null; // ERROR string? name = null; // OK!
نمونهی دیگر آن مثال زیر است:
public class Person { public Address? Address { get; set; }; public string Country => Address?.Country; // ERROR! }
public class Person { public Address? Address { get; set; }; public string? Country => Address?.Country; // OK! }
البته در این حالت باید به مثال زیر دقت داشت:
var node = this; // Initialize non-nullable variable while (node != null) { node = null; // ERROR! }
Node? node = this; // Initialize nullable variable while (node != null) { node = null; // OK! }
2- نوعهای خود را مقدار دهی اولیه کنید.
در مثال زیر:
public class Person { public string Name { get; set; } // ERROR! }
public class Person { public string? Name { get; set; } }
یا یک استثناء را صادر کنید:
public class Person { public string Name { get; set; } public Person(string name) { Name = name ?? throw new ArgumentNullException(nameof(name)); } }
البته در این حالت برای مقدار دهی اولیهی Name، حتما نیاز به تعریف یک سازندهاست و در این حالت کدهایی را که از سازندهی پیشفرض استفاده کرده بودند (مانند new Person { Name = "Vahid" })، باید تغییر دهید.
راهحل دیگر، مقدار دهی اولیهی این خواص بدون تعریف یک سازندهی اضافی است:
public class Person { public string Name { get; set; } = string.Empty; // -or- public string Name { get; set; } = ""; }
String.Empty Array.Empty<T>() Enumerable.Empty<T>()
public class Person { public Address Address { get; set; } = new Address(); }
اگر با الگوهای طراحی آشنا باشید، یکی از مناسبترین الگوهای طراحی برای پیاده سازی عملیات Undo و Redo استفاده از الگوی طراحی Command هست (مطالعه بیشتر).
قدم دوم: Command باید مشخص کند که هر کاری را چه کسی باید انجام دهد:
Command در سازندهی خود ورودی از نوع Receiver دارد (در ادامه پیاده سازی خواهد شد) و در واقع میخواهد کارها را به Receiver محول نماید.
در این روش ما از delegate استفاده کردیم و به کمک آن یک واسط را بین کلاینت و Command ساختیم (Invoker).
در این الگو یک کلاینت دارم که مشخص میکند چه کاری قرار است انجام شود. یک Command داریم که میگوید هر کاری را چه کسی انجام دهد و یک Receiver داریم که میگوید هر کاری چطور انجام میشود.
قدم اول: کلاینت میخواهد عملیات Undo و Redo انجام شود. من اضافهبر این دو عملیات، عملیات Execute را هم اضافه میکنم. پس کلاینت میخواهد که سه کار Undo و Redo و Execute را انجام دهد.
public class Client { public delegate string Invoker(); public static Invoker Execute;//اضافه کردن یک آیتم جدید public static Invoker Redo;//حرکت به جلو public static Invoker Undo;//حرکت به عقب }
public class Command { public Command(Receiver receiver) { Client.Execute = receiver.Action; Client.Redo = receiver.Foreward; Client.Undo = receiver.Reverse; } }
قدم سوم: بایدمشخص شود هر کاری قرار است چگونه انجام شود:
public class Receiver { private readonly List<string> build = new List<string>(); private readonly List<string> oldBuild = new List<string>(); public string Action() { if (build.Count > 0) oldBuild.Add(build.LastOrDefault()); build.Add(build.Count.ToString(CultureInfo.InvariantCulture)); return build.LastOrDefault(); } public string Reverse() { string last = oldBuild.LastOrDefault(); if (last == null) return "EMPTY"; oldBuild.Remove(last); return last; } public string Foreward() { string oldIndex = oldBuild.LastOrDefault(); int index = oldIndex == null ? -1 : build.IndexOf(oldIndex); if ((index + 1) == build.Count) return "END"; oldBuild.Add(build.ElementAt(index + 1)); return oldBuild.LastOrDefault(); } }
اگر روش بهتری برای پیاده سازی Undo و Redo و Execute دارید، میتوانید جایگزین کنید. این اولین روشی بود که به ذهنم رسید!
قدمهای لازم برای پیاده کردن الگوی Command تا اینجا به پایان میرسند. حالا کافیاست از آن استفاده کنیم:new Command(new Receiver()); Console.WriteLine(Client.Execute()); Console.WriteLine(Client.Execute()); Console.WriteLine(Client.Undo()); Console.WriteLine(Client.Undo()); Console.WriteLine(Client.Undo()); Console.WriteLine(Client.Redo()); Console.WriteLine(Client.Redo()); Console.WriteLine(Client.Redo()); Console.WriteLine(Client.Execute());
در NHibernate چندین و چند روش، جهت تهیه کوئریها وجود دارد که QueryOver یکی از آنها است (+). QueryOver نسبت به LINQ to NH سازگاری بهتری با ساز و کار درونی NHibernate دارد؛ برای مثال امکان یکپارچگی آن با سطح دوم کش. هر چند ظاهر QueryOver با LINQ یکی است، اما در عمل متفاوتند و راه و روش خاص خودش را طلب میکند. برای مثال در LINQ to NH میتواند نوشت x.Property.Contains اما در QueryOver متدی به نام contains قابل استفاده نیست (هر چند در Intellisense ظاهر میشود اما عملا تعریف نشده است و نباید آنرا با LINQ اشتباه گرفت) و سعی در استفاده از آنها به استثناهای زیر ختم میشوند:
Unrecognised method call: System.String:Boolean StartsWith(System.String)
Unrecognised method call: System.String:Boolean Contains(System.String)
using NHibernate.Validator.Constraints;
namespace NH3Test.MappingDefinitions.Domain
{
public class Account
{
public virtual int Id { get; set; }
[NotNullNotEmpty]
[Length(Min = 3, Max = 120, Message = "طول نام باید بین 3 و 120 کاراکتر باشد")]
public virtual string Name { get; set; }
[NotNull]
public virtual int Balance { set; get; }
}
}
1) یافتن رکوردهایی که در یک مجموعهی مشخص قرار دارند. برای مثال balance آنها مساوی 10 و 12 است:
var list = new[] { 12,10};
var resultList = session.QueryOver<Account>()
.WhereRestrictionOn(p => p.Balance)
.IsIn(list)
.List();
SELECT
this_.AccountId as AccountId0_0_,
this_.Name as Name0_0_,
this_.Balance as Balance0_0_
FROM
Accounts this_
WHERE
this_.Balance in (
@p0 /* = 10 */, @p1 /* = 12 */
)
2) پیاده سازی همان متد Contains ذکر شده، در QueryOver:
var accountsContianX = session.QueryOver<Account>()
.WhereRestrictionOn(x => x.Name)
.IsLike("X", NHibernate.Criterion.MatchMode.Anywhere)
.List();
SELECT
this_.AccountId as AccountId0_0_,
this_.Name as Name0_0_,
this_.Balance as Balance0_0_
FROM
Accounts this_
WHERE
this_.Name like @p0 /* = %X% */
در اینجا بر اساس مقادیر مختلف MatchMode میتوان متدهای StartsWith (MatchMode.Start) ، EndsWith (MatchMode.End) ، Equals (MatchMode.Exact) را نیز تهیه نمود.
انجام مثال دوم راه سادهتری نیز دارد. قسمت WhereRestrictionOn و IsLike به صورت یک سری extension متد ویژه در فضای نام NHibernate.Criterion تعریف شدهاند. ابتدا این فضای نام را به کلاس جاری افزوده و سپس میتوان نوشت :
using NHibernate.Criterion;
...
var accountsContianX = session.QueryOver<Account>()
.Where(x => x.Name.IsLike("%X%"))
.List();
این فضای نام شامل چهار extension method به نامهای IsLike ، IsInsensitiveLike ، IsIn و IsBetween است.
چگونه extension method سفارشی خود را تهیه کنیم؟
بهترین کار این است که به سورس NHibernate ، فایلهای RestrictionsExtensions.cs و ExpressionProcessor.cs که تعاریف متد IsLike در آنها وجود دارد مراجعه کرد. در اینجا میتوان با نحوهی تعریف و سپس ثبت آن در رجیستری extension methods مرتبط با QueryOver توسط متد عمومی RegisterCustomMethodCall آشنا شد. در ادامه سه کار را میتوان انجام داد:
-متد مورد نظر را در کدهای خود (نه کدهای اصلی NH) اضافه کرده و سپس با فراخوانی RegisterCustomMethodCall آنرا قابل استفاده نمائید.
-متد خود را به سورس اصلی NH اضافه کرده و کامپایل کنید.
-متد خود را به سورس اصلی NH اضافه کرده و کامپایل کنید (بهتر است همان روش نامگذاری بکار گرفته شده در فایلهای ذکر شده رعایت شود). یک تست هم برای آن بنویسید (تست نویسی هم یک سری اصولی دارد (+)). سپس یک patch از آن روی آن ساخته (+) و برای تیم NH ارسال نمائید (تا جایی که دقت کردم از کلیه ارسالهایی که آزمون واحد نداشته باشند، صرفنظر میشود).
مثال:
میخواهیم extension متد جدیدی به نام Year را به QueryOver اضافه کنیم. این متد را هم بر اساس توابع توکار بانکهای اطلاعاتی، تهیه خواهیم نمود. لیست کامل این نوع متدهای بومی SQL را در فایل Dialect.cs سورسهای NH میتوان یافت (البته به صورت پیش فرض از متد extract برای جداسازی قسمتهای مختلف تاریخ استفاده میکند. این متد در فایلهای Dialect مربوط به بانکهای اطلاعاتی مختلف، متفاوت است و برحسب بانک اطلاعاتی جاری به صورت خودکار تغییر خواهد کرد).
using System;
using System.Linq.Expressions;
using NHibernate;
using NHibernate.Criterion;
using NHibernate.Impl;
namespace NH3Test.ConsoleApplication
{
public static class MyQueryOverExts
{
public static bool YearIs(this DateTime projection, int year)
{
throw new Exception("Not to be used directly - use inside QueryOver expression");
}
public static ICriterion ProcessAnsiYear(MethodCallExpression methodCallExpression)
{
string property = ExpressionProcessor.FindMemberExpression(methodCallExpression.Arguments[0]);
object value = ExpressionProcessor.FindValue(methodCallExpression.Arguments[1]);
return Restrictions.Eq(
Projections.SqlFunction("year", NHibernateUtil.DateTime, Projections.Property(property)),
value);
}
}
public class QueryOverExtsRegistry
{
public static void RegistrMyQueryOverExts()
{
ExpressionProcessor.RegisterCustomMethodCall(
() => MyQueryOverExts.YearIs(DateTime.Now, 0),
MyQueryOverExts.ProcessAnsiYear);
}
}
}
اکنون برای استفاده خواهیم داشت:
QueryOverExtsRegistry.RegistrMyQueryOverExts(); //یکبار در ابتدای اجرای برنامه باید ثبت شود
...
var data = session.QueryOver<Account>()
.Where(x => x.AddDate.YearIs(2010))
.List();
برای مثال اگر بانک اطلاعاتی انتخابی از نوع SQLite باشد، خروجی SQL مرتبط به شکل زیر خواهد بود:
SELECT
this_.AccountId as AccountId0_0_,
this_.Name as Name0_0_,
this_.Balance as Balance0_0_,
this_.AddDate as AddDate0_0_
FROM
Accounts this_
WHERE
strftime("%Y", this_.AddDate) = @p0 /* =2010 */
هر چند ما تابع year را در متد ProcessAnsiYear ثبت کردهایم اما بر اساس فایل SQLiteDialect.cs ، تعاریف مرتبط و مخصوص این بانک اطلاعاتی (مانند متد strftime فوق) به صورت خودکار دریافت میگردد و کد ما مستقل از نوع بانک اطلاعاتی خواهد بود.
نکته جالب!
LINQ to NH هم قابل بسط است؛ کاری که در ORM های دیگر به این سادگی نیست. چند مثال در این زمینه:
چگونه تابع سفارشی SQL Server خود را به صورت یک extension method تعریف و استفاده کنیم: (+) ، یک نمونه دیگر: (+) و نمونهای دیگر: (+).
نکته تکمیلی
3- تنظیم خصوصیات موجود در کلاس پایه
4 - فرم ثبت و ویرایش متناظر با یک موجودیت
در راستای تکمیل مطلب جاری و مطلب «پیاده سازی Conventional UI در ASP.NET MVC» برای رسیدن به یک قالب مشخص و جلوگیری از تکرار، میتوان به شکل زیر عمل کرد:
1- انتقال قسمتهای مشترک فرمها به یک پارشالویو به عنوان Layout فرمها
//_EntityFormLayout.cshtml @inherits EntityFormRazorPage<dynamic> @{ Layout = null; } <div class="modal-header"> <h4 class="modal-title" asp-if="IsNew">Create New @EntityDisplayName</h4> <h4 class="modal-title" asp-if="!IsNew">Edit @EntityDisplayName</h4> <button type="button" class="close" data-dismiss="modal">×</button> </div> <form asp-action="@(IsNew ? CreateActionName : EditActionName)" asp-modal-form="@FormId"> <div class="modal-body"> <input type="hidden" name="continue-editing" value="true" asp-permission="@EditPermission"/> <input asp-for="@Version" type="hidden"/> <input asp-for="@Id" type="hidden"/> @RenderBody() </div> <div class="modal-footer"> <a class="btn btn-light btn-circle" asp-modal-delete-link asp-model-id="@Id" asp-modal-toggle="false" asp-action="@DeleteActionName" asp-if="!IsNew" asp-permission="@DeletePermission" title="Delete Role"> <i class="fa fa-trash text-danger"></i> </a> <a class="btn btn-light btn-circle" title="Refresh Role" asp-if="!IsNew" asp-modal-link asp-modal-toggle="false" asp-action="@EditActionName" asp-route-id="@Id"> <i class="fa fa-repeat"></i> </a> <a class="btn btn-light btn-circle mr-auto" title="New Role" asp-modal-link asp-modal-toggle="false" asp-permission="@CreatePermission" asp-action="@CreateActionName"> <i class="fa fa-plus"></i> </a> <button type="button" class="btn btn-light" data-dismiss="modal"> <i class="fa fa-ban"></i> Cancel </button> <button type="submit" class="btn btn-outline-primary"> <i class="fa fa-save"></i> Save Changes </button> </div> </form>
با توجه به اینکه مدل متناظر با یک ویو در Layout آن نیز قابل دسترس میباشد. بدین ترتیب امکان دسترسی به خصوصیاتی مانند Id و Version یا متد IsNew وجود دارد؛ این خصوصیات در کلاس MasterModel به عنوان پایه مدل/DTO/ویومدلهای ثبت/ویرایش، تعریف شدهاند.
قراداد ما استفاده از همان مدل/DTOها به عنوان ویومدل میباشد که در سناریوهای خاص پیشنهاد شد که از مدلی با نام موجودیت + کلمه ModalViewModel یا FormViewModel استفاده شود. برای انتقال سایر دیتا و متادیتای مورد نیاز برای ساخت فرم میتوان از ViewBag و ViewData پس از امکان تعریف ویومدل پایه (دارای خصوصیات مورد نیاز Layout) که در این طراحی ممکن نیست، استفاده کرد.
2- طراحی یک EntityFormRazorPage پایهبرای رسیدن به کدی با خوانایی بالا کلاسی را به عنوان پایه ویوهای فرمها و پارشالویو EntityFormLayout، به شکل زیر طراحی میکنیم. در اینجا فرم ما یکسری خصوصیات موجود در کلاس پایه خود را مقداردهی خواهد کرد و در ادامه به دلیل ذخیره شدن این اطلاعات در ViewData، در Layout نیز قابل دسترس خواهند بود.
public abstract class EntityFormRazorPage<T> : RazorPage<T> { protected string EntityName { get => ViewData[nameof(EntityName)].ToString(); set => ViewData[nameof(EntityName)] = value; } protected string EntityDisplayName { get => ViewData[nameof(EntityDisplayName)].ToString(); set => ViewData[nameof(EntityDisplayName)] = value; } protected string DeletePermission { get => ViewData[nameof(DeletePermission)].ToString(); set => ViewData[nameof(DeletePermission)] = value; } protected string CreatePermission { get => ViewData[nameof(CreatePermission)].ToString(); set => ViewData[nameof(CreatePermission)] = value; } protected string EditPermission { get => ViewData[nameof(EditPermission)].ToString(); set => ViewData[nameof(EditPermission)] = value; } protected string CreateActionName { get => ViewData.TryGetValue(nameof(CreateActionName), out var value) ? value.ToString() : "Create"; set => ViewData[nameof(CreateActionName)] = value; } protected string EditActionName { get => ViewData.TryGetValue(nameof(EditActionName), out var value) ? value.ToString() : "Edit"; set => ViewData[nameof(EditActionName)] = value; } protected string DeleteActionName { get => ViewData.TryGetValue(nameof(DeleteActionName), out var value) ? value.ToString() : "Delete"; set => ViewData[nameof(DeleteActionName)] = value; } protected string FormId => $"{EntityName}Form"; protected bool IsNew => (Model as dynamic).IsNew(); protected string Id => (Model as dynamic).Id.ToString(CultureInfo.InvariantCulture); protected byte[] Version => (Model as dynamic).Version; }
برای این منظور لازم است کلاس پایه را با دایرکتیو inherits مشخص کرده و سپس کار تنظیم Layout و سایر خصوصیات مورد نیاز را انجام دهید:
//_BlogPartial.cshtml @inherits EntityFormRazorPage<BlogModel> @{ Layout = "_EntityFormLayout"; EntityName = "Blog"; DeletePermission = PermissionNames.Blogs_Delete; CreatePermission = PermissionNames.Blogs_Create; EditPermission = PermissionNames.Blogs_Edit; EntityDisplayName = "Blog"; }
//_BlogPartial.cshtml @inherits EntityFormRazorPage<BlogModel> @{ Layout = "_EntityFormLayout"; ... } <div class="form-group row"> <div class="col col-md-8"> <label asp-for="Title" class="col-form-label text-md-left"></label> <input asp-for="Title" autocomplete="off" class="form-control"/> <span asp-validation-for="Title" class="text-danger"></span> </div> </div> <div class="form-group row"> <div class="col"> <label asp-for="Url" class="col-form-label text-md-left"></label> <input asp-for="Url" class="form-control" type="url"/> <span asp-validation-for="Url" class="text-danger"></span> </div> </div>
و یا اگر از EditorTemplates استفاده میکنید:
//_BlogPartial.cshtml @inherits EntityFormRazorPage<BlogModel> @{ Layout = "_EntityFormLayout"; EntityName = "Blog"; DeletePermission = PermissionNames.Blogs_Delete; CreatePermission = PermissionNames.Blogs_Create; EditPermission = PermissionNames.Blogs_Edit; EntityDisplayName = "Blog"; } @Html.EditorForModel()
پ.ن: از همین روش برای ساخت لیستهای یکدست متناظر با موجودیتها نیز میتوان ایده گرفت؛ همچنین امکان تعریف و تنظیم Layoutهای متناسب با شرایط مختلف نیز در این حالت به راحتی ممکن است. در ادامه اگر در سیستم متادیتای غنی متناظر با موجودیتها وجود داشته باشد، چه بسا صرفا با مشخص کردن نام موجودیت به باقی خصوصیات تنظیم شده در کد بالا دسترسی داشته باشیم.