پس از آشنایی مقدماتی با Kendo UI DataSource، اکنون میخواهیم از آن جهت صفحه بندی، مرتب سازی و جستجوی پویای سمت سرور استفاده کنیم. در مثال قبلی، هر چند صفحه بندی فعال بود، اما پس از دریافت تمام اطلاعات، این اعمال در سمت کاربر انجام و مدیریت میشد.
مدل برنامه
در اینجا قصد داریم لیستی را با ساختار کلاس Product در اختیار Kendo UI گرید قرار دهیم:
پیشنیاز تامین داده مخصوص Kendo UI Grid
برای ارائه اطلاعات مخصوص Kendo UI Grid، ابتدا باید درنظر داشت که این گرید، درخواستهای صفحه بندی خود را با فرمت ذیل ارسال میکند. همانطور که مشاهده میکنید، صرفا یک کوئری استرینگ با فرمت JSON را دریافت خواهیم کرد:
سپس این گرید نیاز به سه فیلد، در خروجی JSON نهایی خواهد داشت:
فیلد Data که رکوردهای گرید را تامین میکنند. فیلد Total که بیانگر تعداد کل رکوردها است و Aggregates که برای گروه بندی بکار میرود.
میتوان برای تمام اینها، کلاس و Parser تهیه کرد و یا ... پروژهی سورس بازی به نام Kendo.DynamicLinq نیز چنین کاری را میسر میسازد که در ادامه از آن استفاده خواهیم کرد. برای نصب آن تنها کافی است دستور ذیل را صادر کنید:
Kendo.DynamicLinq به صورت خودکار System.Linq.Dynamic را نیز نصب میکند که از آن جهت صفحه بندی پویا استفاده خواهد شد.
تامین کنندهی داده سمت سرور
همانند مطلب کار با Kendo UI DataSource ، یک ASP.NET Web API Controller جدید را به پروژه اضافه کنید و همچنین مسیریابیهای مخصوص آنرا به فایل global.asax.cs نیز اضافه نمائید.
تمام کدهای این کنترلر همین چند سطر فوق هستند. با توجه به ساختار کوئری استرینگی که در ابتدای بحث عنوان شد، نیاز است آنرا توسط کتابخانهی JSON.NET تبدیل به یک نمونه از DataSourceRequest نمائیم. این کلاس در Kendo.DynamicLinq تعریف شدهاست و حاوی اطلاعاتی مانند take و skip کوئری LINQ نهایی است.
ProductDataSource.LatestProducts صرفا یک لیست جنریک تهیه شده از کلاس Product است. در نهایت با استفاده از متد الحاقی جدید ToDataSourceResult، به صورت خودکار مباحث صفحه بندی سمت سرور به همراه مرتب سازی اطلاعات، صورت گرفته و اطلاعات نهایی با فرمت DataSourceResult بازگشت داده میشود. DataSourceResult نیز در Kendo.DynamicLinq تعریف شده و سه فیلد یاد شدهی Data، Total و Aggregates را تولید میکند.
تا اینجا کارهای سمت سرور این مثال به پایان میرسد.
تهیه View نمایش اطلاعات ارسالی از سمت سرور
اعمال مباحث بومی سازی
- در اینجا چند فایل js و css جدید اضافه شدهاند. فایل kendo.rtl.min.css جهت تامین مباحث RTL توکار Kendo UI کاربرد دارد.
- سپس سه فایل kendo.culture.fa-IR.js، kendo.culture.fa.js و kendo.messages.en-US.js نیز اضافه شدهاند. فایلهای fa و fa-Ir آن هر چند به ظاهر برای ایران طراحی شدهاند، اما نام ماههای موجود در آن عربی است که نیاز به ویرایش دارد. به همین جهت به سورس این فایلها، جهت ویرایش نهایی نیاز خواهد بود که در پوشهی src\js\cultures مجموعهی اصلی Kendo UI موجود هستند (ر.ک. فایل پیوست).
- فایل kendo.messages.en-US.js حاوی تمام پیامهای مرتبط با Kendo UI است. برای مثال«رکوردهای 10 تا 15 از 1000 ردیف» را در اینجا میتوانید به فارسی ترجمه کنید.
- متد kendo.culture کار مشخص سازی فرهنگ بومی برنامه را به عهده دارد. برای مثال در اینجا به fa-IR تنظیم شدهاست. این مورد سبب خواهد شد تا از فایل kendo.culture.fa-IR.js استفاده گردد. اگر مقدار آنرا به fa تنظیم کنید، از فایل kendo.culture.fa.js کمک گرفته خواهد شد.
راست به چپ سازی گرید
تنها کاری که برای راست به چپ سازی Kendo UI Grid باید صورت گیرد، محصور سازی div آن در یک div با کلاس مساوی k-rtl است:
k-rtl و تنظیمات آن در فایل kendo.rtl.min.css قرار دارند که در ابتدای head صفحه تعریف شدهاست.
تامین داده و نمایش گرید
در ادامه کدهای کامل DataSource و Kendo UI Grid را ملاحظه میکنید:
- با تعاریف مقدماتی Kendo UI DataSource پیشتر آشنا شدهایم و قسمت read آن جهت دریافت اطلاعات از سمت سرور کاربرد دارد.
- در اینجا ذکر contentType الزامی است. زیرا ASP.NET Web API بر این اساس است که تصمیم میگیرد، خروجی را به صورت JSON ارائه دهد یا XML.
- با استفاده از parameterMap، سبب خواهیم شد تا پارامترهای ارسالی به سرور، با فرمت صحیحی تبدیل به JSON شده و بدون مشکل به سرور ارسال گردند.
- در قسمت schema باید نام فیلدهای موجود در DataSourceResult دقیقا مشخص شوند تا گرید بداند که data را باید از چه فیلدی استخراج کند و تعداد کل ردیفها در کدام فیلد قرار گرفتهاست.
- نحوهی تعریف model را نیز در اینجا ملاحظه میکنید. ذکر نوع فیلدها در اینجا بسیار مهم است و اگر قید نشوند، در حین جستجوی پویا به مشکل برخواهیم خورد. زیرا پیش فرض نوع تمام فیلدها string است و در این حالت نمیتوان عدد 1 رشتهای را با یک فیلد از نوع int در سمت سرور مقایسه کرد.
- در اینجا serverPaging، serverFiltering و serverSorting نیز به true تنظیم شدهاند. اگر این مقدار دهیها صورت نگیرد، این اعمال در سمت کلاینت انجام خواهند شد.
پس از تعریف DataSource، تنها کافی است آنرا به خاصیت dataSource یک kendoGrid نسبت دهیم.
- autoBind: true سبب میشود تا اطلاعات DataSource بدون نیاز به فراخوانی متد read آن به صورت خودکار دریافت شوند.
- با تنظیم scrollable: false، اعلام میکنیم که قرار است تمام رکوردها در معرض دید قرارگیرند و اسکرول پیدا نکنند.
- pageable: true صفحه بندی را فعال میکند. این مورد نیاز به تنظیم pageSize: 10 در قسمت DataSource نیز دارد.
- با sortable: true مرتب سازی ستونها با کلیک بر روی سرستونها فعال میگردد.
- filterable: true به معنای فعال شدن جستجوی خودکار بر روی فیلدها است. کتابخانهی Kendo.DynamicLinq حاصل آنرا در سمت سرور مدیریت میکند.
- reorderable: true سبب میشود تا کاربر بتواند محل قرارگیری ستونها را تغییر دهد.
- ذکر columnMenu: true اختیاری است. اگر ذکر شود، امکان مخفی سازی انتخابی ستونها نیز مسیر خواهد شد.
- در آخر ستونهای گرید مشخص شدهاند. با تعیین "{format: "{0:c سبب نمایش فیلدهای قیمت با سه رقم جدا کننده خواهیم شد. مقدار ریال آن از فایل فرهنگ جاری تنظیم شده دریافت میگردد. با استفاده از template تعریف شده نیز سبب نمایش فیلد bool به صورت یک checkbox خواهیم شد.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید:
KendoUI03.zip
مدل برنامه
در اینجا قصد داریم لیستی را با ساختار کلاس Product در اختیار Kendo UI گرید قرار دهیم:
namespace KendoUI03.Models { public class Product { public int Id { set; get; } public string Name { set; get; } public decimal Price { set; get; } public bool IsAvailable { set; get; } } }
پیشنیاز تامین داده مخصوص Kendo UI Grid
برای ارائه اطلاعات مخصوص Kendo UI Grid، ابتدا باید درنظر داشت که این گرید، درخواستهای صفحه بندی خود را با فرمت ذیل ارسال میکند. همانطور که مشاهده میکنید، صرفا یک کوئری استرینگ با فرمت JSON را دریافت خواهیم کرد:
/api/products?{"take":10,"skip":0,"page":1,"pageSize":10,"sort":[{"field":"Id","dir":"desc"}]}
{ "Data": [ {"Id":1500,"Name":"نام 1500","Price":2499.0,"IsAvailable":false}, {"Id":1499,"Name":"نام 1499","Price":2498.0,"IsAvailable":true} ], "Total":1500, "Aggregates":null }
میتوان برای تمام اینها، کلاس و Parser تهیه کرد و یا ... پروژهی سورس بازی به نام Kendo.DynamicLinq نیز چنین کاری را میسر میسازد که در ادامه از آن استفاده خواهیم کرد. برای نصب آن تنها کافی است دستور ذیل را صادر کنید:
PM> Install-Package Kendo.DynamicLinq
تامین کنندهی داده سمت سرور
همانند مطلب کار با Kendo UI DataSource ، یک ASP.NET Web API Controller جدید را به پروژه اضافه کنید و همچنین مسیریابیهای مخصوص آنرا به فایل global.asax.cs نیز اضافه نمائید.
using System.Linq; using System.Net.Http; using System.Web.Http; using Kendo.DynamicLinq; using KendoUI03.Models; using Newtonsoft.Json; namespace KendoUI03.Controllers { public class ProductsController : ApiController { public DataSourceResult Get(HttpRequestMessage requestMessage) { var request = JsonConvert.DeserializeObject<DataSourceRequest>( requestMessage.RequestUri.ParseQueryString().GetKey(0) ); var list = ProductDataSource.LatestProducts; return list.AsQueryable() .ToDataSourceResult(request.Take, request.Skip, request.Sort, request.Filter); } } }
ProductDataSource.LatestProducts صرفا یک لیست جنریک تهیه شده از کلاس Product است. در نهایت با استفاده از متد الحاقی جدید ToDataSourceResult، به صورت خودکار مباحث صفحه بندی سمت سرور به همراه مرتب سازی اطلاعات، صورت گرفته و اطلاعات نهایی با فرمت DataSourceResult بازگشت داده میشود. DataSourceResult نیز در Kendo.DynamicLinq تعریف شده و سه فیلد یاد شدهی Data، Total و Aggregates را تولید میکند.
تا اینجا کارهای سمت سرور این مثال به پایان میرسد.
تهیه View نمایش اطلاعات ارسالی از سمت سرور
اعمال مباحث بومی سازی
<head> <meta charset="utf-8" /> <meta http-equiv="Content-Language" content="fa" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>Kendo UI: Implemeting the Grid</title> <link href="styles/kendo.common.min.css" rel="stylesheet" type="text/css" /> <!--شیوه نامهی مخصوص راست به چپ سازی--> <link href="styles/kendo.rtl.min.css" rel="stylesheet" /> <link href="styles/kendo.default.min.css" rel="stylesheet" type="text/css" /> <script src="js/jquery.min.js" type="text/javascript"></script> <script src="js/kendo.all.min.js" type="text/javascript"></script> <!--محل سفارشی سازی پیامها و مسایل بومی--> <script src="js/cultures/kendo.culture.fa-IR.js" type="text/javascript"></script> <script src="js/cultures/kendo.culture.fa.js" type="text/javascript"></script> <script src="js/messages/kendo.messages.en-US.js" type="text/javascript"></script> <style type="text/css"> body { font-family: tahoma; font-size: 9pt; } </style> <script type="text/javascript"> // جهت استفاده از فایل: kendo.culture.fa-IR.js kendo.culture("fa-IR"); </script> </head>
- سپس سه فایل kendo.culture.fa-IR.js، kendo.culture.fa.js و kendo.messages.en-US.js نیز اضافه شدهاند. فایلهای fa و fa-Ir آن هر چند به ظاهر برای ایران طراحی شدهاند، اما نام ماههای موجود در آن عربی است که نیاز به ویرایش دارد. به همین جهت به سورس این فایلها، جهت ویرایش نهایی نیاز خواهد بود که در پوشهی src\js\cultures مجموعهی اصلی Kendo UI موجود هستند (ر.ک. فایل پیوست).
- فایل kendo.messages.en-US.js حاوی تمام پیامهای مرتبط با Kendo UI است. برای مثال«رکوردهای 10 تا 15 از 1000 ردیف» را در اینجا میتوانید به فارسی ترجمه کنید.
- متد kendo.culture کار مشخص سازی فرهنگ بومی برنامه را به عهده دارد. برای مثال در اینجا به fa-IR تنظیم شدهاست. این مورد سبب خواهد شد تا از فایل kendo.culture.fa-IR.js استفاده گردد. اگر مقدار آنرا به fa تنظیم کنید، از فایل kendo.culture.fa.js کمک گرفته خواهد شد.
راست به چپ سازی گرید
تنها کاری که برای راست به چپ سازی Kendo UI Grid باید صورت گیرد، محصور سازی div آن در یک div با کلاس مساوی k-rtl است:
<div class="k-rtl"> <div id="report-grid"></div> </div>
تامین داده و نمایش گرید
در ادامه کدهای کامل DataSource و Kendo UI Grid را ملاحظه میکنید:
<script type="text/javascript"> $(function () { var productsDataSource = new kendo.data.DataSource({ transport: { read: { url: "api/products", dataType: "json", contentType: 'application/json; charset=utf-8', type: 'GET' }, parameterMap: function (options) { return kendo.stringify(options); } }, schema: { data: "Data", total: "Total", model: { fields: { "Id": { type: "number" }, //تعیین نوع فیلد برای جستجوی پویا مهم است "Name": { type: "string" }, "IsAvailable": { type: "boolean" }, "Price": { type: "number" } } } }, error: function (e) { alert(e.errorThrown); }, pageSize: 10, sort: { field: "Id", dir: "desc" }, serverPaging: true, serverFiltering: true, serverSorting: true }); $("#report-grid").kendoGrid({ dataSource: productsDataSource, autoBind: true, scrollable: false, pageable: true, sortable: true, filterable: true, reorderable: true, columnMenu: true, columns: [ { field: "Id", title: "شماره", width: "130px" }, { field: "Name", title: "نام محصول" }, { field: "IsAvailable", title: "موجود است", template: '<input type="checkbox" #= IsAvailable ? checked="checked" : "" # disabled="disabled" ></input>' }, { field: "Price", title: "قیمت", format: "{0:c}" } ] }); }); </script>
- در اینجا ذکر contentType الزامی است. زیرا ASP.NET Web API بر این اساس است که تصمیم میگیرد، خروجی را به صورت JSON ارائه دهد یا XML.
- با استفاده از parameterMap، سبب خواهیم شد تا پارامترهای ارسالی به سرور، با فرمت صحیحی تبدیل به JSON شده و بدون مشکل به سرور ارسال گردند.
- در قسمت schema باید نام فیلدهای موجود در DataSourceResult دقیقا مشخص شوند تا گرید بداند که data را باید از چه فیلدی استخراج کند و تعداد کل ردیفها در کدام فیلد قرار گرفتهاست.
- نحوهی تعریف model را نیز در اینجا ملاحظه میکنید. ذکر نوع فیلدها در اینجا بسیار مهم است و اگر قید نشوند، در حین جستجوی پویا به مشکل برخواهیم خورد. زیرا پیش فرض نوع تمام فیلدها string است و در این حالت نمیتوان عدد 1 رشتهای را با یک فیلد از نوع int در سمت سرور مقایسه کرد.
- در اینجا serverPaging، serverFiltering و serverSorting نیز به true تنظیم شدهاند. اگر این مقدار دهیها صورت نگیرد، این اعمال در سمت کلاینت انجام خواهند شد.
پس از تعریف DataSource، تنها کافی است آنرا به خاصیت dataSource یک kendoGrid نسبت دهیم.
- autoBind: true سبب میشود تا اطلاعات DataSource بدون نیاز به فراخوانی متد read آن به صورت خودکار دریافت شوند.
- با تنظیم scrollable: false، اعلام میکنیم که قرار است تمام رکوردها در معرض دید قرارگیرند و اسکرول پیدا نکنند.
- pageable: true صفحه بندی را فعال میکند. این مورد نیاز به تنظیم pageSize: 10 در قسمت DataSource نیز دارد.
- با sortable: true مرتب سازی ستونها با کلیک بر روی سرستونها فعال میگردد.
- filterable: true به معنای فعال شدن جستجوی خودکار بر روی فیلدها است. کتابخانهی Kendo.DynamicLinq حاصل آنرا در سمت سرور مدیریت میکند.
- reorderable: true سبب میشود تا کاربر بتواند محل قرارگیری ستونها را تغییر دهد.
- ذکر columnMenu: true اختیاری است. اگر ذکر شود، امکان مخفی سازی انتخابی ستونها نیز مسیر خواهد شد.
- در آخر ستونهای گرید مشخص شدهاند. با تعیین "{format: "{0:c سبب نمایش فیلدهای قیمت با سه رقم جدا کننده خواهیم شد. مقدار ریال آن از فایل فرهنگ جاری تنظیم شده دریافت میگردد. با استفاده از template تعریف شده نیز سبب نمایش فیلد bool به صورت یک checkbox خواهیم شد.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید:
KendoUI03.zip
در این قسمت میخواهیم اطلاعات اتاقهای ثبت شده را به همراه تصاویر مرتبط با آنها، حذف کنیم و همچنین به یک خطای مهم در حین کار با EF-Core برسیم و متوجه شویم که روش کار با DbContext در برنامههای مبتنی بر Blazor Server .... با روش کار متداول با آن در برنامههای Web API، یکی نیست!
مشکل حذف تصاویر آپلود شده
در قسمت قبل، این امکان را مهیا کردیم که کاربران بتوانند پیش از ثبت اطلاعات یک اتاق، تصاویر آنرا به سرور آپلود کنند. یعنی تصاویری که در ابتدای کار آپلود میشوند، هنوز در بانک اطلاعاتی ثبت نشدهاند و هیچ رکوردی از آنها موجود نیست. در این حالت اگر کاربری تصاویری را آپلود کرده و سپس بر روی دکمهی back کلیک کند، با تعدادی تصویر آپلود شدهی غیرمنتسب به اتاقهای موجود، مواجه خواهیم شد. همچنین اگر شخصی به قسمت ویرایش تصاویر مراجعه کند و با کلیک بر روی دکمهی حذف یک تصویر، آنرا حذف کند، این حذف باید در بانک اطلاعاتی هم منعکس شود؛ در غیر اینصورت باز هم کاربر میتواند تصویری را حذف کند، اما در آخر بر روی دکمهی به روز رسانی اطلاعات رکورد کلیک نکند. در این حالت در دفعات بعدی مراجعهی به اطلاعات یک چنین اتاقی، با نقص اطلاعات تصاویری مواجه میشویم که در لیست تصاویر منتسب به یک اتاق وجود دارند، اما اصل فایل تصویری متناظر با آنها از سرور حذف شدهاست.
حذف اطلاعات تصاویر، در حالت ثبت اطلاعات
زمانیکه قرار است اطلاعات اتاقی برای اولین بار ثبت شود، حذف تصاویر آپلود شدهی مرتبط با آن سادهاست؛ چون هنوز اصل رکورد اتاق ثبت نشدهاست و این تصاویر در این لحظه، به رکوردی تعلق ندارند. بنابراین ابتدا متد رویدادگردان DeletePhoto را به دکمهی حذف اطلاعات هر تصویر نمایش داده شده، انتساب میدهیم:
و سپس آنرا به صورت زیر تکمیل میکنیم:
- با هر بار کلیک بر روی دکمهی Delete، شیء HotelRoomImageDTO متناظری به متد DeletePhoto ارسال میشود.
- در این شیء، مقدار خاصیت RoomImageUrl، همواره با نام پوشهای که فایلهای تصویری در آن آپلود شدهاند، شروع میشود. به همین جهت نام پوشه را از آن حذف کرده و بر این اساس، متد FileUploadService.DeleteFile را فراخوانی میکنیم تا تصویر جاری را از سرور حذف کند.
- سپس با فراخوانی متد Remove بر روی لیست تصاویر موجود، سبب به روز رسانی UI نیز خواهیم شد و به این ترتیب، تصویری که فایل آن از سرور حذف شده، از UI نیز حذف خواهد شد.
حذف تصاویر، در زمان ویرایش اطلاعات یک اتاق تعریف شده
همانطور که در ابتدای بحث نیز عنوان شد، نمیخواهیم در حالت ویرایش یک رکورد، با کلیک بر روی حذف یک تصویر، بلافاصله آنرا از سرور نیز حذف کنیم. چون ممکن است کاربری تصویری را حذف کند، اما بجای ذخیره سازی اطلاعات رکورد، بر روی دکمهی back کلیک کند. بنابراین در اینجا حذف تصاویر را صرفا به حذف آنها از UI محدود میکنیم و حذف نهایی را به زمان کلیک بر روی دکمهی ذخیره سازی اطلاعات در حال ویرایش، موکول خواهیم کرد.
به همین جهت در ابتدا با کلیک بر روی دکمهی حذف، ابتدا با حذف آن تصویر از HotelRoomImages، سبب به روز رسانی UI خواهیم شد، اما این تصویر را واقعا حذف نمیکنیم. در اینجا فقط نام آنرا در یک لیست، برای حذف نهایی، ذخیره سازی خواهیم کرد:
به این ترتیب اگر کاربر بر روی دکمهی back کلیک کند، اتفاق خاصی رخ نمیدهد؛ نه رکوردی از بانک اطلاعاتی و نه فایل تصویری از سرور حذف میشود.
سپس در جائیکه کار مدیریت ثبت اطلاعات صورت میگیرد، پس از به روز رسانی رکورد متناظر با یک اتاق، بر اساس لیست DeletedImageFileNames، فایلهای علامتگذاری شدهی برای حذف را نیز واقعا از سرور حذف میکنیم:
در اینجا باز هم نیازی نیست تا یک حلقه را تشکیل دهیم و اطلاعات را مستقیما از جدول تصاویر حذف کنیم. HotelRoomModel ارسال شدهی به متد UpdateHotelRoomAsync، چون به همراه لیست جدید HotelRoomImages است (که توسط فراخوانی HotelRoomModel.HotelRoomImages.Remove به روز شدهاست)، در حین Update، تصاویری که در این لیست وجود نداشته باشند، به صورت خودکار توسط EF-Core از سر دیگر رابطه حذف میشوند.
نمایش «لطفا منتظر بمانید» در حین آپلود تصاویر
در ادامه میخواهیم تا پایان نمایش آپلود تصاویر، پیام «لطفا منتظر بمانید» را به همراه یک spinner نمایش دهیم. بنابراین در ابتدا کلاسهای جدید زیر را به فایل wwwroot\css\site.css اضافه میکنیم:
سپس برای مدیریت نمایش spinner فوق، در ابتدای کار آپلود، فیلدIsImageUploadProcessStarted را به true تنظیم کرده و در پایان کار، آنرا false میکنیم. به همین جهت نیاز به یک try/finally خواهد بود:
پس از آن فقط کافی است بر اساس مقدار جاری این فیلد، ذیل فیلد InputFile، پیامی را نمایش دهیم:
دریافت تائیدیهی حذف، پس از کلیک بر روی دکمههای حذف تصاویر
در قسمت 12 این سری، کامپوننت Confirmation.razor را توسعه دادیم. در اینجا میخواهیم با کلیک بر روی دکمههای حذف تصاویر، ابتدا توسط این کامپوننت، تائیدیهای دریافت شود و در صورت تائید، آن تصویر انتخابی را حذف کنیم.
به همین جهت در ابتدا فایل Confirmation.razor را به پوشهی جدید Pages\Components کپی میکنیم. سپس فضای نام آنرا به فایل BlazorServer\BlazorServer.App\_Imports.razor اضافه میکنیم تا در تمام کامپوننتهای برنامه قابل استفاده شود:
سپس در ابتدا کامپوننت Confirmation را به صورت زیر اضافه میکنیم:
- ref تعریف شده سبب میشود تا بتوان متدهای عمومی تعریف شدهی در این کامپوننت، مانند Show و Hide را فراخوانی کرد.
- سپس روالهای رویدادگردان OnCancel و OnConfirm به متدهایی در کامپوننت جاری متصل شدهاند.
- در آخر پیامی تعریف شدهاست.
برای اینکه کامپوننت فوق عمل کند، نیاز است تغییرات زیر را به قسمت کدها اعمال کنیم:
- توسط وهلهی Confirmation1، میتوان متد Show را زمانیکه بر روی دکمهی Delete هر تصویر کلیک میشود، فراخوانی کنیم. قبل از آن مشخصات شیء تصویر درخواستی را در فیلد ImageToBeDeleted ذخیره میکنیم تا پس از تائید کاربر، دقیقا بر اساس اطلاعات آن بتوانیم متد OnConfirmDeleteImageClicked را پردازش کنیم.
- در اینجا محتوای متد DeletePhoto اصلی را (متدی را که تا پیش از این مرحله تکمیل کردیم) به متد جدید OnConfirmDeleteImageClicked منتقل کردهایم. یعنی در ابتدا فقط یک modal نمایش داده میشود. پس از اینکه کاربر عملیات حذف را تائید کرد، رویداد OnConfirm، سبب فراخوانی متد OnConfirmDeleteImageClicked خواهد شد (که همان DeletePhoto قبل از این تغییرات است).
حذف کامل یک اتاق به همراه تمام تصاویر منتسب به آن
مرحلهی آخر این قسمت، اضافه کردن دکمهی حذف، به ردیفهای کامپوننت نمایش لیست اتاقها است که این مورد نیز باید به همراه دریافت تائیدیهی حذف و همچنین حذف تمام وابستگیهای اتاق ثبت شده باشد:
در کامپوننت BlazorServer\BlazorServer.App\Pages\HotelRoom\HotelRoomList.razor، دکمهی Delete را به نحو فوق اضافه کردهایم که با کلیک بر روی آن، روال رویدادگردان HandleDeleteRoom اجرا شده و room متناظری را دریافت میکند.
اکنون برای مدیریت دریافت تائیدیهی حذف از کاربر، کامپوننت Confirmation را اضافه کرده:
و به نحو زیر تکمیل میکنیم:
با کلیک بر روی دکمهی حذف، متد HandleDeleteRoom اجرا شده و فیلد RoomToBeDeleted را مقدار دهی میکند. از این فیلد پس از دریافت تائید، در متد OnConfirmDeleteRoomClicked برای حذف اتاق انتخابی استفاده شدهاست.
مشکل! این روش استفادهی از DbContext کار نمیکند!
اگر برنامه را اجرا کرده و سعی در حذف یک ردیف کنیم، به خطای زیر میرسیم:
عنوان میکند که متد OnConfirmDeleteRoomClicked، بر روی ترد دیگری نسبت به ترد اولیهای که DbContext بر روی آن ایجاد شده، در حال اجرا است و چون DbContext برای یک چنین سناریوهایی، thread-safe نیست، اجازهی استفادهی از آنرا نمیدهد. در مورد روش حل این مشکل ویژه، در قسمت بعد بحث خواهیم کرد.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-18.zip
مشکل حذف تصاویر آپلود شده
در قسمت قبل، این امکان را مهیا کردیم که کاربران بتوانند پیش از ثبت اطلاعات یک اتاق، تصاویر آنرا به سرور آپلود کنند. یعنی تصاویری که در ابتدای کار آپلود میشوند، هنوز در بانک اطلاعاتی ثبت نشدهاند و هیچ رکوردی از آنها موجود نیست. در این حالت اگر کاربری تصاویری را آپلود کرده و سپس بر روی دکمهی back کلیک کند، با تعدادی تصویر آپلود شدهی غیرمنتسب به اتاقهای موجود، مواجه خواهیم شد. همچنین اگر شخصی به قسمت ویرایش تصاویر مراجعه کند و با کلیک بر روی دکمهی حذف یک تصویر، آنرا حذف کند، این حذف باید در بانک اطلاعاتی هم منعکس شود؛ در غیر اینصورت باز هم کاربر میتواند تصویری را حذف کند، اما در آخر بر روی دکمهی به روز رسانی اطلاعات رکورد کلیک نکند. در این حالت در دفعات بعدی مراجعهی به اطلاعات یک چنین اتاقی، با نقص اطلاعات تصاویری مواجه میشویم که در لیست تصاویر منتسب به یک اتاق وجود دارند، اما اصل فایل تصویری متناظر با آنها از سرور حذف شدهاست.
حذف اطلاعات تصاویر، در حالت ثبت اطلاعات
زمانیکه قرار است اطلاعات اتاقی برای اولین بار ثبت شود، حذف تصاویر آپلود شدهی مرتبط با آن سادهاست؛ چون هنوز اصل رکورد اتاق ثبت نشدهاست و این تصاویر در این لحظه، به رکوردی تعلق ندارند. بنابراین ابتدا متد رویدادگردان DeletePhoto را به دکمهی حذف اطلاعات هر تصویر نمایش داده شده، انتساب میدهیم:
@if (HotelRoomModel.HotelRoomImages.Count > 0) { var serial = 1; foreach (var roomImage in HotelRoomModel.HotelRoomImages) { <div class="col-md-2 mt-3"> <div class="room-image" style="background: url('@roomImage.RoomImageUrl') 50% 50%; "> <span class="room-image-title">@serial</span> </div> <button type="button" @onclick="()=>DeletePhoto(roomImage)" class="btn btn-outline-danger btn-block mt-4">Delete</button> </div> serial++; } }
@code { private const string UploadFolder = "Uploads"; private void DeletePhoto(HotelRoomImageDTO imageDto) { var imageFileName = imageDto.RoomImageUrl.Replace($"{UploadFolder}/", "", StringComparison.OrdinalIgnoreCase); if (HotelRoomModel.Id == 0 && Title == "Create") { FileUploadService.DeleteFile(imageFileName, WebHostEnvironment.WebRootPath, UploadFolder); HotelRoomModel.HotelRoomImages.Remove(imageDto); } } }
- در این شیء، مقدار خاصیت RoomImageUrl، همواره با نام پوشهای که فایلهای تصویری در آن آپلود شدهاند، شروع میشود. به همین جهت نام پوشه را از آن حذف کرده و بر این اساس، متد FileUploadService.DeleteFile را فراخوانی میکنیم تا تصویر جاری را از سرور حذف کند.
- سپس با فراخوانی متد Remove بر روی لیست تصاویر موجود، سبب به روز رسانی UI نیز خواهیم شد و به این ترتیب، تصویری که فایل آن از سرور حذف شده، از UI نیز حذف خواهد شد.
حذف تصاویر، در زمان ویرایش اطلاعات یک اتاق تعریف شده
همانطور که در ابتدای بحث نیز عنوان شد، نمیخواهیم در حالت ویرایش یک رکورد، با کلیک بر روی حذف یک تصویر، بلافاصله آنرا از سرور نیز حذف کنیم. چون ممکن است کاربری تصویری را حذف کند، اما بجای ذخیره سازی اطلاعات رکورد، بر روی دکمهی back کلیک کند. بنابراین در اینجا حذف تصاویر را صرفا به حذف آنها از UI محدود میکنیم و حذف نهایی را به زمان کلیک بر روی دکمهی ذخیره سازی اطلاعات در حال ویرایش، موکول خواهیم کرد.
به همین جهت در ابتدا با کلیک بر روی دکمهی حذف، ابتدا با حذف آن تصویر از HotelRoomImages، سبب به روز رسانی UI خواهیم شد، اما این تصویر را واقعا حذف نمیکنیم. در اینجا فقط نام آنرا در یک لیست، برای حذف نهایی، ذخیره سازی خواهیم کرد:
@code { private List<string> DeletedImageFileNames = new List<string>(); private void DeletePhoto(HotelRoomImageDTO imageDto) { var imageFileName = imageDto.RoomImageUrl.Replace($"{UploadFolder}/", "", StringComparison.OrdinalIgnoreCase); if (HotelRoomModel.Id == 0 && Title == "Create") { // ... } else { // Edit Mode DeletedImageFileNames.Add(imageFileName); HotelRoomModel.HotelRoomImages.Remove(imageDto); // Update UI } }
سپس در جائیکه کار مدیریت ثبت اطلاعات صورت میگیرد، پس از به روز رسانی رکورد متناظر با یک اتاق، بر اساس لیست DeletedImageFileNames، فایلهای علامتگذاری شدهی برای حذف را نیز واقعا از سرور حذف میکنیم:
private async Task HandleHotelRoomUpsert() { // ... if (HotelRoomModel.Id != 0 && Title == "Update") { // Update Mode var updatedRoomDto = await HotelRoomService.UpdateHotelRoomAsync(HotelRoomModel.Id, HotelRoomModel); foreach(var imageFileName in DeletedImageFileNames) { FileUploadService.DeleteFile(imageFileName, WebHostEnvironment.WebRootPath, UploadFolder); } // await AddHotelRoomImageAsync(updatedRoomDto); await JsRuntime.ToastrSuccess($"The `{HotelRoomModel.Name}` updated successfully."); } else { // ... } } }
نمایش «لطفا منتظر بمانید» در حین آپلود تصاویر
در ادامه میخواهیم تا پایان نمایش آپلود تصاویر، پیام «لطفا منتظر بمانید» را به همراه یک spinner نمایش دهیم. بنابراین در ابتدا کلاسهای جدید زیر را به فایل wwwroot\css\site.css اضافه میکنیم:
.spinner { border: 16px solid silver !important; border-top: 16px solid #337ab7 !important; border-radius: 50% !important; width: 80px !important; height: 80px !important; animation: spin 700ms linear infinite !important; top: 50% !important; left: 50% !important; transform: translate(-50%, -50%); position: absolute !important; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
@code { private bool IsImageUploadProcessStarted; private async Task HandleImageUpload(InputFileChangeEventArgs args) { try { IsImageUploadProcessStarted = true; // ... } finally { IsImageUploadProcessStarted = false; } } }
<InputFile OnChange="HandleImageUpload" multiple></InputFile> <div class="row"> @if (IsImageUploadProcessStarted) { <div class="col-md-12"> <span><i class="spinner"></i> Please wait.. Images are uploading...</span> </div> }
دریافت تائیدیهی حذف، پس از کلیک بر روی دکمههای حذف تصاویر
در قسمت 12 این سری، کامپوننت Confirmation.razor را توسعه دادیم. در اینجا میخواهیم با کلیک بر روی دکمههای حذف تصاویر، ابتدا توسط این کامپوننت، تائیدیهای دریافت شود و در صورت تائید، آن تصویر انتخابی را حذف کنیم.
به همین جهت در ابتدا فایل Confirmation.razor را به پوشهی جدید Pages\Components کپی میکنیم. سپس فضای نام آنرا به فایل BlazorServer\BlazorServer.App\_Imports.razor اضافه میکنیم تا در تمام کامپوننتهای برنامه قابل استفاده شود:
@using BlazorServer.App.Pages.Components
<Confirmation @ref="Confirmation1" OnCancel="OnCancelDeleteImageClicked" OnConfirm="@(()=>OnConfirmDeleteImageClicked(ImageToBeDeleted))"> <div> Do you want to delete @ImageToBeDeleted?.RoomImageUrl image? </div> </Confirmation>
- سپس روالهای رویدادگردان OnCancel و OnConfirm به متدهایی در کامپوننت جاری متصل شدهاند.
- در آخر پیامی تعریف شدهاست.
برای اینکه کامپوننت فوق عمل کند، نیاز است تغییرات زیر را به قسمت کدها اعمال کنیم:
private Confirmation Confirmation1; private HotelRoomImageDTO ImageToBeDeleted; private void OnCancelDeleteImageClicked() { // Confirmation1.Hide(); } private void DeletePhoto(HotelRoomImageDTO imageDto) { ImageToBeDeleted = imageDto; Confirmation1.Show(); } private void OnConfirmDeleteImageClicked(HotelRoomImageDTO imageDto) {
- در اینجا محتوای متد DeletePhoto اصلی را (متدی را که تا پیش از این مرحله تکمیل کردیم) به متد جدید OnConfirmDeleteImageClicked منتقل کردهایم. یعنی در ابتدا فقط یک modal نمایش داده میشود. پس از اینکه کاربر عملیات حذف را تائید کرد، رویداد OnConfirm، سبب فراخوانی متد OnConfirmDeleteImageClicked خواهد شد (که همان DeletePhoto قبل از این تغییرات است).
حذف کامل یک اتاق به همراه تمام تصاویر منتسب به آن
مرحلهی آخر این قسمت، اضافه کردن دکمهی حذف، به ردیفهای کامپوننت نمایش لیست اتاقها است که این مورد نیز باید به همراه دریافت تائیدیهی حذف و همچنین حذف تمام وابستگیهای اتاق ثبت شده باشد:
<td> <NavLink href="@($"hotel-room/edit/{room.Id}")" class="btn btn-primary">Edit</NavLink> <button class="btn btn-danger" @onclick="()=>HandleDeleteRoom(room)">Delete</button> </td>
اکنون برای مدیریت دریافت تائیدیهی حذف از کاربر، کامپوننت Confirmation را اضافه کرده:
<Confirmation @ref="Confirmation1" OnCancel="OnCancelDeleteRoomClicked" OnConfirm="OnConfirmDeleteRoomClicked"> <div> Do you want to delete @RoomToBeDeleted?.Name? </div> </Confirmation>
@code { private List<HotelRoomDTO> HotelRooms = new List<HotelRoomDTO>(); private HotelRoomDTO RoomToBeDeleted; private Confirmation Confirmation1; private void OnCancelDeleteRoomClicked() { // Confirmation1.Hide(); } private void HandleDeleteRoom(HotelRoomDTO roomDto) { RoomToBeDeleted = roomDto; Confirmation1.Show(); } private async Task OnConfirmDeleteRoomClicked() { if(RoomToBeDeleted is null) { return; } await HotelRoomService.DeleteHotelRoomAsync(RoomToBeDeleted.Id); HotelRooms.Remove(RoomToBeDeleted); // Update UI }
مشکل! این روش استفادهی از DbContext کار نمیکند!
اگر برنامه را اجرا کرده و سعی در حذف یک ردیف کنیم، به خطای زیر میرسیم:
An exception occurred while iterating over the results of a query for context type 'BlazorServer.DataAccess.ApplicationDbContext'. System.InvalidOperationException: A second operation was started on this context before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-18.zip
در این قسمت ویژگیهای بصری را مانند مشخص سازی مسیر انتخاب شده، در منوی سایت و همچنین نمایش «لطفا منتظر بمانید» را در حین نمایش قسمتهایی که با تاخیر از سرور دریافت میشوند، پیاده سازی خواهیم کرد.
تزئین مسیر انتخاب شده در منوی سایت
برای بهبود ظاهر برنامه نیاز است منوی سایت را به نحوی تغییر دهیم که مشخص کند، اکنون کاربر کدام گزینه را انتخاب کردهاست. این مورد شامل سلسه مراتب مسیریابیها نیز میشود؛ برای مثال فعالسازی حالت انتخاب شدهی منوی سایت، به همراه برگهی انتخاب شده در یکی از Child Routes.
برای پیاده سازی این قابلیت، دایرکتیو ویژهای به نام routerLinkActive تدارک دیده شدهاست. این دایرکتیو را میتوان به یک anchor tag و یا المان والد آن انتساب داد. مقدار آنرا نیز میتوان به یکی از کلاسهای CSS برنامه مانند کلاس active تعریف شدهی در بوت استرپ تنظیم کرد. هر زمانیکه این مسیریابی فعال شود، مسیریاب به صورت خودکار این کلاس را با درج آن، به المان مرتبط اضافه میکند و برعکس.
برای نمونه فایل src\app\product\product-edit\product-edit.component.html را گشوده و سپس تغییرات ذیل را اعمال کنید:
در اینجا دایرکتیوهای routerLinkActive، به هر کدام از لینکهای تعریف شده اضافه گردیدهاند. مقدار active در اینجا، به کلاس active بوت استرپ اشاره میکند. یا حتی میتوان تعدادی کلاس جدا شدهی با کاما را نیز در اینجا ذکر کرد.
یک نکته: از آنجائیکه در اینجا مقدار active یک string است و نه یک خاصیت یا عبارت متغیر، به همین جهت نیازی نیست تا این دایرکتیو را به صورت [routerLinkActive] تعریف کنیم.
همانطور که مشاهده میکنید، همین دو تنظیم ساده سبب مشخص شدن برگهی انتخابی شدهاند.
منوی بالای سایت نیز چنین تنظیماتی را نیاز دارد. برای این منظور به فایل src\app\app.component.html که دربرگیرندهی منوی سایت است مراجعه کرده و تغییرات ذیل را اعمال میکنیم:
اینبار routerLinkActive به المانهای li اعمال شدهاست؛ چون این المانهای لیست، شیوه نامهی المانهای anchor را بازنویسی میکنند و اگر routerLinkActive را به لینکها اعمال میکردیم، تغییری مشاهده نمیشد.
همانطور که مشاهده میکنید، در این حالت انتخاب منوی نمایش لیست محصولات، سبب تزئین آن به حالت انتخاب شده نیز گردیدهاست.
مشکل! در همین حالت که مسیر نمایش لیست محصولات انتخاب شدهاست، لینک افزودن یک محصول جدید را نیز انتخاب کنید:
اینبار هر دو گزینه با هم انتخاب شدهاند. علت اینجا است که این دو مسیر دارای root URL segment یکسانی هستند؛ یا همان products/ در اینجا. به همین جهت routerLinkActive هر دو را به عنوان فعال انتخاب کردهاست. برای مدیریت میدان دید آن میتوان از دایرکتیو دیگری به نام routerLinkActiveOptions استفاده کرد:
routerLinkActiveOptions را تنها به ریشهی مسیر products اعمال کردهایم؛ چون این مسیر است که میتواند با تمام مسیرهای مشتق شدهی از آن نیز تطابق داشته باشد. تنظیم exact: true آن سبب خواهد شد تا تطابق با مسیرهای مشتق شدهی از آن ندید گرفته شوند.
اکنون کاربران بهتر میتوانند درک کنند در کجای برنامه قرار دارند.
افزودن آیکن خطا به برگهای که دارای مشکل اعتبارسنجی است
در ادامه میخواهیم اگر برگهای دارای مشکلات اعتبارسنجی بود، آیکن خطایی را در کنار برچسب آن برگه نمایش دهیم. به این ترتیب مدیریت چندین برگه برای کاربران سادهتر خواهد شد و به سادگی میتوانند برگههای مشکل دار را پیدا کنند.
در انتهای مطلب «مسیریابی در Angular - قسمت پنجم - تعریف Child Routes» متد isValid را تعریف کردیم. این متد مسیر یک tab را دریافت کرده و اگر اعتبارسنجی آن مشکلی نداشت، مقدار true را بر میگرداند. از این متد جهت نمایش آیکن خطای اعتبارسنجی برگهها استفاده خواهیم کرد.
در اینجا دو span را تعریف کردهایم که با کمک دایرکتیو ngClass سبب نمایش آیکن exclamation-sign در صورت وجود یک خطای اعتبارسنجی میشوند. به عبارتی اگر برگهای معتبر نباشد، سبب درج کلاس آن در span جاری میشود:
رخدادهای مسیریابی
هر زمانیکه کاربری مسیرهای مختلف برنامه را پیمایش میکند، مسیریاب تعدادی رخداد را نیز تولید خواهد کرد. از این رخدادها جهت تحت نظر قرار دادن، عیبیابی و یا اجرای منطقی میتوان استفاده کرد. این رخدادها شامل موارد ذیل هستند:
- NavigationStart، با آغاز پیمایش یک مسیر رخ میدهد.
- RoutesRecognized، با تشخیص و تطابق یک مسیر، با یکی از المانهای تعریف شدهی در تنظیمات مسیریابی رخ میدهد.
- NavigationEnd، با پایان پیمایش یک مسیر رخ میدهد.
- NavigationCancel، در صورت لغو پیمایش یک مسیریابی توسط محافظهای مسیرها و یا هدایت به یک جهت دیگر رخ میدهد.
- NavigationError، با شکست پیمایش یک مسیر رخ میدهد.
این رخدادها با فعالسازی تنظیم enableTracing تنظیمات مسیریابی به true فعال میشوند. برای این منظور فایل src\app\app-routing.module.ts را گشوده و به نحو ذیل تغییر دهید:
پس از این تغییر، اگر به developer tools مرورگر دقت کنید، یک چنین خروجی را میتوان مشاهده کرد:
در اینجا ترتیب اجرای رخدادهای متفاوت پیمایش مسیر نمایش لیست محصولات را مشاهده میکنید.
- Router به هر مسیر، یک id خود افزایش یابنده را به صورت خودکار نسبت میدهد. برای نمونه، این مسیر خاص، id:2 را یافتهاست. از این id میتوان برای دسترسی به مجموعهای از رخدادها استفاده کرد.
- در این خروجی، url همان آدرس اصلی مسیر است و urlAfterRedirects به معنای مسیری است که پس از تنظیم redirect در تنظیمات مسیریابی (در صورت وجود) حاصل شدهاست.
- یکی از روشهایی که برای دیباگ مسیریابیها میتوان استفاده کرد، همین فعالسازی enableTracing است.
کار با رخدادهای مسیریابی با کدنویسی
به رخدادهایی که در کنسول developer tools مرورگر مشاهده کردید، با کدنویسی نیز میتوان دسترسی یافت. برای مثال میتوان یک تصویر چرخنده یا لطفا منتظر بمانید را در آغاز پیمایش یک مسیریابی نمایش داد و سپس در پایان پیمایش این مسیریابی، آنرا مخفی کرد. این events نیز از نوع Observable بوده و برای کار با آنها باید مشترکشان شد:
شیء router به همراه خاصیت events است که با گوش فرادادن به رخدادهای صادر شدهی توسط آن میتوان دریافت چه نوع رخدادی هم اکنون صادر شدهاست.
در مثال جاری این سری، در «مسیریابی در Angular - قسمت چهارم - پیش واکشی اطلاعات»، سبب شدیم تا کل اطلاعات مورد نیاز یک مسیر، پیش از نمایش آن از سرور دریافت شوند تا به این صورت ابتدا یک قاب خالی نمایش داده نشده و پس از مدتی تکمیل شود. هرچند تجربهی کاربری این روش بهتر از روش قبلی است، اما هنوز هم کاربر تاخیری را در ابتدا حس خواهد کرد (به اندازهی زمان delay تنظیم شده)، بدون اینکه راهنمایی به او ارائه شود. در این حالت بهتر است در ابتدای کار، یک تصویر چرخنده نمایش داده شود تا کاربر متوجه شود، نیاز است اندکی منتظر بماند.
در اینجا میخواهیم این تصویر چرخنده برای تمام مسیرهای برنامه فعال شود. به همین جهت گوش فرادادن به رخدادها را در نقطهی آغازین برنامه و یا همان src\app\app.component.ts انجام میدهیم:
کدهای کامل AppComponent را جهت گوش فرادادن به رخدادهای شروع و یا خاتمه/لغو/شکست پیمایش یک مسیریابی، در اینجا مشاهده میکنید.
- ابتدا وابستگیهای لازم آن import شدهاند.
- سپس میخواهیم خاصیت عمومی loading را در شروع به پیمایش یک مسیر، به true تنظیم کنیم و اگر این پیمایش به هر نحوی خاتمه یافت، آنرا false خواهیم کرد.
اکنون برای استفادهی از این خاصیت عمومی و نمایش تصویر چرخنده، نیاز است قالب src\app\app.component.html را ویرایش کنیم:
با افزودن span فوق به ابتدای فایل app.component.html به تغییرات خاصیت loading واکنش نشان خواهیم داد. کلاسهای CSS ایی را که در اینجا اضافه شدهاند، به فایل src\styles.css اضافه میکنیم:
اکنون مسیرهایی که دارای route resolver هستند (مانند نمایش جزئیات/ویرایش یک محصول)، به همراه یک spinner نمایش داده خواهند شد و سایر مسیرها ابتدا نمایش داده خواهند شد و سپس اطلاعات آنها از سرور دریافت میشود (مانند مسیر نمایش لیست محصولات که دارای route resolver نیست).
البته میتوان این true/false کردن loading را به ابتدا و انتهای کار یک Observable، مانند حالت نمایش لیست محصولات نیز منتقل کرد. اما در این حالت باید span مرتبط را نیز به قالب همان کامپوننت انتقال داد و دیگر سراسری نخواهد بود.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-routing-lab-06.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng s -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
تزئین مسیر انتخاب شده در منوی سایت
برای بهبود ظاهر برنامه نیاز است منوی سایت را به نحوی تغییر دهیم که مشخص کند، اکنون کاربر کدام گزینه را انتخاب کردهاست. این مورد شامل سلسه مراتب مسیریابیها نیز میشود؛ برای مثال فعالسازی حالت انتخاب شدهی منوی سایت، به همراه برگهی انتخاب شده در یکی از Child Routes.
برای پیاده سازی این قابلیت، دایرکتیو ویژهای به نام routerLinkActive تدارک دیده شدهاست. این دایرکتیو را میتوان به یک anchor tag و یا المان والد آن انتساب داد. مقدار آنرا نیز میتوان به یکی از کلاسهای CSS برنامه مانند کلاس active تعریف شدهی در بوت استرپ تنظیم کرد. هر زمانیکه این مسیریابی فعال شود، مسیریاب به صورت خودکار این کلاس را با درج آن، به المان مرتبط اضافه میکند و برعکس.
برای نمونه فایل src\app\product\product-edit\product-edit.component.html را گشوده و سپس تغییرات ذیل را اعمال کنید:
<div class="wizard"> <a [routerLink]="['info']" routerLinkActive="active"> Basic Information </a> <a [routerLink]="['tags']" routerLinkActive="active"> Search Tags </a> </div>
یک نکته: از آنجائیکه در اینجا مقدار active یک string است و نه یک خاصیت یا عبارت متغیر، به همین جهت نیازی نیست تا این دایرکتیو را به صورت [routerLinkActive] تعریف کنیم.
همانطور که مشاهده میکنید، همین دو تنظیم ساده سبب مشخص شدن برگهی انتخابی شدهاند.
منوی بالای سایت نیز چنین تنظیماتی را نیاز دارد. برای این منظور به فایل src\app\app.component.html که دربرگیرندهی منوی سایت است مراجعه کرده و تغییرات ذیل را اعمال میکنیم:
<ul class="nav navbar-nav"> <li routerLinkActive="active"> <a [routerLink]="['/home']">Home</a> </li> <li routerLinkActive="active"> <a [routerLink]="['/products']">Product List</a> </li> <li routerLinkActive="active"> <a [routerLink]="['/products', 0, 'edit']">Add Product</a> </li> </ul>
همانطور که مشاهده میکنید، در این حالت انتخاب منوی نمایش لیست محصولات، سبب تزئین آن به حالت انتخاب شده نیز گردیدهاست.
مشکل! در همین حالت که مسیر نمایش لیست محصولات انتخاب شدهاست، لینک افزودن یک محصول جدید را نیز انتخاب کنید:
اینبار هر دو گزینه با هم انتخاب شدهاند. علت اینجا است که این دو مسیر دارای root URL segment یکسانی هستند؛ یا همان products/ در اینجا. به همین جهت routerLinkActive هر دو را به عنوان فعال انتخاب کردهاست. برای مدیریت میدان دید آن میتوان از دایرکتیو دیگری به نام routerLinkActiveOptions استفاده کرد:
<li routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }"> <a [routerLink]="['/products']">Product List</a> </li>
اکنون کاربران بهتر میتوانند درک کنند در کجای برنامه قرار دارند.
افزودن آیکن خطا به برگهای که دارای مشکل اعتبارسنجی است
در ادامه میخواهیم اگر برگهای دارای مشکلات اعتبارسنجی بود، آیکن خطایی را در کنار برچسب آن برگه نمایش دهیم. به این ترتیب مدیریت چندین برگه برای کاربران سادهتر خواهد شد و به سادگی میتوانند برگههای مشکل دار را پیدا کنند.
در انتهای مطلب «مسیریابی در Angular - قسمت پنجم - تعریف Child Routes» متد isValid را تعریف کردیم. این متد مسیر یک tab را دریافت کرده و اگر اعتبارسنجی آن مشکلی نداشت، مقدار true را بر میگرداند. از این متد جهت نمایش آیکن خطای اعتبارسنجی برگهها استفاده خواهیم کرد.
<div class="wizard"> <a [routerLink]="['info']" routerLinkActive="active"> Basic Information <span [ngClass]="{'glyphicon glyphicon-exclamation-sign': !isValid('info')}"></span> </a> <a [routerLink]="['tags']" routerLinkActive="active"> Search Tags <span [ngClass]="{'glyphicon glyphicon-exclamation-sign': !isValid('tags')}"></span> </a> </div>
رخدادهای مسیریابی
هر زمانیکه کاربری مسیرهای مختلف برنامه را پیمایش میکند، مسیریاب تعدادی رخداد را نیز تولید خواهد کرد. از این رخدادها جهت تحت نظر قرار دادن، عیبیابی و یا اجرای منطقی میتوان استفاده کرد. این رخدادها شامل موارد ذیل هستند:
- NavigationStart، با آغاز پیمایش یک مسیر رخ میدهد.
- RoutesRecognized، با تشخیص و تطابق یک مسیر، با یکی از المانهای تعریف شدهی در تنظیمات مسیریابی رخ میدهد.
- NavigationEnd، با پایان پیمایش یک مسیر رخ میدهد.
- NavigationCancel، در صورت لغو پیمایش یک مسیریابی توسط محافظهای مسیرها و یا هدایت به یک جهت دیگر رخ میدهد.
- NavigationError، با شکست پیمایش یک مسیر رخ میدهد.
این رخدادها با فعالسازی تنظیم enableTracing تنظیمات مسیریابی به true فعال میشوند. برای این منظور فایل src\app\app-routing.module.ts را گشوده و به نحو ذیل تغییر دهید:
@NgModule({ imports: [RouterModule.forRoot(routes/*, { useHash: true }*/, { enableTracing: true })],
در اینجا ترتیب اجرای رخدادهای متفاوت پیمایش مسیر نمایش لیست محصولات را مشاهده میکنید.
- Router به هر مسیر، یک id خود افزایش یابنده را به صورت خودکار نسبت میدهد. برای نمونه، این مسیر خاص، id:2 را یافتهاست. از این id میتوان برای دسترسی به مجموعهای از رخدادها استفاده کرد.
- در این خروجی، url همان آدرس اصلی مسیر است و urlAfterRedirects به معنای مسیری است که پس از تنظیم redirect در تنظیمات مسیریابی (در صورت وجود) حاصل شدهاست.
- یکی از روشهایی که برای دیباگ مسیریابیها میتوان استفاده کرد، همین فعالسازی enableTracing است.
کار با رخدادهای مسیریابی با کدنویسی
به رخدادهایی که در کنسول developer tools مرورگر مشاهده کردید، با کدنویسی نیز میتوان دسترسی یافت. برای مثال میتوان یک تصویر چرخنده یا لطفا منتظر بمانید را در آغاز پیمایش یک مسیریابی نمایش داد و سپس در پایان پیمایش این مسیریابی، آنرا مخفی کرد. این events نیز از نوع Observable بوده و برای کار با آنها باید مشترکشان شد:
this.router.events.subscribe((routerEvent: Event) => { if (routerEvent instanceof NavigationStart) { //... } });
در مثال جاری این سری، در «مسیریابی در Angular - قسمت چهارم - پیش واکشی اطلاعات»، سبب شدیم تا کل اطلاعات مورد نیاز یک مسیر، پیش از نمایش آن از سرور دریافت شوند تا به این صورت ابتدا یک قاب خالی نمایش داده نشده و پس از مدتی تکمیل شود. هرچند تجربهی کاربری این روش بهتر از روش قبلی است، اما هنوز هم کاربر تاخیری را در ابتدا حس خواهد کرد (به اندازهی زمان delay تنظیم شده)، بدون اینکه راهنمایی به او ارائه شود. در این حالت بهتر است در ابتدای کار، یک تصویر چرخنده نمایش داده شود تا کاربر متوجه شود، نیاز است اندکی منتظر بماند.
در اینجا میخواهیم این تصویر چرخنده برای تمام مسیرهای برنامه فعال شود. به همین جهت گوش فرادادن به رخدادها را در نقطهی آغازین برنامه و یا همان src\app\app.component.ts انجام میدهیم:
import { Router, Event, NavigationStart, NavigationEnd, NavigationError, NavigationCancel } from '@angular/router'; import { AuthService } from './user/auth.service'; import { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { pageTitle: string = 'Routing Lab'; loading: boolean = true; constructor(private authService: AuthService, private router: Router) { router.events.subscribe((routerEvent: Event) => { this.checkRouterEvent(routerEvent); }); } checkRouterEvent(routerEvent: Event): void { if (routerEvent instanceof NavigationStart) { this.loading = true; } if (routerEvent instanceof NavigationEnd || routerEvent instanceof NavigationCancel || routerEvent instanceof NavigationError) { this.loading = false; } } logOut(): void { this.authService.logout(); this.router.navigateByUrl('/welcome'); } }
- ابتدا وابستگیهای لازم آن import شدهاند.
- سپس میخواهیم خاصیت عمومی loading را در شروع به پیمایش یک مسیر، به true تنظیم کنیم و اگر این پیمایش به هر نحوی خاتمه یافت، آنرا false خواهیم کرد.
اکنون برای استفادهی از این خاصیت عمومی و نمایش تصویر چرخنده، نیاز است قالب src\app\app.component.html را ویرایش کنیم:
<span class="glyphicon glyphicon-refresh glyphicon-spin spinner" *ngIf="loading"></span>
/* Spinner */ .spinner { font-size:300%; position:absolute; top: 50%; left: 50%; z-index:10 } .glyphicon-spin { -webkit-animation: spin 1000ms infinite linear; animation: spin 1000ms infinite linear; } @-webkit-keyframes spin { 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); } 100% { -webkit-transform: rotate(359deg); transform: rotate(359deg); } } @keyframes spin { 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); } 100% { -webkit-transform: rotate(359deg); transform: rotate(359deg); } }
اکنون مسیرهایی که دارای route resolver هستند (مانند نمایش جزئیات/ویرایش یک محصول)، به همراه یک spinner نمایش داده خواهند شد و سایر مسیرها ابتدا نمایش داده خواهند شد و سپس اطلاعات آنها از سرور دریافت میشود (مانند مسیر نمایش لیست محصولات که دارای route resolver نیست).
البته میتوان این true/false کردن loading را به ابتدا و انتهای کار یک Observable، مانند حالت نمایش لیست محصولات نیز منتقل کرد. اما در این حالت باید span مرتبط را نیز به قالب همان کامپوننت انتقال داد و دیگر سراسری نخواهد بود.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-routing-lab-06.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng s -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
1- نیاز به تواناییهای موجود در برنامههای Desktop را دارید اما همچنین نیاز است تا آنها را تحت وب نیز ارائه دهید.
یکی از دلایل اقبال به برنامههای تحت وب در سازمانها عدم نیاز به نصب آنها و توزیع هر چه سادهتر اینگونه برنامهها در شبکه است. تنها کافی است چند فایل را بر روی سرور به روز رسانی کنید و پس از آن تمام کلاینتها از آخرین نگارش برنامه شما بهرهمند خواهند شد (+). توزیع برنامههای سیلورلایت نیز به همین منوال است. علاوه بر آن استفاده از فناورهایی مانند MEF امکان ماژولار ساختن برنامه و دریافت آخرین ماژولهای تهیه شده (فایلهای XAP مجزای از برنامه به صورت افزونه) را بر اساس انتخاب و سطح دسترسی کاربر نیز میسر میسازد.
2- نیاز است تا یک برنامهی گرافیکی تمام عیار را تحت وب ارائه دهید.
تواناییهای XAML به همراه یکی از زبانهای دات نت جهت خلق جلوههای بصری، پویانمایی و گرافیکی بسیار بسیار فراتر از کتابخانههای جاوا اسکریپتی موجود هستند و نکتهی مهم آنها هم این است که لازم نیست حتما یک متخصص مثلا جاوا اسکریپت باشید تا بتوانید برای مثال پویانمایی را ارائه دهید. امکان استفاده از انواع و اقسام قلمها و قرار دادن آنها در برنامه، امکان استفاده از گرافیک برداری و غیره را نیز لحاظ کنید.
3- برنامهی شما نیاز است تا از طریق وب توزیع شود اما نیاز به سطح دسترسی بیشتری نسبت به یک برنامهی وب معمولی دارد.
تمام برنامههای توزیع شده از طریق مرورگرها محدود به سطوح دسترسی آنها نیز هستند. اما امکان نصب خارج از مرورگر برنامههای سیلورلایت نیز وجود دارد. در این حالت میتوان در صورت نیاز و همچنین تائید صریح کاربر، به سطوح دسترسی بیشتری دست یافت. برای مثال دسترسی به اسکنر در یک برنامهی وب متداول بیمعنا است. اما سیلورلایت 4 در حالت اجرای در خارج از مرورگر امکان تعامل با اشیاء COM را نیز دارد.
4- برنامهی وب شما نیاز است تا مدت زمان زیادی فعال باقی بماند.
یک برنامه دریافت ایمیل یا یک برنامه مونیتورینگ را در نظر بگیرید. اینگونه برنامهها باید مرتبا بدون نیاز به دخالت کاربر، فعال باقی بمانند و با سرور ارتباط داشته باشند. نوشتن اینگونه برنامهها با HTML و جاوا اسکریپت و فناوریهای مشابه واقعا مشکل بوده و نیاز به دانش فنی بالایی دارند. اما این مساله و حیات یک برنامه سیلورلایت تا زمانیکه مرورگر بسته نشده است جزو خواص اولیه اینگونه برنامهها است.
5- از مشکلات مدیریت حالت در برنامههای متداول وب به تنگ آمدهاید.
اگر برای مثال برنامه نویس ASP.NET باشید حتما با مباحث State management آشنایی دارید (از سشن و کوکی گرفته تا ViewState (ایی که همه به نحوی قصد کوچک کردن آنرا دارند!) و غیره). تمام اینها هم برای این است که بتوان تجربهی کاری برنامههای دسکتاپ را در محیط مرورگرها شبیه سازی کرد. این مشکلات در سیلورلایت حل شده است. یک برنامهی سیلورلایت State full است نه Stateless . همچنین اگر از حافظهای هم استفاده میکند این مورد در سمت کاربر است و نه سمت سرور و نه منقضی شدن زود هنگام سشنها و صدها ترفند برای مقیاس پذیری همین مسالهی بسیار کوچک با تعداد کاربران بالا در برنامههای متداول وب.
به عبارتی تصور کنید که برنامهی دسکتاپ سالهای قبل شما هم اکنون داخل مرورگر دارد اجرا میشود و چیزی به نام وب سرور وجود ندارد که پس از نمایش صفحهی وب شما، کلیهی اشیاء مرتبط با آنرا در سمت سرور تخریب کند چون باید پاسخگوی کاربران همزمان بیشماری باشد و منابع سرور هم محدود است. (سیلورلایت یک فناوری سمت کاربر است. بنابراین وب سرور صرفا نقش توزیع آنرا به عهده دارد یا حداکثر ارائهی یک وب سرویس جهت تعاملات بعدی مانند کار با بانک اطلاعاتی)
6- نیاز دارید تا برنامهی وب شما تحت تمام مرورگرها به یک شکل به نظر برسد و همچنین رفتار یکسانی هم داشته باشد.
هیچ وقت روزی را فراموش نمیکنم که حین پرداخت الکترونیکی بانک XYZ به کمک مرورگر فایرفاکس، دکمهی پرداخت در مرحلهی آخر، کار نمیکرد! هر چقدر روی آن کلیک میکردم اتفاقی نمیافتاد! تراکنش برگشت خورد و همین خرید ساده با مرورگر IE به سادگی انجام شد.
با سیلورلایت این مشکلات را نخواهید داشت زیرا کار نمایش برنامه شما توسط افزونهی مربوطه صورت میگیرد و این افزونه مستقل است از نوع مرورگر شما.
7- نیاز است برنامهی وب شما در حالت آفلاین هم کار کند.
برنامههای سیلورلایت تنها زمانیکه نیاز به دریافت یا ثبت اطلاعاتی از سرور داشته باشند، باید آنلاین باشند. همچنین این برنامهها دسترسی به مفهوم جدیدی به نام Isolated Storage دارند که در آن میتوان اطلاعات را به ازای هر کاربر آن هم با ضریب امنیتی بالا بر روی هارد شخص ذخیره کرد و زمان آنلاین شدن برنامه آنها را به سرور انتقال داد.
8- برنامه وب شما نیاز است تا با فایلهای مالتی مدیا تعامل داشته و آنها را پخش کند.
حتی تگ Video در HTML5 نیز به پای تواناییهای مالتی مدیا در Silverlight مانند smooth streaming, multicasting, editing, video brushes نمیرسد. برای مثال با استفاده از video brushes میتوان یک فایل ویدیویی در حال پخش را بر روی یک وجه یک شیء در حال پویانمایی نقاشی و نمایش داد.
9- نیاز به پشتیبانی از multi-touch در برنامهی وب شما وجود دارد.
برخلاف HTML ، تعاملات multi-touch در Silverlight میسر است.
10- نیاز به ایجاد برنامههای بازی تحت وب دارید.
به طور قطع میتوان بازییهایی در حد Pong را با جاوا اسکریپت هم ایجاد کرد، اما اگر نیاز به تولید بازیهایی جدیتر وجود داشت برای مثال انتقال بازی Quake به محیط وب، Silverlight در این زمینه هم حرفهای زیادی برای گفتن دارد (+).
11- نیاز به تولید برنامهی دسکتاپ چند سکویی دارید.
سیلورلایت هم اکنون تحت ویندوز، MAC OS-X ، لینوکس و ... پشتیبانی میشود (+). همچنین برنامههای سیلورلایت قابلیت اجرای در خارج از مرورگر را هم دارند.
با سیلورلایت دیگر نیازی نخواهد بود تا کاربران لینوکسی ابتدا Wine را نصب کنند تا بتوانند از یک برنامهی ویندوزی که انتقال پذیر نیست در لینوکس هم بتوانند استفاده کنند؛ چون پروژهی مون لایت لینوکسی برای این منظور مهیا است.
12- نیاز به تولید برنامههای تحت وب سریع و با کارآیی بالا دارید.
فایلهای نهایی Silverlight با توجه به ماهیت کامپایل شدهی آنها به طور قطع از کدهای جاوا اسکریپتی سمت کلاینت که باید توسط مرورگر تفسیر و پردازش شوند (و هر کدام هم از موتور خاص خودشان استفاده میکنند)، سریعتر اجرا میشوند (+).
13- از پیچیدگیهای پیاده سازی برنامههای متداول وب خسته شدهاید.
هنوز هم با تمام پیشرفتهای حاصل، تولید برنامههای وب پیشرفته مشکل است. از یک طرف ناسازگاری یک سری از مرورگرها با یک سری از قابلیتها را باید در نظر داشت، تا فراگیری فریم ورکهای Ajax و غیره تا مشکل بودن طراحی کنترلهای جدید فراتر از آن چیزی که HTML استاندارد ارائه میدهد. بله، به طور قطع دانش فنی بالایی در این زمینه در طی سالیان تولید شده است، اما باز هم فراگیری و تسلط به آنها زمان قابل توجهی را طلب میکند.
در سیلورلایت کلیه تعاملات با شبکه به صورت پیش فرض غیرهمزمان است (همان ایدهی اصلی Ajax) همچنین با توجه به state full بودن اینگونه برنامهها، عملا برنامه نویسها بدون درگیر شدن با مفاهیم اجکسی و مدیریت حالت، برنامهی پیشرفتهی وبی را در مدت زمان کوتاهی تولید کردهاند و این برنامه در تمام مرورگرهایی که قابلیت بارگذاری افزونهی سیلورلایت را دارند به یک شکل و کیفیت اجرا میشود.
14- در زمینه میزان مصرف پهنای باند ملاحظاتی ویژهای وجود دارد.
یک برنامهی سیلورلایت تنها یکبار باید دریافت شود. پس از آن در سمت کاربر کش خواهد شد (تا زمان به روز رسانی بعدی برنامه در سرور). همین مساله در دفعات بعدی مراجعه کاربر به سایت نقش قابل توجهی را در کاهش میزان مصرف پهنای باند (یا به قولی میزان کمتر data transfer) کلی دارد.
15- فرصت کافی برای فراگیری انبوهی از فناوریهای مختلف را ندارید!
بله! برای ایجاد یک برنامهی تحت وب که کاربر آن پس از مشاهده بگوید WOW نیاز است به HTML ، JS ، CSS ، AJAX ، یکی از فناوریهای سمت سرور و ... مسلط بود (علاوه بر اینکه باید بدانید فلان کد JS در IE کار میکند اما در فایرفاکس خیر. فایرفاکس فلان قسمت CSS را پشتیبانی میکند اما IE خیر! و ...).
اما برای استفاده از سیلورلایت فقط کافی است به XAML و یکی از زبانهای دات نت مانند سی شارپ یا VB.NET مسلط باشید (البته هیچ وقت از دست ASP.NET خلاص نخواهید شد! حداقل در حد راه اندازی یک وب سرویس یا مفاهیم امنیتی آن).
این مورد خصوصا برای افرادی که برنامه نویس دسکتاپ هستند اما علاقمندند تا برنامهی وب نیز تولید کنند بسیار مهم است. با حداقل آموزش میتوانند تواناییهای خود را به وب نیز گسترش دهند. علاوه بر آن عمدهی دانش Silverlight شما جهت تولید برنامههای WPF (با توجه به اینکه Silverlight فرزند WPF محسوب میشود) یا Windows phone 7 و غیره نیز میتواند بکار گرفته شود.
16- نیاز به اجرای کدهای چند ریسمانی در سمت کاربر دارید.
تا این لحظه پشتیبانی رسمی از مباحث چند ریسمانی در JavaScript و استانداردهای مرتبط با آن وجود ندارد. Silverlight به اکثر امکانات Threading موجود در دات نت فریم ورک دسترسی داشته و دانش فعلی شما قابل انتقال است.
و دست آخر باید به نکته اشاره کرد که هدف از Silverlight ساخت وب سایت معمولی نیست. این نوع کارها را با همان ابزارهای متداول انجام دهید. هدف اصلی آن ساخت برنامه است (Application در مقابل Web site). مشتریهای اصلی این نوع برنامهها هم بیشتر سازمانها و اینترانتهای پر سرعت و بستهی آنها هستند که نه نگران حجم افزونهی سیلورلایت هستند و نه مشکلی با حجم برنامهی سیلورلایت شما در یک شبکهی داخلی پر سرعت دارند.
مطالب
EF Code First #2
در قسمت قبل با تنظیمات و قراردادهای ابتدایی EF Code first آشنا شدیم، هرچند این تنظیمات حجم کدنویسی ابتدایی راه اندازی سیستم را به شدت کاهش میدهند، اما کافی نیستند. در این قسمت نگاهی سطحی و مقدماتی خواهیم داشت بر امکانات مهیا جهت تنظیم ویژگیهای مدلهای برنامه در EF Code first.
تنظیمات EF Code first توسط اعمال متادیتای خواص
اغلب متادیتای مورد نیاز جهت اعمال تنظیمات EF Code first در اسمبلی System.ComponentModel.DataAnnotations.dll قرار دارند. بنابراین اگر مدلهای خود را در اسمبلی و پروژه class library جداگانهای تعریف و نگهداری میکنید (مثلا به نام DomainClasses)، نیاز است ابتدا ارجاعی را به این اسمبلی به پروژه جاری اضافه نمائیم. همچنین تعدادی دیگر از متادیتای قابل استفاده در خود اسمبلی EntityFramework.dll قرار دارند. بنابراین در صورت نیاز باید ارجاعی را به این اسمبلی نیز اضافه نمود.
همان مثال قبل را در اینجا ادامه میدهیم. دو کلاس Blog و Post در آن تعریف شده (به این نوع کلاسها POCO – the Plain Old CLR Objects نیز گفته میشود)، به همراه کلاس Context که از کلاس DbContext مشتق شده است. ابتدا دیتابیس قبلی را دستی drop کنید. سپس در کلاس Blog، خاصیت public int Id را مثلا به public int MyTableKey تغییر دهید و پروژه را اجرا کنید. برنامه بلافاصله با خطای زیر متوقف میشود:
One or more validation errors were detected during model generation:
\tSystem.Data.Entity.Edm.EdmEntityType: : EntityType 'Blog' has no key defined.
زیرا EF Code first در این کلاس خاصیتی به نام Id یا BlogId را نیافتهاست و امکان تشکیل Primary key جدول را ندارد. برای رفع این مشکل تنها کافی است ویژگی Key را به این خاصیت اعمال کنیم:
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace EF_Sample01.Models
{
public class Blog
{
[Key]
public int MyTableKey { set; get; }
همچنین تعدادی ویژگی دیگر مانند MaxLength و Required را نیز میتوان بر روی خواص کلاس اعمال کرد:
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace EF_Sample01.Models
{
public class Blog
{
[Key]
public int MyTableKey { set; get; }
[MaxLength(100)]
public string Title { set; get; }
[Required]
public string AuthorName { set; get; }
public IList<Post> Posts { set; get; }
}
}
این ویژگیها دو مقصود مهم را برآورده میسازند:
الف) بر روی ساختار بانک اطلاعاتی تشکیل شده تاثیر دارند:
CREATE TABLE [dbo].[Blogs](
[MyTableKey] [int] IDENTITY(1,1) NOT NULL,
[Title] [nvarchar](100) NULL,
[AuthorName] [nvarchar](max) NOT NULL,
CONSTRAINT [PK_Blogs] PRIMARY KEY CLUSTERED
(
[MyTableKey] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF,
IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
همانطور که ملاحظه میکنید در اینجا طول فیلد Title به 100 تنظیم شده است و همچنین فیلد AuthorName اینبار NOT NULL است. به علاوه primary key نیز بر اساس ویژگی Key اعمالی تعیین شده است.
البته برای اجرای کدهای تغییر کرده مدل، فعلا بانک اطلاعاتی قبلی را دستی میتوان حذف کرد تا بتوان به ساختار جدید رسید. در مورد جزئیات مبحث DB Migration در قسمتهای بعدی مفصلا بحث خواهد شد.
ب) اعتبار سنجی اطلاعات پیش از ارسال کوئری به بانک اطلاعاتی
برای مثال اگر در حین تعریف وهلهای از کلاس Blog، خاصیت AuthorName مقدار دهی نگردد، پیش از اینکه رفت و برگشتی به بانک اطلاعاتی صورت گیرد، یک validation error را دریافت خواهیم کرد. یا برای مثال اگر طول اطلاعات خاصیت Title بیش از 100 حرف باشد نیز مجددا در حین ثبت اطلاعات، یک استثنای اعتبار سنجی را مشاهده خواهیم کرد. البته امکان تعریف پیغامهای خطای سفارشی نیز وجود دارد. برای این حالت تنها کافی است پارامتر ErrorMessage این ویژگیها را مقدار دهی کرد. برای مثال:
[Required(ErrorMessage = "لطفا نام نویسنده را مشخص نمائید")]
public string AuthorName { set; get; }
نکتهی مهمی که در اینجا وجود دارد، وجود یک اکوسیستم هماهنگ و سازگار است. این نوع اعتبار سنجی هم با EF Code first هماهنگ است و هم برای مثال در ASP.NET MVC به صورت خودکار جهت اعتبار سنجی سمت سرور و کلاینت یک مدل میتواند مورد استفاده قرار گیرد و مفاهیم و روشهای مورد استفاده در آن نیز یکی است.
تنظیمات EF Code first به کمک Fluent API
اگر علاقمند به استفاده از متادیتا، جهت تعریف قیود و ویژگیهای خواص کلاسهای مدل خود نیستید، روش دیگری نیز در EF Code first به نام Fluent API تدارک دیده شده است. در اینجا امکان تعریف همان ویژگیها توسط کدنویسی نیز وجود دارد، به علاوه اعمال قیود دیگری که توسط متادیتای مهیا قابل تعریف نیستند.
محل تعریف این قیود، کلاس Context که از کلاس DbContext مشتق شده است، میباشد و در اینجا، کار با تحریف متد OnModelCreating شروع میشود:
using System.Data.Entity;
using EF_Sample01.Models;
namespace EF_Sample01
{
public class Context : DbContext
{
public DbSet<Blog> Blogs { set; get; }
public DbSet<Post> Posts { set; get; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>().HasKey(x => x.MyTableKey);
modelBuilder.Entity<Blog>().Property(x => x.Title).HasMaxLength(100);
modelBuilder.Entity<Blog>().Property(x => x.AuthorName).IsRequired();
base.OnModelCreating(modelBuilder);
}
}
}
به کمک پارامتر modelBuilder، امکان دسترسی به متدهای تنظیم کننده ویژگیهای خواص یک مدل یا موجودیت وجود دارد. در اینجا چون میتوان متدها را به صورت یک زنجیره به هم متصل کرد و همچنین حاصل نهایی شبیه به جمله بندی انگلیسی است، به آن Fluent API یا API روان نیز گفته میشود.
البته در این حالت امکان تعریف ErrorMessage وجود ندارد و برای این منظور باید از همان data annotations استفاده کرد.
نحوه مدیریت صحیح تعاریف نگاشتها به کمک Fluent API
OnModelCreating محل مناسبی جهت تعریف حجم انبوهی از تنظیمات کلاسهای مختلف مدلهای برنامه نیست. در حد سه چهار سطر مشکلی ندارد اما اگر بیشتر شد بهتر است از روش زیر استفاده شود:
using System.Data.Entity;
using EF_Sample01.Models;
using System.Data.Entity.ModelConfiguration;
namespace EF_Sample01
{
public class BlogConfig : EntityTypeConfiguration<Blog>
{
public BlogConfig()
{
this.Property(x => x.Id).HasColumnName("MyTableKey");
this.Property(x => x.RowVersion).HasColumnType("Timestamp");
}
}
با ارث بری از کلاس EntityTypeConfiguration، میتوان به ازای هر کلاس مدل، تنظیمات را جداگانه انجام داد. به این ترتیب اصل SRP یا Single responsibility principle نقض نخواهد شد. سپس برای استفاده از این کلاسهای Config تک مسئولیتی به نحو زیر میتوان اقدام کرد:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new BlogConfig());
نحوه تنظیمات ابتدایی نگاشت کلاسها به بانک اطلاعاتی در EF Code first
الزامی ندارد که EF Code first حتما با یک بانک اطلاعاتی از نو تهیه شده بر اساس پیش فرضهای آن کار کند. در اینجا میتوان از بانکهای اطلاعاتی موجود نیز استفاده کرد. اما در این حالت نیاز خواهد بود تا مثلا نام جدولی خاص با کلاسی مفروض در برنامه، یا نام فیلدی خاص که مطابق استانداردهای نامگذاری خواص در سی شارپ تعریف نشده، با خاصیتی در یک کلاس تطابق داده شوند. برای مثال اینبار تعاریف کلاس Blog را به نحو زیر تغییر دهید:
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace EF_Sample01.Models
{
[Table("tblBlogs")]
public class Blog
{
[Column("MyTableKey")]
public int Id { set; get; }
[MaxLength(100)]
public string Title { set; get; }
[Required(ErrorMessage = "لطفا نام نویسنده را مشخص نمائید")]
public string AuthorName { set; get; }
public IList<Post> Posts { set; get; }
[Timestamp]
public byte[] RowVersion { set; get; }
}
}
در اینجا فرض بر این است که نام جدول متناظر با کلاس Blog در بانک اطلاعاتی مثلا tblBlogs است و نام خاصیت Id در بانک اطلاعاتی مساوی فیلدی است به نام MyTableKey. چون نام خاصیت را مجددا به Id تغییر دادهایم، دیگر ضرورتی به ذکر ویژگی Key وجود نداشته است. برای تعریف این دو از ویژگیهای Table و Column جهت سفارشی سازی نامهای خواص و کلاس استفاده شده است.
یا اگر در کلاس خود خاصیتی محاسبه شده بر اساس سایر خواص، تعریف شده است و قصد نداریم آنرا به فیلدی در بانک اطلاعاتی نگاشت کنیم، میتوان از ویژگی NotMapped برای مزین سازی و تعریف آن کمک گرفت.
به علاوه اگر از نام پیش فرض کلید خارجی تشکیل شده خرسند نیستید میتوان به کمک ویژگی ForeignKey، نسبت به تعریف مقداری جدید مطابق تعاریف یک بانک اطلاعاتی موجود، اقدام کرد.
همچنین خاصیت دیگری به نام RowVersion در اینجا اضافه شده که با ویژگی TimeStamp مزین گردیده است. از این خاصیت ویژه برای بررسی مسایل همزمانی ثبت اطلاعات در EF استفاده میشود. به علاوه بانک اطلاعاتی میتواند به صورت خودکار آنرا در حین ثبت مقدار دهی کند.
تمام این تغییرات را به کمک Fluent API نیز میتوان انجام داد:
modelBuilder.Entity<Blog>().ToTable("tblBlogs");
modelBuilder.Entity<Blog>().Property(x => x.Id).HasColumnName("MyTableKey");
modelBuilder.Entity<Blog>().Property(x => x.RowVersion).HasColumnType("Timestamp");
تبدیل پروژههای قدیمی EF به کلاسهای EF Code first به صورت خودکار
روش متداول کار با EF از روز اول آن، مهندسی معکوس خودکار اطلاعات یک بانک اطلاعاتی و تبدیل آن به یک فایل EDMX بوده است. هنوز هم میتوان از این روش در اینجا نیز بهره جست. برای مثال اگر قصد دارید یک پروژه قدیمی را تبدیل به نمونه جدید Code first کنید، یا یک بانک اطلاعاتی موجود را مهندسی معکوس کنید، بر روی پروژه در Solution explorer کلیک راست کرده و گزینه Add|New Item را انتخاب کنید. سپس از صفحه ظاهر شده، ADO.NET Entity data model را انتخاب کرده و در ادامه گزینه «Generate from database» را انتخاب کنید. این روال مرسوم کار با EF Database first است.
پس از اتمام کار به entity data model designer مراجعه کرده و بر روی صفحه کلیک راست نمائید. از منوی ظاهر شده گزینه «Add code generation item» را انتخاب کنید. سپس در صفحه باز شده از لیست قالبهای موجود، گزینه «ADO.NET DbContext Generator» را انتخاب نمائید. این گزینه به صورت خودکار اطلاعات فایل EDMX قدیمی یا موجود شما را تبدیل به کلاسهای مدل Code first معادل به همراه کلاس DbContext معرف آنها خواهد کرد.
روش دیگری نیز برای انجام اینکار وجود دارد. نیاز است افزونهی به نام Entity Framework Power Tools را دریافت کنید. پس از نصب، از منوی Entity Framework آن گزینهی «Reverse Engineer Code First» را انتخاب نمائید. در اینجا میتوان مشخصات اتصال به بانک اطلاعاتی را تعریف و سپس نسبت به تولید خودکار کدهای مدلها و DbContext مرتبط اقدام کرد.
استراتژیهای مقدماتی تشکیل بانک اطلاعاتی در EF Code first
اگر مثال این سری را دنبال کرده باشید، مشاهده کردهاید که با اولین بار اجرای برنامه، یک بانک اطلاعاتی پیش فرض نیز تولید خواهد شد. یا اگر تعاریف ویژگیهای یک فیلد را تغییر دادیم، نیاز است تا بانک اطلاعاتی را دستی drop کرده و اجازه دهیم تا بانک اطلاعاتی جدیدی بر اساس تعاریف جدید مدلها تشکیل شود که ... هیچکدام از اینها بهینه نیستند.
در اینجا دو استراتژی مقدماتی را در حین آغاز یک برنامه میتوان تعریف کرد:
System.Data.Entity.Database.SetInitializer(new DropCreateDatabaseIfModelChanges<Context>());
// or
System.Data.Entity.Database.SetInitializer(new DropCreateDatabaseAlways<Context>());
میتوان بانک اطلاعاتی را در صورت تغییر اطلاعات یک مدل به صورت خودکار drop کرده و نسبت به ایجاد نمونهای جدید اقدام کرد (DropCreateDatabaseIfModelChanges)؛ یا در حین آزمایش برنامه همیشه (DropCreateDatabaseAlways) با شروع برنامه، ابتدا باید بانک اطلاعاتی drop شده و سپس نمونه جدیدی تولید گردد.
محل فراخوانی این دستور هم باید در نقطه آغازین برنامه، پیش از وهله سازی اولین DbContext باشد. مثلا در برنامههای وب در متد Application_Start فایل global.asax.cs یا در برنامههای WPF در متد سازنده کلاس App میتوان بانک اطلاعاتی را آغاز نمود.
البته الزامی به استفاده از کلاسهای DropCreateDatabaseIfModelChanges یا DropCreateDatabaseAlways وجود ندارد. میتوان با پیاده سازی اینترفیس IDatabaseInitializer از نوع کلاس Context تعریف شده در برنامه، همان عملیات را شبیه سازی کرد یا سفارشی نمود:
public class MyInitializer : IDatabaseInitializer<Context>
{
public void InitializeDatabase(Context context)
{
if (context.Database.Exists() ||
context.Database.CompatibleWithModel(throwIfNoMetadata: false))
context.Database.Delete();
context.Database.Create();
}
}
سپس برای استفاده از این کلاس در ابتدای برنامه، خواهیم داشت:
System.Data.Entity.Database.SetInitializer(new MyInitializer());
نکته:
اگر از یک بانک اطلاعاتی موجود استفاده میکنید (محیط کاری) و نیازی به پیش فرضهای EF Code first ندارید و همچنین این بانک اطلاعاتی نیز نباید drop شود یا تغییر کند، میتوانید تمام این پیش فرضها را با دستور زیر غیرفعال کنید:
Database.SetInitializer<Context>(null);
بدیهی است این دستور نیز باید پیش از ایجاد اولین وهله از شیء DbContext فراخوانی شود.
همچنین باید درنظر داشت که در آخرین نگارشهای پایدار EF Code first، این موارد بهبود یافتهاند و مبحثی تحت عنوان DB Migration ایجاد شده است تا نیازی نباشد هربار بانک اطلاعاتی drop شود و تمام اطلاعات از دست برود. میتوان صرفا تغییرات کلاسها را به بانک اطلاعاتی اعمال کرد که به صورت جداگانه، در قسمتی مجزا بررسی خواهد شد. به این ترتیب دیگر نیازی به drop بانک اطلاعاتی نخواهد بود. به صورت پیش فرض در صورت از دست رفتن اطلاعات یک استثناء را سبب خواهد شد (که توسط برنامه نویس قابل تنظیم است) و در حالت خودکار یا دستی با تنظیمات ویژه قابل اعمال است.
تنظیم استراتژیهای آغاز بانک اطلاعاتی در فایل کانفیگ برنامه
الزامی ندارد که حتما متد Database.SetInitializer را دستی فراخوانی کنیم. با اندکی تنظیم فایلهای app.config و یا web.config نیز میتوان نوع استراتژی مورد استفاده را تعیین کرد:
<appSettings>
<add key="DatabaseInitializerForType MyNamespace.MyDbContextClass, MyAssembly"
value="MyNamespace.MyInitializerClass, MyAssembly" />
</appSettings>
<appSettings>
<add key="DatabaseInitializerForType MyNamespace.MyDbContextClass, MyAssembly"
value="Disabled" />
</appSettings>
یکی از دو حالت فوق باید در قسمت appSettings فایل کانفیگ برنامه تنظیم شود. حالت دوم برای غیرفعال کردن پروسه آغاز بانک اطلاعاتی و اعمال تغییرات به آن، بکار میرود.
برای نمونه در مثال جاری، جهت استفاده از کلاس MyInitializer فوق، میتوان از تنظیم زیر نیز استفاده کرد:
<appSettings>
<add key="DatabaseInitializerForType EF_Sample01.Context, EF_Sample01"
value="EF_Sample01.MyInitializer, EF_Sample01" />
</appSettings>
اجرای کدهای ویژه در حین تشکیل یک بانک اطلاعاتی جدید
امکان سفارشی سازی این آغاز کنندههای پیش فرض نیز وجود دارد. برای مثال:
public class MyCustomInitializer : DropCreateDatabaseIfModelChanges<Context>
{
protected override void Seed(Context context)
{
context.Blogs.Add(new Blog { AuthorName = "Vahid", Title = ".NET Tips" });
context.Database.ExecuteSqlCommand("CREATE INDEX IX_title ON tblBlogs (title)");
base.Seed(context);
}
}
در اینجا با ارث بری از کلاس DropCreateDatabaseIfModelChanges یک آغاز کننده سفارشی را تعریف کردهایم. سپس با تحریف متد Seed آن میتوان در حین آغاز یک بانک اطلاعاتی، تعدادی رکورد پیش فرض را به آن افزود. کار ذخیره سازی نهایی در متد base.Seed انجام میشود.
برای استفاده از آن اینبار در حین فراخوانی متد System.Data.Entity.Database.SetInitializer، از کلاس MyCustomInitializer استفاده خواهیم کرد.
و یا توسط متد context.Database.ExecuteSqlCommand میتوان دستورات SQL را مستقیما در اینجا اجرا کرد. عموما دستوراتی در اینجا مدنظر هستند که توسط ORMها پشتیبانی نمیشوند. برای مثال تغییر collation یک ستون یا افزودن یک ایندکس و مواردی از این دست.
سطح دسترسی مورد نیاز جهت فراخوانی متد Database.SetInitializer
استفاده از متدهای آغاز کننده بانک اطلاعاتی نیاز به سطح دسترسی بر روی بانک اطلاعاتی master را در SQL Server دارند (زیرا با انجام کوئری بر روی این بانک اطلاعاتی مشخص میشود، آیا بانک اطلاعاتی مورد نظر پیشتر تعریف شده است یا خیر). البته این مورد حین کار با SQL Server CE شاید اهمیتی نداشته باشد. بنابراین اگر کاربری که با آن به بانک اطلاعاتی متصل میشویم سطح دسترسی پایینی دارد نیاز است Persist Security Info=True را به رشته اتصالی اضافه کرد. البته این مورد را پس از انجام تغییرات بر روی بانک اطلاعاتی جهت امنیت بیشتر حذف کنید (یا به عبارتی در محیط کاری Persist Security Info=False باید باشد).
Server=(local);Database=yourDatabase;User ID=yourDBUser;Password=yourDBPassword;Trusted_Connection=False;Persist Security Info=True
تعیین Schema و کاربر فراخوان دستورات SQL
در EF Code first به صورت پیش فرض همه چیز بر مبنای کاربری با دسترسی مدیریتی یا dbo schema در اس کیوال سرور تنظیم شده است. اما اگر کاربر خاصی برای کار با دیتابیس تعریف گردد که در هاستهای اشتراکی بسیار مرسوم است، دیگر از دسترسی مدیریتی dbo خبری نخواهد بود. اینبار نام جداول ما بجای dbo.tableName مثلا someUser.tableName میباشند و عدم دقت به این نکته، اجرای برنامه را غیرممکن میسازد.
برای تغییر و تعیین صریح کاربر متصل شده به بانک اطلاعاتی اگر از متادیتا استفاده میکنید، روش زیر باید بکارگرفته شود:
[Table("tblBlogs", Schema="someUser")]
public class Blog
و یا در حالت بکارگیری Fluent API به نحو زیر قابل تنظیم است:
modelBuilder.Entity<Blog>().ToTable("tblBlogs", schemaName:"someUser");
تا اینجا اگر به کدهای کامپوننت فرم لاگینی که ایجاد کردیم دقت کنید، تبدیل شدهاست به محلی برای انباشت حجم قابل توجهی از کد. به این ترتیب اگر قرار باشد فرمهای جدیدی را تعریف کنیم، نیاز خواهد بود قسمتهای عمدهای از این کدها را در هر جایی تکرار کنیم. بنابراین جهت کاهش مسئولیتهای آن، نیاز است بازسازی کد (refactoring) قابل ملاحظهای بر روی آن صورت گیرد.
تشخیص قسمتهایی که قابلیت استخراج از کامپوننت لاگین را دارند
قصد داریم قسمتهایی از کامپوننت لاگین فعلی را استخراج کرده و آنها را درون یک کامپوننت با قابلیت استفادهی مجدد قرار دهیم:
- خاصیت state: میخواهیم تمام فرمهایی را که تعریف میکنیم، دارای خاصیت errors باشند. بنابراین این خاصیت قابلیت استفادهی مجدد را دارد.
- خاصیت schema: قابلیت استفادهی مجدد را ندارد و مختص فرم لاگین تعریف شدهاست. این منطق از هر فرمی با فرم دیگر، متفاوت است.
- متد validate: در این متد، هیچ نوع وابستگی از آن به مفهوم لاگین وجود ندارد و کاملا قابلیت استفادهی مجدد را دارد. تنها this.state.account آن وابستهی به کامپوننت لاگین است و بدیهی است شیء account را در سایر فرمها نخواهیم داشت و ممکن است نام آن movie یا customer باشد. بنابراین قاعدهای را در اینجا تعریف میکنیم، بر این مبنا که از این پس، تمام فرمهای ما دارای خاصیتی به نام data خواهند بود که بیانگر اطلاعات آن فرم میباشد. با این تغییر، برای مثال در فرم لاگین، data به شیء account تنظیم میشود و در فرمی دیگر به شیء customer.
- متد validateProperty: همانند متد validate است و کاملا قابلیت استفادهی مجدد را دارد.
- متد handleSubmit: قسمت ابتدایی این متد که شامل غیرفعال کردن post back به سرور و اعتبارسنجی فرم است، قابلیت استفادهی مجدد را دارد. اما قسمت دوم آن مانند ارسال فرم به سرور و یا هر عملیات دیگری، از یک فرم به فرم دیگر میتواند متفاوت باشد.
- متد handleChange: این متد نیز قابلیت استفادهی مجدد را دارد؛ چون میخواهیم در تمام فرمها در حین تایپ اطلاعات، کار اعتبارسنجی ورودیها صورت گیرد. این متد نیز به this.state.account وابستهاست که قاعدهی تعریف خاصیت data در state، میتواند این مشکل را حل کند.
- متد رندر: طراحی آن کاملا وابستهاست به نوع فرمی که مدنظر میباشد؛ اما دکمهی submit آن خیر. بجز برچسب دکمهی submit، مابقی قسمتهای آن مانند کلاسهای CSS و منطق فعالسازی و غیرفعالسازی آن، قابلیت استفادهی مجدد را دارند.
بنابراین در ادامه کار، refactoring کامپوننت فرم لاگین را برای استخراج قسمتهای با قابلیت استفادهی مجدد آن، انجام خواهیم داد.
تبدیل قسمتهای با قابلیت استفادهی مجدد کامپوننت لاگین، به یک کامپوننت عمومی
ابتدا کامپوننت عمومی Form را که قابلیت استفادهی مجدد دارد، در فایل جدید src\components\common\form.jsx تعریف کرده و سپس کامپوننت فرم لاگین را طوری تغییر میدهیم که از آن، بجای کلاس پیشفرض Component، ارث بری کند. به این ترتیب تمام متدهای تعریف شدهی در این کامپوننت با قابلیت استفادهی مجدد، در کامپوننتهای مشتق شدهی از آن، در دسترس خواهند بود.
1- در ادامه همانطور که عنوان شد، خاصیت state فرمها باید دارای شیء data و شیء errors باشند تا توسط آنها بتوان اطلاعات کل فرم و اطلاعات خطاهای اعتبارسنجی را ذخیره کرد:
با این تغییر، به فرم login بازگشته و خاصیت account موجود در state آنرا به data تغییر نام میدهیم. برای اینکار بهتر است دکمهی F2 را بر روی نام انتخاب شدهی account در VSCode فشار دهید تا تکست باکس تغییر نام آن ظاهر شود. مزیت کار با این ابزار refactoring توکار، اصلاح خودکار تمام ارجاعات به account قبلی، با این نام جدید است. همچنین نام تمام خواصی و متغیرهایی را هم که به account تنظیم کرده بودیم، به data تغییر میدهیم تا کار به روز رسانی state بر روی data صورت گیرد و نه account قبلی. در این حالت شاید استفاده از امکانات replace کلی ادیتور، بهتر از استفاده از ویژگی F2 باشد.
2- در ادامه، کاری با خاصیت schema تعریف شدهی در کامپوننت لاگین نداریم؛ چون کاملا مختص به آن است. اما متدهای validate و validateProperty آنرا طور کامل cut کرده و به کامپوننت Form، منتقل میکنیم. با این انتقال، چون این متدها از کتابخانهی Joi استفاده میکنند، باید import آنرا نیز به ابتدای ماژول جدید فرم، اضافه کرد:
3- سپس متد رندر کامپوننت Form را کاملا حذف میکنیم؛ چون این کامپوننت قرار نیست چیزی را رندر کند.
4- در قسمت دوم متد handleSubmit، برای مثال قرار است ارسال دادهها به سرور صورت گیرد. به همین جهت آنرا تبدیل به متدی مانند doSubmit کرده و سپس کل متد handleSubmit را نیز به کامپوننت Form منتقل میکنیم.
5- متد handleChange را نیز از کامپوننت فرم لاگین cut کرده و به کامپوننت Form منتقل میکنیم.
6- پس از این نقل و انتقالات، کار ارث بری از کامپوننت فرم را در کامپوننت فرم لاگین انجام میدهیم:
اکنون اگر برنامه را ذخیره کرده و اجرا کنیم، همانند قبل و آنچیزی که در انتهای قسمت قبلی به آن رسیدیم، بدون مشکل کار میکند؛ اما کدهای کامپوننت فرم لاگین به شدت کاهش یافته و ساده شدهاست. همچنین اگر دفعهی بعد، نیاز به ایجاد فرمی وجود داشت، دیگر نیازی به تکرار این حجم از کد نیست. تنها نیاز خواهیم داشت تا state را تعریف کرده و schema را اضافه کنیم و همچنین نیاز است متد doSumbit را پیاده سازی کنیم تا مشخص شود پس از تکمیل فرم و اعتبارسنجی آن، قرار است چه رخدادی واقع شود.
کدهای کامل کامپوننت فرم را از پیوست انتهای بحث میتوانید دریافت کنید؛ البته تمام متدهای آنرا در قسمت قبل تکمیل کرده بودیم و در اینجا صرفا یکسری cut/paste صورت گرفتند.
ساده کردن و بهبود پیاده سازی متد رندر
1- در متد رندر فعلی کامپوننت فرم لاگین، اگر به دکمهی submit آن دقت کنیم، بجز برچسب آن، مابقی قسمتهای آن در تمام فرمهای دیگری که تعریف خواهیم کرد، یکسان خواهند بود. به همین جهت این قسمت را میتوان تبدیل به یک متد کمکی در کلاس Form کرد:
سپس در متد رندر کامپوننت فرم لاگین، تنها کافی است بجای المان button قبلی، از متد فوق استفاده کنیم:
2- در قسمتهای قبل، برچسب، فیلدهای ورودی و تگها و کلاسهای بوت استرپی را به کامپوننت Input منتقل کردیم، تا به یک فرم سادهتر و با قابلیت نگهداری بالاتری برسیم. هرچند این هدف حاصل شده، اما باز هم تعاریف المانهای Input قرارگرفتهی در متد رندر کامپوننت لاگین، دارای الگوی تکراری ذکر یک خاصیت مشخص، تعریف رویدادگردانهای مشخص و اطلاعات اعتبارسنجی کاملا مشخصی هستند. به همین جهت تعریف المان Input را هم مانند متد renderButton فوق میتوان به کلاس پایه Form انتقال داد:
همانطور که مشاهده میکنید، با استفاده از [] و دسترسی پویای به خواص اشیاء، میتوان رندر المان Input را تبدیل به متدی با قابلیت نگهداری بهتر کرد و از تکرار ویژگیهای name ، label ، value ، onChange و error به ازای هر فیلد مورد نیاز، پرهیز کرد. اکنون با این تغییر، متد رندر کامپوننت فرم لاگین به صورت زیر خلاصه میشود که بسیار بهتر است از تعریف تعداد قابل ملاحظهای div و کلاس بوت استرپی، تعریف المانها، اتصال تک تک آنها به خواص تعریف شده، اتصال آنها به رویداد گردانها و همچنین به اعتبارسنجها:
3- تا اینجا فرم لاگین تعریف شده، یک مشکل کوچک را دارد: فیلد پسورد آن، از نوع text تعریف شده و اطلاعات وارد شده را همانند یک textbox معمولی نمایش میدهد. برای رفع این مشکل، پارامتر type را با یک مقدار پیشفرض پر استفاده، تعریف کرده و به المان Input اعمال میکنیم:
سپس این type را در قسمتی که المان مرتبط را رندر میکنیم، با password مقدار دهی خواهیم کرد:
نیازی به ذکر type، در اولین renderInput ذکر شده، نیست؛ چون مقدار این پارامتر را ازمقدار پیشفرض text، دریافت میکند.
البته این تغییرات تا به اینجا کار نخواهند کرد؛ چون هنوز کلاس المان Input را جهت پذیرش ویژگی جدید type، ویرایش نکردهایم. بنابراین به فایل src\components\common\input.jsx مراجعه کرده و type را به آن اعمال میکنیم:
اکنون اگر تغییرات را ذخیره کرده و به مرورگر مراجعه کنیم، فیلد کلمهی عبور، دیگر حروف وارد شده را نمایش نمیدهد و بر اساس نوع استاندارد password، عمل میکند.
4- مشکل! آیا باید به ازای هر ویژگی جدیدی که قرار است به این input اعمال کنیم، مانند type در اینجا، نیاز است یک پارامتر جدید را تعریف و سپس از آن استفاده کرد؟ در این حالت اینترفیس این کامپوننت از کنترل خارج میشود و همچنین هربار باید آنرا ویرایش کرد و تغییر داد. به علاوه اگر به تعریف این input دقت کنیم، نام 4 ویژگی آن، با مقادیری که دریافت میکنند، هم نام هستند (ویژگی value با مقدار value و ...):
در کامپوننت جاری، منهای پارامترهایی که نام ویژگیهای تعریف شده، با نام آن پارامترها در تمام قسمتهای کامپوننت (نه فقط المان input)، یکی نیستند (name، label و error)، مابقی را میتوان توسط یک «rest operator»، به این متد ارسال کرد:
بنابراین منهای name، label و error که در قسمتهای دیگر کامپوننت استفاده میشوند، مابقی پارامترهای این کامپوننت تابعی را حذف کرده و با یک rest operator، دریافت میکنیم. سپس آنها را به کمک یک spread operator، در المان input، گسترده و درج میکنیم. شبیه به اینکار را در قسمت 15 و بخش «ارسال props سفارشی در حین مسیریابی به کامپوننتها» آن انجام داده بودیم. با کمک عملگرهای rest و spread، به سادگی میتوان هرنوع ویژگی جدیدی را که برای کار با المان input نیاز داریم، به کامپوننت جاری ارسال کرد؛ بدون اینکه نیازی باشد هربار تعریف پارامترهای آن را تغییر دهیم. پارامتر rest تعریف شده، یعنی هر خاصیت دیگری را بجز سه خاصیت name، label و error، به صورت خودکار به این کامپوننت تابعی ارسال کن.
با این تغییر در کامپوننت Input، سایر قسمتهای برنامه نیازی به تغییر ندارند. برای مثال در متد renderInput، سه ویژگی name، label و error تبدیل به سه پارامتر دریافتی از props میشوند (ترتیب ذکر آنها اهمیتی ندارد). مابقی ویژگیهای تعریف شدهی در آن، به صورت خودکار در قسمت input {...rest} درج خواهند شد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-20.zip
تشخیص قسمتهایی که قابلیت استخراج از کامپوننت لاگین را دارند
قصد داریم قسمتهایی از کامپوننت لاگین فعلی را استخراج کرده و آنها را درون یک کامپوننت با قابلیت استفادهی مجدد قرار دهیم:
- خاصیت state: میخواهیم تمام فرمهایی را که تعریف میکنیم، دارای خاصیت errors باشند. بنابراین این خاصیت قابلیت استفادهی مجدد را دارد.
- خاصیت schema: قابلیت استفادهی مجدد را ندارد و مختص فرم لاگین تعریف شدهاست. این منطق از هر فرمی با فرم دیگر، متفاوت است.
- متد validate: در این متد، هیچ نوع وابستگی از آن به مفهوم لاگین وجود ندارد و کاملا قابلیت استفادهی مجدد را دارد. تنها this.state.account آن وابستهی به کامپوننت لاگین است و بدیهی است شیء account را در سایر فرمها نخواهیم داشت و ممکن است نام آن movie یا customer باشد. بنابراین قاعدهای را در اینجا تعریف میکنیم، بر این مبنا که از این پس، تمام فرمهای ما دارای خاصیتی به نام data خواهند بود که بیانگر اطلاعات آن فرم میباشد. با این تغییر، برای مثال در فرم لاگین، data به شیء account تنظیم میشود و در فرمی دیگر به شیء customer.
- متد validateProperty: همانند متد validate است و کاملا قابلیت استفادهی مجدد را دارد.
- متد handleSubmit: قسمت ابتدایی این متد که شامل غیرفعال کردن post back به سرور و اعتبارسنجی فرم است، قابلیت استفادهی مجدد را دارد. اما قسمت دوم آن مانند ارسال فرم به سرور و یا هر عملیات دیگری، از یک فرم به فرم دیگر میتواند متفاوت باشد.
- متد handleChange: این متد نیز قابلیت استفادهی مجدد را دارد؛ چون میخواهیم در تمام فرمها در حین تایپ اطلاعات، کار اعتبارسنجی ورودیها صورت گیرد. این متد نیز به this.state.account وابستهاست که قاعدهی تعریف خاصیت data در state، میتواند این مشکل را حل کند.
- متد رندر: طراحی آن کاملا وابستهاست به نوع فرمی که مدنظر میباشد؛ اما دکمهی submit آن خیر. بجز برچسب دکمهی submit، مابقی قسمتهای آن مانند کلاسهای CSS و منطق فعالسازی و غیرفعالسازی آن، قابلیت استفادهی مجدد را دارند.
بنابراین در ادامه کار، refactoring کامپوننت فرم لاگین را برای استخراج قسمتهای با قابلیت استفادهی مجدد آن، انجام خواهیم داد.
تبدیل قسمتهای با قابلیت استفادهی مجدد کامپوننت لاگین، به یک کامپوننت عمومی
ابتدا کامپوننت عمومی Form را که قابلیت استفادهی مجدد دارد، در فایل جدید src\components\common\form.jsx تعریف کرده و سپس کامپوننت فرم لاگین را طوری تغییر میدهیم که از آن، بجای کلاس پیشفرض Component، ارث بری کند. به این ترتیب تمام متدهای تعریف شدهی در این کامپوننت با قابلیت استفادهی مجدد، در کامپوننتهای مشتق شدهی از آن، در دسترس خواهند بود.
1- در ادامه همانطور که عنوان شد، خاصیت state فرمها باید دارای شیء data و شیء errors باشند تا توسط آنها بتوان اطلاعات کل فرم و اطلاعات خطاهای اعتبارسنجی را ذخیره کرد:
import React, { Component } from "react"; class Form extends Component { state = { data:{}, errors:{} }
2- در ادامه، کاری با خاصیت schema تعریف شدهی در کامپوننت لاگین نداریم؛ چون کاملا مختص به آن است. اما متدهای validate و validateProperty آنرا طور کامل cut کرده و به کامپوننت Form، منتقل میکنیم. با این انتقال، چون این متدها از کتابخانهی Joi استفاده میکنند، باید import آنرا نیز به ابتدای ماژول جدید فرم، اضافه کرد:
import Joi from "@hapi/joi";
3- سپس متد رندر کامپوننت Form را کاملا حذف میکنیم؛ چون این کامپوننت قرار نیست چیزی را رندر کند.
4- در قسمت دوم متد handleSubmit، برای مثال قرار است ارسال دادهها به سرور صورت گیرد. به همین جهت آنرا تبدیل به متدی مانند doSubmit کرده و سپس کل متد handleSubmit را نیز به کامپوننت Form منتقل میکنیم.
doSubmit = () => { // call the server console.log("Submitted!"); };
5- متد handleChange را نیز از کامپوننت فرم لاگین cut کرده و به کامپوننت Form منتقل میکنیم.
6- پس از این نقل و انتقالات، کار ارث بری از کامپوننت فرم را در کامپوننت فرم لاگین انجام میدهیم:
import Form from "./common/form"; // ... class LoginForm extends Form {
اکنون اگر برنامه را ذخیره کرده و اجرا کنیم، همانند قبل و آنچیزی که در انتهای قسمت قبلی به آن رسیدیم، بدون مشکل کار میکند؛ اما کدهای کامپوننت فرم لاگین به شدت کاهش یافته و ساده شدهاست. همچنین اگر دفعهی بعد، نیاز به ایجاد فرمی وجود داشت، دیگر نیازی به تکرار این حجم از کد نیست. تنها نیاز خواهیم داشت تا state را تعریف کرده و schema را اضافه کنیم و همچنین نیاز است متد doSumbit را پیاده سازی کنیم تا مشخص شود پس از تکمیل فرم و اعتبارسنجی آن، قرار است چه رخدادی واقع شود.
کدهای کامل کامپوننت فرم را از پیوست انتهای بحث میتوانید دریافت کنید؛ البته تمام متدهای آنرا در قسمت قبل تکمیل کرده بودیم و در اینجا صرفا یکسری cut/paste صورت گرفتند.
ساده کردن و بهبود پیاده سازی متد رندر
1- در متد رندر فعلی کامپوننت فرم لاگین، اگر به دکمهی submit آن دقت کنیم، بجز برچسب آن، مابقی قسمتهای آن در تمام فرمهای دیگری که تعریف خواهیم کرد، یکسان خواهند بود. به همین جهت این قسمت را میتوان تبدیل به یک متد کمکی در کلاس Form کرد:
renderButton(label) { return ( <button disabled={this.validate()} className="btn btn-primary"> {label} </button> ); }
{this.renderButton("Login")}
2- در قسمتهای قبل، برچسب، فیلدهای ورودی و تگها و کلاسهای بوت استرپی را به کامپوننت Input منتقل کردیم، تا به یک فرم سادهتر و با قابلیت نگهداری بالاتری برسیم. هرچند این هدف حاصل شده، اما باز هم تعاریف المانهای Input قرارگرفتهی در متد رندر کامپوننت لاگین، دارای الگوی تکراری ذکر یک خاصیت مشخص، تعریف رویدادگردانهای مشخص و اطلاعات اعتبارسنجی کاملا مشخصی هستند. به همین جهت تعریف المان Input را هم مانند متد renderButton فوق میتوان به کلاس پایه Form انتقال داد:
import Input from "./input"; //... renderInput(name, label) { const { data, errors } = this.state; return ( <Input name={name} label={label} value={data[name]} onChange={this.handleChange} error={errors[name]} /> );
render() { return ( <form onSubmit={this.handleSubmit}> {this.renderInput("username", "Username")} {this.renderInput("password", "Password")} {this.renderButton("Login")} </form> ); }
3- تا اینجا فرم لاگین تعریف شده، یک مشکل کوچک را دارد: فیلد پسورد آن، از نوع text تعریف شده و اطلاعات وارد شده را همانند یک textbox معمولی نمایش میدهد. برای رفع این مشکل، پارامتر type را با یک مقدار پیشفرض پر استفاده، تعریف کرده و به المان Input اعمال میکنیم:
renderInput(name, label, type = "text") { const { data, errors } = this.state; return ( <Input name={name} type={type} label={label} value={data[name]} onChange={this.handleChange} error={errors[name]} /> ); }
سپس این type را در قسمتی که المان مرتبط را رندر میکنیم، با password مقدار دهی خواهیم کرد:
render() { return ( <form onSubmit={this.handleSubmit}> {this.renderInput("username", "Username")} {this.renderInput("password", "Password", "password")} {this.renderButton("Login")} </form> ); }
البته این تغییرات تا به اینجا کار نخواهند کرد؛ چون هنوز کلاس المان Input را جهت پذیرش ویژگی جدید type، ویرایش نکردهایم. بنابراین به فایل src\components\common\input.jsx مراجعه کرده و type را به آن اعمال میکنیم:
import React from "react"; const Input = ({ name, type, label, value, error, onChange }) => { return ( <div className="form-group"> <label htmlFor={name}>{label}</label> <input value={value} onChange={onChange} id={name} name={name} type={type} className="form-control" /> {error && <div className="alert alert-danger">{error}</div>} </div> ); }; export default Input;
4- مشکل! آیا باید به ازای هر ویژگی جدیدی که قرار است به این input اعمال کنیم، مانند type در اینجا، نیاز است یک پارامتر جدید را تعریف و سپس از آن استفاده کرد؟ در این حالت اینترفیس این کامپوننت از کنترل خارج میشود و همچنین هربار باید آنرا ویرایش کرد و تغییر داد. به علاوه اگر به تعریف این input دقت کنیم، نام 4 ویژگی آن، با مقادیری که دریافت میکنند، هم نام هستند (ویژگی value با مقدار value و ...):
<input value={value} name={name} type={type} onChange={onChange} id={name} className="form-control" />
import React from "react"; const Input = ({ name, label, error, ...rest }) => { return ( <div className="form-group"> <label htmlFor={name}>{label}</label> <input {...rest} name={name} id={name} className="form-control" /> {error && <div className="alert alert-danger">{error}</div>} </div> ); }; export default Input;
با این تغییر در کامپوننت Input، سایر قسمتهای برنامه نیازی به تغییر ندارند. برای مثال در متد renderInput، سه ویژگی name، label و error تبدیل به سه پارامتر دریافتی از props میشوند (ترتیب ذکر آنها اهمیتی ندارد). مابقی ویژگیهای تعریف شدهی در آن، به صورت خودکار در قسمت input {...rest} درج خواهند شد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-20.zip
مطالب
ASP.NET MVC #21
آشنایی با تکنیکهای Ajax در ASP.NET MVC
اهمیت آشنایی با Ajax، ارائه تجربه کاربری بهتری از برنامههای وب، به مصرف کنندگان نهایی آن میباشد. به این ترتیب میتوان درخواستهای غیرهمزمانی (asynchronous) را با فرمت XML یا Json به سرور ارسال کرد و سپس نتیجه نهایی را که حجم آن نسبت به یک صفحه کامل بسیار کمتر است، به کاربر ارائه داد. غیرهمزمان بودن درخواستها سبب میشود تا ترد اصلی رابط کاربری برنامه قفل نشده و کاربر در این بین میتواند به سایر امور خود بپردازد. به این ترتیب میتوان برنامههای وبی را که شبیه به برنامههای دسکتاپ هستند تولید نمود؛ کل صفحه مرتبا به سرور ارسال نمیشود، flickering و چشمک زدن صفحه کاهش خواهد یافت (چون نیازی به ترسیم مجدد کل صفحه نخواهد بود و عموما قسمتی جزئی از یک صفحه به روز میشود) یا بدون نیاز به ارسال کل صفحه به سرور، به کاربری خواهیم گفت که آیا اطلاعاتی که وارد کرده است معتبر میباشد یا نه (نمونهای از آن را در قسمت Remote validation اعتبار سنجی اطلاعات ملاحظه نمودید).
مروری بر محتویات پوشه Scripts یک پروژه جدید ASP.NET MVC در ویژوال استودیو
با ایجاد هر پروژه ASP.NET MVC جدیدی در ویژوال استودیو، یک سری اسکریپت هم به صورت خودکار در پوشه Scripts آن اضافه میشوند. تعدادی از این فایلها توسط مایکروسافت پیاده سازی شدهاند. برای مثال:
MicrosoftAjax.debug.js
MicrosoftAjax.js
MicrosoftMvcAjax.debug.js
MicrosoftMvcAjax.js
MicrosoftMvcValidation.debug.js
MicrosoftMvcValidation.js
این فایلها از ASP.NET MVC 3 به بعد، صرفا جهت سازگاری با نگارشهای قبلی قرار دارند و استفاده از آنها اختیاری است. بنابراین با خیال راحت آنها را delete کنید! روش توصیه شده جهت پیاده سازی ویژگیهای Ajax ایی، استفاده از کتابخانههای مرتبط با jQuery میباشد؛ از این جهت که 100ها افزونه برای کار با آن توسط گروه وسیعی از برنامه نویسها در سراسر دنیا تاکنون تهیه شده است. به علاوه فریم ورک jQuery تنها منحصر به اعمال Ajax ایی نیست و از آن جهت دستکاری DOM (document object model) و CSS صفحه نیز میتوان استفاده کرد. همچنین حجم کمی نیز داشته، با انواع و اقسام مرورگرها سازگار است و مرتبا هم به روز میشود.
در این پوشه سه فایل دیگر پایه کتابخانه jQuery نیز قرار دارند:
jquery-xyz-vsdoc.js
jquery-xyz.js
jquery-xyz.min.js
فایل vsdoc برای ارائه نهایی برنامه طراحی نشده است. هدف از آن ارائه Intellisense بهتری از jQuery در VS.NET میباشد. فایلی که باید به کلاینت ارائه شود، فایل min یا فشرده شده آن است. اگر به آن نگاهی بیندازیم به نظر obfuscated مشاهده میشود. علت آن هم حذف فواصل، توضیحات و همچنین کاهش طول متغیرها است تا اندازه فایل نهایی به حداقل خود کاهش پیدا کند. البته این فایل از دیدگاه مفسر جاوا اسکریپت یک مرورگر، فایل بینقصی است!
اگر علاقمند هستید که سورس اصلی jQuery را مطالعه کنید، به فایل jquery-xyz.js مراجعه نمائید.
محل الحاق اسکریپتهای عمومی مورد نیاز برنامه نیز بهتر است در فایل master page یا layout برنامه باشد که به صورت پیش فرض اینکار انجام شده است.
سایر فایلهای اسکریپتی که در این پوشه مشاهده میشوند، یک سری افزونه عمومی یا نوشته شده توسط تیم ASP.NET MVC برفراز jQuery هستند.
به چهار نکته نیز حین استفاده از اسکریپتهای موجود باید دقت داشت:
الف) همیشه از متد Url.Content همانند تعاریفی که در فایل Views\Shared\_Layout.cshtml مشاهده میکنید، برای مشخص سازی مسیر ریشه سایت، استفاده نمائید. به این ترتیب صرفنظر از آدرس جاری صفحه، همواره آدرس صحیح قرارگیری پوشه اسکریپتها در صفحه ذکر خواهد شد.
ب) ترتیب فایلهای js مهم هستند. ابتدا باید کتابخانه اصلی jQuery ذکر شود و سپس افزونههای آنها.
ج) اگر اسکریپتهای jQuery در فایل layout سایت تعریف شدهاند؛ نیازی به تعریف مجدد آنها در Viewهای سایت نیست.
د) اگر View ایی به اسکریپت ویژهای جهت اجرا نیاز دارد، بهتر است آنرا به شکل یک section داخل view تعریف کرد و سپس به کمک متد RenderSection این قسمت را در layout سایت مقدار دهی نمود. مثالی از آنرا در قسمت 20 این سری مشاهده نمودید (افزودن نمایش جمع هر ستون گزارش).
یک نکته
اگر آخرین به روز رسانیهای ASP.NET MVC را نیز نصب کرده باشید، فایلی به نام packages.config به صورت پیش فرض به هر پروژه جدید ASP.NET MVC اضافه میشود. به این ترتیب VS.NET به کمک NuGet این امکان را خواهد یافت تا شما را از آخرین به روز رسانیهای این کتابخانهها مطلع کند.
آشنایی با Ajax Helpers توکار ASP.NET MVC
اگر به تعاریف خواص و متدهای کلاس WebViewPage دقت کنیم:
using System;
namespace System.Web.Mvc
{
public abstract class WebViewPage<TModel> : WebViewPage
{
protected WebViewPage();
public AjaxHelper<TModel> Ajax { get; set; }
public HtmlHelper<TModel> Html { get; set; }
public TModel Model { get; }
public ViewDataDictionary<TModel> ViewData { get; set; }
public override void InitHelpers();
protected override void SetViewData(ViewDataDictionary viewData);
}
}
علاوه بر خاصیت Html که وهلهای از آن امکان دسترسی به Html helpers توکار ASP.NET MVC را در یک View فراهم میکند، خاصیتی به نام Ajax نیز وجود دارد که توسط آن میتوان به تعدادی متد AjaxHelper توکار دسترسی داشت. برای مثال توسط متد Ajax.ActionLink میتوان قسمتی از صفحه را به کمک ویژگیهای Ajax، به روز رسانی کرد.
مثالی در مورد به روز رسانی قسمتی از صفحه به کمک متد Ajax.ActionLink
ابتدا نیاز است فایل Views\Shared\_Layout.cshtml را اندکی ویرایش کرد. برای این منظور سطر الحاق jquery.unobtrusive-ajax.min.js را به فایل layout برنامه اضافه نمائید (اگر این سطر اضافه نشود، متد Ajax.ActionLink همانند یک لینک معمولی رفتار خواهد کرد):
<head>
<title>@ViewBag.Title</title>
<link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
<script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.unobtrusive-ajax.min.js")" type="text/javascript"></script>
</head>
سپس مدل ساده و منبع داده زیر را نیز به پروژه اضافه کنید:
namespace MvcApplication18.Models
{
public class Employee
{
public int Id { set; get; }
public string Name { set; get; }
}
}
using System.Collections.Generic;
namespace MvcApplication18.Models
{
public static class EmployeeDataSource
{
public static IList<Employee> CreateEmployees()
{
var list = new List<Employee>();
for (int i = 0; i < 1000; i++)
{
list.Add(new Employee { Id = i + 1, Name = "name " + i });
}
return list;
}
}
}
در ادامه کنترلر جدیدی را به برنامه با محتوای زیر اضافه کنید:
using System.Linq;
using System.Web.Mvc;
using MvcApplication18.Models;
namespace MvcApplication18.Controllers
{
public class HomeController : Controller
{
[HttpGet]
public ActionResult Index()
{
return View();
}
[HttpPost] //for IE-8
public ActionResult EmployeeInfo(int? id)
{
if (!Request.IsAjaxRequest())
return View("Error");
if (!id.HasValue)
return View("Error");
var list = EmployeeDataSource.CreateEmployees();
var data = list.Where(x => x.Id == id.Value).FirstOrDefault();
if (data == null)
return View("Error");
return PartialView(viewName: "_EmployeeInfo", model: data);
}
}
}
بر روی متد Index کلیک راست کرده و گزینه Add view را انتخاب کنید. یک View خالی را به آن اضافه نمائید. همچنین بر روی متد EmployeeInfo کلیک راست کرده و با انتخاب گزینه Add view در صفحه ظاهر شده یک partial view را اضافه نمائید. جهت تمایز بین partial view و view هم بهتر است نام partial view با یک underline شروع شود.
کدهای partial view مورد نظر را به نحو زیر تغییر دهید:
@model MvcApplication18.Models.Employee
<strong>Name:</strong> @Model.Name
سپس کدهای View متناظر با متد Index را نیز به صورت زیر اعمال کنید:
@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
<div id="EmployeeInfo">
@Ajax.ActionLink(
linkText: "Get Employee-1 info",
actionName: "EmployeeInfo",
controllerName: "Home",
routeValues: new { id = 1 },
ajaxOptions: new AjaxOptions
{
HttpMethod = "POST",
InsertionMode = InsertionMode.Replace,
UpdateTargetId = "EmployeeInfo",
LoadingElementId = "Progress"
})
</div>
<div id="Progress" style="display: none">
<img src="@Url.Content("~/Content/images/loading.gif")" alt="loading..." />
</div>
توضیحات جزئیات کدهای فوق
متد Ajax.ActionLink لینکی را تولید میکند که با کلیک کاربر بر روی آن، اطلاعات اکشن متد واقع در کنترلری مشخص، به کمک ویژگیهای jQuery Ajax دریافت شده و سپس در مقصدی که توسط UpdateTargetId مشخص میگردد، بر اساس مقدار InsertionMode، درج خواهد شد (میتواند قبل از آن درج شود یا پس از آن و یا اینکه کل محتوای مقصد را بازنویسی کند). HttpMethod آن هم به POST تنظیم شده تا با IE مشکلی نباشد. از این جهت که IE پیغامهای GET را کش میکند و مساله ساز خواهد شد. توسط پارامتر routeValues، آرگومان مورد نظر به متد EmployeeInfo ارسال خواهد شد.
به علاوه یکی دیگر از خواص کلاس AjaxOptions، برای معرفی حالت بروز خطایی در سمت سرور به نام OnFailure در نظر گرفته شده است. در اینجا میتوان نام یک متد JavaScript ایی را مشخص کرده و پیغام خطای عمومی را در صورت فراخوانی آن به کاربر نمایش داد. یا توسط خاصیت Confirm آن میتوان یک پیغام را پیش از ارسال اطلاعات به سرور به کاربر نمایش داد.
به این ترتیب در مثال فوق، id=1 به متد EmployeeInfo به صورت غیرهمزمان ارسال میگردد. سپس کارمندی بر این اساس یافت شده و در ادامه partial view مورد نظر بر اساس اطلاعات کاربر مذکور، رندر خواهد شد. نتیجه کار، در یک div با id مساوی EmployeeInfo درج میگردد (InsertionMode.Replace). متد Ajax.ActionLink از این جهت داخل div تعریف شدهاست که پس از کلیک کاربر و جایگزینی محتوا، محو شود. اگر نیازی به محو آن نبود، آنرا خارج از div تعریف کنید.
عملیات دریافت اطلاعات از سرور ممکن است مدتی طول بکشد (برای مثال دریافت اطلاعات از بانک اطلاعاتی). به همین جهت بهتر است در این بین از تصاویری که نمایش دهنده انجام عملیات است، استفاده شود. برای این منظور یک div با id مساوی Progress تعریف شده و id آن به LoadingElementId انتساب داده شده است. این div با توجه به display: none آن، در ابتدای امر به کاربر نمایش داده نخواهد شد؛ در آغاز کار دریافت اطلاعات از سرور توسط متد Ajax.ActionLink نمایان شده و پس از خاتمه کار مجددا مخفی خواهد شد.
به علاوه اگر به کدهای فوق دقت کرده باشید، از متد Request.IsAjaxRequest نیز استفاده شده است. به این ترتیب میتوان تشخیص داد که آیا درخواست رسیده از طرف jQuery Ajax صادر شده است یا خیر. البته آنچنان روش قابل ملاحظهای نیست؛ چون امکان دستکاری Http Headers همیشه وجود دارد؛ اما بررسی آن ضرری ندارد. البته این نوع بررسیها را در ASP.NET MVC بهتر است تبدیل به یک فیلتر سفارشی نمود؛ به این ترتیب حجم if و else نویسی در متدهای کنترلرها به حداقل خواهد رسید. برای مثال:
[AttributeUsage(AttributeTargets.Class|AttributeTargets.Method)]
public class AjaxOnlyAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
base.OnActionExecuting(filterContext);
}
else
{
throw new InvalidOperationException("This operation can only be accessed via Ajax requests");
}
}
}
و برای استفاده از آن خواهیم داشت:
[AjaxOnly]
public ActionResult SomeAjaxAction()
{
return Content("Hello!");
}
در مورد کلمه unobtrusive در قسمت بررسی نحوه اعتبار سنجی اطلاعات، توضیحاتی را ملاحظه نمودهاید. در اینجا نیز از ویژگیهای data-* برای معرفی پارامترهای مورد نیاز حین ارسال اطلاعات به سرور، استفاده میگردد. برای مثال خروجی متد Ajax.ActionLink به شکل زیر است. به این ترتیب امکان حذف کدهای جاوا اسکریپت از صفحه فراهم میشود و توسط یک فایل jquery.unobtrusive-ajax.min.js که توسط تیم ASP.NET MVC تهیه شده، اطلاعات مورد نیاز به سرور ارسال خواهد گردید:
<a data-ajax="true" data-ajax-loading="#Progress" data-ajax-method="POST"
data-ajax-mode="replace" data-ajax-update="#EmployeeInfo"
href="/Home/EmployeeInfo/1">Get Employee-1 info</a>
در کل این روش قابلیت نگهداری بهتری نسبت به روش اسکریپت نویسی مستقیم داخل صفحات را به همراه دارد. به علاوه جدا سازی افزونه اسکریپت وفق دهنده این اطلاعات با متد jQuery.Ajax از صفحه جاری، که امکان کش شدن آنرا به سادگی میسر میسازد.
به روز رسانی اطلاعات قسمتی از صفحه بدون استفاده از متد Ajax.ActionLink
الزامی به استفاده از متد Ajax.ActionLink و فایل jquery.unobtrusive-ajax.min.js وجود ندارد. اینکار را مستقیما به کمک jQuery نیز میتوان به نحو زیر انجام داد:
<a href="#" onclick="LoadEmployeeInfo()">Get Employee-1 info</a>
@section javascript
{
<script type="text/javascript">
function LoadEmployeeInfo() {
showProgress();
$.ajax({
type: "POST",
url: "/Home/EmployeeInfo",
data: JSON.stringify({ id: 1 }),
contentType: "application/json; charset=utf-8",
dataType: "json",
// controller is returning a simple text, not json
complete: function (xhr, status) {
var data = xhr.responseText;
if (status === 'error' || !data) {
//handleError
}
else {
$('#EmployeeInfo').html(data);
}
hideProgress();
}
});
}
function showProgress() {
$('#Progress').css("display", "block");
}
function hideProgress() {
$('#Progress').css("display", "none");
}
</script>
}
توضیحات:
توسط متد jQuery.Ajax نیز میتوان درخواستهای Ajax ایی خود را به سرور ارسال کرد. در اینجا type نوع http verb مورد نظر را مشخص میکند که به POST تنظیم شده است. Url آدرس کنترلر را دریافت میکند. البته حین استفاده از متد توکار Ajax.ActionLink، این لینک به صورت خودکار بر اساس تعاریف مسیریابی برنامه تنظیم میشود. اما در صورت استفاده مستقیم از jQuery.Ajax باید دقت داشت که با تغییر تعاریف مسیریابی برنامه نیاز است تا این Url نیز به روز شود.
سه سطر بعدی نوع اطلاعاتی را که باید به سرور POST شوند مشخص میکند. نوع json است و همچنین contentType آن برای ارسال اطلاعات یونیکد ضروری است. از متد JSON.stringify برای تبدیل اشیاء به رشته کمک گرفتهایم. این متد در تمام مرورگرهای امروزی به صورت توکار پشتیبانی میشود و استفاده از آن سبب خواهد شد تا اطلاعات به نحو صحیحی encode شده و به سرور ارسال شوند. بنابراین این رشته ارسالی اطلاعات را به صورت دستی تهیه نکنید؛ چون کاراکترهای زیادی هستند که ممکن است مشکل ساز شده و باید پیش از ارسال به سرور اصطلاحا escape یا encode شوند.
متداول است از پارامتر success برای دریافت نتیجه عملیات متد jQuery.Ajax استفاده شود. اما در اینجا از پارامتر complete آن استفاده شده است. علت هم اینجا است که return PartialView یک رشته را بر میگرداند. پارامتر success انتظار دریافت خروجی از نوع json را دارد. به همین جهت در این مثال خاص باید از پارامتر complete استفاده کرد تا بتوان به رشته بدون فرمت خروجی بدون مشکل دسترسی پیدا کرد.
به علاوه چون از یک section برای تعریف اسکریپتهای مورد نیاز استفاده کردهایم، برای درج خودکار آن در هدر صفحه باید قسمت هدر فایل layout برنامه را به صورت زیر مقدار دهی کرد:
@RenderSection("javascript", required: false)
دسترسی به اطلاعات یک مدل در View، به کمک jQuery Ajax
اگر جزئی از صفحه که قرار است به روز شود، پیچیده است، روش استفاده از partial viewها توصیه میشود؛ برای مثال میتوان اطلاعات یک مدل را به همراه یک گرید کامل از اطلاعات، رندر کرد و سپس در صفحه درج نمود. اما اگر تنها به اطلاعات چند خاصیت از مدلی نیاز داشتیم، میتوان از روشهایی با سربار کمتر نیز استفاده کرد. برای مثال متد جدید زیر را به کنترلر Home اضافه کنید:
[HttpPost] //for IE-8
public ActionResult EmployeeInfoData(int? id)
{
if (!Request.IsAjaxRequest())
return Json(false);
if (!id.HasValue)
return Json(false);
var list = EmployeeDataSource.CreateEmployees();
var data = list.Where(x => x.Id == id.Value).FirstOrDefault();
if (data == null)
return Json(false);
return Json(data);
}
سپس View برنامه را نیز به نحو زیر تغییر دهید:
<a href="#" onclick="LoadEmployeeInfoData()">Get Employee-2 info</a>
@section javascript
{
<script type="text/javascript">
function LoadEmployeeInfoData() {
showProgress();
$.ajax({
type: "POST",
url: "/Home/EmployeeInfoData",
data: JSON.stringify({ id: 1 }),
contentType: "application/json; charset=utf-8",
dataType: "json",
// controller is returning the json data
success: function (result) {
if (result) {
alert(result.Id + ' - ' + result.Name);
}
hideProgress();
},
error: function (result) {
alert(result.status + ' ' + result.statusText);
hideProgress();
}
});
}
function showProgress() {
$('#Progress').css("display", "block");
}
function hideProgress() {
$('#Progress').css("display", "none");
}
</script>
}
در این مثال، کنترلر برنامه، اطلاعات مدل را تبدیل به Json کرده و بازگشت خواهد داد. سپس میتوان به اطلاعات این مدل و خواص آن در View برنامه، در پارامتر success متد jQuery.Ajax، مطابق کدهای فوق دسترسی یافت. اینبار چون خروجی کنترلر تعریف شده از نوع Json است، امکان استفاده از پارامتر success فراهم شده است. همه چیز هم در اینجا خودکار است؛ تبدیل یک شیء به Json و برعکس.
یک نکته: اگر نوع متد کنترلر، HttpGet باشد، نیاز خواهد بود تا پارامتر دوم متد بازگشت Json، مساوی JsonRequestBehavior.AllowGet قرار داده شود.
ارسال اطلاعات فرمها به سرور، به کمک ویژگیهای Ajax
متد کمکی توکار دیگری به نام Ajax.BeginForm در ASP.NET MVC وجود دارد که کار ارسال غیرهمزمان اطلاعات یک فرم را به سرور انجام داده و سپس اطلاعاتی را از سرور دریافت و قسمتی از صفحه را به روز خواهد کرد. مکانیزم کاری کلی آن بسیار شبیه به متد Ajax.ActionLink میباشد. در ادامه با تکمیل مثال قسمت جاری، به بررسی این ویژگی خواهیم پرداخت.
ابتدا متد جستجوی زیر را به کنترلر برنامه اضافه کنید:
[HttpPost] //for IE-8
public ActionResult SearchEmployeeInfo(string data)
{
if (!Request.IsAjaxRequest())
return Content(string.Empty);
if (string.IsNullOrWhiteSpace(data))
return Content(string.Empty);
var employeesList = EmployeeDataSource.CreateEmployees();
var list = employeesList.Where(x => x.Name.Contains(data)).ToList();
if (list == null || !list.Any())
return Content(string.Empty);
return PartialView(viewName: "_SearchEmployeeInfo", model: list);
}
سپس بر روی نام متد کلیک راست کرده و گزینه add view را انتخاب کنید. در صفحه باز شده، گزینه create a stronlgly typed view را انتخاب کرده و قالب scaffolding را هم بر روی list قرار دهید. سپس گزینه ایجاد partial view را نیز انتخاب کنید. نام آنرا هم _SearchEmployeeInfo وارد نمائید. برای نمونه خروجی حاصل به نحو زیر خواهد بود:
@model IEnumerable<MvcApplication18.Models.Employee>
<table>
<tr>
<th>
Name
</th>
</tr>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
</tr>
}
</table>
تا اینجا یک متد جستجو را ایجاد کردهایم که میتواند لیستی از رکوردهای کارمندان را بر اساس قسمتی از نام آنها که توسط کاربری جستجو شده است، بازگشت دهد. سپس این اطلاعات را به partial view مورد نظر ارسال کرده و یک جدول را بر اساس آن تولید خواهیم نمود.
اکنون به فایل Index.cshtml مراجعه کرده و فرم Ajax ایی زیر را اضافه نمائید:
@using (Ajax.BeginForm(actionName: "SearchEmployeeInfo",
controllerName: "Home",
ajaxOptions: new AjaxOptions
{
HttpMethod = "POST",
InsertionMode = InsertionMode.Replace,
UpdateTargetId = "EmployeeInfo",
LoadingElementId = "Progress"
}))
{
@Html.TextBox("data")
<input type="submit" value="Search" />
}
اینبار بجای استفاده از متد Html.BeginForm از متد Ajax.BeginForm استفاده شده است. به کمک آن اطلاعات Html.TextBox تعریف شده، به کنترلر Home و متد SearchEmployeeInfo آن، بر اساس HttpMethod تعریف شده، ارسال گردیده و نتیجه آن در یک div با id مساوی EmployeeInfo درج میگردد. همچنین اگر اطلاعاتی یافت نشد، به کمک متد return Content یک رشته خالی بازگشت داده میشود.
متد Ajax.BeginForm نیز از ویژگیهای data-* برای تعریف اطلاعات مورد نیاز ارسالی به سرور استفاده میکند. بنابراین نیاز به سطر الحاق jquery.unobtrusive-ajax.min.js در فایل layout برنامه جهت وفق دادن این اطلاعات unobtrusive به اطلاعات مورد نیاز متد jQuery.Ajax وجود دارد.
<form action="/Home/SearchEmployeeInfo" data-ajax="true"
data-ajax-loading="#Progress" data-ajax-method="POST"
data-ajax-mode="replace" data-ajax-update="#EmployeeInfo"
id="form0" method="post">
<input id="data" name="data" type="text" value="" />
<input type="submit" value="Search" />
</form>
کتابخانه کمکی «ASP.net MVC Awesome - jQuery Ajax Helpers»
علاوه بر متدهای توکار Ajax همراه با ASP.NET MVC، سایر علاقمندان نیز یک سری Ajax helper را بر اساس افزونههای jQuery تدارک دیدهاند که از آدرس زیر قابل دریافت هستند:
http://awesome.codeplex.com/
افزودن فرمها به کمک jQuery.Ajax و فعال سازی اعتبار سنجی سمت کلاینت
در ASP.NET MVC چون ViewState حذف شده است، امکان تزریق فرمهای جدید به صفحه یا به روز رسانی قسمتی از صفحه توسط jQuery Ajax به سهولت و بدون دریافت پیغام «viewstate is corrupted» در حین ارسال اطلاعات به سرور، میسر است.
در این حالت باید به یک نکته مهم نیز دقت داشت: «اعتبار سنجی سمت کلاینت دیگر کار نمیکند». علت اینجا است که در حین بارگذاری متداول یک صفحه، متد زیر به صورت خودکار فراخوانی میگردد:
$.validator.unobtrusive.parse("#{form-id}");
اما با به روز رسانی قسمتی از صفحه، دیگر اینچنین نخواهد بود و نیاز است این فراخوانی را دستی انجام دهیم. برای مثال:
$.ajax
({
url: "/{controller}/{action}/{id}",
type: "get",
success: function(data)
{
$.validator.unobtrusive.parse("#{form-id}");
}
});
//or
$.get('/{controller}/{action}/{id}', function (data) { $.validator.unobtrusive.parse("#{form-id}"); });
شبیه به همین مساله را حین استفاده از Ajax.BeginForm نیز باید مد نظر داشت:
@using (Ajax.BeginForm(
"Action1",
"Controller",
null,
new AjaxOptions {
OnSuccess = "onSuccess",
UpdateTargetId = "result"
},
null)
)
{
<input type="submit" value="Save" />
}
var onSuccess = function(result) {
// enable unobtrusive validation for the contents
// that was injected into the <div id="result"></div> node
$.validator.unobtrusive.parse("#result");
};
در این مثال در پارامتر UpdateTargetId، مجددا یک فرم رندر میشود. بنابراین اعتبار سنجی سمت کلاینت آن دیگر کار نخواهد کرد مگر اینکه با مقدار دهی خاصیت OnSuccess، مجددا متد unobtrusive.parse را فراخوانی کنیم.