JQuery Datatables برای برنامه نویسان وب یک پلاگین کاربردی و معروف محسوب میشود. Angular Datatables نسخه همگام شده با AngularJs است. کار با این پلاگین خیلی ساده است و کسانی که با کارکرد Datatables آشنایی دارند هیچ مشکلی با نسخه AngularJs نخواهند داشت. از امکانات ویژه این پلاگین میتوان به Binding ساده و خودکار و امکان تغییر Optionها در سمت کنترلر و مدیریت promise اشاره کرد.
اشتراکها
در این مطلب قصد داریم کتابخانهای با قابلیت استفادهی مجدد را جهت بکارگیری «PersianDatePicker یک DatePicker شمسی به زبان JavaScript که از تاریخ سرور استفاده میکند» ارائه دهیم. نکات ارائه شدهی در آنرا میتوان جهت تبدیل و استفادهی از تمام DatePickerهای مشابه نیز بکاربرد.
نیازهای یک ورودی تاریخ سازگار با EditForm
- باید قابلیت استفادهی مجدد را داشته باشد. یعنی باید به صورت یک کامپوننت مجزا و یا به صورت یک کتابخانهی مجزا ارائه شود.
- باید با سیستم اعتبارسنجی EditForm یکپارچه باشد.
- باید جنریک باشد. یعنی باید بتوان در صورت نیاز DateTime ، DateTimeOffset و DateOnly و نمونههای nullable آنهارا توسط این کامپوننت دریافت کرد و ورودی و خروجی آن رشتهای نباشد.
نیاز به ارثبری از <InputBase<T جهت ارائهی کامپوننتهایی سازگار با EditForm
تقریبا تمام کامپوننتهای استاندارد EditForm ارائه شدهی توسط Blazor، از کامپوننت پایهای به نام <InputBase<T مشتق میشوند. این کلاس، یک کلاس abstract است که قابلیتهای بیشتری را نسبت به یک input سادهی HTML ای مانند اعتبارسنجی سازگار با EditForm ارائه میدهد. به همین جهت توصیه میشود تا اگر خواستید یک کامپوننت ورودی را برای استفادهی در Blazor و EditForm آن طراحی کنید، با ارثبری از این کلاس شروع کنید و صرفا کار را با یک input ساده، شروع نکنید.
برای استفادهی از آن، ابتدای کامپوننت Blazor ما به این صورت شروع خواهد شد:
که دو متد را برای بازنویسی در اختیار ما قرار میدهد:
علت وجود این دو متد، این است که مرورگرها، رشتهها را در اختیار ما قرار میدهند و ما باید راهی را برای تبدیل T به یک رشته و عکس آن را ارائه دهیم. با بازنویسی متد TryParseValueFromString، میتوان رشتهی دریافتی از کاربر را تبدیل به T کرد و اگر این تبدیل میسر نبود، با مقدار دهی validationErrorMessage، مشکل را به کاربر، با یک پیام شکست اعتبارسنجی، اعلام کرد. کار متد FormatValueAsString، تبدیل T به یک رشتهاست تا در input واقع در صفحه، نمایش داده شود. در اینجا میتوان فرمت خاصی را به شیء دریافتی اعمال و نمایش داد.
ایجاد یک کتابخانهی جدید برای محصور سازی DatePicker جاوااسکریپتی
چون قصد استفادهی مجدد از این کامپوننت جدید را در پروژههای مختلف داریم، بهتر است آنرا تبدیل به یک «کتابخانهی Blazor» کنیم. به همین جهت کتابخانهی فرضی BlazorPersianJavaScriptDatePicker.Lib را در اینجا ایجاد کردهایم.
در ابتدا دو فایل PersianDatePicker.js و PersianDatePicker.css موجود و مدنظر را در پوشههای js و css پوشهی wwwroot این کتابخانه کپی میکنیم. بنابراین استفاده کنندهی از آن، مانند پروژهی blazor wasm جدیدی به نام BlazorPersianJavaScriptDatePicker، باید ارجاعاتی را به آنها به صورت زیر اضافه کند:
همچنین در فایل Imports.razor_ آن نیز بهتر است فضای نام این کتابخانه، ذکر شود تا به سادگی بتوان از کامپوننت PersianDatePicker در آن استفاده کرد:
شروع به پیاده سازی کامپوننت PersianDatePicker
در ادامه کامپوننت جدید PersianDatePicker.razor را به پروژهی کتابخانه اضافه میکنیم. قسمت razor آن به صورت زیر است:
همانطور که مشاهده میکنید، کار با جنریک تعریف کردن و ارثبری از InputBase شروع میشود.
در اینجا با کلیک بر روی دکمهی 📅، کار فراخوانی متد PersianDatePicker.Show مربوط به datePicker جاوا اسکریپتی صورت میگیرد. همچنین هر طراحی را که در اینجا ارائه دهیم، قالب UI پیشفرض InputBase را بازنویسی میکند.
نیاز به دریافت تاریخ تنظیم شدهی توسط کدهای جاوااسکریپتی در کامپوننت Blazor
کتابخانههای جاوااسکریپتی با مقداردهی مستقیم textbox.value سبب تغییر مقدار آن میشوند. نکتهی مهم اینجا است که نه فقط Blazor این تغییرات را ردیابی نمیکند، بلکه اگر با استفاده از متد استاندارد جاوااسکریپتی addEventListener به تغییرات این input گوش فرا دهیم، هیچ رخدادی را مشاهده نخواهیم کرد. به همین جهت نیاز است اندکی کدهای PersianDatePicker.js را تغییر دهیم (و این مورد جهت تمام کتابخانههای مشابه یکسان است):
در اینجا پس از تغییر خاصیت value، باید به صورت دستی سبب بروز رخداد change شد تا addEventListenerها بتوانند این رخداد را ردیابی کنند. به همین جهت فایل مجزایی را به نام wwwroot\js\activateDatePicker.js به کتابخانهی blazor اضافه میکنیم:
هدف از این کدها این است که جهت element یا همان datePicker جاری، بتوان رخداد change را ثبت کرد و به تغییرات آن گوش فرا داد تا هر زمانیکه کدهای جاوا اسکریپتی datePicker سبب تغییری در خاصیت value شدند، بتوان آنرا به کامپوننت Blazor ارسال کرد. وهلهای از این کامپوننت توسط objectReference در اینجا دریافت شده و سپس متد OnInputFieldChanged کامپوننت را با مقدار جدید وارد شده، فراخوانی میکند.
بنابراین این فایل جدید نیز باید به index.html مصرف کننده اضافه شود:
فعالسازی DatePicker در اولین بار نمایش کامپوننت Blazor
تا اینجا زیرساخت دریافت مقدار تنظیمی توسط کاربر را در کامپوننت Blazor فراهم کردیم. اکنون نوبت به استفادهی از آن است:
- اگر دقت کرده باشید در تعاریف razor کامپوننت، "ref="ElementReference@ وجود دارد که یک ElementReference است و توسط آن میتوان در متد OnAfterRenderAsync، ارجاعی را به المان جاری، به کدهای جاوااسکریپتی متد enableDatePicker ارسال کرد.
- همچنین چون نمیخواهیم متد OnInputFieldChanged را به صورت static تعریف کنیم، نیاز است تا یک DotNetObjectReference را ایجاد و به متد enableDatePicker ارسال کرد تا توسط آن بتوان به یک instance method کلاس جاری دسترسی یافت و به سادگی مقادیر کامپوننت را تغییر داد:
- در پایان کار کامپوننت، باید این DotNetObjectReference را Dispose کرد.
نیاز به تبدیل T به تاریخ رشتهای و برعکس
زیر ساخت تبدیلات جنریک تاریخ میلادی به شمسی در کتابخانهی « DNTPersianUtils.Core » پیشبینی شدهاست و فقط کافی است از آن استفاده کنیم. با وجود این زیرساخت، تهیهی کامپوننتهای جنریک تاریخ شمسی بسیار ساده میشود:
در اینجا قسمت نهایی و تکمیلی کامپوننت محصور کنندهی DatePicker را مشاهده میکنید که بسیار سادهاست:
- InputBase به همراه یک خاصیت عمومی دوطرفهی Value است که امکان تعریفی مانند bind-Value@ را میسر میکند.
- این Value به همراه یک خاصیت متناظر رشتهای به نام CurrentValueAsString نیز هست که در اینجا از آن استفاده میکنیم و کار با آن، بایندینگ دوطرفه و همچنین اعتبارسنجی خودکار و فعالسازی متدهای بازنویسی شدهی InputBase را میسر میکند.
- پیاده سازی متدهای بازنویسی شدهی جنریک TryParseValueFromString و FormatValueAsString، با استفاده از دو متد TryParsePersianDateToDateTimeOrDateTimeOffset و FormatDateToShortPersianDate کتابخانهی « DNTPersianUtils.Core » انجام شدهاند و اصل کار تهیهی یک کامپوننت جنریک تاریخ شمسی را انجام میدهند.
استفادهی از کامپوننت Blazor تهیه شده
یک کامپوننت تاریخ شمسی باید بتواند تمام حالات و نوعهای زیر را پوشش دهد که به لطف جنریک بودن کامپوننت تهیه شده، این امر میسر است:
سپس از این کامپوننت، در صفحهی Index مثال پیوست به صورت زیر استفاده شدهاست:
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: BlazorPersianJavaScriptDatePicker.zip
نیازهای یک ورودی تاریخ سازگار با EditForm
- باید قابلیت استفادهی مجدد را داشته باشد. یعنی باید به صورت یک کامپوننت مجزا و یا به صورت یک کتابخانهی مجزا ارائه شود.
- باید با سیستم اعتبارسنجی EditForm یکپارچه باشد.
- باید جنریک باشد. یعنی باید بتوان در صورت نیاز DateTime ، DateTimeOffset و DateOnly و نمونههای nullable آنهارا توسط این کامپوننت دریافت کرد و ورودی و خروجی آن رشتهای نباشد.
نیاز به ارثبری از <InputBase<T جهت ارائهی کامپوننتهایی سازگار با EditForm
تقریبا تمام کامپوننتهای استاندارد EditForm ارائه شدهی توسط Blazor، از کامپوننت پایهای به نام <InputBase<T مشتق میشوند. این کلاس، یک کلاس abstract است که قابلیتهای بیشتری را نسبت به یک input سادهی HTML ای مانند اعتبارسنجی سازگار با EditForm ارائه میدهد. به همین جهت توصیه میشود تا اگر خواستید یک کامپوننت ورودی را برای استفادهی در Blazor و EditForm آن طراحی کنید، با ارثبری از این کلاس شروع کنید و صرفا کار را با یک input ساده، شروع نکنید.
برای استفادهی از آن، ابتدای کامپوننت Blazor ما به این صورت شروع خواهد شد:
@typeparam T @inherits InputBase<T>
protected override bool TryParseValueFromString( string? value, [MaybeNullWhen(false)] out T result, [NotNullWhen(false)] out string? validationErrorMessage) { // ... } protected override string FormatValueAsString(T? value) { // ... }
ایجاد یک کتابخانهی جدید برای محصور سازی DatePicker جاوااسکریپتی
چون قصد استفادهی مجدد از این کامپوننت جدید را در پروژههای مختلف داریم، بهتر است آنرا تبدیل به یک «کتابخانهی Blazor» کنیم. به همین جهت کتابخانهی فرضی BlazorPersianJavaScriptDatePicker.Lib را در اینجا ایجاد کردهایم.
در ابتدا دو فایل PersianDatePicker.js و PersianDatePicker.css موجود و مدنظر را در پوشههای js و css پوشهی wwwroot این کتابخانه کپی میکنیم. بنابراین استفاده کنندهی از آن، مانند پروژهی blazor wasm جدیدی به نام BlazorPersianJavaScriptDatePicker، باید ارجاعاتی را به آنها به صورت زیر اضافه کند:
<link href="_content/BlazorPersianJavaScriptDatePicker.Lib/css/PersianDatePicker.css" rel="stylesheet"/> <script src="_content/BlazorPersianJavaScriptDatePicker.Lib/js/PersianDatePicker.js?v=1"></script>
@using BlazorPersianJavaScriptDatePicker.Lib
شروع به پیاده سازی کامپوننت PersianDatePicker
در ادامه کامپوننت جدید PersianDatePicker.razor را به پروژهی کتابخانه اضافه میکنیم. قسمت razor آن به صورت زیر است:
@typeparam T @inherits InputBase<T> <div> <span style="cursor:pointer" onclick="PersianDatePicker.Show(document.getElementById('@ElementId'), '@Today')"> 📅 </span> <input @attributes="@AdditionalAttributes" type="text" dir="ltr" @ref="ElementReference" name="@ElementId" id="@ElementId" autocapitalize="off" autocorrect="off" autocomplete="off" value="@EnteredValue" @oninput="OnInput"/> @if (ValueExpression is not null) { <ValidationMessage For="@ValueExpression"/> } </div>
در اینجا با کلیک بر روی دکمهی 📅، کار فراخوانی متد PersianDatePicker.Show مربوط به datePicker جاوا اسکریپتی صورت میگیرد. همچنین هر طراحی را که در اینجا ارائه دهیم، قالب UI پیشفرض InputBase را بازنویسی میکند.
نیاز به دریافت تاریخ تنظیم شدهی توسط کدهای جاوااسکریپتی در کامپوننت Blazor
کتابخانههای جاوااسکریپتی با مقداردهی مستقیم textbox.value سبب تغییر مقدار آن میشوند. نکتهی مهم اینجا است که نه فقط Blazor این تغییرات را ردیابی نمیکند، بلکه اگر با استفاده از متد استاندارد جاوااسکریپتی addEventListener به تغییرات این input گوش فرا دهیم، هیچ رخدادی را مشاهده نخواهیم کرد. به همین جهت نیاز است اندکی کدهای PersianDatePicker.js را تغییر دهیم (و این مورد جهت تمام کتابخانههای مشابه یکسان است):
function setValue(date) { _textBox.value = date; // NOTE: To notify the addEventListener('change', fn) _textBox.dispatchEvent(new Event('change')); _textBox.focus(); hide(); try { _textBox.onchange(); }catch(ex) {} }
window.activateDatePicker = { enableDatePicker: function (element, objectReference) { element.addEventListener('change', function (evt) { objectReference.invokeMethodAsync("OnInputFieldChanged", this.value); }); } };
بنابراین این فایل جدید نیز باید به index.html مصرف کننده اضافه شود:
<script src="_content/BlazorPersianJavaScriptDatePicker.Lib/js/activateDatePicker.js?v=1"></script>
فعالسازی DatePicker در اولین بار نمایش کامپوننت Blazor
تا اینجا زیرساخت دریافت مقدار تنظیمی توسط کاربر را در کامپوننت Blazor فراهم کردیم. اکنون نوبت به استفادهی از آن است:
public partial class PersianDatePicker<T> : IDisposable { private bool _isDisposed; private DotNetObjectReference<PersianDatePicker<T>>? _objectReference; private string ElementId { get; } = Guid.NewGuid().ToString("N"); private ElementReference? ElementReference { set; get; } private string Today { get; } = DateTime.Now.ToShortPersianDateString(); [Inject] private IJSRuntime JsRuntime { set; get; } = default!; public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { _objectReference = DotNetObjectReference.Create(this); await JsRuntime.InvokeVoidAsync("activateDatePicker.enableDatePicker", ElementReference, _objectReference); EnteredValue = CurrentValueAsString; StateHasChanged(); } } protected override void Dispose(bool disposing) { base.Dispose(disposing); if (!_isDisposed) { try { _objectReference?.Dispose(); } finally { _isDisposed = true; } } } }
- همچنین چون نمیخواهیم متد OnInputFieldChanged را به صورت static تعریف کنیم، نیاز است تا یک DotNetObjectReference را ایجاد و به متد enableDatePicker ارسال کرد تا توسط آن بتوان به یک instance method کلاس جاری دسترسی یافت و به سادگی مقادیر کامپوننت را تغییر داد:
[JSInvokable] public void OnInputFieldChanged(string? value)
نیاز به تبدیل T به تاریخ رشتهای و برعکس
زیر ساخت تبدیلات جنریک تاریخ میلادی به شمسی در کتابخانهی « DNTPersianUtils.Core » پیشبینی شدهاست و فقط کافی است از آن استفاده کنیم. با وجود این زیرساخت، تهیهی کامپوننتهای جنریک تاریخ شمسی بسیار ساده میشود:
public partial class PersianDatePicker<T> : IDisposable { private string? _enteredValue; private string? EnteredValue { set => _enteredValue = value; get => UsePersianNumbers ? _enteredValue.ToPersianNumbers() : _enteredValue; } [Parameter] public bool UsePersianNumbers { set; get; } [Parameter] public string ParsingErrorMessage { get; set; } = "لطفا در ورودی {0} تاریخ شمسی معتبری را وارد نمائید."; [Parameter] public int BeginningOfCentury { set; get; } = 1400; private void OnInput(ChangeEventArgs e) { SetCurrentValue(e.Value as string); } private void SetCurrentValue(string? value) { EnteredValue = value; CurrentValueAsString = value; } [JSInvokable] public void OnInputFieldChanged(string? value) { SetCurrentValue(value); } protected override void OnInitialized() { base.OnInitialized(); SanityCheck(); } protected override bool TryParseValueFromString( string? value, [MaybeNullWhen(false)] out T result, [NotNullWhen(false)] out string? validationErrorMessage) { validationErrorMessage = string.Format(CultureInfo.InvariantCulture, ParsingErrorMessage, DisplayName); if (!value.TryParsePersianDateToDateTimeOrDateTimeOffset(out result, BeginningOfCentury)) { return false; } if (result is null) { throw new InvalidOperationException(validationErrorMessage); } validationErrorMessage = null; return true; } protected override string FormatValueAsString(T? value) { return !string.IsNullOrWhiteSpace(EnteredValue) ? EnteredValue : value.FormatDateToShortPersianDate(); } private void SanityCheck() { if (!Value.IsDateTimeOrDateTimeOffsetType()) { throw new InvalidOperationException( "The `Value` type is not a supported `date` type. DateTime, DateTime?, DateTimeOffset and DateTimeOffset? are supported."); } } // ... }
- InputBase به همراه یک خاصیت عمومی دوطرفهی Value است که امکان تعریفی مانند bind-Value@ را میسر میکند.
- این Value به همراه یک خاصیت متناظر رشتهای به نام CurrentValueAsString نیز هست که در اینجا از آن استفاده میکنیم و کار با آن، بایندینگ دوطرفه و همچنین اعتبارسنجی خودکار و فعالسازی متدهای بازنویسی شدهی InputBase را میسر میکند.
- پیاده سازی متدهای بازنویسی شدهی جنریک TryParseValueFromString و FormatValueAsString، با استفاده از دو متد TryParsePersianDateToDateTimeOrDateTimeOffset و FormatDateToShortPersianDate کتابخانهی « DNTPersianUtils.Core » انجام شدهاند و اصل کار تهیهی یک کامپوننت جنریک تاریخ شمسی را انجام میدهند.
استفادهی از کامپوننت Blazor تهیه شده
یک کامپوننت تاریخ شمسی باید بتواند تمام حالات و نوعهای زیر را پوشش دهد که به لطف جنریک بودن کامپوننت تهیه شده، این امر میسر است:
using System.ComponentModel.DataAnnotations; namespace BlazorPersianJavaScriptDatePicker.ViewModels; public class InputPersianDateViewModel { [Required] public string Name { set; get; } = default!; [Required] public DateTime BirthDayGregorian { set; get; } = DateTime.Now.AddYears(-40); public DateTime? LoginAt { set; get; } = DateTime.Now.AddMinutes(-2); [Required] public DateTimeOffset LogoutAt { set; get; } public DateTimeOffset? RegisterAt { set; get; } = DateTimeOffset.Now.AddMinutes(-10); }
<EditForm Model="Model" OnValidSubmit="DoSave"> <DataAnnotationsValidator/> <div> <label>تاریخ تولد</label> <div> <PersianDatePicker @bind-Value="Model.BirthDayGregorian" UsePersianNumbers="false" /> </div> </div> <button type="submit">ارسال</button> </EditForm>
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: BlazorPersianJavaScriptDatePicker.zip
اشتراکها
Angular v13 منتشر شد
نظرات مطالب
بررسی مشکلات AngularJS 1.x
اگر Angular 1 and Angular 2 integration قسمت why upgrade را مطالعه کنید میبینید که بیشتر مشکلات برطرف شده اند.
حتی Future work مثل Server-side rendering خیلی جذاب و مفید میتونه باشه.
در قسمت قبلی درباره علت نیاز به الگوهای طراحی در JavaScript و Function Spaghetti code صحبت شد. در این قسمت Closure در JavaScript مورد بررسی قرار میگیرد.
در JavaScript میتوان توابع تو در تو نوشت (nested functions) ، زمانی که یک تابع درون تابع دیگر تعریف میشود تابع درونی به تمام متغیرها و توابع تابع بیرونی (Parent) دسترسی دارد.
Douglas Crockford برای تعریف Closure میگوید :
an inner function always has access to the vars and parameters of its outer function, even after the outer
function has returned
یک تابع درونی (nested) همیشه به متغیرها و پارامترها تابع بیرونی دسترسی دارد ، حتی اگر تابع بیرونی مقدار برگردانده باشد.
تابع زیر را در نظر بگیرید :
// The getDate() function returns a nested function which refers the 'date' variable defined // by outer function getDate() function getDate() { var date = new Date(); // This variable stays around even after function returns // nested function return function () { return date.getMilliseconds(); } }
اکنون اگر به صورت زیر تابع getDate فراخوانی شود مشاهده میشود که تابع درونی (با کامنت nested function مشخص شده است.) به شیء date دسترسی دارد.
// Once getDate() is executed the variable date should be out of scope and it is, but since // the inner function // referenes date, this value is available to the inner function. var dt = getDate(); alert(dt()); alert(dt());
خروجی هر 2 alert یک مقدار خواهد بود.
اگر از فردی که به تازگی رو به JavaScript آورده است خواسته شود تابعی بنویسد که میلی ثانیهی زمان جاری را برگداند احتمالا همچین کدی تحویل میدهد :
function myNonClosure() { var date = new Date(); return date.getMilliseconds(); }
در کد بالا پس از اجرای myNonClosure متغیر date از بین خواهد رفت ، این مسئله در دنیای JavaScript طبیعی هست.
این مثال را در نظر بگیرید :
var MyDate = function () { var date = new Date(); var getMilliSeconds = function () { return date.getMilliseconds(); } } var dt = new MyDate(); alert(dt.getMilliSeconds()); // This will throw error as getMilliSeconds is not accessible.
در صورت اجرای مثال بالا خطایی با این مضمون دریافت خواهد شد که getMilliSeconds دستیابی پذیر نیست. (کپسوله شده) برای اینکه آن را دستیابی پذیر کنیم کد را به این صورت تغییر میدهیم :
// This is closure var MyDate = function () { var date = new Date(); // variable stays around even after function returns var getMilliSeconds = function () { return date.getMilliseconds(); }; return { getMs : getMilliSeconds } }
آنچه در تابع بالا انجام شده کپسوله سازی همهی منطق کار (منطق کار در اینجا برگرداندن میلی ثانیه زمان جاری میباشد) در یک فضای نام به نام MyDate میباشد. همچنین فقط متدهای عمومی در اختیار استفاده کننده این تابع قرار داده شده است. برای استفاده میتوان بدین صورت عمل کرد :
var dt = new MyDate(); alert(dt.getMs()); // This should work.
در کد بالا برای توابع و متغیرهای درونی یک container ایجاد کردیم که باعث جلوگیری از تداخل در نام متغیرها با دیگر کدها خواهد شد . (برای مشاهدهی تداخلها به قسمت قبلی توجه کنید.)
اگر بخواهیم Closure را تشبیه کنیم ، Closure شبیه به کلاسها در C# یا Java هست.
Closure یک حوزه (scope) برای متغیرها و توابع درونی خودش ایجاد میکند.
jQuery بهترین مثال کاربردی برای Closure میباشد :
(function($) { // $() is available here })(jQuery);
در ادامه این مفاهیم بیشتر توضیح داده میشودند ، اکنون میخواهیم مشکلی که در قسمت قبلی مطرح کردیم به کمک Closure حل کنیم :
در آن مثال گفته شد که اگر :
// file1.js function saveState(obj) { // write code here to saveState of some object alert('file1 saveState'); } // file2.js (remote team or some third party scripts) function saveState(obj, obj2) { // further code... alert('file2 saveState"); }
اگر تابعی به نام saveState در 2 فایل مختلف داشته باشیم و این 2 فایل را بدین صورت در برنامه آدرس دهیم :
<script src="file1.js" type="text/javascript"></script> <script src="file2.js" type="text/javascript"></script>
تابع saveState در فایل دوم تابع saveState فایل اول را override میکند. یک از توابع بالا را به صورت زیر باز نویسی میکنیم و منطق کار را کپسوله میکنیم :
function App() { var save = function (o) { // write code to save state here.. // you have acces to 'o' here... alert(o); }; return { saveState: save }; }
بدون نگرانی تداخل saveState با بقیه saveStateها در هر پلاگین یا فایل دیگری میتوان از saveState میتوان اینگونه استفاده کرد :
var app = new App(); app.saveState({ name: "rajesh"});
برای اطلاعات بیشتر در مورد Closure ها این لینک را بررسی کنید.
روشهای زیادی برای بازگشت چندین مقدار از یک متد وجود دارند؛ مانند استفادهی از آرایهها برای بازگشت اشیایی از یک جنس، ایجاد یک کلاس سفارشی با خواص متفاوت و استفاده از پارامترهای out و ref همانند روشهای متداول در C و ++C. در این بین روش دیگری نیز به نام Tuples از زمان NET 4.0. برای بازگشت چندین شیء با نوعهای مختلف، ارائه شدهاست که در C# 7 نحوهی تعریف و استفادهی از آنها بهبود قابل ملاحظهای یافتهاست.
Tuple چیست؟
هدف از کار با Tupleها، عدم تعریف یک کلاس جدید به همراه خواص آن، جهت بازگشت بیش از یک مقدار از یک متد، توسط وهلهای از این کلاس جدید میباشد. برای مثال اگر بخواهیم از متدی، دو مقدار شهر و ناحیه را بازگشت دهیم، یک روش آن، ایجاد کلاس مکان زیر است:
و سپس، وهله سازی و بازگشت آن:
اما توسط Tuples، بدون نیاز به تعریف یک کلاس جدید، باز هم میتوان به همین دو خروجی، دسترسی یافت:
مشکلات نوع Tuple در نگارشهای قبلی دات نت
هرچند Tuples از زمان دات نت 4 در دسترس هستند، اما دارای این کمبودها و مشکلات میباشند:
الف) پارامترهای خروجی آنها ثابت و با نامهایی مانند Item1، Item2 و امثال آن هستند که در حین استفاده، به علت ضعف نامگذاری، کاربرد آنها دقیقا مشخص نیست و کاملا بیمعنا هستند:
ب) Reference Type هستند (کلاس هستند) و در زمان وهله سازی، میزان مصرف حافظهی بیشتری را نسبت به Value Types (معادل Tuples در C# 7) دارند.
ج) Tuples در دات نت 4، صرفا یک کتابخانهی اضافه شدهی به فریم ورک بوده و زبانهای دات نتی، پشتیبانی توکاری را از آنها جهت بهبود و یا ساده سازی تعریف آنها، ارائه نمیدهند.
ایجاد Tuples در C# 7
برای ایجاد Tuples در سی شارپ 7، از پرانتزها به همراه ذکر نام و نوع پارامترها استفاده میشود.
در مثال فوق، یک Tuple ایجاد شدهاست و در آن مقدار 3 به x1 و مقدار "one" به s1 انتساب داده شدهاند. به این عملیات deconstruction هم میگویند.
دسترسی به این مقادیر نیز همانند متغیرهای معمولی است.
اگر سعی کنیم این قطعه کد را کامپایل نمائیم، با خطای ذیل متوقف خواهیم شد:
برای رفع این مشکل نیاز است بستهی نیوگت ذیل را نیز نصب کرد:
تعاریف متغیرهای بازگشتی، خارج از پرانتزها هم میتوانند صورت گیرند:
بازگشت Tuples از متدها
متد ذیل، دو خروجی نتیجه و باقیماندهی تقسیم دو عدد صحیح را باز میگرداند:
برای این منظور، نوع خروجی متد به صورت (int, int) و همچنین مقدار بازگشتی نیز به صورت یک Tuple از نتیجه و باقیماندهی تقسیم، تعریف شدهاست.
در ادامه نحوهی استفادهی از این متد را مشاهده میکنید:
در اینجا امکان استفادهی از var نیز برای تعریف نوع متغیرهای دریافتی از یک Tuple نیز وجود دارد و کامپایلر به صورت خودکار نوع آنها را بر اساس نوع خروجی tuple مشخص میکند:
و یا حتی چون نوع var پارامترها در اینجا یکی است و در هر دو حالت به int اشاره میکند، میتوان این var را در خارج از پرانتز هم قرار داد:
و یا برای نمونه متد GetHumanData دات نت 4 ابتدای بحث را به صورت ذیل میتوان در C# 7 بازنویسی کرد:
و سپس به نحو واضحتری از آن استفاده نمود؛ بدون استفادهی اجباری از Item1 و غیره (هرچند هنوز هم میتوان از آنها استفاده کرد):
پشت صحنهی Tuples در C# 7
همانطور که عنوان شد، برای اینکه بتوانید قطعه کدهای فوق را کامپایل کنید، نیاز به بستهی نیوگت System.ValueTuple است. در حقیقت کامپایلر خروجی متد فوق را به نحو ذیل تفسیر میکند:
برای مثال قطعه کد
توسط کامپایلر به قطعه کد ذیل ترجمه میشود:
- برخلاف نگارشهای پیشین دات نت که Tuples در آنها reference type بودند، این ValueTuple یک struct است و به همین جهت سربار تخصیص حافظهی کمتری را به همراه داشته و از لحاظ کارآیی و میزان مصرف حافظه بهینهتر عمل میکند.
- همچنین در اینجا محدودیتی از لحاظ تعداد پارامترهای ذکر شدهی در یک Tuple وجود ندارد.
در اینجا هم مانند قبل (دات نت 4) 8 آیتم را میتوان تعریف کرد؛ اما چون آخرین آیتم ValueTuple تعریف شده نیز یک Tuple است، در عمل محدودیتی از نظر تعداد پارامتر نخواهیم داشت.
مفهوم Tuple Literals
همانند نگارشهای پیشین دات نت، خروجی یک Tuple را میتوان به یک متغیر از نوع var و یا ValueType نیز نسبت داد:
در این حالت برای دسترسی به مقادیر Tuple همانند قبل باید از فیلدهای Item1 و Item2 و ... استفاده کرد.
به علاوه در سی شارپ 7 میتوان برای اعضای یک Tuple نام نیز تعریف کرد که به آنها Tuple literals گویند:
در این حالت زمانیکه Tuple به یک متغیر از نوع var نسبت داده میشود، میتوان به خروجی آن بر اساس نامهای اعضای Tuple، بجای ذکر Item1 و ... دسترسی یافت که خوانایی بیشتری دارند.
و یا هنگام تعریف نوع خروجی، میتوان نام پارامترهای متناظر را نیز ذکر کرد که به آن named elements هم میگویند:
و نمونهای از کاربرد آن به صورت ذیل است که در اینجا خروجی Tuple صرفا به یک متغیر از نوع var نسبت داده شدهاست و توسط نام پارامترهای خروجی متد، میتوان به اعضای Tuple دسترسی یافت.
مفهوم Deconstructing Tuples
مفهوم deconstruction که در ابتدای بحث عنوان شد صرفا مختص به Tuples نیست. در C# 7 میتوان مشخص کرد که چگونه یک نوع خاص، به اجزای آن تجزیه شود. برای مثال کلاس شخص ذیل را درنظر بگیرید:
- در اینجا یک متد جدید را به نام Deconstruct مشاهده میکنید. کار این متد جدید که توسط کامپایلر استفاده خواهد شد، ارائهی روشی است برای «تجزیهی» یک نوع، به یک Tuple. متد Deconstruct تعریف شدهی در اینجا توسط پارامترهایی از نوع out، دو خروجی را مشخص میکنند. امکان تعریف این متد ویژه، به صورتیکه یک Tuple را بازگرداند، وجود ندارد.
- علت تعریف این دو خروجی هم به constructor و یا سازندهی کلاس بر میگردد که دو ورودی را دریافت میکند. اگر یک کلاس چندین سازنده داشته باشد، به همان تعداد میتوان متد Deconstruct تعریف کرد؛ به همراه خروجیهایی متناظر با نوع پارامترهای سازندهها.
- علت استفادهی از نوع خروجی out نیز این است که در #C نمیتوان چندین overload را صرفا بر اساس نوع خروجیهای متفاوت متدها تعریف کرد.
- متد Deconstruct به صورت خودکار در زمان تجزیهی یک شیء به یک tuple فراخوانی میشود. در مثال زیر، شیء p1 به یک Tuple تجزیه شدهاست و این تجزیه بر اساس متد Deconstruct این کلاس مفهوم پیدا میکند:
امکان تعریف متد Deconstruct، به صورت یک متد الحاقی
روش اول تعریف متد ویژهی Deconstruct را در مثال قبل، در داخل کلاس اصلی مشاهده کردید. روش دیگر آن، استفادهی از متدهای الحاقی است که در این مورد خاص نیز مجاز است:
در اینجا کلاس مستطیل دارای سازندهای با دو پارامتر است؛ اما متد Deconstruct آن به صورت یک متد الحاقی، خارج از کلاس اصلی تعریف شدهاست.
اکنون امکان انتساب وهلهای از این کلاس به یک Tuple وجود دارد:
امکان جایگزین کردن Anonymous types با Tuples
قطعه کد ذیل را در نظر بگیرید:
در اینجا خروجی LINQ تهیه شده یک لیست anonymously typed است؛ با محدودیتهایی مانند عدم امکان استفادهی از خروجی آن در سایر اسمبلیها. این نوعهای ویژه تنها محدود هستند به همان اسمبلی که در آن تعریف میشوند. اما در C# 7 میتوان قطعه کد فوق را با Tuples به صورت ذیل بازنویسی کرد که این محدودیتها را هم ندارد (با هدف به حداقل رساندن تعداد ViewModelهای تعریفی یک برنامه):
سایر کاربردهای Tuples
از Tuples صرفا برای تعریف چندین خروجی از یک متد استفاده نمیشود. در ذیل نحوهی استفادهی از آنها را جهت تعریف کلید ترکیبی یک شیء دیکشنری و یا استفادهی از آنها را در آرگومان جنریک یک متد async هم مشاهده میکنید:
Tuple چیست؟
هدف از کار با Tupleها، عدم تعریف یک کلاس جدید به همراه خواص آن، جهت بازگشت بیش از یک مقدار از یک متد، توسط وهلهای از این کلاس جدید میباشد. برای مثال اگر بخواهیم از متدی، دو مقدار شهر و ناحیه را بازگشت دهیم، یک روش آن، ایجاد کلاس مکان زیر است:
public class Location { public string City { get; set; } public string State { get; set; } public Location(string city, string state) { City = city; State = state; } }
var location = new Location("Lake Charles","LA");
var location = new Tuple<string,string>("Lake Charles","LA"); // Print out the address var address = $"{location.Item1}, {location.Item2}";
مشکلات نوع Tuple در نگارشهای قبلی دات نت
هرچند Tuples از زمان دات نت 4 در دسترس هستند، اما دارای این کمبودها و مشکلات میباشند:
static Tuple<int, string, string> GetHumanData() { return Tuple.Create(10, "Marcus", "Miller"); }
var data = GetHumanData(); Console.WriteLine("What is this value {0} or this {1}", data.Item1, data.Item3);
ج) Tuples در دات نت 4، صرفا یک کتابخانهی اضافه شدهی به فریم ورک بوده و زبانهای دات نتی، پشتیبانی توکاری را از آنها جهت بهبود و یا ساده سازی تعریف آنها، ارائه نمیدهند.
ایجاد Tuples در C# 7
برای ایجاد Tuples در سی شارپ 7، از پرانتزها به همراه ذکر نام و نوع پارامترها استفاده میشود.
(int x1, string s1) = (3, "one"); Console.WriteLine($"{x1} {s1}");
دسترسی به این مقادیر نیز همانند متغیرهای معمولی است.
اگر سعی کنیم این قطعه کد را کامپایل نمائیم، با خطای ذیل متوقف خواهیم شد:
error CS8179: Predefined type 'System.ValueTuple`2' is not defined or imported
PM> install-package System.ValueTuple
تعاریف متغیرهای بازگشتی، خارج از پرانتزها هم میتوانند صورت گیرند:
int x2; string s2; (x2, s2) = (42, "two"); Console.WriteLine($"{x2} {s2}");
بازگشت Tuples از متدها
متد ذیل، دو خروجی نتیجه و باقیماندهی تقسیم دو عدد صحیح را باز میگرداند:
static (int, int) Divide(int x, int y) { int result = x / y; int reminder = x % y; return (result, reminder); }
در ادامه نحوهی استفادهی از این متد را مشاهده میکنید:
(int result, int reminder) = Divide(11, 3); Console.WriteLine($"{result} {reminder}");
در اینجا امکان استفادهی از var نیز برای تعریف نوع متغیرهای دریافتی از یک Tuple نیز وجود دارد و کامپایلر به صورت خودکار نوع آنها را بر اساس نوع خروجی tuple مشخص میکند:
(var result1, var reminder1) = Divide(11, 3); Console.WriteLine($"{result1} {reminder1}");
var (result1, reminder1) = Divide(11, 3);
و یا برای نمونه متد GetHumanData دات نت 4 ابتدای بحث را به صورت ذیل میتوان در C# 7 بازنویسی کرد:
static (int, string, string) GetHumanData() { return (10, "Marcus", "Miller"); }
(int Age, string FirstName, string LastName) results = GetHumanData(); Console.WriteLine(results.Age); Console.WriteLine(results.FirstName); Console.WriteLine(results.LastName);
پشت صحنهی Tuples در C# 7
همانطور که عنوان شد، برای اینکه بتوانید قطعه کدهای فوق را کامپایل کنید، نیاز به بستهی نیوگت System.ValueTuple است. در حقیقت کامپایلر خروجی متد فوق را به نحو ذیل تفسیر میکند:
ValueTuple<int, int> tuple1 = Divide(11, 3);
(int, int) n = (1,1); System.Console.WriteLine(n.Item1);
ValueTuple<int, int> n = new ValueTuple<int, int>(1, 1); System.Console.WriteLine(n.Item1);
- همچنین در اینجا محدودیتی از لحاظ تعداد پارامترهای ذکر شدهی در یک Tuple وجود ندارد.
(int,int,int,int,int,int,int,(int,int))
مفهوم Tuple Literals
همانند نگارشهای پیشین دات نت، خروجی یک Tuple را میتوان به یک متغیر از نوع var و یا ValueType نیز نسبت داد:
var tuple2 = ("Stephanie", 7); Console.WriteLine($"{tuple2.Item1}, {tuple2.Item2}");
به علاوه در سی شارپ 7 میتوان برای اعضای یک Tuple نام نیز تعریف کرد که به آنها Tuple literals گویند:
var tuple3 = (Name: "Matthias", Age: 6); Console.WriteLine($"{tuple3.Name} {tuple3.Age}");
و یا هنگام تعریف نوع خروجی، میتوان نام پارامترهای متناظر را نیز ذکر کرد که به آن named elements هم میگویند:
static (int radius, double area) CalculateAreaOfCircle(int radius) { return (radius, Math.PI * Math.Pow(radius, 2)); }
var circle = CalculateAreaOfCircle(2); Console.WriteLine($"A circle of radius, {circle.radius}," + $" has an area of {circle.area:N2}.");
مفهوم Deconstructing Tuples
مفهوم deconstruction که در ابتدای بحث عنوان شد صرفا مختص به Tuples نیست. در C# 7 میتوان مشخص کرد که چگونه یک نوع خاص، به اجزای آن تجزیه شود. برای مثال کلاس شخص ذیل را درنظر بگیرید:
class Person { private readonly string _firstName; private readonly string _lastName; public Person(string firstname, string lastname) { _firstName = firstname; _lastName = lastname; } public override String ToString() => $"{_firstName} {_lastName}"; public void Deconstruct(out string firstname, out string lastname) { firstname = _firstName; lastname = _lastName; } }
- علت تعریف این دو خروجی هم به constructor و یا سازندهی کلاس بر میگردد که دو ورودی را دریافت میکند. اگر یک کلاس چندین سازنده داشته باشد، به همان تعداد میتوان متد Deconstruct تعریف کرد؛ به همراه خروجیهایی متناظر با نوع پارامترهای سازندهها.
- علت استفادهی از نوع خروجی out نیز این است که در #C نمیتوان چندین overload را صرفا بر اساس نوع خروجیهای متفاوت متدها تعریف کرد.
- متد Deconstruct به صورت خودکار در زمان تجزیهی یک شیء به یک tuple فراخوانی میشود. در مثال زیر، شیء p1 به یک Tuple تجزیه شدهاست و این تجزیه بر اساس متد Deconstruct این کلاس مفهوم پیدا میکند:
var p1 = new Person("Katharina", "Nagel"); (string first, string last) = p1; Console.WriteLine($"{first} {last}");
امکان تعریف متد Deconstruct، به صورت یک متد الحاقی
روش اول تعریف متد ویژهی Deconstruct را در مثال قبل، در داخل کلاس اصلی مشاهده کردید. روش دیگر آن، استفادهی از متدهای الحاقی است که در این مورد خاص نیز مجاز است:
public class Rectangle { public Rectangle(int height, int width) { Height = height; Width = width; } public int Width { get; } public int Height { get; } } public static class RectangleExtensions { public static void Deconstruct(this Rectangle rectangle, out int height, out int width) { height = rectangle.Height; width = rectangle.Width; } }
اکنون امکان انتساب وهلهای از این کلاس به یک Tuple وجود دارد:
var r1 = new Rectangle(100, 200); (int height, int width) = r1; Console.WriteLine($"height: {height}, width: {width}");
امکان جایگزین کردن Anonymous types با Tuples
قطعه کد ذیل را در نظر بگیرید:
List<Employee> allEmployees = new List<Employee>() { new Employee { ID = 1L, Name = "Fred", Salary = 50000M }, new Employee { ID = 2L, Name = "Sally", Salary = 60000M }, new Employee { ID = 3L, Name = "George", Salary = 70000M } }; var wellPaid = from oneEmployee in allEmployees where oneEmployee.Salary > 50000M select new { EmpName = oneEmployee.Name, Income = oneEmployee.Salary };
var wellPaid = from oneEmployee in allEmployees where oneEmployee.Salary > 50000M orderby oneEmployee.Salary descending select (EmpName: oneEmployee.Name, Income: oneEmployee.Salary); var highestPaid = wellPaid.First().EmpName;
سایر کاربردهای Tuples
از Tuples صرفا برای تعریف چندین خروجی از یک متد استفاده نمیشود. در ذیل نحوهی استفادهی از آنها را جهت تعریف کلید ترکیبی یک شیء دیکشنری و یا استفادهی از آنها را در آرگومان جنریک یک متد async هم مشاهده میکنید:
public Task<(int index, T item)> FindAsync<T>(IEnumerable<T> input, Predicate<T> match) { var dictionary = new Dictionary<(int, int), string>(); throw new NotSupportedException(); }
فعال سازی تنظیمات مسیریابی
یکی دیگر از تغییرات عمدهی ASP.NET Core با نگارشهای قبلی آن، نحوهی مدیریت مسیریابیهای سیستم است. در نگارشهای قبلی مبتنی بر HTTP Moduleها، مسیریابیها توسط یک HTTP Module مخصوص، با pipeline اصلی ASP.NET یکپارچه شدهاند و زمانیکه مسیر درخواستی با تنظیمات سیستم تطابق داشته باشد، پردازش کار به HTTP Handler مخصوص ASP.NET MVC منتقل میشود:
اما در ASP.NET Core مبتنی بر میان افزارها، زیر ساخت مسیریابی به صورت زیر تغییر کردهاست:
میان افزار ASP.NET MVC را که در قسمت قبل فعال کردیم، باید بتواند کنترلر و اکشن متد متناظر با URL درخواستی را مشخص کند. این تصمیم گیری نیز بر اساس تنظیماتی به نام Routing انجام میشود. در قسمت قبل، حالت ساده و پیش فرض این تنظیمات را مورد استفاده قرار دادیم
که مطابق سورس ASP.NET Core، معادل است با فراخوانی متد app.UseMvc، با قالب پیش فرضی به صورت زیر:
قالب مشخص شدهی در اینجا به ASP.NET MVC میگوید که از کدام قسمتهای URL باید نام کلاس کنترلر (کلاس ویژهای که به کلمهی Controller ختم میشود) و نام اکشن متد متناظر با آنرا انتخاب کند (اکشن متد، متدی است عمومی در آن کلاس).
روش دیگر معرفی این تنظیمات، استفاده از Attribute routing است:
مسیریابیهای قراردادی
در قسمت قبل، یک POCO Controller را به صورت ذیل تعریف کردیم و این کنترلر، بدون تعریف هیچ نوع مسیریابی خاصی در دسترس بود:
علت کار کردن مسیریابی آن نیز به ذکر متد app.UseMvcWithDefaultRoute در کلاس آغازین برنامه بر میگردد و همانطور که عنوان شد، این فراخوانی را میتوان با فراخوانی واضحتر ذیل جایگزین کرد:
پارامتر این متد که جایگزین متد ConfigureRoutes، در نگارشهای قبلی ASP.NET MVC شدهاست، از نوع IRouteBuilder میباشد.
در این تعاریف، هر کدام از قسمتهای قرارگرفتهی داخل {}، مشخص کنندهی قسمتی از URL دریافتی بوده و نامهای controller و action در اینجا جزو نامهای از پیش مشخص شده هستند و برای نگاشت اطلاعات مورد استفاده قرار میگیرند. برای مثال اگر آدرس home/index/ درخواست شد، برنامه به کلاس HomeController و متد عمومی Index آن هدایت میشود. همچنین قسمت آخر این پردازش به ?id ختم شدهاست. وجود ?، به معنای اختیاری بودن این پارامتر است و اگر در URL ذکر شود، به پارامتر id این اکشن متد، نگاشت خواهد شد. مواردی که پس از = ذکر شدهاند، مقادیر پیش فرض مسیریابی هستند. برای مثال اگر صرفا آدرس home/ درخواست شود، مقدار اکشن متد آن با مقدار پیش فرض index جایگزین خواهد شد و اگر تنها مسیر / درخواست شود، کنترل Home و اکشن متد Index آن پردازش میشوند.
در اینجا به هر تعدادی که نیاز است میتوان متدهای routes.MapRoute را فراخوانی و استفاده کرد؛ اما ترتیب تعریف آنها حائز اهمیت است. هر مسیریابی که در ابتدای لیست اضافه شود، حق تقدم بالاتری خواهد داشت و هر تطابقی با یکی از مسیریابیهای تعریف شده، در همان سطح سبب خاتمهی پردازش سایر مسیریابیها میشود.
استفاده از Attributes برای تعریف مسیریابیها
بجای تعریف قرار دادهای پیش فرض مسیریابی در کلاس آغازین برنامه، میتوان از ویژگی Route نیز استفاده کرد. هرچند روش تعریف مسیریابیهای قراردادی، از نگارشهای آغازین ASP.NET MVC به همراه آن بودهاند، اما با زیاد شدن تعداد کنترلرها و مسیریابیهای سفارشی هر کدام، اینبار با نگاه کردن به یک کنترلر، سریع نمیتوان تشخیص داد که چه مسیریابیهای خاصی به آن مرتبط هستند. برای ساده سازی مدیریت برنامههای بزرگ و ساده سازی تعاریف مسیریابیهای خاص آنها، استفاده از ویژگی Route نیز به ASP.NET MVC اضافه شدهاست.
یک مثال: کنترلر About را درنظر بگیرید:
این کلاس و کنترلر، به صورت پیش فرض نیاز به تعریف هیچ نوع مسیریابی جدیدی ندارد. همان مسیریابی پیش فرض ثبت شدهی در کلاس آغازین برنامه، تمام متدهای عمومی آن یا همان اکشن متدهای آنرا پوشش میدهد. برای مثال جهت رسیدن به اکشن متد SiteName آن، میتوان آدرس /About/SiteName/ را درخواست داد.
اما اگر آدرس /About/ را درخواست دهیم چطور؟ چون در مسیریابی پیش فرض، تعریف {action=Index} را داریم، یعنی هر زمانیکه در URL درخواستی، قسمت action آن ذکر نشد، آنرا با index جایگزین کن و این کنترلر دارای متد Index نیست. در ادامه اگر بخواهیم متد Hello را تبدیل به متد پیش فرض این کنترلر کنیم، میتوان با استفاده از ویژگی Route به صورت ذیل عمل کرد:
در اینجا با اولین Route تعریف شده، مشخص کردهایم که اگر قسمت اول URL درخواستی معادل about بود، پردازش برنامه باید به این کنترلر هدایت شود. بدیهی است الزامی به یکی بودن نام Route، با نام کنترلر، وجود ندارد. همچنین Route تعریف شدهی با رشتهی خالی، به معنای مسیریابی پیش فرض است. یعنی اگر آدرس /about/ درخواست داده شد، اکشن متد پیش فرض آن، متد Hello خواهد بود. در این حالت، ذکر Route بعدی برای اکشن متد SiteName الزامی است و اگر اینکار صورت نگیرد، به استثنای ذیل خواهیم رسید:
که عنوان کردهاست در این حالت مشخص نیست که اکشن متد پیش فرض، کدام است.
روش بهتر و refactoring friendly آن نیز به صورت ذیل است:
عموما مرسوم است که نام مسیریابی کنترلر همان نام کنترلر باشد و نام مسیریابی اکشن متد، همان نام اکشن متد مربوطه. به همین جهت میتوان از توکنهای ویژهی [controller] و [action] نیز در اینجا استفاده کرد که دقیقا به همان نام کنترلر و اکشن متد متناظر با آنها تفسیر خواهند شد. مزیت اینکار این است که در صورت تغییر نام متدها یا کنترلرها، دیگر نیازی نیست تا نامهای تعریف شدهی در ویژگیهای Route را نیز تغییر داد.
یک نکته: در حین تعریف مسیریابی یک کنترلر میتوان پیشوندهایی را نیز ذکر کرد؛ برای مثال:
وجود api در اینجا به این معنا است که از این پس تنها آدرس /api/about/ پردازش خواهد شد و اگر صرفا آدرس /about/ درخواست شود، با خطای 404 و یا یافت نشد، کار خاتمه مییابد.
تعریف قیود، برای مسیریابیهای تعریف شده
فرض کنید به کنترلر About فوق، اکشن متد ذیل را که یک خروجی JSON را بازگشت میدهد، اضافه کردهایم:
در اینجا تعریف مسیریابی آن با users/ و user معانی کاملا متفاوتی را دارند. اگر مسیریابی Users/{userid}/ را تعریف کنیم، یعنی مسیر ذیل از ریشهی سایت باید درخواست شود: http://localhost:7742/users/1
و اگر مسیریابی Users/{userid} را تعریف کنیم، یعنی این مسیریابی پس از ذکر کنترلر about، به عنوان یک اکشن متد آن مفهوم پیدا میکند:
http://localhost:7742/about/users/1
در هر دو حالت، ذکر پارامتر userid الزامی است (چون با ? مشخص نشدهاست)؛ مانند:
در اینجا اگر بخواهیم نوع پارامتر درخواستی را نیز دقیقا مشخص و مقید کنیم، میتوان به صورت ذیل عمل کرد:
اگر این کار را انجام ندهیم، با درخواست مسیر http://localhost:7742/dnt/about/users/test مقدار صفر به userId ارسال میشود (چون پارامتر test عددی نیست). اگر تنظیم فوق را انجام دهیم، کاربر خطای 404 را دریافت میکند.
قیودی را که در اینجا میتوان ذکر کرد به شرح زیر هستند:
• alpha - معادل است با (a-z, A-Z).
• bool - برای تطابق با مقادیر بولی.
• datetime - برای تطابق با تاریخ میلادی.
• decimal - برای تطابق با ورودیهای اعشاری.
• double - برای تطابق با اعداد اعشاری 64 بیتی.
• float - برای تطابق با اعداد اعشاری 32 بیتی.
• guid - برای تطابق با GUID ها
• int - برای تطابق با اعداد صحیح 32 بیتی.
• length - برای تعیین طول رشته.
• long - برای تطابق با اعداد صحیح 64 بیتی.
• max - برای ذکر حداکثر مقدار یک عدد صحیح.
• maxlength - جهت ذکر حداکثر طول رشتهی مجاز ورودی.
• min - برای ذکر حداقل مقدار یک عدد صحیح.
• minlength - جهت ذکر حداقل طول رشتهی مجاز ورودی.
• range - ذکر بازهی اعداد صحیح مجاز.
• regex - ذکر یک عبارت با قاعده جهت مشخص سازی الگوی قابل پذیرش.
برای ترکیب چندین قید مختلف نیز میتوان از : استفاده کرد:
ذکر نام Route برای ساده سازی تعریف آدرسی به آن
در حین تعریف یک Route میتوان نام دلخواهی را نیز به آن انتساب داد (همانند نام default مسیریابی ثبت شدهی در کلاس آغازین برنامه):
مزیت آن این است که اکنون برای اشارهی به این مسیریابی خاص میتوان از این نام تعریف شده استفاده کرد:
پارامتر اول ذکر شده، نام مسیریابی و پارامتر دوم، پارامترهای مرتبط با این مسیریابی هستند.
مشخص سازی ترتیب پردازش مسیریابیها
ترتیب مسیریابیهای ثبت شدهی در کلاس آغازین برنامه، همان ترتیب افزوده شدن و ذکر آنها است.
در اینجا میتوان از خاصیت order نیز استفاده کرد و اعداد کوچکتر، ابتدا پردازش میشوند (مقدار پیش فرض آن نیز صفر است):
امکان تعریف قیود سفارشی
اگر قیودی که تا اینجا ذکر شدند، برای کار شما مناسب نبودند و نیاز بود تا الگوریتم خاصی را جهت محدود سازی دسترسی به یک مسیریابی خاص پیاده سازی کنید، میتوان به صورت ذیل عمل کرد:
در اینجا یک کلاس جدید را که اینترفیس IRouteConstraint را پیاده سازی میکند تعریف کردهایم:
سپس در متد match آن بررسی کردهایم که اگر userid=10 بود، خطای 404 صادر شود.
در آخر برای ثبت و معرفی آن باید به متد ConfigureServices کلاس آغازین برنامه مراجعه کرد:
پس از آن، این نام جدید ثبت شدهی در اینجا، به نحو ذیل قابل استفاده است:
به این ترتیب userid باید از نوع int بوده و همچنین قید custom را نیز پوشش دهد (یعنی userid=10 نباشد).
یک نکته: اگر به سورس ASP.NET Core مراجعه کنید ، تمام قیودی را که پیشتر نام بردیم (مانند int، guid و امثال آن) نیز به همین روش تعریف و پیشتر ثبت شدهاند.
معرفی بستهی نیوگت Microsoft.AspNetCore.SpaServices
مسیریابیهای پیش فرض ASP.NET Core با مسیریابیهای برنامههای SPA مانند AngularJS (و امثال آن) تداخل دارند؛ از این جهت که درخواستهای رسیدهی به سرور، ابتدا به موتور پردازشی ASP.NET وارد میشوند و اگر یافت نشدند، کاربر با پیام 404 مواجه خواهد شد و دیگر در اینجا برنامه به مسیریابی خاص مثلا AngularJS 2.0 هدایت نمیشود.
برای این موارد مرسوم است که یک fallback route را در انتهای مسیریابیهای موجود اضافه کنند (به آن catch all هم میگویند)
در اینجا هر درخواستی که با مسیریابی default تطابق نداشت، توسط الگوی عمومی {url*} پردازش میشود و این پردازش در نهایت سبب راه اندازی برنامهی SPA میگردد. اما مشکل اینجا است که برای فایلهای استاتیک غیرموجود مانند تصاویر، فایلهای js و css نیز خروجی HTML ایی خواهیم داشت؛ بجای خروجی 404 و یافت نشد.
برای حل این مشکل مایکروسافت بستهای را به نام Microsoft.AspNetCore.SpaServices ارائه داده است.
برای افزودن آن بر روی گره references کلیک راست کرده و گزینهی manage nuget packages را انتخاب کنید. سپس در برگهی browse آن Microsoft.AspNetCore.SpaServices را جستجو کرده و نصب نمائید:
انجام این مراحل معادل هستند با افزودن یک سطر ذیل به فایل project.json برنامه:
پس از بازیابی و نصب آن، اکنون catch all را حذف کرده و با یک سطر routes.MapSpaFallbackRoute ذیل جایگزین کنید:
و برای یادآوری مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 4 - فعال سازی پردازش فایلهای استاتیک» در AngularJS 2.0، علاوه بر عمومی کردن پوشهی wwwroot توسط UseFileServer نیاز است پوشهی node_modules را هم با تنظیمات ذیل عمومی کرد و در معرض دید عموم قرار داد (جایی که بستههای node.js نصب میشوند):
یکی دیگر از تغییرات عمدهی ASP.NET Core با نگارشهای قبلی آن، نحوهی مدیریت مسیریابیهای سیستم است. در نگارشهای قبلی مبتنی بر HTTP Moduleها، مسیریابیها توسط یک HTTP Module مخصوص، با pipeline اصلی ASP.NET یکپارچه شدهاند و زمانیکه مسیر درخواستی با تنظیمات سیستم تطابق داشته باشد، پردازش کار به HTTP Handler مخصوص ASP.NET MVC منتقل میشود:
اما در ASP.NET Core مبتنی بر میان افزارها، زیر ساخت مسیریابی به صورت زیر تغییر کردهاست:
میان افزار ASP.NET MVC را که در قسمت قبل فعال کردیم، باید بتواند کنترلر و اکشن متد متناظر با URL درخواستی را مشخص کند. این تصمیم گیری نیز بر اساس تنظیماتی به نام Routing انجام میشود. در قسمت قبل، حالت ساده و پیش فرض این تنظیمات را مورد استفاده قرار دادیم
app.UseMvcWithDefaultRoute();
public static IApplicationBuilder UseMvcWithDefaultRoute(this IApplicationBuilder app) { if (app == null) throw new ArgumentNullException("app"); return app.UseMvc((Action<IRouteBuilder>) (routes => routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}"))); }
روش دیگر معرفی این تنظیمات، استفاده از Attribute routing است:
[Route("[controller]/[action]")]
مسیریابیهای قراردادی
در قسمت قبل، یک POCO Controller را به صورت ذیل تعریف کردیم و این کنترلر، بدون تعریف هیچ نوع مسیریابی خاصی در دسترس بود:
namespace Core1RtmEmptyTest.Controllers { public class HomeController { public string Index() { return "Running a POCO controller!"; } } }
app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); });
در این تعاریف، هر کدام از قسمتهای قرارگرفتهی داخل {}، مشخص کنندهی قسمتی از URL دریافتی بوده و نامهای controller و action در اینجا جزو نامهای از پیش مشخص شده هستند و برای نگاشت اطلاعات مورد استفاده قرار میگیرند. برای مثال اگر آدرس home/index/ درخواست شد، برنامه به کلاس HomeController و متد عمومی Index آن هدایت میشود. همچنین قسمت آخر این پردازش به ?id ختم شدهاست. وجود ?، به معنای اختیاری بودن این پارامتر است و اگر در URL ذکر شود، به پارامتر id این اکشن متد، نگاشت خواهد شد. مواردی که پس از = ذکر شدهاند، مقادیر پیش فرض مسیریابی هستند. برای مثال اگر صرفا آدرس home/ درخواست شود، مقدار اکشن متد آن با مقدار پیش فرض index جایگزین خواهد شد و اگر تنها مسیر / درخواست شود، کنترل Home و اکشن متد Index آن پردازش میشوند.
در اینجا به هر تعدادی که نیاز است میتوان متدهای routes.MapRoute را فراخوانی و استفاده کرد؛ اما ترتیب تعریف آنها حائز اهمیت است. هر مسیریابی که در ابتدای لیست اضافه شود، حق تقدم بالاتری خواهد داشت و هر تطابقی با یکی از مسیریابیهای تعریف شده، در همان سطح سبب خاتمهی پردازش سایر مسیریابیها میشود.
استفاده از Attributes برای تعریف مسیریابیها
بجای تعریف قرار دادهای پیش فرض مسیریابی در کلاس آغازین برنامه، میتوان از ویژگی Route نیز استفاده کرد. هرچند روش تعریف مسیریابیهای قراردادی، از نگارشهای آغازین ASP.NET MVC به همراه آن بودهاند، اما با زیاد شدن تعداد کنترلرها و مسیریابیهای سفارشی هر کدام، اینبار با نگاه کردن به یک کنترلر، سریع نمیتوان تشخیص داد که چه مسیریابیهای خاصی به آن مرتبط هستند. برای ساده سازی مدیریت برنامههای بزرگ و ساده سازی تعاریف مسیریابیهای خاص آنها، استفاده از ویژگی Route نیز به ASP.NET MVC اضافه شدهاست.
یک مثال: کنترلر About را درنظر بگیرید:
using Microsoft.AspNetCore.Mvc; namespace Core1RtmEmptyTest.Controllers { public class AboutController : Controller { public ActionResult Hello() { return Content("Hello from DNT!"); } public ActionResult SiteName() { return Content("DNT"); } } }
اما اگر آدرس /About/ را درخواست دهیم چطور؟ چون در مسیریابی پیش فرض، تعریف {action=Index} را داریم، یعنی هر زمانیکه در URL درخواستی، قسمت action آن ذکر نشد، آنرا با index جایگزین کن و این کنترلر دارای متد Index نیست. در ادامه اگر بخواهیم متد Hello را تبدیل به متد پیش فرض این کنترلر کنیم، میتوان با استفاده از ویژگی Route به صورت ذیل عمل کرد:
using Microsoft.AspNetCore.Mvc; namespace Core1RtmEmptyTest.Controllers { [Route("About")] public class AboutController : Controller { [Route("")] public ActionResult Hello() { return Content("Hello from DNT!"); } [Route("SiteName")] public ActionResult SiteName() { return Content("DNT"); } } }
AmbiguousActionException: Multiple actions matched. The following actions matched route data and had all constraints satisfied: Core1RtmEmptyTest.Controllers.AboutController.Hello (Core1RtmEmptyTest) Core1RtmEmptyTest.Controllers.AboutController.SiteName (Core1RtmEmptyTest)
روش بهتر و refactoring friendly آن نیز به صورت ذیل است:
using Microsoft.AspNetCore.Mvc; namespace Core1RtmEmptyTest.Controllers { [Route("[controller]")] public class AboutController : Controller { [Route("")] public ActionResult Hello() { return Content("Hello from DNT!"); } [Route("[action]")] public ActionResult SiteName() { return Content("DNT"); } } }
یک نکته: در حین تعریف مسیریابی یک کنترلر میتوان پیشوندهایی را نیز ذکر کرد؛ برای مثال:
[Route("api/[controller]")]
تعریف قیود، برای مسیریابیهای تعریف شده
فرض کنید به کنترلر About فوق، اکشن متد ذیل را که یک خروجی JSON را بازگشت میدهد، اضافه کردهایم:
//[Route("/Users/{userid}")] [Route("Users/{userid}")] public IActionResult GetUsers(int userId) { return Json(new { userId = userId }); }
و اگر مسیریابی Users/{userid} را تعریف کنیم، یعنی این مسیریابی پس از ذکر کنترلر about، به عنوان یک اکشن متد آن مفهوم پیدا میکند:
http://localhost:7742/about/users/1
در هر دو حالت، ذکر پارامتر userid الزامی است (چون با ? مشخص نشدهاست)؛ مانند:
[Route("/Users/{userid:int?}")]
[Route("Users/{userid:int}")]
قیودی را که در اینجا میتوان ذکر کرد به شرح زیر هستند:
• alpha - معادل است با (a-z, A-Z).
• bool - برای تطابق با مقادیر بولی.
• datetime - برای تطابق با تاریخ میلادی.
• decimal - برای تطابق با ورودیهای اعشاری.
• double - برای تطابق با اعداد اعشاری 64 بیتی.
• float - برای تطابق با اعداد اعشاری 32 بیتی.
• guid - برای تطابق با GUID ها
• int - برای تطابق با اعداد صحیح 32 بیتی.
• length - برای تعیین طول رشته.
• long - برای تطابق با اعداد صحیح 64 بیتی.
• max - برای ذکر حداکثر مقدار یک عدد صحیح.
• maxlength - جهت ذکر حداکثر طول رشتهی مجاز ورودی.
• min - برای ذکر حداقل مقدار یک عدد صحیح.
• minlength - جهت ذکر حداقل طول رشتهی مجاز ورودی.
• range - ذکر بازهی اعداد صحیح مجاز.
• regex - ذکر یک عبارت با قاعده جهت مشخص سازی الگوی قابل پذیرش.
برای ترکیب چندین قید مختلف نیز میتوان از : استفاده کرد:
[Route("/Users/{userid:int:max(1000):min(10)}")]
ذکر نام Route برای ساده سازی تعریف آدرسی به آن
در حین تعریف یک Route میتوان نام دلخواهی را نیز به آن انتساب داد (همانند نام default مسیریابی ثبت شدهی در کلاس آغازین برنامه):
[Route("/Users/{userid:int}", Name="GetUserById")]
string uri = Url.Link("GetUserById", new { userid = 1 });
مشخص سازی ترتیب پردازش مسیریابیها
ترتیب مسیریابیهای ثبت شدهی در کلاس آغازین برنامه، همان ترتیب افزوده شدن و ذکر آنها است.
در اینجا میتوان از خاصیت order نیز استفاده کرد و اعداد کوچکتر، ابتدا پردازش میشوند (مقدار پیش فرض آن نیز صفر است):
[Route("/Users/{userid:int}", Name = "GetUserById", Order = 1)]
امکان تعریف قیود سفارشی
اگر قیودی که تا اینجا ذکر شدند، برای کار شما مناسب نبودند و نیاز بود تا الگوریتم خاصی را جهت محدود سازی دسترسی به یک مسیریابی خاص پیاده سازی کنید، میتوان به صورت ذیل عمل کرد:
using System; using System.Globalization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; namespace Core1RtmEmptyTest { public class CustomRouteConstraint : IRouteConstraint { public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection) { object value; if (!values.TryGetValue(routeKey, out value) || value == null) { return false; } long longValue; if (value is long) { longValue = (long)value; return longValue != 10; } var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); if (long.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out longValue)) { return longValue != 10; } return false; } } }
public class CustomRouteConstraint : IRouteConstraint
در آخر برای ثبت و معرفی آن باید به متد ConfigureServices کلاس آغازین برنامه مراجعه کرد:
public void ConfigureServices(IServiceCollection services) { services.AddRouting(options =>options.ConstraintMap.Add("Custom", typeof(CustomRouteConstraint)));
[Route("/Users/{userid:int:custom}")]
یک نکته: اگر به سورس ASP.NET Core مراجعه کنید ، تمام قیودی را که پیشتر نام بردیم (مانند int، guid و امثال آن) نیز به همین روش تعریف و پیشتر ثبت شدهاند.
معرفی بستهی نیوگت Microsoft.AspNetCore.SpaServices
مسیریابیهای پیش فرض ASP.NET Core با مسیریابیهای برنامههای SPA مانند AngularJS (و امثال آن) تداخل دارند؛ از این جهت که درخواستهای رسیدهی به سرور، ابتدا به موتور پردازشی ASP.NET وارد میشوند و اگر یافت نشدند، کاربر با پیام 404 مواجه خواهد شد و دیگر در اینجا برنامه به مسیریابی خاص مثلا AngularJS 2.0 هدایت نمیشود.
برای این موارد مرسوم است که یک fallback route را در انتهای مسیریابیهای موجود اضافه کنند (به آن catch all هم میگویند)
app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); routes.MapRoute( name: "spa-fallback", template: "{*url}", defaults: new { controller = "Home", action = "Index" }); });
برای حل این مشکل مایکروسافت بستهای را به نام Microsoft.AspNetCore.SpaServices ارائه داده است.
برای افزودن آن بر روی گره references کلیک راست کرده و گزینهی manage nuget packages را انتخاب کنید. سپس در برگهی browse آن Microsoft.AspNetCore.SpaServices را جستجو کرده و نصب نمائید:
انجام این مراحل معادل هستند با افزودن یک سطر ذیل به فایل project.json برنامه:
{ "dependencies": { //same as before "Microsoft.AspNetCore.SpaServices": "1.0.0-beta-000007" },
app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); routes.MapSpaFallbackRoute("spa-fallback", new { controller = "Home", action = "Index" }); });
// Serve wwwroot as root app.UseFileServer(); // Serve /node_modules as a separate root (for packages that use other npm modules client side) app.UseFileServer(new FileServerOptions { // Set root of file server FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "node_modules")), // Only react to requests that match this path RequestPath = "/node_modules", // Don't expose file system EnableDirectoryBrowsing = false });
We are going to talk about:
What you get out of the box with ASP.NET Core 2.0
How to use Postman to test your API
Changing the default configuration of our project
Testing the content negotiation
Restricting media types
More about formatters
Implementing a custom formatter
Consuming API programmatically