class Program { static void Main(string[] args) { Book book = new Book(); var codeprop = book.GetType().GetProperty("Code", BindingFlags.Public | BindingFlags.Instance); codeprop.SetValue(book, 20, null); var value = codeprop.GetValue(book, null); } } public class Book { public int code; public int Code { get { return code; } } }
class Visitor { visit(item){} } class BookVisitor extends Visitor { visit(book) { var cost=0; if(book.getPrice() > 50) { cost = book.getPrice()*0.50 } else{ cost = book.getPrice() } console.log("Book name: "+ book.getName() + "\n" + "ID: " + book.getID() + "\n" + "cost: "+ cost); return cost; } } class Book{ constructor(id,name,price){ this.id = id this.name = name this.price = price } getPrice(){ return this.price } getName(){ return this.name } getID(){ return this.id } accept(visitor){ return visitor.visit(this) } } var visitor = new BookVisitor() var book1 = new Book("#1234","lordOftheRings",80) book1.accept(visitor)
class Book{ constructor(id,name,price){ this.id = id this.name = name this.price = price } //code... }
- id
- name
- price
getPrice(){ return this.price } getName(){ return this.name } getID(){ return this.id }
accept(visitor){ return visitor.visit(this) }
class Visitor { visit(item){} }
class BookVisitor extends Visitor { visit(book) { var cost=0; if(book.getPrice() > 50) { cost = book.getPrice()*0.50 } else{ cost = book.getPrice() } console.log("Book name: "+ book.getName() + "\n" + "ID: " + book.getID() + "\n" + "cost: "+ cost); return cost; } }
- زمانیکه نیاز است عملیاتی مشابه، بر روی شیءهای متفاوتی از یک data structure انجام شود.
- زمانیکه نیاز است عملیاتی خاص، بر روی شیءهای متفاوتی از data structure انجام شود.
- زمانیکه میخواهید توسعه پذیری را برای کتابخانهها (libraries) یا فریم ورکها (frameworks) اضافه کنید.
شاید سادهترین تعریف برای Saltarelle این باشد که «کامپایلریست که کدهای C# را به جاوا اسکریپت تبدیل میکند». محاسن زیادی را میتوان برای اینگونه کامپایلرها نام برد؛ مخصوصا در پروژههای سازمانی که نگهداری از کدهای جاوا اسکریپت بسیار سخت و گاهی خارج از توان است و این شاید مهمترین عامل ظهور ابزارهای جدید از قبیل Typescript باشد.
در هر صورت اگر حوصله و وقت کافی برای تجهیز تیم نرم افزاری، به دانش یک زبان جدید مانند Typescript نباشد، استفاده از توان و دانش تیم تولید، از زبان C# سادهترین راه حل است و اگر ابزاری مطمئن برای استفاده از حداکثر قدرت JavaScript همراه با امکانات نگهداری و توسعه کدها وجود داشته باشد، بی شک Saltarelle یکی از بهترینهای آنهاست.
قبلا کامپایلر هایی از این دست مانند Script# وجود داشتند، اما فاقد همه امکانات C# بوده وعملا قدرت کامل C# در کد نویسی وجود نداشت. اما با توجه به ادعای توسعه دهندگان این کامپایلر سورس باز در استفادهی حداکثری از کلیه ویژگیهای C# 5 و با وجود Library های متعدد میتوان Saltarelle را عملا یک کامپایلر موفق در این زمینه دانست.
برای استفاده از Saltarelle در یک برنامه وب ساده باید یک پروژه Console Application به Solution اضافه کرد و پکیج Saltarelle.Compiler را از nuget نصب نمایید. بعد از نصب این پکیج، کلیه Reference ها از پروژه حدف میشوند و هر بار Build توسط کامپایلر Saltarelle انجام میشود. البته با اولین Build، مقداری Error را خواهید دید که برای از بین بردنشان نیاز است پکیج Saltarelle.Runtime را نیز در این پروژه نصب نمایید:
PM> Install-Package Saltarelle.Compiler PM> Install-Package Saltarelle.Runtime
در صورتیکه کماکان Build نهایی با Error همرا بود، یکبار این پروژه را Unload و سپس مجددا Load نمایید
UI یک پروژه وب MVC است و Client یک Console Application که پکیجهای مورد نیاز Saltarelle روی آن نصب شده است.
در صورتیکه پروژه را Build نماییم و نگاهی به پوشهی Debug بیاندازیم، یک فایل JavaScript همنام پروژه وجود دارد:
برای اینکه بعد از هر بار Build ، فایل اسکریپت به پوشهی مربوطه در پروژه UI منتقل شود کافیست کد زیر را در Post Build پروژه Client بنویسیم:
copy "$(TargetDir)$(TargetName).js" "$(SolutionDir)SalratelleSample.UI\Scripts"
اکنون پس از هر بار Build ، فایل اسکریپت مورد نظر در پوشهی Scripts پروژه UI آپدیت میشود:
در ادامه کافیست فایل اسکریپت را به layout اضافه کنیم.
<script src="~/Scripts/SaltarelleSample.Client.js"></script>
در پوشهی Saltarelle.Runtime در پکیجهای نصب شده، یک فایل اسکریپت به نام mscorlib.min.js نیز وجود دارد که حاوی اسکریپتهای مورد نیاز Saltarelle در هنگام اجراست. آن را به پوشه اسکریپتهای پروژه UI کپی نمایید و سپس به Layout اضافه کنید.
<script src="~/Scripts/mscorlib.min.js"></script> <script src="~/Scripts/SaltarelleSample.Client.js"></script>
حال نوبت به اضافه نمودن libraryهای مورد نیازمان است. برای دسترسی به آبجکت هایی از قبیل document, window, element و غیره در جاوااسکریپت میتوان پکیج Saltarelle.Web را در پروژهی Client نصب نمود و برای دسترسی به اشیاء و فرمانهای jQuery، پکیج Salratelle.jQuery را نصب نمایید.
> Install-Package Saltarelle.Web > Install-Package Saltarelle.jQuery
به این libraryها imported library میگویند. در واقع، در زمان کامپایل، برای این libraryها فایل اسکریپتی تولید نمیشود و فقط آبجکتهای #C هستند که که هنگام کامپایل تبدیل به کدهای ساده اسکریپت میشوند که اگر اسکریپت مربوط به آنها به صفحه اضافه نشده باشد، اجرای اسکریپت با خطا مواجه میشود.
به طور سادهتر وقتی از jQuery library استفاده میکنید هیچ فایل اسکریپت اضافهای تولید نمیشود، اما باید اسکریپت jQuery به صفحه شما اضافه شده باشد.
<script src="~/Scripts/jquery-1.10.2.min.js"></script>
مثال ما یک اپلیکیشن ساده برای خواندن فیدهای همین سایت است. ابتدا کدهای سمت سرور را در پروژه UI می نویسیم.
کلاسهای مورد نیاز ما برای این فید ریدر:
public class Feed { public string FeedId { get; set; } public string Title { get; set; } public string Address { get; set; } } public class Item { public string Title { get; set; } public string Link { get; set; } public string Description { get; set; } }
و یک کلاس برای مدیریت منطق برنامه
public class SiteManager { private static List<Feed> _feeds; public static List<Feed> Feeds { get { if (_feeds == null) _feeds = CreateSites(); return _feeds; } } private static List<Feed> CreateSites() { return new List<Feed>() { new Feed(){ FeedId = "1", Title = "آخرین تغییرات سایت", Address = "https://www.dntips.ir/rss.xml" }, new Feed(){ FeedId = "2", Title = "مطالب سایت", Address = "https://www.dntips.ir/feeds/posts" }, new Feed(){ FeedId = "3", Title = "نظرات سایت", Address = "https://www.dntips.ir/feeds/comments" }, new Feed(){ FeedId = "4", Title = "خلاصه اشتراک ها", Address = "https://www.dntips.ir/feed/news" }, }; } public static IEnumerable<Item> GetNews(string id) { XDocument feedXML = XDocument.Load(Feeds.Find(s=> s.FeedId == id).Address); var feeds = from feed in feedXML.Descendants("item") select new Item { Title = feed.Element("title").Value, Link = feed.Element("link").Value, Description = feed.Element("description").Value }; return feeds; } }
کلاس SiteManager فقط یک لیست از فیدها دارد و متدی که با گرفتن شناسهی فید ، یک لیست از آیتمهای موجود در آن فید ایجاد میکند.
حال دو ApiController برای دریافت دادهها ایجاد میکنیم
public class FeedController : ApiController { // GET api/<controller> public IEnumerable<Feed> Get() { return SiteManager.Feeds; } } public class ItemsController : ApiController { // GET api/<controller>/5 public IEnumerable<Item> Get(string id) { return SiteManager.GetNews(id); } }
در View پیشفرض که Index از کنترلر Home است، یک Html ساده برای
فرم صفحه اضافه میکنیم
<div> <div> <h2>Feeds</h2> <ul id="Feeds"> </ul> </div> <div> <h2>Items</h2> <p id="FeedItems"> </p> </div> </div>
در المنت Feeds لیست فیدها را قرار میدهیم و در FeedItems آیتمهای مربوط به هر فید. حال به سراغ کدهای سمت کلاینت میرویم و به جای جاوا اسکریپت از Saltarelle استفاده میکنیم.
کلاس Program را از پروژه Client باز میکنیم و متد Main را به شکل زیر تغییر میدهیم:
static void Main() { jQuery.OnDocumentReady(() => { FillFeeds(); }); }
بعد از کامپایل شدن، کد #C شارپ بالا به صورت زیر در میآید:
$SaltarelleSample_Client_$Program.$main = function() { $(function() { $SaltarelleSample_Client_$Program.$fillFeeds(); }); }; $SaltarelleSample_Client_$Program.$main();
و این همان متد معروف jQuery است که Saltarelle.jQuery برایمان ایجاد کرده است.
متد FillFeeds را به شکل زیر پیاده سازی میکنیم
private static void FillFeeds() { jQuery.Ajax(new jQueryAjaxOptions() { Url = "/api/feed", Type = "GET", Success = (d,t,r) => { // Fill var ul = jQuery.Select("#Feeds"); jQuery.Each((List<Feed>)d, (idx,i) => { var li = jQuery.Select("<li>").Text(i.Title).CSS("cursor", "pointer"); li.Click(eve => { FillData(i.FeedId); }); ul.Append(li); }); } }); }
آبجکت jQuery، متدی به نام Ajax دارد که یک شی از کلاس jQueryAjaxOptions را به عنوان پارامتر میپذیرد. این کلاس کلیه خصوصیات متد Ajax در jQuery را پیاده سازی میکند. نکته شیرین آن توانایی نوشتن lambda برای Delegate هاست.
خاصیت Success یک Delegate است که 3 پارامتر ورودی را میپذیرد.
public delegate void AjaxRequestCallback(object data, string textStatus, jQueryXmlHttpRequest request);
data همان مقداریست که api باز میگرداند که یک لیست از Feed هاست. برای زیبایی کار، من یک کلاس Feed در پروژه Client اضافه میکنم که خصوصیاتی مشترک با کلاس اصلی سمت سرور دارد و مقدار برگشی Ajax را به آن تبدیل میکنم.
کلاس Feed و Item
[PreserveMemberCase()] public class Feed { //[ScriptName("FeedId")] public string FeedId; //[ScriptName("Title")] public string Title; //[ScriptName("Address")] public string Address; } [PreserveMemberCase()] public class Item { // [ScriptName("Title")] public string Title; // [ScriptName("Link")] public string Link; // [ScriptName("Description")] public string Description; }
jQuery.Each((List<Feed>)d, (idx,i) => { var li = jQuery.Select("<li>").Text(i.Title).CSS("cursor", "pointer"); li.Click(eve => { FillData(i.FeedId); }); ul.Append(li); });
به ازای هر آیتمی که در شیء بازگشتی وجود دارد، با استفاد از متد each در jQuery یک li ایجاد میکنیم. همان طور که میبینید کلیه خواص، به شکل Fluent قابل اضافه شدن میباشد. سپس برای li یک رویداد کلیک که در صورت وقوع، متد FillData را با شناسه فید کلیک شده فراخوانی میکند و در آخر li را به المنت ul اضافه میکنیم.
برای هر کلیک هم مانند مثال بالا api را با شناسهی فید مربوطه فراخوانی کرده و به ازای هر آیتم، یک سطر ایجاد میکنیم.
private static void FillData(string p) { jQuery.Ajax(new jQueryAjaxOptions() { Url = "/api/items/" + p, Type = "GET", Success = (d, t, r) => { var content = jQuery.Select("#FeedItems"); content.Html(""); foreach (var item in (List<Item>)d) { var row = jQuery.Select("<div>").AddClass("row").CSS("direction", "rtl"); var link = jQuery.Select("<a>").Attribute("href", item.Link).Text(item.Title); row.Append(link); content.Append(row); } } }); }
در این مثال ما از Saltarelle.jQuery برای استفاده از jQuery.js استفاده نمودیم. libraryهای متعددی برای Saltarelle از قبیل linq,angular,knockout,jQueryUI,nodeJs ایجاد شده و همچنین قابلیتهای زیادی برای نوشتن imported libraryهای سفارشی نیز وجود دارد.
مطمئنا استفاده از چنین کامپایلرهایی راه حلی سریع برای رهایی از مشکلات متعدد کد نویسی با جاوا اسکریپت در نرم افزارهای بزرگ مقیاس است. اما مقایسه آنها با ابزارهایی از قبیل typescript احتیاج به زمان و تجربه کافی در این زمینه دارد.
var container = new UnityContainer();
container.RegisterType<ISomeService, SomeService>(new PerRequestLifetimeManager()); container.RegisterType<ISomeBusiness, SomeBusiness>(new PerRequestLifetimeManager()); container.RegisterType<ISomeController, SomeController>(new PerRequestLifetimeManager());
DependencyResolver.SetResolver(new UnityDependencyResolver(container));
var someService=(ISomeService)DependencyResolver.Current.GetService(typeof(ISomeService)); var data=someService.GetData();
در لایه سرویس یک کلاس Service Factory داریم که قرار است همه سرویسها، برای برقراری ارتباط با یکدیگر از آن استفاده کنند.این کلاس معمولا در لایه سرویس به اشکال گوناگونی پیاده سازی میشود که کارش وهه سازی از Interfaceهای درخواستی است. اما برای یکپارچه کردن آن با Unity من آنرا به شکل زیر پیاده سازی کرده ام
public class ServiceFactory : MarshalByRefObject { static IUnityContainer uContainer = new UnityContainer(); public static Type DataContextType { get; set; } public static void Initialise(IUnityContainer unityContainer, Type dbContextType) { uContainer = unityContainer; DataContextType = dbContextType; uContainer.RegisterType(typeof(BaseDataContext), DataContextType, new HierarchicalLifetimeManager()); } public static T Create<T>() { return (T)Activator.CreateInstance<T>(); } public static T Create<T>(string fullTypeName) { return (T)System.Reflection.Assembly.GetExecutingAssembly().CreateInstance(fullTypeName); } public static T Create<T>(Type entityType) { return (T)Activator.CreateInstance(entityType); } public static dynamic Create(Type entityType) { return Activator.CreateInstance(entityType); } public static T Get<T>() { return uContainer.Resolve<T>(); } public static object Get(Type type) { return uContainer.Resolve(type); } }
در این کلاس ما بجای ایجاد داینامیک آبجکتها، از Unity استفاده کردهایم. در همان ابتدا که برنامهی وب ما برای اولین بار اجرا میشود و بعد از Register کردن کلاسها، میتوانیم container را به صورت پارامتر سازنده به کلاس Service Factory ارسال کنیم. به این ترتیب برای استفاده از سرویسها در لایه Business از Unity بهره میبریم.
البته استفاده از Unity برای DataContext خیلی منطقی نیست و بهتر است نوع DataContext را در ابتدا بگیریم و هرجا نیاز داشتیم با استفاده از متد Create از آن وهله سازی بکنیم.
بایدها و نبایدهای استفاده از IoC Containers
بنده structuremap را از نوگت گرفتم پوشه ای برایم ایجاد شده که حاوی دوکلاس بود یکی IoC.cs و SmDependencyResolver.cs سوالی که داشتم آیا IOC همان servicelocator است.؟
آیا ObjectFactory.GetInstance همان کار servicelocator را انجام میدهد.؟
آیا var processor = container.Resolve<IArgumentsParser> هم همان کار servicelocator را انام میدهد
مستند سازی ASP.NET Core 2x API توسط OpenAPI Swagger - قسمت چهارم - تکمیل مستندات نوعهای خروجی API
در برنامههای ASP.NET Core، اطلاعات OpenAPI بر اساس سرویس توکاری به نام ApiExplorer تولید میشود که کار آن فراهم آوردن متادیتای مرتبط با یک برنامهی وب است. برای مثال توسط این سرویس میتوان به لیست کنترلرها، متدها و پارامترهای آنها دسترسی یافت. Swashbuckle.AspNetCore به کمک ApiExplorer کار تولید OpenAPI Specification را انجام میدهد. برای فعالسازی این سرویس نیازی نیست کار خاصی انجام شود و زمانیکه ()services.AddMvc را فراخوانی میکنیم، ثبت و معرفی این سرویس نیز جزئی از آن است.
اهمیت تولید Response Types صحیح
در قسمتهای قبل مشاهده کردیم که اگر متدی برای مثال در قسمتی از آن return NotFound یا 404 را داشته باشد، این نوع از خروجی، در OpenAPI Specification تولیدی لحاظ نمیشود و ناقص است و یا حتی ممکن است Response Type پیشفرض تولیدی که 200 است، ارتباطی به هیچکدام از نوعهای خروجی یک اکشن متد نداشته باشد و نیاز به اصلاح آن است. این مورد برای تکمیل مستندات یک API ضروری است و استفاده کنندگان از یک API باید بدانند چون نوع خروجیهایی را ممکن است در شرایط مختلف، دریافت کنند.
روش تغییر و اصلاح Response Type پیشفرض OpenAPI Specification
اکشن متد GetBook کنترلر کتابها، دارای دو نوع return Ok و return NotFound است؛ اما OpenAPI Specification تولیدی پیشفرض، تنها حالت return Ok یا 200 را مستند میکند. برای تکمیل مستندات این اکشن متد، میتوان به صورت زیر عمل کرد:
/// <summary> /// Get a book by id for a specific author /// </summary> /// <param name="authorId">The id of the book author</param> /// <param name="bookId">The id of the book</param> /// <returns>An ActionResult of type Book</returns> /// <response code="200">Returns the requested book</response> [HttpGet("{bookId}")] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task<ActionResult<Book>> GetBook(Guid authorId, Guid bookId)
در اینجا StatusCodes.Status400BadRequest را نیز مشاهده میکنید. هرچند حالت return BadRequest در کدهای این اکشن متد وجود خارجی ندارد، اما در صورت بروز مشکلی در فراخوانی و یا پردازش آن، به صورت خودکار توسط فریمورک بازگشت داده میشود. بنابراین مستندسازی آن نیز ضروری است.
برای آزمایش آن، برنامه را اجرا کنید. در قسمت مستندات متد فوق، اکنون سه حالت 404، 400 و 200 قابل مشاهده هستند. برای نمونه بر روی دکمهی try it out آن کلیک کرده و زمانیکه authorId و bookId را درخواست میکند، دو Guid اتفاقی و کاملا بیربط را وارد کنید. همچنین Controls Accept header را نیز بر روی application/json قرار دهید. سپس بر روی دکمهی execute در ذیل آن کلیک نمائید. یک چنین خروجی 404 کاملی را مشاهده خواهید کرد:
در این تصویر، response body بر اساس rfc 7807 تولید میشود و استاندارد گزارش مشکلات یک API است. این مورد اکنون به صورت یک اسکیمای جدید در انتهای مستندات تولیدی نیز قابل مشاهدهاست:
بهبود مستندات تشخیص نوعهای مدلهای خروجی اکشن متدها
مورد دیگری که در اینجا جالب توجه است، تشخیص نوع خروجی، در حالت return Ok است:
در اینجا اگر بر روی لینک Schema، بجای Example value پیشفرض کلیک کنیم، تصویر فوق حاصل میشود. تشخیص این نوع، به علت استفادهی از ActionResult از نوع Book، به صورت زیر است که در ASP.NET Core 2.1 برای همین منظور (تکمیل مستندات Swagger) معرفی شدهاست:
public async Task<ActionResult<Book>> GetBook(Guid authorId, Guid bookId)
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Book))]
یک نکته: در این تصویر، در قسمت توضیحات حالت 200، عبارت "Returns the requested book" مشاهده میشود. اما در حالتهای دیگر response types تعریف شده، عبارات پیشفرض bad request و یا not found نمایش داده شدهاند. نحوهی بازنویسی این پیشفرضها، با تکمیل مستندات XMLای اکشن متد و ذکر response code دلخواه، به صورت زیر است:
/// <response code="200">Returns the requested book</response>
استفاده از API Analyzers برای بهبود OpenAPI Specification تولیدی
اکنون این سؤال مطرح میشود که پس از این تغییرات، هنوز چه مواردی در OpenAPI Specification تولیدی ما وجود خارجی ندارند و بهتر است اضافه شوند. برای پاسخ به این سؤال، از زمان ارائهی ASP.NET Core 2.2، بستهی جدید Microsoft.AspNetCore.Mvc.Api.Analyzers نیز ارائه شدهاست که کار آن دقیقا بررسی همین نقایص و کمبودها است. بنابراین ابتدا آنرا به فایل OpenAPISwaggerDoc.Web.csproj اضافه کرده و سپس دستور dotnet restore را صادر میکنیم:
<Project Sdk="Microsoft.NET.Sdk.Web"> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Mvc.Api.Analyzers" Version="2.2.0" /> </ItemGroup>
Controllers\BooksController.cs(40,17): warning API1000: Action method returns undeclared status code '404'. Controllers\BooksController.cs(89,13): warning API1000: Action method returns undeclared status code '201'.
/// <summary> /// Get the books for a specific author /// </summary> /// <param name="authorId">The id of the book author</param> /// <returns>An ActionResult of type IEnumerable of Book</returns> [HttpGet()] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesDefaultResponseType] public async Task<ActionResult<IEnumerable<Book>>> GetBooks(Guid authorId)
کار آن مدیریت تمام حالتهای دیگری است که هنوز توسط ProducesResponseTypeها تعریف یا پیشبینی نشدهاند. هرچند وجود آن میتواند در یک چنین مواردی مفید باشد، اما همواره تعریف صریح نوعهای خروجی نسبت به استفادهی از یک حالت پیشفرض برای تمام آنها، ترجیح داده میشود.
ساده سازی کدهای تکراری تعریف ProducesResponseTypeها
مواردی مانند StatusCodes.Status400BadRequest و یا 406 را در حالت عدم قبول درخواست (مثلا با انتخاب یک accept header اشتباه) و یا 500 را در صورت وجود استثنائی در سمت سرور، باید به تمام اکشن متدها نیز اضافه کرد؛ چون میتوانند تحت شرایطی، نوعهای خروجی معتبری باشند. برای خلاصه سازی این عملیات، یا میتوان این ویژگیها را بجای قراردادن آنها در بالای تعریف امضای اکشن متدها، به بالای تعریف کلاس کنترلر انتقال داد. با اینکار ویژگی که به یک کنترلر اعمال شده باشد به تمام اکشن متدهای آن نیز اعمال خواهد شد و یا حتی برای عدم تعریف این ویژگیهای تکراری به ازای هر کنترلر موجود، میتوان آنها را به صورت سراسری تعریف کرد:
namespace OpenAPISwaggerDoc.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddMvc(setupAction => { setupAction.Filters.Add( new ProducesResponseTypeAttribute(StatusCodes.Status400BadRequest)); setupAction.Filters.Add( new ProducesResponseTypeAttribute(StatusCodes.Status406NotAcceptable)); setupAction.Filters.Add( new ProducesResponseTypeAttribute(StatusCodes.Status500InternalServerError)); setupAction.Filters.Add( new ProducesDefaultResponseTypeAttribute()); setupAction.ReturnHttpNotAcceptable = true; // ...
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: OpenAPISwaggerDoc-04.zip
در قسمت بعد، روشهای دیگری را برای تکمیل مستندات خروجی API بررسی میکنیم.
export class Book { constructor( public id, public title:string, public pages:Array ){} } return this._http.get('getBook/1') .map(function(res){ var data = res.json(); return new Book(data.id, data.title, data.pages); })
struct Product { name: String, price: f32, in_stock: bool, }
در ادامه بیایید تابع catalog_sales_tax را به عنوان روشی برای ساختار Product، با استفاده از یک implementation block اضافه کنیم.
impl Product { fn calculate_sales_tax(&self) -> f32 { self.price * 0.1 } }
اکنون میتوانیم یک نمونه محصول را ایجاد کنیم و از روش محاسبه مالیات استفاده کنیم:
fn main() { let book = Product { name: String::from("Book"), price: 28.85, in_stock: true, }; let sales_tax = book.calculate_sales_tax(); println!("Sales tax: {}", sales_tax); }
fn calculate_sales_tax(&self) -> f32 { self.price * 0.1 }
fn set_price(&mut self, price: f32) { self.price = price; }
fn buy(self) { let name: String = self.name; println!("{} was bought", name); }
Associated Functions
Associated functions از کلمه کلیدی self استفاده نمیکنند و با استفاده از سینتکس :: فراخوانی میشوند. با این حال، هنگام فراخوانی توابع مرتبط در متدهایی که از self استفاده میکنند، '.' بجای آن از سینتکس '::' استفاده میشود:
impl Product { fn new(name: String, price: f32) -> Product { Product { name, price, in_stock: true, } } }
اکنون، میتوانیم یک نمونه محصول را با استفاده از تابع جدید مرتبط ایجاد کنیم:
fn main() { let book = Product::new(String::from("Book"), 30.0); }
نکته بعدی جهت فعال سازی EnableCors چه کار خاصی باید انجام شود؟
من فعال سازی انجام دادم و در WebApiConfig برای همه کنترلرها فعال سازی انجام دادم اما آدرس login ظاهرا جز کنترلرها نیست این تنظیمات اعمال نمیشود و عملا برنامه بلا استفاده خواهد شد. قصد من استفاده از توکن بین چند دامنه خاص است. اما فعلا ازمایشی جهت استفاده همه دامنهها فعال سازی انجام شده ولی در عمل کار نمیکند. و خطای CORS در مرورگر دریافت میشود.
جهت اعمال به آدرس لاگین چه باید کرد نمونه کد اعمال CORS
public static class WebApiConfig { public static void Register(HttpConfiguration config) { var cors = new EnableCorsAttribute("*", "*", "*"); config.EnableCors(cors); // ... } }
در ابتدای بحث، برای آشنایی بیشتر با HTML Helperها به مطالعه این مقاله بپردازین.
در این مقاله قرار است برای یک HTML Helper خاص، قالب نمایشی اختصاصی خودمان را طراحی کنیم و به نحوی HTML Helper موجود را سفارشی سازی کنیم. به عنوان مثال میخواهیم خروجی یک EditorFor() برای یک نوع خاص، به حالت دلخواهی باشد که ما خودمان آن را تولیدش کردیم؛ یا اصلا نه. حتی میشود برای خروجی یک EditorFor() که خصوصیتی از جنس string را میخواهیم به آن انتساب دهیم، به جای تولید input، یک مقدار متنی را برگردانیم. به این حالت:
<div> @Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" }) <div> @Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" }) </div> </div> <div> @Html.LabelFor(model => model.Genre, htmlAttributes: new { @class = "control-label col-md-2" }) <div> @Html.EditorFor(model => model.Genre, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Genre, "", new { @class = "text-danger" }) </div> </div>
در ادامه یک پروژهی عملی را شروع کرده و در آن کاری را که میخواهیم، انجام میدهیم. پروژهی ما به این شکل میباشد که قرار است در آن به ثبت کتاب بپردازیم و برای هر کتاب هم یک سبک داریم و قسمت سبک کتابهای ما یک Enum است که از قبل میخواهیم مقدارهایش را تعریف کنیم.
مدل برنامه
public class Books { public int Id { get; set; } [Required] [StringLength(255)] public string Name { get; set; } public Genre Genre { get; set; } }
public enum Genre { [Display(Name = "Non Fiction")] NonFiction, Romance, Action, [Display(Name = "Science Fiction")] ScienceFiction }
در داخل کلاس Books یک خصوصیت از جنس Genre برای سبک کتابها داریم و در داخل نوع شمارشی Genre، سبکهای ما تعریف شدهاند. همچنین هر کدام از سبکها هم به ویژگی Display مزین شدهاند تا بتونیم بعدا از مقدار آنها استفاده کنیم.
کنترلر برنامه
public class BookController : Controller { // GET: Book public ActionResult Index() { return View(DataAccess.DataContext.Book.ToList()); } public ActionResult Create() { return View(); } [HttpPost] [ValidateAntiForgeryToken] public ActionResult Create(Books model) { if (!ModelState.IsValid) return View(model); try { DataAccess.DataContext.Book.Add(model); DataAccess.DataContext.SaveChanges(); return RedirectToAction("Index"); } catch (Exception ex) { ModelState.AddModelError("", ex.Message); return View(model); } } public ActionResult Edit(int id) { try { var book = DataAccess.DataContext.Book.Find(id); return View(book); } catch (Exception ex) { return View("Error"); } } [HttpPost] [ValidateAntiForgeryToken] public ActionResult Edit(Books model) { if (!ModelState.IsValid) return View(model); try { DataAccess.DataContext.Book.AddOrUpdate(model); DataAccess.DataContext.SaveChanges(); return RedirectToAction("Index"); } catch (Exception ex) { ModelState.AddModelError("", ex.Message); return View(model); } } public ActionResult Details(int id) { try { var book = DataAccess.DataContext.Book.Find(id); return View(book); } catch (Exception ex) { return View("Error"); } } }
در قسمت کنترلر هم کار خاصی جز عملیات اصلی نوشته نشدهاست. لیست کتابها را از پایگاه داده بیرون آوردیم و از طریق اکشن Index به نمایش گذاشتیم. با اکشنهای Create، Edit و Details هم کارهای روتین مربوط به خودشان را انجام دادیم. نکتهی قابل تذکر، DataAccess میباشد که کلاسی است که با آن ارتباط برقرار شده با EF و سپس اطلاعات واکشی و تزریق میشوند.
View مربوط به اکشن Create برنامه
@using Book.Entities @model Book.Entities.Books @{ ViewBag.Title = "Create"; } <h2>New Book</h2> @using (Html.BeginForm()) { @Html.AntiForgeryToken() <div> <h4>Books</h4> <hr /> @Html.ValidationSummary(true, "", new { @class = "text-danger" }) <div> @Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" }) <div> @Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" }) </div> </div> <div> @Html.LabelFor(model => model.Genre, htmlAttributes: new { @class = "control-label col-md-2" }) <div> @Html.EditorFor(model => model.Genre, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Genre, "", new { @class = "text-danger" }) </div> </div> <div> <div> <input type="submit" value="Create" /> <input type="reset" value="Reset" /> @Html.ActionLink("Back to List", "Index", null, new {@class="btn btn-default"}) </div> </div> </div> } @section Scripts { @Scripts.Render("~/bundles/jqueryval") }
View برنامه هم همان ویویی است که خود Visual Studio برای ما ساختهاست. به جز یک سری دستکاریهایی داخل سیاساس، هدف از گذاشتن View مربوط به Create این بود که قرار است بر روی این قسمت کار کنیم. اگر پروژه رو اجرا کنید و به قسمت Create بروید، مشاهده خواهید کرد که برای Genre یک input ساخته شدهاست که کاربر باید در آن مقدار وارد کند. ولی اگر یادتان باشد، ما سبکهای نگارشی خودمان را در نوع شمارشی Genre ایجاد کرده بودیم. پس عملا باید یک لیست به کاربر نشان داده شود که تا از آن لیست، نوع را انتخاب کند. میتوانیم بیایم همینجا در داخل View مربوطه، بهجای استفاده از HTML Helper پیشفرض، از DropDownList یا EnumFor استفاده کنیم و به طریقی این لیست را ایجاد کنیم. ولی چون قرار است در این مثال به شرح موضوع مقاله خودمان بپردازیم، این کار را انجام نمیدهیم.
در حقیقیت میخوایم متد EditorFor را طوری سفارشی سازی کنیم که برای نوع شمارشی Genre، به صورت خودکار یک لیست ایجاد کرده و برگرداند. از نسخهی سوم ASP.NET MVC به بعد این امکان برای توسعه دهندهها فراهم شدهاست. شما میتوانید در پوشهی Shared داخل پوشه Views برنامه، پوشهای را به اسم EditorTemplates ایجاد کنید؛ همینطور DisplayTemplates و برای نوع خاصی که میخواهید سفارشیسازی را برای آن انجام دهید، یک PartialView بسازید.
Views/Shared/DisplayTemplates/<type>.cshtml
کاری که الان میخواهیم انجام دهیم این است که یک SelectListItem ایجاد کرده تا مقدارهای نوع Genreمان داخلش باشد و بتوانیم به راحتی برای ساختن DropDownList از آن استفاده کنیم. برای این کار Helper مخصوص خودمان را ایجاد میکنیم. پوشهای به اسم Helpers در کنار پوشههای Controllers، Models ایجاد میکنیم و در داخل آن کلاسی به اسم EnumHelpers میسازیم.
public static class EnumHelpers { public static IEnumerable<SelectListItem> GetItems( this Type enumType, int? selectedValue) { if (!typeof(Enum).IsAssignableFrom(enumType)) { throw new ArgumentException("Type must be an enum"); } var names = Enum.GetNames(enumType); var values = Enum.GetValues(enumType).Cast<int>(); var items = names.Zip(values, (name, value) => new SelectListItem { Text = GetName(enumType, name), Value = value.ToString(), Selected = value == selectedValue } ); return items; } static string GetName(Type enumType, string name) { var result = name; var attribute = enumType .GetField(name) .GetCustomAttributes(inherit: false) .OfType<DisplayAttribute>() .FirstOrDefault(); if (attribute != null) { result = attribute.GetName(); } return result; } }
در توضیح کد بالا عنوان کرد که متدها بهصورت متدهای الحاقی به نوع Type نوشته شدند. کار خاصی در بدنهی متدها انجام نشدهاست. در بدنهی متد اول لیست آیتمها را تولید کردیم. در هنگام ساخت SelectListItem برای گرفتن Text، متد GetName را صدا زدیم. برای اینکه بتوانیم مقدار ویژگی Display که در هنگام تعریف نوع شمارشی استفاده کردیم را بدست بیاریم، باید چک کنیم ببینیم که آیا این آیتم به این ویژگی مزین شدهاست یا نه. اگر شده بود مقدار را میگیریم و به خصوصیت Text متد اول انتساب میدهیم.
@using Book.Entities @using Book.Web.Helpers @{ var items = typeof(Genre).GetItems((int?)Model); } @Html.DropDownList("", items, new {@class="form-control"})
کدهایی که در بالا مشاهده میکنید کدهایی میباشند که قرار است داخل PartialViewی Genre قرار دهیم که در پوشهی EditorTemplates ساختیم. ابتدا آمدیم آیتمها را گرفتیم و بعد به DropDownList دادیم تا لیست نوع را برای ما بسازد. حالا اگه برنامه را اجرا کنید میبینید که EditorFor برای شما یه لیست از نوع شمارشی ساخته و حالا قابل استفاده هست.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید