که اینجا دکمهها از سمت راست به چپ، عملیات افزودن، عدم انتخاب، ویرایش و حذف را انجام میدهند. کدهای HTML این پنل را در ادامه مشاهده میکنید:
<div id="CrudPanel" class="row treeview-panel" > <div class="col-lg-7 pull-right"> <input type="text" id="txtLocationTitle" class="form-control" /> </div> <div class="col-lg-5 pull-left" style="text-align: left;"> <button data-toggle="tooltip" data-placement="left" title="افزودن" id="btnAddLocation" class="btn btn-sm btn-success"> <i class="fa fa-plus"></i> </button> <button data-toggle="tooltip" data-placement="left" title="عدم انتخاب" id="btnUnSelect" class="btn btn-sm btn-info"> <i class="fa fa-square-o"></i> </button> <button data-toggle="tooltip" data-placement="left" title="ویرایش" id="btnEditLocation" class="btn btn-sm btn-warning"> <i class="fa fa-pencil"></i> </button> <button data-toggle="tooltip" data-placement="left" title="حذف" id="btnDeleteLocation" class="btn btn-sm btn-danger"> <i class="fa fa-times"></i> </button> </div> </div>
و قطعه کد ذیل مربوط به پنل ویرایش است که در ابتدای کار کلاس hide به آن انتساب داده شده و پنهان میشود:
<div id="EditPanel" class="row edit hide treeview-panel"> <div class="col-lg-7 pull-right"> <input type="text" id="txtLocationEditTitle" class="form-control" /> </div> <div class="col-lg-5 pull-left" style="text-align: left"> <input type="button" value="ویرایش" id="btnEditPanelLocation" data-code="" data-parentId="" class="btn btn-sm btn-success" /> <input type="button" value="انصراف" id="btnCancle" class="btn btn-sm btn-info" /> </div> </div>
در آخر این تکه کد نیز مربوط به KendoUI TreeView است:
<div class="col-lg-6 k-rtl treeview-style"> @(Html.Kendo() .TreeView() .Name("treeview") .DataTextField("Title") .DragAndDrop(false) .DataSource(dataSource => dataSource .Model(model => model.Id("Id")) .Read(read => read.Action(MVC.Admin.Location.ActionNames.GetAllAssetGroupTree, MVC.Admin.Location.Name))) ) </div>
یک نکته
- کلاس k-rtl مربوط به خود treeview میباشد و با این کلاس، درخت ما راست به چپ میشود.
در ادامه cssهای مربوط به کلاسهای treeview-style ،hide و treeview-panel بررسی خواهند شد:
.treeview-style { min-height: 86px; max-height: 300px; overflow: scroll; overflow-x: hidden; position: relative; } .treeview-panel { background-color: #eee; padding: 25px 0 25px 0; } .hide { display: none; }
تا اینجای مقاله، کدهای Html و Css موجود را بررسی کردیم. حالا سراغ قسمت اصلی خواهیم رفت. یعنی عملیات CRUD.
لازم به ذکر است در ابتدای قسمت script باید این چند خط کد نوشته شود:
var treeview = null; $(window).load(function () { treeview = $("#treeview").data("kendoTreeView"); });
در اینجا بعد از بارگذاری کامل صفحه، درخت مورد نظر ما ساخته خواهد شد و میتوان به متغیر treeview در تمام قسمت script دسترسی داشت.
پیاده سازی عملیات افزودن:
$(document).on('click', '#btnAddLocation', function () { var title = $('#txtLocationTitle').val(); var selectedNodeId = null; var selectedNode = treeview.select(); if (selectedNode.length == 0) { selectedNode = null; } else { selectedNodeId = treeview.dataItem(selectedNode).id;// گرفتن آی دی گره انتخاب شده } $.ajax({ url: '@Url.Action(MVC.Admin.Location.CreateByAjax())', type: 'POST', data: { Title: title, ParentId: selectedNodeId }, success: function (data) { debugger; showMessage(data.message, data.notificationType); if (data.result) treeview.dataSource.read(); }, error: function () { showMessage('لطفا مجددا تلاش نمایید', 'warning'); } }); });
توضیحات: مقدار گره جدید را خوانده و در متغیر title قرار میدهیم. گره انتخاب شده را توسط این خط
var selectedNode = treeview.select();
می گیریم و سپس در ادامه بررسی خواهیم کرد تا اگر گرهای انتخاب نشده باشد، به کاربر پیغامی را نشان دهد؛ در غیر این صورت توسط ajax، مقادیر مورد نظر، به اکشن ما در LocationController ارسال میشوند:
[HttpPost] public virtual ActionResult CreateByAjax(AddLocationViewModel locationViewModel) { if (ModelState.IsNotValid()) return JsonResult(false, "عنوان نباید خالی و یا کمتر از دو کاراکتر باشد.", NotificationType.Error); var result = _locationService.Add(locationViewModel);//سرویس مورد نظر برای اضافه کردن به دیتابیس switch (result) { case AddStatus.AddSuccessful: _uow.SaveChanges(); return JsonResult(true, Messages.SaveSuccessfull, NotificationType.Success); case AddStatus.Faild: return JsonResult(false, Messages.SaveFailed, NotificationType.Error); case AddStatus.Exists: return JsonResult(false, Messages.DataExists, NotificationType.Warning); default: return JsonResult(false, Messages.SaveFailed, NotificationType.Error); } }
public virtual JsonResult JsonResult(bool result, string message, string notificationType) { return Json(new { result = result, message = message, notificationType = notificationType }, JsonRequestBehavior.AllowGet); }
اکشن JsonResult که مقادیر نتیجه، پیغام و نوع اطلاع رسانی را میگیرد و یک آبجکت از نوع json را به تابع success ایجکس، ارسال میکند.
public class AddLocationViewModel { [DisplayName("عنوان")] [Required(ErrorMessage ="لطفا عنوان گروه را وارد نمایید"),MinLength(2,ErrorMessage ="طول عنوان خیلی کوتاه میباشد ")] public string Title { get; set; } [DisplayName("گروه پدر")] public Guid? ParentId { get; set; } }
این کلاس viewModel ما میباشد.
public enum AddStatus { AddSuccessful, Faild, Exists }
و این مورد هم کلاس AddStatus از نوع enum.
public class Messages { #region Fields public const string SaveSuccessfull = "اطلاعات با موفقیت ذخیره شد"; public const string SaveFailed = "خطا در ثبت اطلاعات"; public const string DeleteMessage = "کابر گرامی ، آیا از حذف کردن این رکورد مطمئن هستید ؟"; public const string DeleteSuccessfull = "اطلاعات با موفقیت حذف شد"; public const string DeleteFailed = "خطا در حذف اطلاعات ، لطفا مجددا تلاش نمایید"; public const string DeleteHasInclude = "کاربر گرامی ، رکورد مورد نظر هم اکنون در بانک اطلاعاتی سیستم در حال استفاده توسط منابع دیگر میباشد"; public const string NotFoundData = "اطلاعات یافت نشد"; public const string NoAttachmentSelect = "تصویری انتخاب نشده است"; public const string DataExists = "اطلاعات وارد شده در بانک اطلاعاتی موجود میباشد"; public const string DeletedRowHasIncluded = "کاربر گرامی ، رکوردی که قصد حذف آن را دارید هم اکنون در بانک اطلاعاتی سیستم ، توسط سایر بخشها در حال استفاده میباشد"; #endregion }
و این موارد هم مقادیر ثابت فیلدهای مورد استفادهی ما در کلاس Message.
پیاده سازی عملیات حذف
به طور اختصار، عملیات حذف را توضیح میدهم تا به قسمت اصلی مقاله یعنی ویرایش بپردازیم:
$(document).on('click', '#btnDeleteLocation', function () { var selectedNode = treeview.select(); var currentNode = treeview.dataItem(selectedNode); if (selectedNode.length == 0) { showMessage('گزینه ای انتخاب نشده است. لطفا یک گزینه انتخاب نمایید', 'warning'); } else { var selectedNodeId = treeview.dataItem(selectedNode).id; if (currentNode.hasChildren) { var title = 'کاربر گرامی ، با حذف شدن این گره، تمام زیر شاخههای آن حذف میشود. آیا مطمئن هستید ؟ '; DeleteConfirm(selectedNodeId, '@Url.Action(MVC.Admin.Location.DeleteByAjax())', title); } else { $.ajax({ url: '@Url.Action(MVC.Admin.Location.DeleteByAjax())', type: 'POST', data: { id: selectedNodeId }, success: function (data) { debugger; showMessage(data.message, data.notificationType); if (data.result) treeview.remove(selectedNode); }, error: function () { showMessage('لطفا مجددا تلاش نمایید', 'warning'); } }); } } });
این مورد نیز همانند عملیات افزودن عمل میکند. یعنی ابتدا چک میکند که آیا گرهای انتخاب شده است یا خیر؟ و اگر گره انتخابی ما دارای فرزند باشد، به کاربر پیغامی را نشان میدهد و میگوید «گره مورد نظر، دارای فرزند است. آیا مایل به حذف تمام فرزندان آن هستید؟» مانند تصویر زیر:
در نهایت چه گره انتخابی دارای فرزند باشد و چه نباشد، به یک مسیر مشترک ارسال میشوند:
public virtual ActionResult DeleteByAjax(Guid id) { var result = _locationService.Delete(id); switch (result) { case DeleteStatus.Successfull: _uow.SaveChanges(); return DeleteJsonResult(true, Messages.DeleteSuccessfull, NotificationType.Success); case DeleteStatus.NotFound: return DeleteJsonResult(false, Messages.NotFoundData, NotificationType.Error); case DeleteStatus.Failed: return DeleteJsonResult(false, Messages.DeleteFailed, NotificationType.Error); case DeleteStatus.ThisRowHasIncluded: return DeleteJsonResult(false, Messages.DeletedRowHasIncluded, NotificationType.Warning); default: return DeleteJsonResult(false, Messages.DeleteFailed, NotificationType.Error); } }
در سرویس مورد نظر ما یعنی Delete، اگه گرهای دارای فرزند باشد، تمام فرزندان آن را حذف میکند. حتی فرزندان فرزندان آن را:
public DeleteStatus Delete(Guid id) { var model = GetAsModel(id); if (model == null) return DeleteStatus.NotFound; if (!CanDelete(model)) return DeleteStatus.ThisRowHasIncluded; _uow.MarkAsSoftDelete(model, _userManager.GetCurrentUserId()); if (model.Children.Any()) DeleteChildren(model); return DeleteStatus.Successfull; }
private void DeleteChildren(Location model) { foreach (var item in model.Children) { _uow.MarkAsSoftDelete(item, _userManager.GetCurrentUserId()); if (item.Children.Any()) DeleteChildren(item); } }
public class Location:BaseEntity,ISoftDelete { public string Title { get; set; } public Location Parent { get; set; } public Guid? ParentId { get; set; } public bool IsDeleted { get; set; } public virtual ICollection<Location> Children { get; set; } }
و این هم مدل Location که سمت سرور از مدل استفاده میکنیم.
پیاده سازی عملیات ویرایش
حالا به قسمت اصلی مقاله رسیدیم. در اینجا قرار است گرهای را انتخاب نماییم و با زدن دکمه ویرایش و باز شدن پنل آن، آن را ویرایش کنیم. با زدن دکمه ویرایش، کدهای زیر اجرا میشوند:
// Open Edit Panel $(document).on('click', '#btnEditLocation', function () { debugger; var selectedNode = treeview.select(); var currentNode = treeview.dataItem(selectedNode);// با استفاده از این خط، گره انتخاب شده جاری را میگیریم. if (selectedNode.length == 0) { //این شرط به ما میگوید اگر گره ای انتخاب نشده بود پیغامی به کاربر نمایش بده showMessage('گزینه ای انتخاب نشده است. لطفا یک گزینه انتخاب نمایید', 'warning'); } else { var selectedNodeCode = treeview.dataItem(selectedNode).Code; var selectedNodeTitle = treeview.dataItem(selectedNode).Title; var selectedNodeParentId = treeview.dataItem(selectedNode).ParentId; // آی دی یا کد، عنوان و آی دی پدر گره انتخاب شده را با استفاده از این سه خط در اختیار میگیریم $('#CrudPanel').toggleClass('hide'); //المنت کرادپنل که در حال حاضر کاربر آن را میبیند، با این خط کد، پنهان میشود $('#EditPanel').toggleClass('hide'); //المنت ادیت پنل که در حال حاضر از دید کاربر پنهان است، قابل نمایش میشود $("#txtLocationEditTitle").val(selectedNodeTitle); //عنوان گره ای که میخواهیم آن را ویرایش کنیم در تکست باکس مورد نظر قرار میگیرد $("#txtLocationEditTitle").focusTextToEnd(); // با استفاده از این پلاگین، کرسر ماوس در انتهای مقدار دیفالت تکست باکس قرار میگیرد $("#btnEditPanelLocation").attr('data-code', selectedNodeCode); $("#btnEditPanelLocation").attr('data-parentId', selectedNodeParentId == null ? '' : selectedNodeParentId); //مقادیر پرنت آی دی و کد را در دیتا اتریبیوتهای موجود در المنت خودمان قرار میدهیم // Disable clicking in treeview $("#treeview").children().bind('click', function () { return false; }); } }); (function ($) { $.fn.focusTextToEnd = function () { this.focus(); var $thisVal = this.val(); this.val('').val($thisVal); return this; } }(jQuery));
کد زیر باعث میشود تا زمانیکه پنل ویرایش باز است، کاربر نتواند هیچ کلیکی را در عناصر داخل درخت ما، داشته باشد.
$("#treeview").children().bind('click', function () { return false; });
و در نهایت با زدن دکمه ویرایش، پنل ویرایش ما به صورت زیر باز میشود:
همانطور که در تصویر بالا مشاهده میکنید، با انتخاب ساختمان مرکزی و زدن دکمه ویرایش، پنل CRUD ما پنهان و پنل ویرایش ظاهر میگردد. همچنین عنوان گره انتخابی به عنوان پیش فرض تکست باکس ما تنظیم میشود و کاربر نمیتواند گره دیگری را انتخاب کند؛ به شرط آنکه این پنل ویرایش بسته شود.
با تغییر عنوان تکست باکس و زدن دکمهی ویرایش، رویداد زیر رخ میدهد:
// Edit tree node $(document).on('click', '#btnEditPanelLocation', function () { debugger; var code = $("#btnEditPanelLocation").attr('data-code'); var parentId = $("#btnEditPanelLocation").attr('data-parentId'); var title = $("#txtLocationEditTitle").val().trim(); $.ajax({ url: '@Url.Action(MVC.Admin.Location.EditByAjax())', type: 'POST', data: { Code: code, Title: title, ParentId: parentId.length === 0 ? null : parentId }, success: function (data) { debugger; showMessage(data.message, data.notificationType); if (data.result) { treeview.dataSource.read(); CloseEditPanel(); } }, error: function () { showMessage('لطفا مجددا تلاش نمایید', 'warning'); } }); });
[HttpPost] public virtual ActionResult EditByAjax(EditLocationViewModel editLocationViewModel) { if (ModelState.IsNotValid()) return JsonResult(false,"عنوان نباید خالی و یا کمتر از دو کاراکتر باشد.", NotificationType.Error); var result = _locationService.Edit(editLocationViewModel); switch (result) { case EditStatus.Successful: _uow.SaveChanges(); return JsonResult(true, Messages.SaveSuccessfull, NotificationType.Success); case EditStatus.NotFound: return JsonResult(false, Messages.NotFoundData, NotificationType.Error); case EditStatus.Faild: return JsonResult(false, Messages.SaveFailed, NotificationType.Error); case EditStatus.Exists: return JsonResult(false, Messages.DataExists, NotificationType.Warning); default: return JsonResult(false, Messages.SaveFailed, NotificationType.Error); } }
تابع CloseEditPanel بعد از اتمام ویرایش هر گره و یا با زدن دکمه انصراف در شکل بالا، فراخوانی میشود که کد آن به شکل زیر است:
function CloseEditPanel() { $('#CrudPanel').toggleClass('hide'); //پنل کراد ما که در حال حاضر از دید کاربر پنهان است با این خط ظاهر میگردد $('#EditPanel').toggleClass('hide'); //پنل ویرایش ما که در حال حاضر کاربر آن را میبیند، پنهان میشود از دید کاربر $("#txtLocationEditTitle").val(''); //مقدار تکست باکس خالی میشود $("#btnEditPanelLocation").attr('data-code', ''); $("#btnEditPanelLocation").attr('data-parentId', ''); //دیتا اتریبیوتهای ما که مقادیر کد و آی دی والد در آن قرار گرفته نیز خالی میشود // Enable clicking in treeview $("#treeview").children().unbind('click').bind('click', function () { return true; }); //اگر یادتان باشد با یک خط کد به کاربر اجازه ندادیم که با باز شدن پنل ویرایش، گره دیگری را انتخاب نمایی. حالا این خط کد عکس کد قبلیست و به کاربر اجازه میدهد در المنت مورد نظر کلیک کند }
// Cancle edit Node tree $(document).on('click', '#btnCancle', function () { CloseEditPanel(); });
$(document).on('click', '#btnUnSelect', function () { //رویداد عدم انتخاب treeview.select(null); });
public class MyEntity { public Guid Id {get;set;} public string[] Strings { get; set; } }
modelBuilder.Entity<MyEntity>() .Id(e=> e.Id) .Property(e => e.Strings) .HasConversion( v => string.Join(',', v), v => v.Split(',', StringSplitOptions.RemoveEmptyEntries));
ایجاد یک اسمبلی جدید توسط Reflection.Emit
using System; using System.Reflection; using System.Reflection.Emit; namespace FastReflectionTests { class Program { static void Main(string[] args) { var name = "HelloWorld.exe"; var assemblyName = new AssemblyName(name); // ایجاد یک اسمبلی جدید با قابلیت ذخیره سازی آن var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly( name: assemblyName, access: AssemblyBuilderAccess.RunAndSave); // افزودن یک ماژول به اسمبلی var moduleBuilder = assemblyBuilder.DefineDynamicModule(name); // تعریف یک کلاس در این ماژول var programmClass = moduleBuilder.DefineType("Program", TypeAttributes.Public); // افزودن یک متد به این کلاس // این متد خروجی ندارد اما ورودی آن شبیه به متد اصلی یک برنامه کنسول است var mainMethod = programmClass.DefineMethod(name: "Main", attributes: MethodAttributes.Public | MethodAttributes.Static, returnType: null, parameterTypes: new Type[] { typeof(string[]) }); // تعیین بدنه متد اصلی برنامه var il = mainMethod.GetILGenerator(); il.Emit(OpCodes.Ldstr, "Hello World!"); il.Emit(OpCodes.Call, (typeof(Console)).GetMethod("WriteLine", new Type[] { typeof(string) })); il.Emit(OpCodes.Call, (typeof(Console)).GetMethod("ReadKey", new Type[0])); il.Emit(OpCodes.Pop); il.Emit(OpCodes.Ret); // تکمیل کار ایجاد نوع جدید programmClass.CreateType(); // تعیین نقطه شروع فایل اجرایی برنامه کنسول تهیه شده assemblyBuilder.SetEntryPoint(((Type)programmClass).GetMethod("Main")); // ذخیره سازی این اسمبلی بر روی دیسک سخت assemblyBuilder.Save(name); } } }
using System; public class Program { public static void Main(string[] array) { Console.WriteLine("Hello World!"); Console.ReadKey(); } }
سرویس ما متدهای زیر را در دسترس قرار میدهد.
Relative URl | HTTP method | Action |
api/products/ | GET | گرفتن لیست تمام محصولات |
api/products/id/ | GET | گرفتن یک محصول بر اساس شناسه |
api/products?category=category/ | GET | گرفتن یک محصول بر اساس طبقه بندی |
api/products/ | POST | ایجاد یک محصول جدید |
api/products/id/ | PUT | بروز رسانی یک محصول |
api/products/id/ | DELETE | حذف یک محصول |
همانطور که مشاهده میکنید برخی از آدرس ها، شامل شناسه محصول هم میشوند. بعنوان مثال برای گرفتن محصولی با شناسه 28، کلاینت یک درخواست GET را به آدرس زیر ارسال میکند:
http://hostname/api/products/28
منابع
سرویس ما آدرس هایی برای دستیابی به دو نوع منبع (resource) را تعریف میکند:
URI | Resource |
api/products/ | لیست تمام محصولات |
api/products/id/ | یک محصول مشخص |
متد ها
چهار متد اصلی HTTP یعنی همان GET, PUT, POST, DELETE میتوانند بصورت زیر به عملیات CRUD نگاشت شوند:
- متد GET یک منبع (resource) را از آدرس تعریف شده دریافت میکند. متدهای GET هیچگونه تاثیری روی سرور نباید داشته باشند. مثلا حذف رکوردها با متد اکیدا اشتباه است.
- متد PUT یک منبع را در آدرس تعریف شده بروز رسانی میکند. این متد برای ساختن منابع جدید هم میتواند استفاده شود، البته در صورتی که سرور به کلاینتها اجازه مشخص کردن آدرسهای جدید را بدهد. در مثال جاری پشتیبانی از ایجاد منابع توسط متد PUT را بررسی نخواهیم کرد.
- متد POST منبع جدیدی میسازد. سرور آدرس آبجکت جدید را تعیین میکند و آن را بعنوان بخشی از پیام Response بر میگرداند.
- متد DELETE منبعی را در آدرس تعریف شده حذف میکند.
نکته: متد PUT موجودیت محصول (product entity) را کاملا جایگزین میکند. به بیان دیگر، از کلاینت انتظار میرود که آبجکت کامل محصول را برای بروز رسانی ارسال کند. اگر میخواهید از بروز رسانیهای جزئی/پاره ای (partial) پشتیبانی کنید متد PATCH توصیه میشود. مثال جاری متد PATCH را پیاده سازی نمیکند.
یک پروژه Web API جدید بسازید
ویژوال استودیو را باز کنید و پروژه جدیدی از نوع ASP.NET MVC Web Application بسازید. نام پروژه را به "ProductStore" تغییر دهید و OK کنید.
در دیالوگ New ASP.NET Project قالب Web API را انتخاب کرده و تایید کنید.
افزودن یک مدل
یک مدل، آبجکتی است که داده اپلیکیشن شما را نمایندگی میکند. در ASP.NET Web API میتوانید از آبجکتهای Strongly-typed بعنوان مدل هایتان استفاده کنید که بصورت خودکار برای کلاینت به فرمتهای JSON, XML مرتب (Serialize) میشوند. در مثال جاری، دادههای ما محصولات هستند. پس کلاس جدیدی بنام Product میسازیم.
در پوشه Models کلاس جدیدی با نام Product بسازید.
حال خواص زیر را به این کلاس اضافه کنید.
namespace ProductStore.Models { public class Product { public int Id { get; set; } public string Name { get; set; } public string Category { get; set; } public decimal Price { get; set; } } }
افزودن یک مخزن
ما نیاز به ذخیره کردن کلکسیونی از محصولات داریم، و بهتر است این کلکسیون از پیاده سازی سرویس تفکیک شود. در این صورت بدون نیاز به بازنویسی کلاس سرویس میتوانیم منبع دادهها را تغییر دهیم. این نوع طراحی با نام الگوی مخزن یا Repository Pattern شناخته میشود. برای شروع نیاز به یک قرارداد جنریک برای مخزنها داریم.
روی پوشه Models کلیک راست کنید و گزینه Add, New Item را انتخاب نمایید.
نوع آیتم جدید را Interface انتخاب کنید و نام آن را به IProductRepository تغییر دهید.
حال کد زیر را به این اینترفیس اضافه کنید.
namespace ProductStore.Models { public interface IProductRepository { IEnumerable<Product> GetAll(); Product Get(int id); Product Add(Product item); void Remove(int id); bool Update(Product item); } }
namespace ProductStore.Models { public class ProductRepository : IProductRepository { private List<Product> products = new List<Product>(); private int _nextId = 1; public ProductRepository() { Add(new Product { Name = "Tomato soup", Category = "Groceries", Price = 1.39M }); Add(new Product { Name = "Yo-yo", Category = "Toys", Price = 3.75M }); Add(new Product { Name = "Hammer", Category = "Hardware", Price = 16.99M }); } public IEnumerable<Product> GetAll() { return products; } public Product Get(int id) { return products.Find(p => p.Id == id); } public Product Add(Product item) { if (item == null) { throw new ArgumentNullException("item"); } item.Id = _nextId++; products.Add(item); return item; } public void Remove(int id) { products.RemoveAll(p => p.Id == id); } public bool Update(Product item) { if (item == null) { throw new ArgumentNullException("item"); } int index = products.FindIndex(p => p.Id == item.Id); if (index == -1) { return false; } products.RemoveAt(index); products.Add(item); return true; } } }
مخزن ما لیست محصولات را در حافظه محلی نگهداری میکند. برای مثال جاری این طراحی کافی است، اما در یک اپلیکیشن واقعی دادههای شما در یک دیتابیس یا منبع داده ابری ذخیره خواهند شد. همچنین استفاده از الگوی مخزن، تغییر منبع دادهها در آینده را راحتتر میکند.
افزودن یک کنترلر Web API
اگر قبلا با ASP.NET MVC کار کرده باشید، با مفهوم کنترلرها آشنایی دارید. در ASP.NET Web API کنترلرها کلاس هایی هستند که درخواستهای HTTP دریافتی از کلاینت را به اکشن متدها نگاشت میکنند. ویژوال استودیو هنگام ساختن پروژه شما دو کنترلر به آن اضافه کرده است. برای مشاهد آنها پوشه Controllers را باز کنید.
- HomeController یک کنترلر مرسوم در ASP.NET MVC است. این کنترلر مسئول بکار گرفتن صفحات وب است و مستقیما ربطی به Web API ما ندارد.
- ValuesController یک کنترلر نمونه WebAPI است.
کنترلر ValuesController را حذف کنید، نیازی به این آیتم نخواهیم داشت. حال برای اضافه کردن کنترلری جدید مراحل زیر را دنبال کنید.
در پنجره Solution Explorer روی پوشه Controllers کلیک راست کرده و گزینه Add, Controller را انتخاب کنید.
در دیالوگ Add Controller نام کنترلر را به ProductsController تغییر داده و در قسمت Scaffolding Options گزینه Empty API Controller را انتخاب کنید.
حال فایل کنترلر جدید را باز کنید و عبارت زیر را به بالای آن اضافه نمایید.
using ProductStore.Models;
public class ProductsController : ApiController { static readonly IProductRepository repository = new ProductRepository(); }
فراخوانی ()new ProductRepository طراحی جالبی نیست، چرا که کنترلر را به پیاده سازی بخصوصی از این اینترفیس گره میزند. بهتر است از تزریق وابستگی (Dependency Injection) استفاده کنید. برای اطلاعات بیشتر درباره تکنیک DI در Web API به این لینک مراجعه کنید.
گرفتن منابع
ProductStore API اکشنهای متعددی در قالب متدهای HTTP GET در دسترس قرار میدهد. هر اکشن به متدی در کلاس ProductsController مرتبط است.
Relative URl | HTTP Method | Action |
api/products/ | GET | دریافت لیست تمام محصولات |
api/products/id/ | GET | دریافت محصولی مشخص بر اساس شناسه |
api/products?category=category/ | GET | دریافت محصولات بر اساس طبقه بندی |
برای دریافت لیست تمام محصولات متد زیر را به کلاس ProductsController اضافه کنید.
public class ProductsController : ApiController { public IEnumerable<Product> GetAllProducts() { return repository.GetAll(); } // .... }
برای دریافت محصولی مشخص بر اساس شناسه آن متد زیر را اضافه کنید.
public Product GetProduct(int id) { Product item = repository.Get(id); if (item == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } return item; }
نام این متد هم با "Get" شروع میشود اما پارامتری با نام id دارد. این پارامتر به قسمت id مسیر درخواست شده (request URl) نگاشت میشود. تبدیل پارامتر به نوع داده مناسب (در اینجا int) هم بصورت خودکار توسط فریم ورک ASP.NET Web API انجام میشود.
متد GetProduct در صورت نامعتبر بودن پارامتر id استثنایی از نوع HttpResponseException تولید میکند. این استثنا بصورت خودکار توسط فریم ورک Web API به خطای 404 (Not Found) ترجمه میشود.
در آخر متدی برای دریافت محصولات بر اساس طبقه بندی اضافه کنید.
public IEnumerable<Product> GetProductsByCategory(string category) { return repository.GetAll().Where( p => string.Equals(p.Category, category, StringComparison.OrdinalIgnoreCase)); }
اگر آدرس درخواستی پارامترهای query string داشته باشد، Web API سعی میکند پارامترها را با پارامترهای متد کنترلر تطبیق دهد. بنابراین درخواستی به آدرس "api/products?category=category" به این متد نگاشت میشود.
ایجاد منبع جدید
قدم بعدی افزودن متدی به ProductsController برای ایجاد یک محصول جدید است. لیست زیر پیاده سازی ساده ای از این متد را نشان میدهد.
// Not the final implementation! public Product PostProduct(Product item) { item = repository.Add(item); return item; }
- نام این متد با "Post" شروع میشود. برای ساختن محصولی جدید کلاینت یک درخواست HTTP POST ارسال میکند.
- این متد پارامتری از نوع Product میپذیرد. در Web API پارامترهای پیچیده (complex types) بصورت خودکار با deserialize کردن بدنه درخواست بدست میآیند. بنابراین در اینجا از کلاینت انتظار داریم که آبجکتی از نوع Product را با فرمت XML یا JSON ارسال کند.
پیاده سازی فعلی این متد کار میکند، اما هنوز کامل نیست. در حالت ایده آل ما میخواهیم پیام HTTP Response موارد زیر را هم در بر گیرد:
- Response code: بصورت پیش فرض فریم ورک Web API کد وضعیت را به 200 (OK) تنظیم میکند. اما طبق پروتکل HTTP/1.1 هنگامی که یک درخواست POST منجر به ساخته شدن منبعی جدید میشود، سرور باید با کد وضعیت 201 (Created) پاسخ دهد.
- Location: هنگامی که سرور منبع جدیدی میسازد، باید آدرس منبع جدید را در قسمت Location header پاسخ درج کند.
ASP.NET Web API دستکاری پیام HTTP response را آسان میکند. لیست زیر پیاده سازی بهتری از این متد را نشان میدهد.
public HttpResponseMessage PostProduct(Product item) { item = repository.Add(item); var response = Request.CreateResponse<Product>(HttpStatusCode.Created, item); string uri = Url.Link("DefaultApi", new { id = item.Id }); response.Headers.Location = new Uri(uri); return response; }
متد CreateResponse آبجکتی از نوع HttpResponseMessage میسازد و بصورت خودکار آبجکت Product را مرتب (serialize) کرده و در بدنه پاسخ مینویسد. نکته دیگر آنکه مثال جاری، مدل را اعتبارسنجی نمیکند. برای اطلاعات بیشتر درباره اعتبارسنجی مدلها در Web API به این لینک مراجعه کنید.
بروز رسانی یک منبع
بروز رسانی یک محصول با PUT ساده است.
public void PutProduct(int id, Product product) { product.Id = id; if (!repository.Update(product)) { throw new HttpResponseException(HttpStatusCode.NotFound); } }
حذف یک منبع
برای حذف یک محصول متد زیر را به کلاس ProductsController اضافه کنید.
public void DeleteProduct(int id) { Product item = repository.Get(id); if (item == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } repository.Remove(id); }
در مثال جاری متد DeleteProduct نوع void را بر میگرداند، که فریم ورک Web API آن را بصورت خودکار به کد وضعیت 204 (No Content) ترجمه میکند.
Rss آب و هوای هر شهر در یاهو به صورت یک لینک یکتا میباشد؛ به شکل زیر :
و این لینک http://weather.yahooapis.com/forecastrss?w=28350859&u=c اطلاعات آب و هوای تهران را در قالب یک RSS به شما نمایش خواهد داد.
خوب، حالا پارامتر دوم یعنی پارامتر u چکاری را انجام میدهد؟
* چنانچه مقدار پارامتر u برابر c باشد، یعنی شما دمای آب و هوای شهر مد نظر را بر اساس سانتیگراد میخواهید.
* اگر مقدار پارامتر u برابر f باشد، یعنی شما دمای آب و هوای آن شهر مورد نظر را بر اساس فارنهایت میخواهید.
برای گرفتن WOEID شهرها هم به این سایت بروید http://woeid.rosselliot.co.nz و اسم هر شهری که میخواهید بزنید تا WOEID را به شما نمایش دهد.
در این مثال من از یک DropDown استفاده کردم که کاربر با انتخاب هر شهر از DropDown، آب و هوای آن شهر را مشاهده میکند.
Action مربوط به صفحهی Index به صورت زیر میباشد :
[HttpGet] public ActionResult Index() { ViewBag.ProvinceList = _RPosition.Positions; ShowWeatherProvince(8); return View(); }
حال تابعی را که آب و هوای مربوط به هر شهر را نمایش میدهد، به شرح زیر است:
public ActionResult ShowWeatherProvince(int dpProvince) { XDocument rssXml=null; CountryName CountryName = new CountryName(); if (dpProvince != 0) { switch (dpProvince) { case 1: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345768&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Azarbayejan-e Sharqhi" }; break; } case 2: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345767&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Azarbayejan-e Qarbi" }; break; } case 3: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2254335&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Ardebil" }; break; } case 4: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=28350859&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Alborz" }; break; } case 5: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345787&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Esfahan" }; break; } case 6: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345775&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Ilam" }; break; } case 7: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2254463&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Bushehr" }; break; } case 8: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=28350859&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Tehran" }; break; } case 9: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345769&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Chahar Mahall va Bakhtiari" }; break; } case 10: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=56189824&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Razavi Khorasan" }; break; } case 11: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345789&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Shomali Khorasan" }; break; } case 12: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345789&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Jonubi Khorasan" }; break; } case 13: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345778&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Khuzestan" }; break; } case 14: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2255311&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Zanjan" }; break; } case 15: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345784&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Semnan" }; break; } case 16: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345770&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Sistan va Baluchestan" }; break; } case 17: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345772&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Fars" }; break; } case 18: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=20070200&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Qazvin" }; break; } case 19: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2255062&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Qom" }; break; } case 20: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345779&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Kordestan" }; break; } case 21: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2254796&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Kerman" }; break; } case 22: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2254797&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Kermanshah" }; break; } case 23: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345771&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Kohgiluyeh va Buyer Ahmad" }; break; } case 24: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=20070201&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Golestan" }; break; } case 25: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345773&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Gilan" }; break; } case 26: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345782&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Lorestan" }; break; } case 27: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345783&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Markazi" }; break; } case 28: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345780&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Mazandaran" }; break; } case 29: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2254664&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Hamedan" }; break; } case 30: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345776&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Hormozgan" }; break; } case 31: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2253355&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Yazd" }; break; } } ViewBag.Location = CountryName; XNamespace yWeatherNS = "http://xml.weather.yahoo.com/ns/rss/1.0"; List<YahooWeatherRssItem> WeatherList = new List<YahooWeatherRssItem>(); for (int i = 0; i < 4; i++) { YahooWeatherRssItem YahooWeatherRssItem = new YahooWeatherRssItem() { Code = Convert.ToInt32(rssXml.Descendants("item").Elements(yWeatherNS + "forecast").ElementAt(i).Attribute("code").Value), Day = rssXml.Descendants("item").Elements(yWeatherNS + "forecast").ElementAt(i).Attribute("day").Value, Low = rssXml.Descendants("item").Elements(yWeatherNS + "forecast").ElementAt(i).Attribute("low").Value, High = rssXml.Descendants("item").Elements(yWeatherNS + "forecast").ElementAt(i).Attribute("high").Value, Text = rssXml.Descendants("item").Elements(yWeatherNS + "forecast").ElementAt(i).Attribute("text").Value, }; WeatherList.Add(YahooWeatherRssItem); } ViewBag.FeedList = WeatherList; } return PartialView("_Weather"); }
حالا کد مربوط به خواندن فایل Rss را برایتان توضیح میدهم : حلقهی for 0 تا 4 (که در کد بالا مشاهده میکنید)یعنی اطلاعات 4 روز آینده را برایم برگردان.
من تگهای Code ، Day ، Low ، High و text فایل RSS را در این حلقه For میخوانم که البته مقادیر این 4 روز را در لیستی اضافه میکنم که نوع این لیست هم از نوع YahooWeatherRssItem میباشد. من این کلاس را در فایل ضمیمه قرار دادم. اکنون هر کدام از این تگها را برایتان توضیح میدهم:
code : هر آب و هوا کدی دارد .مثلا آب و هوای نیمه ابری یک کد ، آب و هوای آفتابی کدی دیگر و ...
Low: حداقل دمای آن روز را به ما میدهد .
High: حداکثر دمای آن روز را به میدهد .
day: نام روز از هفته را بر میگرداند مثلا شنبه ، یکشنبه و ....
text: که توضیحاتی میدهد مثلا اگر هوا آفتابی باشد مقدار sunny را بر میگرداند و ...
خوب، تا اینجا ما Rss مربوط به هر شهر را خواندیم حالا در قسمت Design باید چکار کنیم .
کدهای html صفحهی Index ما شامل کدهای زیر است :
@{ ViewBag.Title = "Weather"; } <link href="~/Content/User/Weather/Weather.css" rel="stylesheet" /> @section scripts{ <script src="@Url.Content("~/Scripts/jquery-1.6.2.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/jquery.unobtrusive-ajax.min.js")" type="text/javascript"></script> <script type="text/javascript"> $("#dpProvince").change(function () { $(this).parents("form").submit(); }); </script> } <h2>Weather</h2> <div id="Progress"> <img src="~/Images/User/Other/ajax-loader.gif" /> </div> <div id="BoxContent"> @Html.Partial("_Weather")</div> @using (Ajax.BeginForm(actionName: "ShowWeatherProvince", ajaxOptions: new AjaxOptions { UpdateTargetId = "BoxContent", LoadingElementId = "Progress", InsertionMode = InsertionMode.Replace })) { <div style="padding-top:15px;"> <div style="float:left; width:133px; ">Select Your Province</div> <div style="float:left"> @Html.DropDownList("dpProvince", new SelectList(ViewBag.ProvinceList, "Id", "Name"),"Select Your Province", new { @class = "webUserDropDown", @style = "width:172px" })</div> </div> }
@{ List<Weather.YahooWeatherRssItem> Feeds = ViewBag.FeedList; } <div> @{ HtmlString StartTable = new HtmlString("<table class='WeatherTable' cellspacing='0' cellpadding='0'><tbody><tr>"); HtmlString EndTable = new HtmlString("</tr></tbody></table>"); HtmlString StartTD = new HtmlString("<td>"); HtmlString EndTD = new HtmlString("</td>"); } <div style="width: 300px;"> @{ @StartTable foreach (var item in Feeds) { @StartTD <div>@item.Day</div> <div> @{ string FileName = ""; switch (item.Code) { case 0: { FileName = "/Images/User/Weather/Tornado.png"; break; } case 1: { FileName = "/Images/User/Weather/storm2.gif"; break; } case 2: { FileName = "/Images/User/Weather/storm2.gif"; break; } case 3: { FileName = "/Images/User/Weather/storm2.gif"; break; } case 4: { FileName = "/Images/User/Weather/15.gif"; break; } case 5: { FileName = "/Images/User/Weather/29.gif"; break; } case 6: { FileName = "/Images/User/Weather/29.gif"; break; } case 7: { FileName = "/Images/User/Weather/29.gif"; break; } case 8: { FileName = "/Images/User/Weather/26.gif"; break; } case 9: { FileName = "/Images/User/Weather/drizzle.png"; break; } case 10: { FileName = "/Images/User/Weather/26.gif"; break; } case 11: { FileName = "/Images/User/Weather/18.gif"; break; } case 12: { FileName = "/Images/User/Weather/18.gif"; break; } case 13: { FileName = "/Images/User/Weather/19.gif"; break; } case 14: { FileName = "/Images/User/Weather/19.gif"; break; } case 15: { FileName = "/Images/User/Weather/19.gif"; break; } case 16: { FileName = "/Images/User/Weather/22.gif"; break; } case 17: { FileName = "/Images/User/Weather/Hail.png"; break; } case 18: { FileName = "/Images/User/Weather/25.gif"; break; } case 19: { FileName = "/Images/User/Weather/dust.png"; break; } case 20: { FileName = "/Images/User/Weather/fog_icon.png"; break; } case 21: { FileName = "/Images/User/Weather/hazy_icon.png"; break; } case 22: { FileName = "/Images/User/Weather/2017737395.png"; break; } case 23: { FileName = "/Images/User/Weather/32.gif"; break; } case 24: { FileName = "/Images/User/Weather/32.gif"; break; } case 25: { FileName = "/Images/User/Weather/31.gif"; break; } case 26: { FileName = "/Images/User/Weather/7.gif"; break; } case 27: { FileName = "/Images/User/Weather/38.gif"; break; } case 28: { FileName = "/Images/User/Weather/6.gif"; break; } case 29: { FileName = "/Images/User/Weather/35.gif"; break; } case 30: { FileName = "/Images/User/Weather/7.gif"; break; } case 31: { FileName = "/Images/User/Weather/33.gif"; break; } case 32: { FileName = "/Images/User/Weather/1.gif"; break; } case 33: { FileName = "/Images/User/Weather/34.gif"; break; } case 34: { FileName = "/Images/User/Weather/2.gif"; break; } case 35: { FileName = "/Images/User/Weather/freezing_rain.png"; break; } case 36: { FileName = "/Images/User/Weather/30.gif"; break; } case 37: { FileName = "/Images/User/Weather/15.gif"; break; } case 38: { FileName = "/Images/User/Weather/15.gif"; break; } case 39: { FileName = "/Images/User/Weather/15.gif"; break; } case 40: { FileName = "/Images/User/Weather/12.gif"; break; } case 41: { FileName = "/Images/User/Weather/22.gif"; break; } case 42: { FileName = "/Images/User/Weather/22.gif"; break; } case 43: { FileName = "/Images/User/Weather/22.gif"; break; } case 44: { FileName = "/Images/User/Weather/39.gif"; break; } case 45: { FileName = "/Images/User/Weather/thundershowers.png"; break; } case 46: { FileName = "/Images/User/Weather/19.gif"; break; } case 47: { FileName = "/Images/User/Weather/thundershowers.png"; break; } case 3200: { FileName = "/Images/User/Weather/1211810662.png"; break; } } } <img alt='@item.Text' title='@item.Text' src='@FileName'> </div> <div> <span>@item.High°</span> <span>@item.Low°</span> </div> @EndTD } } @EndTable </div> </div>
چنانچه در مورد RSS وضعیت آب و هوای یاهو اطلاعات دقیقتری را میخواهید بدانید به این لینک بروید.
در آموزش بعدی قصد دارم برایتان این بخش را توضیح دهم که بر اساس IP بازدید کننده سایت شما، اطلاعات آب و هوایی شهر بازدید کننده را برایش در سایت نمایش دهد.
Files-06bf65bac63d4dd694b15fc24d4cb074.zip
موفق باشید
EF Code First #12
من چند روز پیش مطلبی رو راجع به ساخت attribute برای compare validation در روش code first عنوان کردم(البته در asp.net web form).که در واقع اصلیترین کاربردش همون کاربرد معروف مقایسه رمز عبور و تکرار رمز عبور هست. یه سری کارایی در این زمینه انجام دادم و خواستم در مورد مشکل مربوطه و در کل ، صحیح یا غلط بودن این روش کمکم کنید.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = true, Inherited = true)] public class CompareAttribute : ValidationAttribute { public CompareAttribute(string originalProperty, string confirmProperty) { OriginalProperty = originalProperty; ConfirmProperty = confirmProperty; } public string ConfirmProperty { get; private set; } public string OriginalProperty { get; private set; } public override bool IsValid(object value) { PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(value); return String.Equals(((User)value).Password, ((User)value).RepeatPassword); } }
//[CompareAttribute("Password", "RepeatPassword", ErrorMessage = "Not Compare!")] public class User { public Int64 UserId { get; set; }
[Required(ErrorMessageResourceName = "Password", ErrorMessageResourceType = typeof(ErrorMessageResource))] public String Password { get; set; }
[NotMapped] [Required(ErrorMessageResourceName = "RepeatPassword", ErrorMessageResourceType = typeof(ErrorMessageResource))] //[CompareAttribute("Password","RepeatPassword" , ErrorMessage = "Not Compare!")] public String RepeatPassword { get; set; } }
وقتی اونو بالای کلاس User میزارم کار میکنه.ولی خب این یه کار خیلی زشتیه! چون به ازای همه propertyها اجرا میشه.ولی وقتی اونو تو جای اصلیش که همون بالای repeat password هست میزارم کار نمیکنه.
یه جورایی مشکل از اینه که نمیشه انگار مقدار property رو به این سمت پاس داد.ولی در حالتی که بالای کلاس میزارمش چون خود کلاس رو پاس میده ، طبیعتاٌ
اسم propertyها رو هم پیدا میکنه.
ممنون میشم راهنماییم کنید .
توسعه تطابق الگو با C#8
C# 7 introduced pattern matching with the extension of the switch statement and the is operator offering the const pattern, the type pattern, and the var pattern. With C# 8 an extension of pattern matching is planned, including the property pattern, the recursive pattern, and a new switch – the switch expression.
C# 8.0 - Async Streams
مقدمه
در Net Core 3. نوعهای جدیدی با عنوانهای <IAsyncEnumerable<T>,IAsyncEnumerator<T> در فضای نام System.Collections.Generic معرفی شدند. همانطور که مشخص است این نوعهای جدید کاملا با نوعهای synchronous خود هم پوشانی دارند و مفاهیم قبلی را به پیاده سازی میکنند.
نوع <IAsyncEnumerable<T متد GetAsyncEnumerator را معرفی میکند تا عملیات enumeration را به صورت async انجام دهد و در خروجی این متد، نوع <IAsyncEnumerator<T را برگشت میدهد؛ بهطوریکه این نوع disposable و دو عضو MoveNextAsync و Current را در خود دارد. اولی برای رسیدن به مقدار بعدی و دومی برای دریافت مقدار فعلی استفاده میشود. این در حالی است که MoveNextAsync بجای برگشت دادن یک bool یک <ValueTask<bool را برگشت میدهد. همچنین این متد، مقدار CancelationToken را همانند سایر فرآیندهایی که به صورت async تعریف میشوند، به صورت اختیاری از ورودی دریافت میکند، تا در صورت لزوم، عملیات جاری را کنسل کند. از طرفی به دلیل اینکه IAsyncEnumerator اینترفیس IAsyncDisposable را پیاده سازی میکند، متد DisposeAsync را نیز در اختیار دارد بهطوریکه بجای void یک ValueTask را برگشت میدهد.
static async IAsyncEnumerable<int> RangeAsync(int start, int count) { for (int i = 0; i < count; i++) { await Task.Delay(i); yield return start + i; } }
در مرحله اول، یک وب سرویس REST را بدون استفاده از IAsyncEnumerable ایجاد میکنیم تا متوجه مشکلات آن شویم و سپس در مرحله بعدی همین وب سرویس را با نوع IAsyncEnumerable بازنویسی میکنیم.
[ApiController] [Route("[controller]")] public class CustomerController : ControllerBase { private readonly IDictionary<int, Customer> _customers; private void FillCustomerFromMemory(int countOfCustomer) { for (int CustomerId = 1; CustomerId <= countOfCustomer; CustomerId++) { _customers.Add(key: CustomerId, new Customer($"name_{CustomerId}", CustomerId)); } } public CustomerController() { _customers = new Dictionary<int, Customer>(); FillCustomerFromMemory(countOfCustomer : 100); } [HttpGet] public async Task<IEnumerable<Customer>> Get() { var output = new List<Customer>(); while (_customers.Any(_ => _.Key % 10 == 0)) { var customer = _customers.First(_ => _.Key % 10 == 0); output.Add(new Customer(customer.Value.Name, customer.Key)); await Task.Delay(500); _customers.Remove(customer); } return output; } public class Customer { public int Id { get; private set; } public string Name { get; private set; } public Customer(string name, int id) { Name = name; Id = id; } } }
[HttpGet] public async IAsyncEnumerable<Customer> Get() { while (_customers.Any(_ => _.Key % 10 == 0)) { var customer = _customers.First(_ => _.Key % 10 == 0); yield return new Customer(customer.Value.Name, customer.Key); _customers.Remove(customer); await Task.Delay(500); } }
در قسمت قبل تلاش کردیم تا یک وب سرویس با قابلیت stream را پیاده سازی کنیم. حال در این بخش کد کلاینت را به صورتی ایجاد میکنیم تا هر سری صرفا یک بلاک ارسال شده توسط سرور را دریافت و آن را Deserialize کند. برای این کار از کتابخانه Newtonsoft.Json استفاده میکنیم.
const int TARGET = 80; var _httpClient = new HttpClient(); using (var response = await _httpClient.GetAsync( "https://localhost:7284/customer", HttpCompletionOption.ResponseHeadersRead)) { var stream = await response.Content.ReadAsStreamAsync(); var _jsonSerializerSettings = new JsonSerializerSettings(); var _serializer = Newtonsoft.Json.JsonSerializer.Create(_jsonSerializerSettings); using TextReader textReader = new StreamReader(stream); using JsonReader jsonReader = new JsonTextReader(textReader); await using (stream.ConfigureAwait(false)) { await jsonReader.ReadAsync().ConfigureAwait(false); while (await jsonReader.ReadAsync().ConfigureAwait(false) && jsonReader.TokenType != JsonToken.EndArray) { Customer customer = _serializer!.Deserialize<Customer>(jsonReader); if (customer.Id == TARGET) { Console.WriteLine(customer.Id + " : " + customer.Name); break; } } } }
استفاده از CancelationToken در جهت استفاده بهینه از منابع
فرض کنید به هر دلیلی، برای مثال خطای داخلی برنامهی کلاینت و یا بسته شدن مرورگر، ارتباط کلاینت با سرور قطع شود. در این صورت سرور از این ماجرا خبردار نمیشود و به کار خود جهت ارسال اطلاعات ادامه میدهد. همانطور که گفته شد، کلاینت به هر دلیلی از دریافت اطلاعات منصرف شده و یا به خطا خورده. پس فرستادن اطلاعات هیچ کاربردی ندارد و سرور در هر مرحله ای از ارسال که باشد، باید به کار خود خاتمه دهد.
برای برطرف کردن مشکل، این سناریو کد سمت سرور را مجدد باز نویسی میکنیم :
[HttpGet] public async IAsyncEnumerable<Customer> Get(CancellationToken cancellationToken) { while (!cancellationToken.IsCancellationRequested && _customers.Any(_ => _.Key % 10 == 0)) { var customer = _customers.First(_ => _.Key % 10 == 0); yield return new Customer(customer.Value.Name, customer.Key); _customers.Remove(customer); await Task.Delay(500,cancellationToken); } }
کلاینت در صورتیکه به اطلاعات مورد نظر از طریق وب سرویس دسترسی پیدا کرد، دیگر تمایلی به ادامه خواندن از جریان داده یا stream را ندارد و از حلقه خواندن اطلاعات خارج میشود. اما سرور همچنان درگیر ارسال اطلاعات است. برای رفع این مشکل کد سمت کلاینت را بازنویسی میکنیم:
const int TARGET = 80; var _httpClient = new HttpClient(); var _cancelationTokenSource = new CancellationTokenSource(); using (var response = await _httpClient.GetAsync( "https://localhost:7284/customer", HttpCompletionOption.ResponseHeadersRead, _cancelationTokenSource.Token)) { var stream = await response.Content.ReadAsStreamAsync(_cancelationTokenSource.Token); var _jsonSerializerSettings = new JsonSerializerSettings(); var _serializer = Newtonsoft.Json.JsonSerializer.Create(_jsonSerializerSettings); using TextReader textReader = new StreamReader(stream); using JsonReader jsonReader = new JsonTextReader(textReader); await using (stream.ConfigureAwait(false)) { await jsonReader.ReadAsync(_cancelationTokenSource.Token).ConfigureAwait(false); while (await jsonReader.ReadAsync(_cancelationTokenSource.Token).ConfigureAwait(false) && jsonReader.TokenType != JsonToken.EndArray) { Customer customer = _serializer!.Deserialize<Customer>(jsonReader); if (customer.Id == TARGET) { Console.WriteLine(customer.Id + " : " + customer.Name); _cancelationTokenSource.Cancel(); break; } } } }
https://learn.microsoft.com/en-us/archive/msdn-magazine/2019/november/csharp-iterating-with-async-enumerables-in-csharp-8
https://code-maze.com/csharp-async-enumerable-yield
Github Link : https://github.com/Ershad95/Stream_REST_API
بررسی قسمتهای مختلف قالب پروژه WPF Framework تهیه شده
new SendMsg().ShowMsg( new AlertConfirmBoxModel { ErrorTitle = "خطای اعتبار سنجی", Errors = errors, }, validationException);
public class DomainResult { public bool Succeed { get; set; } public IEnumerable<Exception> Errors { get; set; } public DomainErrorType ErrorType { get; set; } } public enum DomainErrorType { Validation, Concurrency, Update }
public DomainResult ApplyAllChanges(string userName, bool updateAuditFields = true) ...