JSON.NET یک کتابخانهی سورس باز کار با اشیاء JSON در دات نت است. تاریخچهی آن به 8 سال قبل بر میگردد و توسط یک برنامه نویس نیوزیلندی به نام James Newton King تهیه شدهاست. اولین نگارش آن در سال 2006 ارائه شد؛ مقارن با زمانی که اولین استاندارد JSON نیز ارائه گردید.
این کتابخانه از آن زمان تا کنون، 6 میلیون بار دانلود شدهاست و به علت کیفیت بالای آن، این روزها پایه اصلی بسیاری از کتابخانهها و فریم ورکهای دات نتی میباشد؛ مانند RavenDB تا ASP.NET Web API و SignalR مایکروسافت و همچنین گوگل نیز از آن جهت تدارک کلاینتهای کار با API خود استفاده میکنند.
هرچند دات نت برای نمونه در نگارش سوم آن جهت مصارف WCF کلاسی را به نام DataContractJsonSerializer ارائه کرد، اما کار کردن با آن محدود است به فرمت خاص WCF به همراه عدم انعطاف پذیری و سادگی کار با آن. به علاوه باید درنظر داشت که JSON.NET از دات نت 2 به بعد تا مونو، Win8 و ویندوز فون را نیز پشتیبانی میکند.
برای نصب آن نیز کافی است دستور ذیل را در کنسول پاورشل نیوگت اجرا کنید:
PM> install-package Newtonsoft.Json
معماری JSON.NET
کتابخانهی JSON.NET از سه قسمت عمده تشکیل شدهاست:
الف) JsonSerializer
ب) LINQ to JSON
ج) JSON Schema
الف) JsonSerializer
کار JsonSerializer تبدیل اشیاء دات نتی به JSON و برعکس است. مزیت مهم آن امکانات قابل توجه تنظیم عملکرد و خروجی آن میباشد که این تنظیمات را به شکل ویژگیهای خواص نیز میتوان اعمال نمود. به علاوه امکان سفارشی سازی هر کدام نیز توسط کلاسی به نام JsonConverter، پیش بینی شدهاست.
یک مثال:
var roles = new List<string> { "Admin", "User" }; string json = JsonConvert.SerializeObject(roles, Formatting.Indented);
و یا در مثال ذیل استفاده از یک anonymous object را مشاهده میکنید:
var jsonString = JsonConvert.SerializeObject(new { Id =1, Name = "Test" }, Formatting.Indented);
تنظیمات پیشرفتهتر JSON.NET
مزیت مهم JSON.NET بر سایر کتابخانههای موجود مشابه، قابلیتهای سفارشی سازی قابل توجه آن است. در مثال ذیل نحوهی معرفی JsonSerializerSettings را مشاهده مینمائید:
var jsonData = JsonConvert.SerializeObject(new { Id = 1, Name = "Test", DateTime = DateTime.Now }, new JsonSerializerSettings { Formatting = Formatting.Indented, Converters = { new JavaScriptDateTimeConverter() } });
{ "Id": 1, "Name": "Test", "DateTime": new Date(1409821985245) }
نوشتن خروجی JSON در یک استریم
خروجی متد JsonConvert.SerializeObject یک رشتهاست که در صورت نیاز به سادگی توسط متد File.WriteAllText در یک فایل قابل ذخیره میباشد. اما برای رسیدن به حداکثر کارآیی و سرعت میتوان از استریمها نیز استفاده کرد:
using (var stream = File.CreateText(@"c:\output.json")) { var jsonSerializer = new JsonSerializer { Formatting = Formatting.Indented }; jsonSerializer.Serialize(stream, new { Id = 1, Name = "Test", DateTime = DateTime.Now }); }
تبدیل JSON رشتهای به اشیاء دات نت
اگر رشتهی jsonData ایی را که پیشتر تولید کردیم، بخواهیم تبدیل به نمونهای از شیء User ذیل کنیم:
public class User { public int Id { set; get; } public string Name { set; get; } public DateTime DateTime { set; get; } }
var user = JsonConvert.DeserializeObject<User>(jsonData);
البته در اینجا با توجه به استفاده از JavaScriptDateTimeConverter برای تولید jsonData، نیاز است چنین تنظیمی را نیز در حالت DeserializeObject مشخص کنیم:
var user = JsonConvert.DeserializeObject<User>(jsonData, new JsonSerializerSettings { Converters = { new JavaScriptDateTimeConverter() } });
مقدار دهی یک نمونه یا وهلهی از پیش موجود
متد JsonConvert.DeserializeObject یک شیء جدید را ایجاد میکند. اگر قصد دارید صرفا تعدادی از خواص یک وهلهی موجود، توسط JSON.NET مقدار دهی شوند از متد PopulateObject استفاده کنید:
JsonConvert.PopulateObject(jsonData, user);
کاهش حجم JSON تولیدی
زمانیکه از متد JsonConvert.SerializeObject استفاده میکنیم، تمام خواص عمومی تبدیل به معادل JSON آنها خواهند شد؛ حتی خواصی که مقدار ندارند. این خواص در خروجی JSON، با مقدار null مشخص میشوند. برای حذف این خواص از خروجی JSON نهایی تنها کافی است در تنظیمات JsonSerializerSettings، مقدار NullValueHandling = NullValueHandling.Ignore مشخص گردد.
var jsonData = JsonConvert.SerializeObject(object, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, Formatting = Formatting.Indented });
به علاوه حذف Formatting = Formatting.Indented نیز توصیه میگردد. در این حالت فشردهترین خروجی ممکن حاصل خواهد شد.
مدیریت ارث بری توسط JSON.NET
در مثال ذیل کلاس کارمند و کلاس مدیر را که خود نیز در اصل یک کارمند میباشد، ملاحظه میکنید:
public class Employee { public string Name { set; get; } } public class Manager : Employee { public IList<Employee> Reports { set; get; } }
var employee = new Employee { Name = "User1" }; var manager1 = new Manager { Name = "User2" }; var manager2 = new Manager { Name = "User3" }; manager1.Reports = new[] { employee, manager2 }; manager2.Reports = new[] { employee };
var list = JsonConvert.SerializeObject(manager1, Formatting.Indented);
{ "Reports": [ { "Name": "User1" }, { "Reports": [ { "Name": "User1" } ], "Name": "User3" } ], "Name": "User2" }
- در اینجا مشخص نیست که این اشیاء، کارمند هستند یا مدیر. برای مثال مشخص نیست User2 چه نوعی دارد و باید به کدام شیء نگاشت شود.
- مشکل دوم در مورد کاربر User1 است که در دو قسمت تکرار شدهاست. این شیء JSON اگر به نمونهی معادل دات نتی خود نگاشت شود، به دو وهله از User1 خواهیم رسید و نه یک وهلهی اصلی که سبب تولید این خروجی JSON شدهاست.
برای حل این دو مشکل، تغییرات ذیل را میتوان به JSON.NET اعمال کرد:
var list = JsonConvert.SerializeObject(manager1, new JsonSerializerSettings { Formatting = Formatting.Indented, TypeNameHandling = TypeNameHandling.Objects, PreserveReferencesHandling = PreserveReferencesHandling.Objects });
{ "$id": "1", "$type": "JsonNetTests.Manager, JsonNetTests", "Reports": [ { "$id": "2", "$type": "JsonNetTests.Employee, JsonNetTests", "Name": "User1" }, { "$id": "3", "$type": "JsonNetTests.Manager, JsonNetTests", "Reports": [ { "$ref": "2" } ], "Name": "User3" } ], "Name": "User2" }
- با تنظیم PreserveReferencesHandling = PreserveReferencesHandling.Objects شماره Id خودکاری نیز به خروجی JSON اضافه میگردد. اینبار اگر به گزارش دهندهها با دقت نگاه کنیم، مقدار $ref=2 را خواهیم دید. این مورد سبب میشود تا در حین نگاشت نهایی، دو وهله متفاوت از شیء با Id=2 تولید نشود.
باید دقت داشت که در حین استفاده از JsonConvert.DeserializeObject نیز باید JsonSerializerSettings یاد شده، تنظیم شوند.
ویژگیهای قابل تنظیم در JSON.NET
علاوه بر JsonSerializerSettings که از آن صحبت شد، در JSON.NET امکان تنظیم یک سری از ویژگیها به ازای خواص مختلف نیز وجود دارند.
- برای نمونه ویژگی JsonIgnore معروفترین آنها است:
public class User { public int Id { set; get; } [JsonIgnore] public string Name { set; get; } public DateTime DateTime { set; get; } }
- با استفاده از ویژگی JsonProperty اغلب مواردی را که پیشتر بحث کردیم مانند NullValueHandling، TypeNameHandling و غیره، میتوان تنظیم نمود. همچنین گاهی از اوقات کتابخانههای جاوا اسکریپتی سمت کاربر، از اسامی خاصی که از روشهای نامگذاری دات نتی پیروی نمیکنند، در طراحی خود استفاده میکنند. در اینجا میتوان نام خاصیت نهایی را که قرار است رندر شود نیز صریحا مشخص کرد. برای مثال:
[JsonProperty(PropertyName = "m_name", NullValueHandling = NullValueHandling.Ignore)] public string Name { set; get; }
- استفاده از ویژگی JsonObject به همراه مقدار OptIn آن به این معنا است که از کلیه خواصی که دارای ویژگی JsonProperty نیستند، صرفنظر شود. حالت پیش فرض آن OptOut است؛ یعنی تمام خواص عمومی در خروجی JSON حضور خواهند داشت منهای مواردی که با JsonIgnore مزین شوند.
[JsonObject(MemberSerialization.OptIn)] public class User { public int Id { set; get; } [JsonProperty] public string Name { set; get; } public DateTime DateTime { set; get; } }
- با استفاده از ویژگی JsonConverter میتوان نحوهی رندر شدن مقدار خاصیت را سفارشی سازی کرد. برای مثال:
[JsonConverter(typeof(JavaScriptDateTimeConverter))] public DateTime DateTime { set; get; }
تهیه یک JsonConverter سفارشی
با استفاده از JsonConverterها میتوان کنترل کاملی را بر روی اعمال serialization و deserialization مقادیر خواص اعمال کرد. مثال زیر را در نظر بگیرید:
public class HtmlColor { public int Red { set; get; } public int Green { set; get; } public int Blue { set; get; } } var colorJson = JsonConvert.SerializeObject(new HtmlColor { Red = 255, Green = 0, Blue = 0 }, Formatting.Indented);
public class HtmlColorConverter : JsonConverter { public override bool CanConvert(Type objectType) { return objectType == typeof(HtmlColor); } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { throw new NotSupportedException(); } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { var color = value as HtmlColor; if (color == null) return; writer.WriteValue("#" + color.Red.ToString("X2") + color.Green.ToString("X2") + color.Blue.ToString("X2")); } }
از آنجائیکه این تبدیلگر صرفا قرار است برای حالت serialization استفاده شود، قسمت ReadJson آن پیاده سازی نشدهاست.
در آخر برای استفاده از آن خواهیم داشت:
var colorJson = JsonConvert.SerializeObject(new HtmlColor { Red = 255, Green = 0, Blue = 0 }, new JsonSerializerSettings { Formatting = Formatting.Indented, Converters = { new HtmlColorConverter() } });
آموزش TypeScript #1
نیازهای یک ورودی تاریخ سازگار با 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
MailMessage mail = new MailMessage(); //پارامتر این شی همان حالت معرفی شده در تنظیمات ایمیل سرور میباشد که پیشتر معرفی شد. SmtpClient smtpServer = new SmtpClient("mail.domainName.com"); mail.Subject = "خرید بلیط"; mail.From = new MailAddress("info@domainName.com"); //ایمیل گیرنده نامه mail.To.Add("amir2012@gmail.com"); //متن نامه mail.Body = "خرید بلیط کیش به تهران"; //شماره پورت در اینجا حالت ارسال معمولی و غیر رمز شده مد نظر بوده است smtpServer.Port = 25; //email address ,email password smtpServer.Credentials = new NetworkCredential("info@domainName.com", "password"); smtpServer.EnableSsl = false; smtpServer.Send(mail);
استفاده از StructureMap به عنوان یک IoC Container
<edmx:Edmx xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx" Version="4.0"> <edmx:DataServices> <Schema xmlns="http://docs.oasis-open.org/odata/ns/edm" Namespace="OwinAspNetCore.Models"> <EntityType Name="Product"> <Key> <PropertyRef Name="Id"/> </Key> <Property Name="Id" Type="Edm.Int32" Nullable="false"/> <Property Name="Name" Type="Edm.String"/> <Property Name="Price" Type="Edm.Decimal" Nullable="false"/> </EntityType> </Schema> <Schema xmlns="http://docs.oasis-open.org/odata/ns/edm" Namespace="Default"> <Function Name="TestFunction" IsBound="true"> <Parameter Name="bindingParameter" Type="Collection(OwinAspNetCore.Models.Product)"/> <Parameter Name="Val" Type="Edm.Int32" Nullable="false"/> <Parameter Name="Name" Type="Edm.String"/> <ReturnType Type="Edm.Int32" Nullable="false"/> </Function> <EntityContainer Name="Container"> <EntitySet Name="Products" EntityType="OwinAspNetCore.Models.Product"/> </EntityContainer> </Schema> </edmx:DataServices> </edmx:Edmx>
string postUrl = "http://localhost:port/...."; HttpClient client = new HttpClient(); var response = client.PostAsync(postUrl, new StringContent(JsonConvert.SerializeObject(new { Rating = 5 }), Encoding.UTF8, "application/json")).Result;
آن را نصب نموده و بعد از تکمیل شدن، visual studio را restart کنید.
پروژهی console خود را باز کرده و از طریق Add -> new item، آیتم OData client را جستجو کرده و با نام ProductClient.tt آن را تولید نمایید (نام آن اختیاری است):
فایل ProductClient.tt را که یک T4 code generator میباشد، باز کرده و مقدار ثابت MetadataDocumentUri را به آدرس سرویس odata خود تغییر دهید:
public const string MetadataDocumentUri = "http://localhost:port/odata/";
روی این آیتم کلیک راست و گزینهی Run Custom tool را انتخاب نمایید. این تمام کاری است که نیاز به انجام دادن دارید.
حال فایل Program.cs را باز کرده و آنرا اینگونه تغییر دهید:
using ConsoleApplication1.OwinAspNetCore.Models; using System; using System.Linq; namespace ConsoleApplication1 { public class Program { static void Main(string[] args) { Uri uri = new Uri("http://localhost:24977/odata"); //var context = new Default.Container(uri); var context = new TestNameSpace.TestNameSpace(uri); //get var products = context.Products.Where(pr => pr.Name.Contains("a")) .Take(1).Select(pr => new { Firstname = pr.Name, PriceValue = pr.Price }).ToList(); //add context.AddToProducts(new Product() { Name = "Name1", Price = 123 }); //update Product p = context.Products.First(); p.Name = "changed"; context.UpdateObject(p); //delete context.DeleteObject(context.Products.Last()); //commit context.SaveChanges(); } } }
مشاهده میفرمایید که همهی عملیاتهای لازم برای CRUD، به شرط اینکه در سمت سرور طراحی شده باشند، به راحتی از سمت کلاینت قابل فراخوانی خواهند بود.
از این ویژگی فوق العاده میتوان حتی در کلاینتها جاوااسکریپتی نیز استفاده کرد. فرض کنید نرم افزار تحت وبی را با استفاده از jquery یا angularjs طراحی کردهاید. قاعدتا فراخوانی درخواستهای شما به سمت سرور، چیزی شبیه به این خواهد بود:
//angularjs $http.get("/products/get", {Name: "Test", Company: "Test"}) .then(function(response) { console.log(response.data); }); //jquery $.get("/products/get", {Name: "Test", Company: "Test"}, function(data, status){ console.log("Data: " + data); });
با استفاده از odata و typescript و یک library مربوط به odata client در سمت کلاینت، نرم افزار شما بجای موارد، بالا چیزی شبیه به مثال زیر خواهد بود (با همراه داشتن strongly typed و intellisense کامل)
let product1 = await context.products.filter(c => c.Name.contains("Ali")).toArray(); let product2 = await context.products.getSomeFunction(1, 'Test'); context.product.add({Name: 'Test'} as Product); await context.saveChanges()
در مقالههای آتی به ویژگیهای بیشتری از Odata خواهیم پرداخت.
در این مثال به کمک MVC5، یک کپچای ساده و قابل فهم را تولید و استفاده خواهیم کرد. این نوشته بر اساس این مقاله ایجاد شده و جزئیات زیادی برای درک افراد مبتدی به آن افزوده شده است که امیدوارم راهنمای مفیدی برای علاقمندان باشد.
با کلیک راست بر روی پوشه کنترلر، یک کنترلر به منظور ایجاد کپچا بسازید و اکشن متد زیر را در آن کنترلر ایجاد کنید:
public class CaptchaController : Controller { public ActionResult CaptchaImage(string prefix, bool noisy = true) { var rand = new Random((int)DateTime.Now.Ticks); //generate new question int a = rand.Next(10, 99); int b = rand.Next(0, 9); var captcha = string.Format("{0} + {1} = ?", a, b); //store answer Session["Captcha" + prefix] = a + b; //image stream FileContentResult img = null; using (var mem = new MemoryStream()) using (var bmp = new Bitmap(130, 30)) using (var gfx = Graphics.FromImage((Image)bmp)) { gfx.TextRenderingHint = TextRenderingHint.ClearTypeGridFit; gfx.SmoothingMode = SmoothingMode.AntiAlias; gfx.FillRectangle(Brushes.White, new Rectangle(0, 0, bmp.Width, bmp.Height)); //add noise if (noisy) { int i, r, x, y; var pen = new Pen(Color.Yellow); for (i = 1; i < 10; i++) { pen.Color = Color.FromArgb( (rand.Next(0, 255)), (rand.Next(0, 255)), (rand.Next(0, 255))); r = rand.Next(0, (130 / 3)); x = rand.Next(0, 130); y = rand.Next(0, 30); gfx.DrawEllipse(pen, x - r, y - r, r, r); } } //add question gfx.DrawString(captcha, new Font("Tahoma", 15), Brushes.Gray, 2, 3); //render as Jpeg bmp.Save(mem, System.Drawing.Imaging.ImageFormat.Jpeg); img = this.File(mem.GetBuffer(), "image/Jpeg"); } return img; }
همانطور که از کد فوق پیداست، دو مقدار a و b، به شکل اتفاقی ایجاد میشوند و حاصل جمع آنها در یک Session نگهداری خواهد شد. سپس تصویری بر اساس تصویر a+b ایجاد میشود (مثل 3+4). این تصویر خروجی این اکشن متد است. به سادگی میتوانید این اکشن را بر اساس خواسته خود اصلاح کنید؛ مثلا به جای حاصل جمع دو عدد، از کاربرد چند حرف یا عدد که بصورت اتفاقی تولید کردهاید، استفاده نمائید.
فرض کنید میخواهیم کپچا را هنگام ثبت نام استفاده کنیم.
در فایل AccountViewModels.cs در پوشه مدلها در کلاس RegisterViewModel خاصیت زیر را اضافه کنید:
[Required(ErrorMessage = "لطفا {0} را وارد کنید")] [Display(Name = "حاصل جمع")] public string Captcha { get; set; }
حالا در پوشه View/Account به فایل Register.Cshtml خاصیت فوق را اضافه کنید:
<div class="form-group"> <input type="button" value="" id="refresh" /> @Html.LabelFor(model => model.Captcha) <img alt="Captcha" id="imgcpatcha" src="@Url.Action("CaptchaImage","Captcha")" style="" /> </div>
وظیفه این بخش، نمایش کپچاست. تگ img دارای آدرسی است که توسط اکشن متدی که در ابتدای این مقاله ایجاد نمودهایم تولید میشود. این آدرس تصویر کپچاست. یک دکمه هم با شناسه refresh برای به روز رسانی مجدد تصویر در نظر گرفتهایم.
حالا کد ایجکسی برای آپدیت کپچا توسط دکمه refresh را به شکل زیر بنویسید (من در پایین ویوی Register، اسکریپت زیر را قرار دادم):
<script type="text/javascript"> $(function () { $('#refresh').click(function () { $.ajax({ url: '@Url.Action("CaptchaImage","Captcha")', type: "GET", data: null }) .done(function (functionResult) { $("#imgcpatcha").attr("src", "/Captcha/CaptchaImage?" + functionResult); }); }); }); </script>
آنچه در url نوشته شده است، شاید اصولیترین شکل فراخوانی یک اکشن متد باشد. این اکشن در ابتدای مقاله تحت کنترلری به نام Captcha معرفی شده بود و خروجی آن آدرس یک فایل تصویری است. نوع ارتباط، Get است و هیچ اطلاعاتی به اکشن متد فرستاده نمیشود، اما اکشن متد ما آدرسی را به ما برمیگرداند که تحت نام FunctionResult آن را دریافت کرده و به کمک کد جی کوئری، مقدارش را در ویژگی src تصویر موجود در صفحه جاری جایگزین میکنیم. دقت کنید که برای دسترسی به تصویر، لازم است جایگزینی آدرس، در ویژگی src به شکل فوق صورت پذیرد.*
تنها کار باقیمانده اضافه کردن کد زیر به ابتدای اکشن متد Register درون کنترلر Account است.
if (Session["Captcha"] == null || Session["Captcha"].ToString() != model.Captcha) { ModelState.AddModelError("Captcha", "مجموع اشتباه است"); }
واضح است که اینکار پیش از شرط if(ModelState.IsValidate) صورت میگیرد و وظیفه شرط فوق، بررسی ِ برابریِ مقدار Session تولید شده در اکشن CaptchaImage (ابتدای این مقاله) با مقدار ورودی کاربر است. (مقداری که از طریق خاصیت تولیدی خودمان به آن دسترسی داریم) . بدیهیاست اگر این دو مقدار نابرابر باشند، یک خطا به ModelState اضافه میشود و شرط ModelState.IsValid که در اولین خط بعد از کد فوق وجود دارد، برقرار نخواهد بود و پیغام خطا در صفحه ثبت نام نمایش داده خواهد شد.
تصویر زیر نمونهی نتیجهای است که حاصل خواهد شد :
* اصلاح : دقت کنید بدون استفاده از ایجکس هم میتوانید تصویر فوق را آپدیت کنید:
$('#refresh').click(function () { var d = new Date(); $("#imgcpatcha").attr("src", "Captcha/CaptchaImage?" + d.getTime()); });
رویداد کلیک را با کد فوق جایگزین کنید؛ دو نکته در اینجا وجود دارد :
یک. استفاده از زمان در انتهای آدرس به خاطر مشکلاتیست که فایرفاکس یا IE با اینگونه آپدیتهای تصویری دارند. این دو مرورگر (بر خلاف کروم) تصاویر را نگهداری میکنند و آپدیت به روش فوق به مشکل برخورد میکند مگر آنکه آدرس را به کمک اضافه کردن زمان آپدیت کنید تا مرورگر متوجه داستان شود
دو. همانطور که میبینید آدرس تصویر در حقیقت خروجی یک اکشن است. پس نیازی نیست هر بار این اکشن را به کمک ایجکس صدا بزنیم و روش فوق در مرورگرهای مختلف جواب خواهد داد.