در مطلب قبلی بیشتر از لحاظ تئوریک با وبپک آشنا شدیم و در آخر نیز یک تک اسکریپت را با استفاده از آن باندل کرده و در صفحهی index.html اضافه کردیم.
پروپرتی entry مشخص کنندهی فایل ورودی است که قصد پردازش آن را داریم و پروپرتی output نیز خود یک آبجکت میباشد که در سادهترین حالت، احتیاج به تعریف یک پروپرتی با نام filename را در آن داریم که مشخص کنندهی نام فایل باندل شونده توسط وبپک میباشد.
اینبار برای ورود وپ بک به حالت نظارهگر کافی است وبپک را یک بار از طریق خط فرمان با دستور npm run webpack، فراخوانی کنیم.
در حالتی که وبپک به صورت سراسری نصب شده باشد، با اجرای دستور webpack-dev-server در خط فرمان، وب سرور وبپک شروع به کار خواهد کرد و تنظیمات را نیز از فایل پیکربندی اعمال میکند.
در اینجا پروپرتی جدیدی به قسمت scripts، با نام webpackserver اضافه شدهاست. حال با فراخوانی این اسکریپت با دستور زیر، وب سرور وبپک شروع به کار خواهد کرد:
(دقت کنید که نامهای قرار داده شدهی در قسمت scripts میتوانند به صورت دلخواه باشند و شما میتوانید نامی را که دلخواه خودتان است، برگزینید؛ به طور مثال به جای webpackserver نام دیگری را در فایل package.json برای آن مشخص کنید و در هنگام فراخوانی از آن استفاده کنید).
حال جهت استفاده از این ماژول در فایل main.js تغییرات زیر را اعمال خواهیم کرد:
پس از ذخیرهی تغییرات خواهید دید که وب سرور وبپک از این تغییرات آگاه شده و باندل جدید را خواهد ساخت که در اینجا خروجی مانند تصویر زیر را خواهید دید:
حال برای اینکه این اسکریپت را به وبپک معرفی کنیم، فایل پیکربندی وبپک را باز کرده و تغییرات زیر را در آن اعمال میکنیم :
قابل مشاهده است که قسمت entry، به جای این که یک تک فایل را معرفی کند، تبدیل به یک آرایه شدهاست که هم فایل shared.js را در بر میگیرد و هم فایل main.js را دارد.
برای اینکه به وبپک خبر دهیم که در پروژه در حال استفاده از تایپ اسکریپت هستیم، فایل پیکربندی وبپک را باز کرده و پروپرتی جدیدی را با نام module به آن معرفی میکنیم که خود یک آبجکت میباشد. حال در آبجکت module یک پروپرتی جدید را با نام loaders که جنس آرایهای دارد، اضافه میکنیم. آرایهی loaders شامل همهی loader هایی خواهد بود که شما قصد استفادهی آنها را به همراه وبپک دارید. هر عضو از این آرایه خود نیز یک آبجکت میباشد که دارای سه پروپرتی زیر میباشد:
حال با اجرای دوبارهی وبپک، loader تایپ اسکریپت ابتدا اجرا شده، سپس وبپک وارد کار میشود و فایلها را باندل خواهد کرد. در صورتی که بدون مشکل همه چیز اجرا شود، خروجی مانند تصویر زیر را خواهید داشت:
توجه :
در مطلب قبلی برای استفاده و نصب وبپک دو راه پیشنهاد شد؛ یکی نصب وبپک به صورت سراسری و دیگری به صورت محلی در محیط کاری فعلی پروژه. استفادهی نگارنده به صورت محلی میباشد و برای فراخوانی وبپک از دستور npm run webpack استفاده خواهد شد. در صورتی که از وبپک به صورت سراسری (گلوبال ) استفاده میکنید، به جای این دستور فقط کافی است در خط فرمان دستور webpack را نوشته و آن را اجرا کنید.
اضافه کردن فایل تنظیمات وبپک
وبپک دارای تنظیمات و حالتهای مختلفی برای تولید خروجی نهایی میباشد که میتوان این تنظیمات را به صورت پارامترهای ورودی، در هنگام فراخوانی برای آن مشخص کرد. ولی برای ساده کردن و همچنین عدم الزام به تکرار برای تنظیمات مورد نیاز میتوانیم یک فایل پیکربندی را ایجاد کنیم و موارد مورد نیاز را در آن تعریف کرده و تنها با فراخوانی نام webpack در خط فرمان، به صورت خودکار این تنظیمات خوانده شده و دستورات ما اجرا شوند. TaskRunnerهای گالپ و گرانت نیز دارای یک فایل پیکربندی، برای مشخص کردن تنظیمات مورد نیاز کاربر میباشند.
ساخت فایل پیکربندی وبپک
در محیط کاری پروژه یک فایل جدید را با نام webpack.config.js ایجاد میکنیم، تا پیکر بندی مورد نظرمان را برای وبپک در آن مشخص کنیم (نام این فایل قراردادی است و امکان مشخص کردن فایلی با نام دیگر نیز وجود دارد که در آینده با آن برخورد خواهیم کرد).
این فایل به صورت یک ماژول در فرمت commonjs میباشد (در صورتی که با ماژولهای مختلف آشنا نیستید، مطالعهی این مقاله پیشنهاد میشود ماژولها در es6).
پس از ایجاد فایل پیکربندی در محیط کاری پروژه، محتوای زیر را به آن اضافه خواهیم کرد. این حالت را میتوان سادهترین پیکربندی وبپک دانست و با دستور webpack ./main.js bundle.js که در پایان مطلب قبلی در خط فرمان اجرا کردیم، تفاوتی ندارد.
// webpack.config.js file module.exports = { entry:'./main.js' ,output:{ filename:'bundle.js' } }
حال با اجرای دستور npm run webpack، وبپک به صورت خودکار محتوای فایل پیکربندی را خوانده و تنظیمات تعریف شده را در فایل باندل نهایی ترتیب اثر میدهد.
حالت نظاره گر یا watch mode
اضافه کردن فایل پیکربندی میتواند مفید باشد و ما را از الزام به تکرار برای مشخص کردن پارامترهای مورد نیاز در هر بار اجرای وبپک بینیاز میکند. ولی فرض کنید در حال توسعهی پروژهای هستید و مدام در حال تغییر فایلهای پروژه میباشید. فایلی اضافه، حذف و یا دچار تغییر میشود و برای هر بار انجام شدن پروسهی باندلینگ باید وبپک را فراخوانی کنیم. برای جلوگیری از این پروسهی تکراری، وبپک دارای حالت نظارهگر یا watch mode میباشد. معنای این حالت این است که وبپک تغییرات محیط کاری شما را در نظر میگیرد و با انجام هر تغییری، دوباره باندل مربوطه را از نو میسازد.
برای وارد شدن به این حالت یک راه کار این میباشد که در هنگام فراخوانی وبپک در خط فرمان، پرچم زیر را به آن اضافه کنیم:
//for when webpack is installed globally webpack --watch //for when webpack is installed locally in project npm run webpack -- --watch
(در فراخوانی بالا دو حالت نصب سراسری و محلی وبپک در نظر گرفته شدهاست. حالت اول نکتهای را ندارد. ولی در حالت دوم برای اینکه پارامترهای خط فرمان توسط npm به دست وبپک برسد، احتیاج به اضافه کردن -- میباشد. جهت عدم آشنایی با این مورد میتوانید به اینجا مراجعه کنید: فرستادن پارامتر به اسکریپتهای npm)
راه کار دوم جهت تنظیم کردن وبپک در حالت نظاره گر، اضافه کردن پروپرتی watch به فایل پیکربندی وبپک است. پس از انجام این تغییر، محتوای فایل پیکربندی به این صورت خواهد بود:
//webpack.config.js file module.exports = { entry:'./main.js' ,output:{ filename:'bundle.js' } ,watch :true }
در صورتی که مشکلی وجود نداشته باشد، با اجرای این دستور، کنترل خط فرمان به شما برنخواهد گشت و وبپک در حالت اجرا باقی میماند که در تصویر زیر قابل مشاهدهاست.
حال اگر در اسکریپت main.js تغییری ایجاد کنید، خواهید دید که وبپک به صورت خودکار باندل را از اول خواهد ساخت.
وب سرور وبپک
تا اینجا از وبپک به عنوان یک باندل کننده بهره بردهایم و جهت میزبانی فایلهای پروژه از فایل سیستم و سیستم عامل بهره بردیم. ولی میدانیم که در حین توسعه دادن برنامههای وب، استفاده از فایل سیستم و سیستم عامل مفید نیست و دچار مشکلات عدیدهای هم از سمت مرورگرها و هم از سمت کتابخانههای معروف جاوا اسکریپتی خواهیم شد( مانند مباحث cors و ...). جهت حذف این مشکلات میتوانیم وب سرور مورد علاقهی خود را اجرا کنیم یا از وب سرور فراهم شده توسط وبپک بهره ببریم.
جهت نصب وب سرور وبپک دستور زیر را در خط فرمان اجرا خواهیم کرد ( به صورت سراسری یا محلی به انتخاب شما خواهد بود و قبلا توضیح داده شده است).
// to install globally : npm install -g webpack-dev-server //to install locally in project : npm install -D webpack-dev-server
در صورتی که وبپک به صورت محلی نصب شده باشد، بایستی یک مدخل به قسمت اسکریپتهای package.json برای راهنمایی npm اضافه کنیم. محتویات این فایل پس از تغییرات، از این قرار است:
//package.json file { "name": "dntwebpack", "version": "1.0.0", "description": "", "main": "main.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "webpack": "webpack", "webpackserver": "webpack-dev-server" }, "author": "mehdi", "license": "ISC", "devDependencies": { "webpack": "^1.13.1", "webpack-dev-server": "^1.14.1" } }
npm run webpackserver
در صورتی که همه چیز بدون مشکل باشد، خروجی شبیه به تصویر زیر را مشاهده خواهید کرد که آدرسی که به صورت محلی، سرور بر روی آن میزبان شده است نیز قابل مشاهده است:
باندل کردن اسکریپتهای گوناگون توسط وبپک
تا اینجای کار تنها از یک تک اسکریپت، با نام main.js استفاده کردیم. قطعا پروژههای واقعی از یک تک اسکریپت تشکیل نخواهند شد و اسکریپتهای گوناگونی خواهیم داشت. جهت استفاده از چندین اسکریپت توسط وبپک، دو سناریوی مختلف رخ خواهند داد که هر دو را برسی خواهیم کرد:
اضافه کردن اسکریپتها به صورت داینامیک یا پویا توسط وبپک
در محیط کاری پروژه، یک فایل جدید user.js را اضافه میکنیم که از این فایل در فایل main.js استفاده خواهد شد.
محتوای فایل user.js یک تابع سادهی جاوا اسکریپتی خواهد بود:
// user.js file function userLog() { console.log("ahooy from user module file"); } module.exports={ userLog:userLog }
//main.js file var user = require("./user"); user.userLog(); console.log(`i'm bundled by webpack`);
در تصویر قابل مشاهده است که ماژول user.js نیز وارد باندل شده است.
در صورتی که به مسیری که وبپک در حال میزبانی بر روی آن است در مرورگر خود مراجعه کنید، پیغامهای چاپ شده را در کنسول مشاهده خواهید کرد.
اضافه کردن اسکریپتها به باندل به صورت استاتیک توسط وبپک
قطعا در پروژههای خود از کتابخانههایی که توسط برنامه نویسان دیگر تولید شدهاند مانند جی کوئری و ... استفاده خواهیم کرد. استفاده از این اسکریپتها به صورت داینامیک و ایمپورت کردن آنها در هر ماژول جالب نخواهد بود و یا ممکن است ماژولی که خود شما نوشته اید به صورت اشتراکی بین تمام برنامه اجرا شود. در این گونه از موارد میتوانیم این اسکریپتها را در فایل پیکربندی به وبپک معرفی کنیم تا در هنگام باندلینگ، به باندل وارد شوند.
اسکریپت جدیدی را در پروژه اضافه میکنیم و نامش را shared.js میگذاریم که دارای محتوای زیر است :
// shared.js file console.log('log message from shared module !');
//webpack.config.js file module.exports = { entry:['./shared.js','./main.js'] ,output:{ filename:'bundle.js' } ,watch :true }
در مواقعی که فایل پیکربندی دچار تغییر میشود، بایستی وبپک را متوقف و دوباره اجرا کنید تا تنظیمات جدید، اعمال شوند. پس از راه اندازی دوباره وبپک، در صورت موفقیت آمیز بودن تغییراتتان، خروجی را شبیه تصویر رو به رو خواهید گرفت و مشخص است که فایل shared.js نیز در باندل وارد شده است.
استفاده از Loaderها در وبپک
به صورت پیش فرض وبپک قابلیت باندل کردن ماژولهای جاوا اسکریپت را دارد و همچنین میتواند این فایلها را Minify کند (در مطالب بعدی خواهیم دید). ولی به طور مثال استفاده از تایپ اسکریپت از تواناییهای وبپک به صورت توکار خارج است. اینجاست که Loaderها وارد کار میشوند.
اگر بخواهیم به زبان ساده Loaderها را تعریف کنیم میتوان آنها را کامپوننت هایی دانست که به وبپک فوت و فن کار جدیدی را یاد میدهند.
در ادامه Loader تایپ اسکریپت را نصب خواهیم کرد و به کمک آن فایلهای پروژه را تبدیل به تایپ اسکریپت کرده و در هنگام باندل کردن از وبپک میخواهیم که این فایلها را ترنسپایل کند و سپس باندل را از روی آنها بسازد ( برای مطالعهی ادامهی این مطلب احتیاجی به آشنایی به تایپ اسکریپت نیست و هدف استفاده از یک loader است. ولی در صورت علاقه میتوانید به اینجا مراجعه کنید سری آموزش تایپ اسکریپت)
نصب Loader تایپ اسکریپت
به خط فرمان برگشته و با استفاده از npm، لودر تایپ اسکریپت مورد نیاز وبپک را نصب میکنیم. دستور مورد نیاز این قرار است :
npm install -D ts-loader
( توجه :
در ادامه این مطلب از پیکربندی سادهی یک پروژهی تایپ اسکریپتی استفاده شده است که اعم از ایجاد فایل tsconfig.json و اضافه کردن پوشهی typings به پروژه میباشد.)
فایل main.ts را که یک فایل تایپ اسکریپتی میباشد، به پروژه اضافه میکنیم. محتوای آن به صورت زیر خواهد بود. قابل مشاهده است که از ویژگیهای ES6 در این فایل استفاده شده و این انتظار را از لودر تایپ اسکریپت داریم که این فایل را در هنگام باندلینگ برای ما ترنسپایل کند.
// main.ts file let user = require("./user"); user.userLog(); let mainlogger = () => { console.log(`i'm bundled by webpack in an arrow function`); } mainlogger();
test : یک رجکس میباشد که به loader میگوید به دنبال چه فایلهایی بگردد.
exclude : از جنس رجکس و مشخص کنندهی مسیرهایی است که از پروژه باید جدا شوند و توسط loader پردازش نشوند (مانند فایلهای از قبل کامپایل شدهی کتابخانهها).
loader : مشخص کنندهی نام loader مورد نظر .
محتوای فایل پیکربندی وبپک، پس از معرفی loader تایپ اسکریپت، به این صورت خواهد بود:
//webpack.config.js module.exports = { entry:['./shared.js','./main.js'] ,output:{ filename:'bundle.js' } ,watch :true ,module:{ loaders:[ { test:/\.ts$/ ,exclude:/node_modules/ ,loader:'ts-loader' } ] } }
در این مطلب تنظیمات مختلف وبپک، فایل پیکربندی، استفاده از چندین فایل به همراه وبپک، وب سرور وبپک و همچنین با loaderهای وبپک آشنا شدیم.
دریافت فایلها dntwebpack-part2.zip
روز قبل نیاز بود تا فایلهای mdf و ldf دیتابیسها جابجا شوند (یک هارد بزرگتر و از این مسایل).
برای جابجا کردن این فایلها هم روش معمول detach و سپس attach است. ابتدا روی دیتابیس کلیک راست کرده و detach . حالا فایلها را جابجا میکنید و سپس attach . یا میشود بک آپ کامل گرفت و بعد ری استور کرد.
عموما هم نمیتوان دیتابیس در حال استفاده را detach کرد. باید دیتابیس ابتدا single user شود و بعد میتوان اینکار را انجام داد.
تا اینجای کار متداول است. همه چیز به خوبی انجام شد. سپس در لحظه attach ، دیتابیسها به صورت read only اتچ شدند با آیکونی سیاه رنگ در management studio . (و رنگ من هم بلافاصله به همین رنگ متمایل شد!)
بعد از مدتی جستجو مشخص شد که در اس کیوال سرور 2008 برای کاهش سطح حمله به سرور، از یک سری یوزر با دسترسی کم برای نصب اس کیوال سرور استفاده میشود (بسیار هم خوب) و اس کیوال سرور 2008 ، یک سری یوزر مخصوص را هم در حین نصب ایجاد میکند که به صورت خودکار بر روی پوشه دیتای شما دسترسی full control دارد برای اینکه بتواند کارش را انجام دهد.
حال اگر شما در جای دیگری پوشهای درست کردید و این دیتابیسها را منتقل نمودید، مجددا پیش از هر کاری باید این دسترسی را برقرار کنید و گرنه اس کیوال سرور مجوز write نخواهد داشت؛ به همین جهت دیتابیس به صورت read only در management studio با رنگ مشکی ظاهر میشود.
نام این کاربر مخصوص به صورت زیر است:
SQLServerMSSQLUser$ComputerName$MSSQLSERVER
پس از برقراری دسترسی هم مشکل برطرف نمیشود. باید دستور زیر را نیز اجرا نمود:
ALTER DATABASE myDB SET READ_WRITE
پ.ن.
میتوان دسترسی یوزر سرویس اس کیوال سرور 2008 را نیز مانند نگارشهای قبلی به حالت local system تغییر داد (یا هر اکانت دیگری با دسترسی بالا) تا این مشکلات نباشد؛ ولی بدیهی است سطح حمله به سرور نیز به همین اندازه افزایش مییابد.
تا اینجا مثالهایی را که بررسی کردیم، متکی به خود بودند و اطلاعات هر کدام، از یک درخواست به درخواستی دیگر کاملا متفاوت بود. همچنین اطلاعات ارسالی و یا دریافتی توسط آنها نیز ثابت و از پیش تعیین شده بود.
کار با اطلاعات متغیر دریافتی از سرور
در Postman یک برگهی جدید را باز کنید و سپس آدرس https://httpbin.org/uuid را در حالت Get درخواست نمائید:
این درخواست، یک Guid اتفاقی جدید را باز میگرداند. هربار که بر روی دکمهی Send کلیک کنیم، مقدار Guid دریافتی متفاوت خواهد بود. این خروجی دقیقا حالتی است که در برنامههای دنیای واقعی با آن سر و کار داریم. در این نوع برنامهها، پیشتر نمیتوان مقدار خروجی دریافتی از سرور را پیشبینی کرد. همچنین علاقمندیم این مقدار دریافتی (از درخواست 1) را به Post request ای که در انتهای قسمت قبل ایجاد کردیم (درخواست 2)، ارسال کنیم و اطلاعاتی را بین درخواستها به اشتراک بگذاریم.
برای این منظور، مجموعهی httpbin ای را که در قسمت قبل ایجاد کردیم، انتخاب کرده و Post request ذخیره شدهی در آنرا انتخاب کنید تا مجددا جزئیات آن بازیابی شود:
برای دسترسی به این اطلاعات، میتوان از ویژگی بسیار قدرتمند postman به نام متغیرها استفاده کرد. به همین جهت به برگهی درخواست دریافت guid مراجعه کرده و برگهی Tests آنرا باز کنید:
این قسمت پس از پایان درخواست جاری، اجرا میشود. بنابراین دراینجا فرصت خواهیم داشت تا مقدار دریافتی از سرور را در یک متغیر ذخیره کنیم. سپس میتوان از این متغیر در حین ارسال درخواستی دیگر استفاده کرد. در پنل کنار textbox نوشتن آزمونها، تعدادی نمونه کد هم وجود دارند. برای مثال در اینجا نمونه کد «Set a global variable» را با کلیک بر روی آن انتخاب میکنیم:
در این مثال key/value این متغیر سراسری، با یک کلید مشخص و مقداری ثابت، مقدار دهی شدهاند.
اکنون مجددا بر روی دکمهی Send این درخواست کلیک کنید. پس از اجرای آن، این قطعه کد اجرا شده و یک متغیر سراسری، با کلید uuid و مقدار ثابت مشخص شده، در تمام برگههای postman در دسترس خواهند بود. برای بررسی صحت این عملیات، میتوانید بر روی دکمهای با آیکن چشم، در بالای سمت راست صفحه، کلیک کنید:
در ادامه برای استفادهی از این متغیر سراسری، به برگهی Post request مراجعه کرده و بدنهی درخواست را به صورت زیر ویرایش میکنیم:
در جائیکه بدنهی درخواست درج میشود، متغیرها باید درون {{ }} قرار گیرند.
سپس این درخواست را ارسال کنید، در خروجی دریافتی از سرور httpbin، خاصیت و مقدار جدید id قابل مشاهده هستند که به معنای ارسال صحیح آن به سرور است:
در اینجا، foo، مقداری است که از یک درخواست دیگر خوانده شده و توسط درخواست مجزای post جاری، ارسال گردیدهاست.
در این مرحله بر روی دکمهی Save برگهی uuid کلیک کرده و آنرا در مجموعهی httpbin، ذخیره کنید.
روش دسترسی و به اشتراک گذاری مقدار متغیر uuid دریافتی
تا اینجا متغیر سراسری uuid تنظیم شده، دارای یک مقدار ثابت است. برای تنظیم آن به مقدار Guid دریافتی از سرور، مجددا به برگهی Tests درخواست uuid مراجعه میکنیم و آنرا به نحو زیر تکمیل خواهیم کرد:
pm.response حاوی کل اطلاعات شیء response است و برای مثال بدنهی response دریافتی از سمت سرور httpbin، یک چنین شکلی را دارد:
با فراخوانی متد json بر روی این شیء، اکنون میتوان به اطلاعات آن همانند یک شیء متداول جاوا اسکریپتی که دارای کلید و خاصیت uuid است، دسترسی یافت:
پس از این تنظیم، مجددا درخواست را ارسال کنید که سبب دریافت uuid جدیدی میشود:
برای بررسی نتیجهی این عملیات، با کلیک بر روی دکمهای با آیکن چشم، در بالای سمت راست صفحه، چنین خروجی قابل مشاهده خواهد بود:
که به معنای دسترسی به مقدار متغیر uuid دریافتی از سمت سرور و تنظیم آن به عنوان یک متغیر سراسری است.
اکنون اگر مجددا به برگهی مجزای Post request مراجعه و آنرا ارسال کنیم، در خروجی بدنهی response دریافتی از سمت سرور:
دقیقا ارسال همان مقدار متغیر دریافتی از برگهای دیگر که توسط متغیر {{uuid}} تنظیم شده، قابل مشاهدهاست.
تا اینجا تمام برگههای باز را ذخیره کنید:
در این تصویر، uuid پس از Post request قرار گرفته، چون ترتیب ذخیره سازی آنها به همین صورت بودهاست. اما نیاز است uuid را به پیش از درخواست post، منتقل کرد. برای اینکار میتوان از drag/drop آیتم آن کمک گرفت.
نوشتن یک آزمون ساده
هدف اصلی از برگهی Tests هر درخواست در Postman، نوشتن آزمونهای مختلف است. به همین جهت برای نوشتن یک آزمون ساده، به برگهی Post request که هم اکنون باز است، مراجعه کرده و سپس برگهی Tests آنرا به صورت زیر تنظیم میکنیم:
در اینجا در پنل code snippets کناری آن، بر روی لینک و گزینهی «Status code: code is 200» کلیک کردهایم تا به صورت خودکار، قطعه کد فوق را تولید کند. البته Tests را در قسمتهای بعدی با جزئیات بیشتری بررسی خواهیم کرد. در اینجا بیشتر هدف آشنایی مقدماتی با برگهی Tests آن است.
این آزمون، بررسی میکند که آیا status code بازگشتی از سرور 200 است یا خیر؟ برای اجرای آن، فقط کافی است بر روی دکمه Send این برگه کلیک کنید:
نتیجهی آنرا در برگهی جدید Test Results، در قسمت نمایش response دریافتی از سمت سرور، میتوان مشاهده کرد که بیانگر موفقیت آمیز بودن آزمایش انجام شدهاست.
اگر علاقمندید تا حالت شکست آنرا نیز مشاهده کنید، بجای عدد 200، عدد 404 را وارد کرده و مجددا درخواست را ارسال کنید.
اجرای ترتیبی آیتمهای یک مجموعه
تا اینجا اگر درخواستها را در مجموعهی httpbin ذخیره کرده و همچنین ترتیب آنها را نیز به نحوی که گفته شد، اصلاح کرده باشید، یک چنین شکلی را باید مشاهده کنید:
در اینجا برای اجرای ترتیبی این آیتمها و اجرای گردش کاری کوچکی که ایجاد کردهایم (درخواست post باید پس از درخواست uuid و بر اساس مقدار دریافتی آن از سرور اجرا شود)، میتوان از collection runner برنامهی Postman استفاده کرد:
با کلیک بر روی دکمهی Runner، در نوار ابزار برنامهی Postman، صفحهی Collection runner آن ظاهر میشود. در این صفحه ابتدا httpbin collection را انتخاب میکنیم که سبب نمایش محتویات و اجزای آن خواهد شد. سپس بدون ایجاد هیچگونه تغییری در تنظیمات این صفحه، بر روی دکمهی run httpbin کلیک میکنیم:
پس از اجرای خودکار مراحل این مجموعه، نتیجهی عملیات ظاهر میشود:
در اینجا هر مرحله، پس از مرحلهای دیگر اجرا شده و اگر آزمایشی نیز به همراه آنها بوده باشد نیز اجرا شدهاست. همچنین اگر بر روی نام هر کدام از مراحل کلیک کنیم، میتوان جزئیات آنها را نیز دقیقا بررسی کرد:
امکان اجرای یک چنین قابلیتی توسط برنامهی CLI ای به نام newman نیز به همراه Postman وجود دارد که جهت استفادهی در continuous integration servers میتواند بسیار مفید باشد. جزئیات آنرا در قسمتهای بعدی بررسی خواهیم کرد.
کار با اطلاعات متغیر دریافتی از سرور
در Postman یک برگهی جدید را باز کنید و سپس آدرس https://httpbin.org/uuid را در حالت Get درخواست نمائید:
این درخواست، یک Guid اتفاقی جدید را باز میگرداند. هربار که بر روی دکمهی Send کلیک کنیم، مقدار Guid دریافتی متفاوت خواهد بود. این خروجی دقیقا حالتی است که در برنامههای دنیای واقعی با آن سر و کار داریم. در این نوع برنامهها، پیشتر نمیتوان مقدار خروجی دریافتی از سرور را پیشبینی کرد. همچنین علاقمندیم این مقدار دریافتی (از درخواست 1) را به Post request ای که در انتهای قسمت قبل ایجاد کردیم (درخواست 2)، ارسال کنیم و اطلاعاتی را بین درخواستها به اشتراک بگذاریم.
برای این منظور، مجموعهی httpbin ای را که در قسمت قبل ایجاد کردیم، انتخاب کرده و Post request ذخیره شدهی در آنرا انتخاب کنید تا مجددا جزئیات آن بازیابی شود:
اکنون با مراجعه به برگهی بدنهی درخواست آن، قصد داریم یک خاصیت جدید id را با guid دریافتی از سرور توسط درخواستی دیگر، مقدار دهی کنیم:
برای دسترسی به این اطلاعات، میتوان از ویژگی بسیار قدرتمند postman به نام متغیرها استفاده کرد. به همین جهت به برگهی درخواست دریافت guid مراجعه کرده و برگهی Tests آنرا باز کنید:
این قسمت پس از پایان درخواست جاری، اجرا میشود. بنابراین دراینجا فرصت خواهیم داشت تا مقدار دریافتی از سرور را در یک متغیر ذخیره کنیم. سپس میتوان از این متغیر در حین ارسال درخواستی دیگر استفاده کرد. در پنل کنار textbox نوشتن آزمونها، تعدادی نمونه کد هم وجود دارند. برای مثال در اینجا نمونه کد «Set a global variable» را با کلیک بر روی آن انتخاب میکنیم:
pm.globals.set("uuid", "foo");
اکنون مجددا بر روی دکمهی Send این درخواست کلیک کنید. پس از اجرای آن، این قطعه کد اجرا شده و یک متغیر سراسری، با کلید uuid و مقدار ثابت مشخص شده، در تمام برگههای postman در دسترس خواهند بود. برای بررسی صحت این عملیات، میتوانید بر روی دکمهای با آیکن چشم، در بالای سمت راست صفحه، کلیک کنید:
در ادامه برای استفادهی از این متغیر سراسری، به برگهی Post request مراجعه کرده و بدنهی درخواست را به صورت زیر ویرایش میکنیم:
{ "name": "Vahid", "id": "{{uuid}}" }
سپس این درخواست را ارسال کنید، در خروجی دریافتی از سرور httpbin، خاصیت و مقدار جدید id قابل مشاهده هستند که به معنای ارسال صحیح آن به سرور است:
"json": { "id": "foo", "name": "Vahid" },
در این مرحله بر روی دکمهی Save برگهی uuid کلیک کرده و آنرا در مجموعهی httpbin، ذخیره کنید.
روش دسترسی و به اشتراک گذاری مقدار متغیر uuid دریافتی
تا اینجا متغیر سراسری uuid تنظیم شده، دارای یک مقدار ثابت است. برای تنظیم آن به مقدار Guid دریافتی از سرور، مجددا به برگهی Tests درخواست uuid مراجعه میکنیم و آنرا به نحو زیر تکمیل خواهیم کرد:
pm.response حاوی کل اطلاعات شیء response است و برای مثال بدنهی response دریافتی از سمت سرور httpbin، یک چنین شکلی را دارد:
{ "uuid": "4594e7ad-cae3-487b-bd42-fc49c312c0e9" }
let jsonResponse = pm.response.json(); pm.globals.set("uuid", jsonResponse.uuid);
{ "uuid": "83d437a8-bce6-438b-8693-068e5399182c" }
که به معنای دسترسی به مقدار متغیر uuid دریافتی از سمت سرور و تنظیم آن به عنوان یک متغیر سراسری است.
اکنون اگر مجددا به برگهی مجزای Post request مراجعه و آنرا ارسال کنیم، در خروجی بدنهی response دریافتی از سمت سرور:
"json": { "id": "83d437a8-bce6-438b-8693-068e5399182c", "name": "Vahid" },
تا اینجا تمام برگههای باز را ذخیره کنید:
در این تصویر، uuid پس از Post request قرار گرفته، چون ترتیب ذخیره سازی آنها به همین صورت بودهاست. اما نیاز است uuid را به پیش از درخواست post، منتقل کرد. برای اینکار میتوان از drag/drop آیتم آن کمک گرفت.
نوشتن یک آزمون ساده
هدف اصلی از برگهی Tests هر درخواست در Postman، نوشتن آزمونهای مختلف است. به همین جهت برای نوشتن یک آزمون ساده، به برگهی Post request که هم اکنون باز است، مراجعه کرده و سپس برگهی Tests آنرا به صورت زیر تنظیم میکنیم:
pm.test("Status code is 200", function () { pm.response.to.have.status(200); });
این آزمون، بررسی میکند که آیا status code بازگشتی از سرور 200 است یا خیر؟ برای اجرای آن، فقط کافی است بر روی دکمه Send این برگه کلیک کنید:
نتیجهی آنرا در برگهی جدید Test Results، در قسمت نمایش response دریافتی از سمت سرور، میتوان مشاهده کرد که بیانگر موفقیت آمیز بودن آزمایش انجام شدهاست.
اگر علاقمندید تا حالت شکست آنرا نیز مشاهده کنید، بجای عدد 200، عدد 404 را وارد کرده و مجددا درخواست را ارسال کنید.
اجرای ترتیبی آیتمهای یک مجموعه
تا اینجا اگر درخواستها را در مجموعهی httpbin ذخیره کرده و همچنین ترتیب آنها را نیز به نحوی که گفته شد، اصلاح کرده باشید، یک چنین شکلی را باید مشاهده کنید:
در اینجا برای اجرای ترتیبی این آیتمها و اجرای گردش کاری کوچکی که ایجاد کردهایم (درخواست post باید پس از درخواست uuid و بر اساس مقدار دریافتی آن از سرور اجرا شود)، میتوان از collection runner برنامهی Postman استفاده کرد:
با کلیک بر روی دکمهی Runner، در نوار ابزار برنامهی Postman، صفحهی Collection runner آن ظاهر میشود. در این صفحه ابتدا httpbin collection را انتخاب میکنیم که سبب نمایش محتویات و اجزای آن خواهد شد. سپس بدون ایجاد هیچگونه تغییری در تنظیمات این صفحه، بر روی دکمهی run httpbin کلیک میکنیم:
پس از اجرای خودکار مراحل این مجموعه، نتیجهی عملیات ظاهر میشود:
در اینجا هر مرحله، پس از مرحلهای دیگر اجرا شده و اگر آزمایشی نیز به همراه آنها بوده باشد نیز اجرا شدهاست. همچنین اگر بر روی نام هر کدام از مراحل کلیک کنیم، میتوان جزئیات آنها را نیز دقیقا بررسی کرد:
امکان اجرای یک چنین قابلیتی توسط برنامهی CLI ای به نام newman نیز به همراه Postman وجود دارد که جهت استفادهی در continuous integration servers میتواند بسیار مفید باشد. جزئیات آنرا در قسمتهای بعدی بررسی خواهیم کرد.
SQL Server قابلیت فعال(enable) و غیر فعال(disable) کردن دو قید کلید خارجی و check را برای ما مهیا کرده است.
ما میتوانیم بعد از ساخت جدول و انتشار مقداری داده در آن قیدهایی را ایجاد کنیم. بطور پیشفرض اگر شرط قید ما بر قرار بود قید به طور صحیح ساخته میشود و اگر شرط قید ما بر قرار نباشد قید با خطای conflict مواجه خواهد شد.
بطور کلی غیر فعال کردن قیدها کار درستی نیست. ولی در برخی مواقع برای تسریع در اجرای کد میتوانیم قید را غیر فعال کنیم. بطور مثال اگر یک میلیون داده قرار است در جدول درج شود و مطمئن هستیم که این دادهها جامعیت دادهها را حفظ میکنند آنگاه میتوانیم قید را برای تسریع در عمل درج بطور موفق غیر فعال کنیم.
فعال و غیر فعال کردن از طریق DDL
با غیر فعال کردن قیود دادهها را در وضعیت نامناسبی قرار میدهیم ولی همان طور که اشاره شد بطور موفق اشکالی پیش نخواهد آمد.
در ادامه ابتدا طریقه غیرفعال کردن و مجددا فعال کردن قیود را توسط دستور alter table نشان خواهم داد سپس به سراغ امکانات ویزاردی میرویم. ابتدا یک جدول تک ستونه ایجاد میکنیم:
الان هیچ قیدی روی جدول لحاظ نشده است. پس هر داده که در رنج domain ستون باشد را میتوانیم درج کنیم. پس بطور نمونه این دادهها را درج میکنیم:
حالا تصمیم داریم قیدی روی ستون column1 بگذاریم که توسط آن تنها اعداد مثبت در جدول درج شوند. پس داریم:
ولی چون داده هایی در جدول از قبل وجود داشته اند که قید ما را نقض کنند این قید ساخته نخواهد شد و با پیغام زیر مواجه خواهیم شد:
برای ساخت این قید روی این دادهها تنها راه استفاده از کلید واژههای WITH NOCHECK است یعنی:
و اکنون سعی میکنیم یک مقدار منفی در جدول درج کنیم:
اما قیدی که ساخته بودیم در جدول در حال اعمال شدن است. برای درج مقدار منفی باید غیر را غیر فعال کنیم.
و حالا مقدار منفی را درج میکنیم. و برای برگرداندن وضعیت NOCHECK به وضعیت CHECK باید از کلید واژههای WITH NOCHECK استفاده کنیم. چرا که داده هایی در جدول درج شده اند که قید مورد نظر ما را نقض میکنند.
فعال و غیر فعال کردن از طریق design
در قسمت object explorer قید مورد نظر را پیدا کرده و روی آن راست کلیک کرده و گزینه Modify را انتخاب کنید. سپس در پنجره باز شده در قسمت Table Designer تغییرات مورد نظر خود را اعمال کنید.
ما میتوانیم بعد از ساخت جدول و انتشار مقداری داده در آن قیدهایی را ایجاد کنیم. بطور پیشفرض اگر شرط قید ما بر قرار بود قید به طور صحیح ساخته میشود و اگر شرط قید ما بر قرار نباشد قید با خطای conflict مواجه خواهد شد.
بطور کلی غیر فعال کردن قیدها کار درستی نیست. ولی در برخی مواقع برای تسریع در اجرای کد میتوانیم قید را غیر فعال کنیم. بطور مثال اگر یک میلیون داده قرار است در جدول درج شود و مطمئن هستیم که این دادهها جامعیت دادهها را حفظ میکنند آنگاه میتوانیم قید را برای تسریع در عمل درج بطور موفق غیر فعال کنیم.
فعال و غیر فعال کردن از طریق DDL
با غیر فعال کردن قیود دادهها را در وضعیت نامناسبی قرار میدهیم ولی همان طور که اشاره شد بطور موفق اشکالی پیش نخواهد آمد.
در ادامه ابتدا طریقه غیرفعال کردن و مجددا فعال کردن قیود را توسط دستور alter table نشان خواهم داد سپس به سراغ امکانات ویزاردی میرویم. ابتدا یک جدول تک ستونه ایجاد میکنیم:
CREATE TABLE testTable (column1 integer not null);
الان هیچ قیدی روی جدول لحاظ نشده است. پس هر داده که در رنج domain ستون باشد را میتوانیم درج کنیم. پس بطور نمونه این دادهها را درج میکنیم:
INSERT INTO testTable VALUES (-10), (0), (10), (20), (30), (40)
حالا تصمیم داریم قیدی روی ستون column1 بگذاریم که توسط آن تنها اعداد مثبت در جدول درج شوند. پس داریم:
ALTER TABLE testTable WITH CHECK ADD CONSTRAINT NoNegative CHECK (column1 > 0);
The ALTER TABLE statement conflicted with the CHECK constraint "NoNegative".
برای ساخت این قید روی این دادهها تنها راه استفاده از کلید واژههای WITH NOCHECK است یعنی:
ALTER TABLE testTable WITH NOCHECK ADD CONSTRAINT NoNegative CHECK (column1 > 0);
و اکنون سعی میکنیم یک مقدار منفی در جدول درج کنیم:
INSERT INTO testTable VALUES (-5) /* The INSERT statement conflicted with the CHECK constraint "NoNegative". */
اما قیدی که ساخته بودیم در جدول در حال اعمال شدن است. برای درج مقدار منفی باید غیر را غیر فعال کنیم.
ALTER TABLE TestTable NOCHECK CONSTRAINT NoNegative
و حالا مقدار منفی را درج میکنیم. و برای برگرداندن وضعیت NOCHECK به وضعیت CHECK باید از کلید واژههای WITH NOCHECK استفاده کنیم. چرا که داده هایی در جدول درج شده اند که قید مورد نظر ما را نقض میکنند.
ALTER TABLE TestTable WITH NOCHECK CHECK CONSTRAINT NoNegative
فعال و غیر فعال کردن از طریق design
در قسمت object explorer قید مورد نظر را پیدا کرده و روی آن راست کلیک کرده و گزینه Modify را انتخاب کنید. سپس در پنجره باز شده در قسمت Table Designer تغییرات مورد نظر خود را اعمال کنید.
در Blazor 8x میتوان صفحات SSR ای را به همراه Blazor server islands و یا Blazor WASM islands داشت؛ یعنی یک کامپوننت Blazor Server که داخل یک صفحهی معمولی SSR قرار گرفته و با سرور، ارتباط SiganlR برقرار میکند و یا یک کامپوننت Blazor WASM که در قسمتی از صفحهی SSR درج شده و درون مرورگر کاربر اجرا میشود. به هر کدام از اینها یک «جزیرهی تعاملی» گفته میشود (interactive island). در این قسمت، نکات مرتبط با جزایر تعاملی Blazor Server را بررسی میکنیم.
بررسی یک مثال: تهیه یک برنامهی Blazor 8x برای نمایش لیست محصولات، به همراه جزئیات آنها
به لطف وجود SSR در Blazor 8x، میتوان HTML نهایی کامپوننتها و صفحات Blazor را همانند صفحات MVC و یا Razor pages، در سمت سرور تهیه و بازگشت داد. این خروجی در نهایت یک static HTML بیشتر نیست و گاهی از اوقات ما به بیش از یک خروجی ساده HTML ای نیاز داریم.
در این مثال که بر اساس قالب dotnet new blazor --interactivity Server تهیه میشود، قصد داریم موارد زیر را پیاده سازی کنیم:
- صفحهای که یک لیست محصولات فرضی را نمایش میدهد : بر اساس SSR
- صفحهای که جزئیات یک محصول را نمایش میدهد: بر اساس SSR
- دکمهای در ذیل قسمت نمایش جزئیات یک محصول، برای دریافت و نمایش لیست محصولات مشابه و مرتبط: بر اساس Blazor server islands
یعنی تا جائیکه ممکن است قصد نداریم تمام صفحات و تمام قسمتهای برنامه را با فعالسازی سراسری حالت تعاملی Blazor server که در قسمتهای قبل در مورد آن توضیح داده شد، پیاده سازی کنیم. میخواهیم فقط قسمت کوچکی از این سناریو را که واقعا نیاز به یک چنین قابلیتی را دارد، توسط یک جزیرهی تعاملی Blazor server واقع شدهی در قسمتی از یک صفحهی استاتیک SSR، مدیریت کنیم.
مدل برنامه: رکوردی برای ذخیره سازی اطلاعات یک محصول
در اینجا، هدف تعریف لیستی از محصولات فرضی است؛ به همراه خاصیتی که Id محصولات مشابه را نگهداری میکند (خاصیت Related).
سرویس برنامه: سرویسی برای بازگشت لیست محصولات
چون Blazor Server و SSR هر دو بر روی سرور اجرا میشوند، از لحاظ دسترسی به اطلاعات و کار با سرویسها، هماهنگی کاملی وجود داشته و میتوان کدهای یکسان و یکدستی را در اینجا بکار گرفت.
در ادامه کدهای کامل سرویس Services\ProductStore.cs را مشاهده میکنید:
هدف از این سرویس، ارائهی لیست تمام محصولات، دریافت اطلاعات یک محصول و همچنین یافتن لیست محصولات مشابه یک محصول خاص است.
این سرویس را باید در فایل Program.cs برنامه به صورت زیر معرفی کرد تا در فایلهای razor برنامهی جاری قابل دسترسی شود:
تکمیل صفحهی نمایش لیست محصولات
قصد داریم زمانیکه کاربر برای مثال به آدرس فرضی http://localhost:5136/products مراجعه کرد، با تصویر لیستی از محصولات مواجه شود:
کدهای این صفحه را که در فایل Components\Pages\Store\ProductsList.razor قرار میگیرند، در ادامه مشاهده میکنید:
توضیحات:
- جهت دسترسی به سرویس لیست محصولات، ابتدا سرویس IProductStore به این صفحه تزریق شدهاست.
- سپس در روال رویدادگردان آغازین OnInitializedAsync، کار دریافت اطلاعات و انتساب آن به لیستی، صورت گرفتهاست.
- در این متد جهت شبیه سازی یک عملیات async از یک Task.Delay استفاده شدهاست.
- چون این صفحه، یک صفحهی SSR عادی است، بدون تعریف ویژگی StreamRendering در آن، پس از اجرای برنامه، هیچگاه قسمت loading که در حالت products == null_ قرار است ظاهر شود، نمایش داده نمیشود؛ چون در این حالت (حذف نوع رندر)، صفحهی نهایی که به کاربر ارائه خواهد شد، یک صفحهی استاتیک کاملا رندر شدهی در سمت سرور است و کاربر باید تا زمان پایان این رندر در سمت سرور، منتظر بماند و سپس صفحهی نهایی را دریافت و مشاهده کند. در حالت Streaming rendering، ابتدا میتوان یک قالب HTML ای را بازگشت داد و سپس مابقی محتوای آنرا به محض آماده شدن در طی چند مرحله بازگشت داد.
- لینکهای نمایش داده شدهی در اینجا، به صفحهی ProductDetails اشاره میکنند که در آن، جزئیات محصول انتخابی نمایش داده میشوند.
تکمیل صفحهی نمایش جزئیات یک محصول
در صفحهی کامپوننت Components\Pages\Store\ProductDetails.razor، کار نمایش جزئیات محصول انتخابی صورت میگیرد:
توضیحات:
- باتوجه به نحوهی تعریف مسیریابی این صفحه، پارامتر ProductId از طریق آدرسی مانند http://localhost:5136/ProductDetails/1 دریافت میشود.
- سپس این ProductId را در روال رخدادگردان OnInitializedAsync، برای یافتن جزئیات محصول انتخابی از سرویس تزریقی IProductStore، بکار میگیریم.
- در اینجا نیز از Task.Delay برای شبیه سازی یک عملیات طولانی async مانند دریافت اطلاعات از یک بانک اطلاعاتی، کمک گرفته شدهاست.
- همچنین برای نمایش قسمت loading صفحه در حالت SSR، بازهم از StreamRendering استفاده کردهایم.
- اگر دقت کرده باشید، ذیل تصویر اطلاعات محصول، دکمهای نیز جهت بارگذاری اطلاعات محصولات مشابه، قرار دارد که ProductId محصول انتخابی را دریافت میکند:
بنابراین در ادامه کامپوننت RelatedProducts فوق را تکمیل میکنیم.
تکمیل کامپوننت نمایش لیست محصولات مشابه و مرتبط
در فایل Components\Pages\Store\RelatedProducts.razor، کار نمایش یک دکمه و سپس نمایش لیستی از محصولات مشابه، صورت میگیرد:
تعاملی کردن کامپوننت نمایش لیست محصولات مشابه
مشکل! اگر در این حالت برنامه را اجرا کرده و بر روی دکمهی related products کلیک کنیم، هیچ اتفاقی رخ نمیدهد! یعنی روال رویدادگران LoadRelatedProducts اصلا اجرا نمیشود. علت اینجا است که صفحات SSR، در نهایت یک static HTML بیشتر نیستند و فاقد قابلیتهای تعاملی، مانند واکنش نشان دادن به کلیک بر روی یک دکمه هستند.
محدودیتی که به همراه صفحات SSR وجود دارد این است: این نوع کامپوننتها و صفحات فقط یکبار رندر میشوند و نه بیشتر. بله میتوان بر روی آنها دهها دکمه، نوارهای لغزان، دراپداون و غیره را قرار داد، اما ... نمیتوان هیچگونه تعاملی را با آنها داشت. کامپوننت نهایی رندر شده و نمایش داده شده، دیگر در هیچجائی اجرا نمیشود. در این حالت است که میتوان تصمیم گرفت که نیاز است قسمتی از این صفحه، تعاملی شود.
به همین جهت باید نحوهی رندر کامپوننت RelatedProducts را به صورت یک جزیرهی تعاملی Blazor server درآورد تا رویداد منتسب به دکمهی related products موجود در آن، پردازش شود. بنابراین به صفحهی ProductDetails.razor مراجعه کرده و rendermode@ این کامپوننت را به صورت زیر به حالت InteractiveServer تغییر میدهیم:
اکنون اگر برنامه را مجددا اجرا کرده و بر روی دکمهی نمایش محصولات مشابه قرار گرفته در ذیل جزئیات یک محصول کلیک کنیم، بدون مشکل کار میکند:
نحوهی پردازش پشت صحنهی این نوع صفحات هم جالب است. برای اینکار به برگهی network مخصوص developer tools مرورگر مراجعه کرده و مراحل رسیدن به صفحهی نمایش جزئیات محصول را طی میکنیم:
- اگر دقت کنید، جابجایی بین صفحات، با استفاده از fetch انجام شده؛ یعنی با اینکه این صفحات در اصل static HTML خالص هستند، اما ... کار full reload صفحه مانند ASP.NET Web forms قدیمی انجام نمیشود (و یا حتی برنامههای MVC و Razor pages) و نمایش صفحات، Ajax ای است و با fetch استاندارد آن صورت میگیرد تا هنوز هم حس و حال SPA بودن برنامه حفظ شود. همچنین اطلاعات DOM کل صفحه را هم بهروز رسانی نمیکند؛ فقط موارد تغییر یافته در اینجا به روز رسانی خواهند شد.
این موارد توسط فایل blazor.web.js درج شدهی در کامپوننت آغازین App.razor، به صورت خودکار مدیریت میشوند:
به علاوه در این حالت ایجکسی fetch، کار دریافت مجدد فایلهای استاتیک مرتبط یک صفحه، مانند فایلهای js.، css.، تصاویر و غیره، مجددا انجام نمیشود که این مورد خود مزیتی است نسبت به حالت متداول برنامههای ASP.NET Core MVC و یا Razor pages. در حالت Blazor 8x SSR، فقط یک partial update از نوع Ajax ای انجام میشود.
به این قابلیت، enhanced navigation هم گفته میشود. برای مثال زمانیکه یک فرم SSR را در Blazor 8x به سمت سرور ارسال میکنیم، موقعیت scroll به صورت خودکار ذخیره و بازیابی میشود تا کاربر با یک full post back مواجه نشده و موقعیت جاری خود را در صفحه از دست ندهد (چنین ایدهای، یک زمانی در برنامههای ASP.NET Web forms هم برقرار بود و هست! به نظر مایکروسافت هنوز دلتنگ طراحی قدیمی ASP.NET Web forms است!).
- همچنین به محض نمایش صفحهی جزئیات محصول، پس از پایان کار نمایش آن، یک اتصال وبسوکت هم برقرار شده که مرتبط با جزیرهی تعاملی Blazor server تعریف شده، یا همان کامپوننت RelatedProducts است.
- یک disconnect را هم در اینجا مشاهده میکنید. اگر به یک صفحهی تعاملی مراجعه کنیم، همانطور که مشخص است، یک اتصال SignalR برقرار میشود (که به آن در اینجا circuit هم میگویند). اما اگر از این صفحه به سمت یک صفحهی SSR حرکت کنیم، پس از نمایش آن صفحه، اتصال SignalR قبلی که دیگر نیازی به آن نیست، بسته خواهد شد تا منابع سمت سرور، رها شوند.
در حین disconnect، شماره ID اتصال SignalR ای که دیگر به آن نیازی نیست، به برنامه ارسال میشود تا به صورت خودکار در سمت سرور بسته شود. تمام این موارد توسط blazor.web.js فریمورک، مدیریت میشوند.
در این تصویر ابتدا به آدرس http://localhost:5136/ProductDetails/1 مراجعه کردهایم که سبب برقراری اتصال یک وبسوکت شدهاست. سپس با کلیک بر روی دکمهی back، به صفحهی SSR مشاهدهی لیست محصولات برگشتهایم. در این حالت، دستور قطع اتصال SignalR قبلی صادر شدهاست.
نحوهی مدیریت Pre-rendering در جزایر تعاملی Blazor 8x
به صورت پیشفرض زمانیکه از حالت رندر InteractiveServer استفاده میکنیم، قابلیت pre-rendering آن نیز فعال است. یعنی ابتدا حداقل قالب و قسمتهای ثابت کامپوننت، در سمت سرور پردازش و رندر شده و سپس به سمت کلاینت ارسال میشوند. در این حالت کاربر، تجربهی کاربری روانتری را شاهد خواهد بود؛ چون برای مدتی نباید منتظر آماده شدن کل UI مرتبط باشد و حداقل، قسمتهایی از صفحه که تعاملی نیستند، قابل دسترسی و مشاهده هستند.
اگر به هر دلیلی نیاز به غیرفعال کردن این قابلیت را دارید، باید به صورت زیر عمل کرد:
در این حالت اگر برنامه را اجرا کنید، در حین نمایش صفحهی اصلی در برگیرندهی از نوع SSR، فقط جای این کامپوننت در صفحه مشخص میشود و پس از برقراری اتصال با سرور از طریق اتصال SignalR، شاهد UI کامپوننت RelatedProducts خواهیم بود، که نسبت به قبل، وقفهای را سبب خواهد شد.
نحوهی تعریف خواص استاتیک InteractiveServer بکار گرفته شده و یا کلاس InteractiveServerRenderMode را در ادامه مشاهده میکنید. جهت سهولت تعریف این موارد، سطر زیر که یک using static است، به فایل Imports.razor_ اضافه شدهاست:
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید: Blazor8x-Server-Normal.zip
بررسی یک مثال: تهیه یک برنامهی Blazor 8x برای نمایش لیست محصولات، به همراه جزئیات آنها
به لطف وجود SSR در Blazor 8x، میتوان HTML نهایی کامپوننتها و صفحات Blazor را همانند صفحات MVC و یا Razor pages، در سمت سرور تهیه و بازگشت داد. این خروجی در نهایت یک static HTML بیشتر نیست و گاهی از اوقات ما به بیش از یک خروجی ساده HTML ای نیاز داریم.
در این مثال که بر اساس قالب dotnet new blazor --interactivity Server تهیه میشود، قصد داریم موارد زیر را پیاده سازی کنیم:
- صفحهای که یک لیست محصولات فرضی را نمایش میدهد : بر اساس SSR
- صفحهای که جزئیات یک محصول را نمایش میدهد: بر اساس SSR
- دکمهای در ذیل قسمت نمایش جزئیات یک محصول، برای دریافت و نمایش لیست محصولات مشابه و مرتبط: بر اساس Blazor server islands
یعنی تا جائیکه ممکن است قصد نداریم تمام صفحات و تمام قسمتهای برنامه را با فعالسازی سراسری حالت تعاملی Blazor server که در قسمتهای قبل در مورد آن توضیح داده شد، پیاده سازی کنیم. میخواهیم فقط قسمت کوچکی از این سناریو را که واقعا نیاز به یک چنین قابلیتی را دارد، توسط یک جزیرهی تعاملی Blazor server واقع شدهی در قسمتی از یک صفحهی استاتیک SSR، مدیریت کنیم.
مدل برنامه: رکوردی برای ذخیره سازی اطلاعات یک محصول
namespace BlazorDemoApp.Models; public record Product { public int Id { get; set; } public required string Title { get; set; } public required string Description { get; set; } public decimal Price { get; set; } public List<int> Related { get; set; } = new(); }
سرویس برنامه: سرویسی برای بازگشت لیست محصولات
چون Blazor Server و SSR هر دو بر روی سرور اجرا میشوند، از لحاظ دسترسی به اطلاعات و کار با سرویسها، هماهنگی کاملی وجود داشته و میتوان کدهای یکسان و یکدستی را در اینجا بکار گرفت.
در ادامه کدهای کامل سرویس Services\ProductStore.cs را مشاهده میکنید:
using BlazorDemoApp.Models; namespace BlazorDemoApp.Services; public interface IProductStore { IList<Product> GetAllProducts(); Product GetProduct(int id); IList<Product> GetRelatedProducts(int productId); } public class ProductStore : IProductStore { private static readonly List<Product> ProductsDataSource = new() { new Product { Id = 1, Title = "Smart speaker", Price = 22m, Description = "This smart speaker delivers excellent sound quality and comes with built-in voice control, offering an impressive music listening experience.", Related = new List<int> { 2, 3 }, }, new Product { Id = 2, Title = "Regular speaker", Price = 89m, Description = "Enjoy room-filling sound with this regular speaker. With its slick design, it perfectly fits into any room in your house.", Related = new List<int> { 1, 3 }, }, new Product { Id = 3, Title = "Speaker cable", Price = 12m, Description = "This high-quality speaker cable ensures a reliable and clear audio connection for your sound system.", }, }; public IList<Product> GetAllProducts() => ProductsDataSource; public Product GetProduct(int id) => ProductsDataSource.Single(p => p.Id == id); public IList<Product> GetRelatedProducts(int productId) { var product = ProductsDataSource.Single(x => x.Id == productId); return ProductsDataSource.Where(p => product.Related.Contains(p.Id)).ToList(); } }
این سرویس را باید در فایل Program.cs برنامه به صورت زیر معرفی کرد تا در فایلهای razor برنامهی جاری قابل دسترسی شود:
builder.Services.AddScoped<IProductStore, ProductStore>();
تکمیل صفحهی نمایش لیست محصولات
قصد داریم زمانیکه کاربر برای مثال به آدرس فرضی http://localhost:5136/products مراجعه کرد، با تصویر لیستی از محصولات مواجه شود:
کدهای این صفحه را که در فایل Components\Pages\Store\ProductsList.razor قرار میگیرند، در ادامه مشاهده میکنید:
@page "/Products" @using BlazorDemoApp.Models @using BlazorDemoApp.Services @inject IProductStore Store @attribute [StreamRendering] <h3>Products</h3> @if (_products == null) { <p>Loading...</p> } else { @foreach (var item in _products) { <a href="/ProductDetails/@item.Id"> <div> <div> <h5>@item.Title</h5> </div> <div> <h5>@item.Price.ToString("c")</h5> </div> </div> </a> } } @code { private IList<Product>? _products; protected override Task OnInitializedAsync() => GetProductsAsync(); private async Task GetProductsAsync() { await Task.Delay(1000); // Simulates asynchronous loading to demonstrate streaming rendering _products = Store.GetAllProducts(); } }
- جهت دسترسی به سرویس لیست محصولات، ابتدا سرویس IProductStore به این صفحه تزریق شدهاست.
- سپس در روال رویدادگردان آغازین OnInitializedAsync، کار دریافت اطلاعات و انتساب آن به لیستی، صورت گرفتهاست.
- در این متد جهت شبیه سازی یک عملیات async از یک Task.Delay استفاده شدهاست.
- چون این صفحه، یک صفحهی SSR عادی است، بدون تعریف ویژگی StreamRendering در آن، پس از اجرای برنامه، هیچگاه قسمت loading که در حالت products == null_ قرار است ظاهر شود، نمایش داده نمیشود؛ چون در این حالت (حذف نوع رندر)، صفحهی نهایی که به کاربر ارائه خواهد شد، یک صفحهی استاتیک کاملا رندر شدهی در سمت سرور است و کاربر باید تا زمان پایان این رندر در سمت سرور، منتظر بماند و سپس صفحهی نهایی را دریافت و مشاهده کند. در حالت Streaming rendering، ابتدا میتوان یک قالب HTML ای را بازگشت داد و سپس مابقی محتوای آنرا به محض آماده شدن در طی چند مرحله بازگشت داد.
- لینکهای نمایش داده شدهی در اینجا، به صفحهی ProductDetails اشاره میکنند که در آن، جزئیات محصول انتخابی نمایش داده میشوند.
تکمیل صفحهی نمایش جزئیات یک محصول
در صفحهی کامپوننت Components\Pages\Store\ProductDetails.razor، کار نمایش جزئیات محصول انتخابی صورت میگیرد:
@page "/ProductDetails/{ProductId}" @using BlazorDemoApp.Models @using BlazorDemoApp.Services @inject IProductStore Store @attribute [StreamRendering] @if (_product == null) { <p>Loading...</p> } else { <div> <div> <h5> @_product.Title (@_product.Price.ToString("C")) </h5> <p> @_product.Description </p> </div> @if (_product.Related.Count > 0) { <div> <RelatedProducts ProductId="Convert.ToInt32(ProductId)" /> </div> } </div> <NavLink href="/Products">Back</NavLink> } @code { private Product? _product; [Parameter] public string? ProductId { get; set; } protected override Task OnInitializedAsync() => GetProductAsync(); private async Task GetProductAsync() { await Task.Delay(1000); // Simulates asynchronous loading to demonstrate streaming rendering _product = Store.GetProduct(Convert.ToInt32(ProductId)); } }
- باتوجه به نحوهی تعریف مسیریابی این صفحه، پارامتر ProductId از طریق آدرسی مانند http://localhost:5136/ProductDetails/1 دریافت میشود.
- سپس این ProductId را در روال رخدادگردان OnInitializedAsync، برای یافتن جزئیات محصول انتخابی از سرویس تزریقی IProductStore، بکار میگیریم.
- در اینجا نیز از Task.Delay برای شبیه سازی یک عملیات طولانی async مانند دریافت اطلاعات از یک بانک اطلاعاتی، کمک گرفته شدهاست.
- همچنین برای نمایش قسمت loading صفحه در حالت SSR، بازهم از StreamRendering استفاده کردهایم.
- اگر دقت کرده باشید، ذیل تصویر اطلاعات محصول، دکمهای نیز جهت بارگذاری اطلاعات محصولات مشابه، قرار دارد که ProductId محصول انتخابی را دریافت میکند:
<RelatedProducts ProductId="Convert.ToInt32(ProductId)" />
تکمیل کامپوننت نمایش لیست محصولات مشابه و مرتبط
در فایل Components\Pages\Store\RelatedProducts.razor، کار نمایش یک دکمه و سپس نمایش لیستی از محصولات مشابه، صورت میگیرد:
@using BlazorDemoApp.Models @using BlazorDemoApp.Services @inject IProductStore Store <button @onclick="LoadRelatedProducts">Related products</button> @if (_loadRelatedProducts) { @if (_relatedProducts == null) { <p>Loading...</p> } else { <div> @foreach (var item in _relatedProducts) { <a href="/ProductDetails/@item.Id"> <div> <h5>@item.Title (@item.Price.ToString("C"))</h5> </div> </a> } </div> } } @code{ private IList<Product>? _relatedProducts; private bool _loadRelatedProducts; [Parameter] public int ProductId { get; set; } private async Task LoadRelatedProducts() { _loadRelatedProducts = true; await Task.Delay(1000); // Simulates asynchronous loading to demonstrate InteractiveServer mode _relatedProducts = Store.GetRelatedProducts(ProductId); } }
تعاملی کردن کامپوننت نمایش لیست محصولات مشابه
مشکل! اگر در این حالت برنامه را اجرا کرده و بر روی دکمهی related products کلیک کنیم، هیچ اتفاقی رخ نمیدهد! یعنی روال رویدادگران LoadRelatedProducts اصلا اجرا نمیشود. علت اینجا است که صفحات SSR، در نهایت یک static HTML بیشتر نیستند و فاقد قابلیتهای تعاملی، مانند واکنش نشان دادن به کلیک بر روی یک دکمه هستند.
محدودیتی که به همراه صفحات SSR وجود دارد این است: این نوع کامپوننتها و صفحات فقط یکبار رندر میشوند و نه بیشتر. بله میتوان بر روی آنها دهها دکمه، نوارهای لغزان، دراپداون و غیره را قرار داد، اما ... نمیتوان هیچگونه تعاملی را با آنها داشت. کامپوننت نهایی رندر شده و نمایش داده شده، دیگر در هیچجائی اجرا نمیشود. در این حالت است که میتوان تصمیم گرفت که نیاز است قسمتی از این صفحه، تعاملی شود.
به همین جهت باید نحوهی رندر کامپوننت RelatedProducts را به صورت یک جزیرهی تعاملی Blazor server درآورد تا رویداد منتسب به دکمهی related products موجود در آن، پردازش شود. بنابراین به صفحهی ProductDetails.razor مراجعه کرده و rendermode@ این کامپوننت را به صورت زیر به حالت InteractiveServer تغییر میدهیم:
<RelatedProducts ProductId="Convert.ToInt32(ProductId)" @rendermode="@InteractiveServer"/>
نحوهی پردازش پشت صحنهی این نوع صفحات هم جالب است. برای اینکار به برگهی network مخصوص developer tools مرورگر مراجعه کرده و مراحل رسیدن به صفحهی نمایش جزئیات محصول را طی میکنیم:
- اگر دقت کنید، جابجایی بین صفحات، با استفاده از fetch انجام شده؛ یعنی با اینکه این صفحات در اصل static HTML خالص هستند، اما ... کار full reload صفحه مانند ASP.NET Web forms قدیمی انجام نمیشود (و یا حتی برنامههای MVC و Razor pages) و نمایش صفحات، Ajax ای است و با fetch استاندارد آن صورت میگیرد تا هنوز هم حس و حال SPA بودن برنامه حفظ شود. همچنین اطلاعات DOM کل صفحه را هم بهروز رسانی نمیکند؛ فقط موارد تغییر یافته در اینجا به روز رسانی خواهند شد.
این موارد توسط فایل blazor.web.js درج شدهی در کامپوننت آغازین App.razor، به صورت خودکار مدیریت میشوند:
<script src="_framework/blazor.web.js"></script>
به علاوه در این حالت ایجکسی fetch، کار دریافت مجدد فایلهای استاتیک مرتبط یک صفحه، مانند فایلهای js.، css.، تصاویر و غیره، مجددا انجام نمیشود که این مورد خود مزیتی است نسبت به حالت متداول برنامههای ASP.NET Core MVC و یا Razor pages. در حالت Blazor 8x SSR، فقط یک partial update از نوع Ajax ای انجام میشود.
به این قابلیت، enhanced navigation هم گفته میشود. برای مثال زمانیکه یک فرم SSR را در Blazor 8x به سمت سرور ارسال میکنیم، موقعیت scroll به صورت خودکار ذخیره و بازیابی میشود تا کاربر با یک full post back مواجه نشده و موقعیت جاری خود را در صفحه از دست ندهد (چنین ایدهای، یک زمانی در برنامههای ASP.NET Web forms هم برقرار بود و هست! به نظر مایکروسافت هنوز دلتنگ طراحی قدیمی ASP.NET Web forms است!).
- همچنین به محض نمایش صفحهی جزئیات محصول، پس از پایان کار نمایش آن، یک اتصال وبسوکت هم برقرار شده که مرتبط با جزیرهی تعاملی Blazor server تعریف شده، یا همان کامپوننت RelatedProducts است.
- یک disconnect را هم در اینجا مشاهده میکنید. اگر به یک صفحهی تعاملی مراجعه کنیم، همانطور که مشخص است، یک اتصال SignalR برقرار میشود (که به آن در اینجا circuit هم میگویند). اما اگر از این صفحه به سمت یک صفحهی SSR حرکت کنیم، پس از نمایش آن صفحه، اتصال SignalR قبلی که دیگر نیازی به آن نیست، بسته خواهد شد تا منابع سمت سرور، رها شوند.
در حین disconnect، شماره ID اتصال SignalR ای که دیگر به آن نیازی نیست، به برنامه ارسال میشود تا به صورت خودکار در سمت سرور بسته شود. تمام این موارد توسط blazor.web.js فریمورک، مدیریت میشوند.
در این تصویر ابتدا به آدرس http://localhost:5136/ProductDetails/1 مراجعه کردهایم که سبب برقراری اتصال یک وبسوکت شدهاست. سپس با کلیک بر روی دکمهی back، به صفحهی SSR مشاهدهی لیست محصولات برگشتهایم. در این حالت، دستور قطع اتصال SignalR قبلی صادر شدهاست.
نحوهی مدیریت Pre-rendering در جزایر تعاملی Blazor 8x
به صورت پیشفرض زمانیکه از حالت رندر InteractiveServer استفاده میکنیم، قابلیت pre-rendering آن نیز فعال است. یعنی ابتدا حداقل قالب و قسمتهای ثابت کامپوننت، در سمت سرور پردازش و رندر شده و سپس به سمت کلاینت ارسال میشوند. در این حالت کاربر، تجربهی کاربری روانتری را شاهد خواهد بود؛ چون برای مدتی نباید منتظر آماده شدن کل UI مرتبط باشد و حداقل، قسمتهایی از صفحه که تعاملی نیستند، قابل دسترسی و مشاهده هستند.
اگر به هر دلیلی نیاز به غیرفعال کردن این قابلیت را دارید، باید به صورت زیر عمل کرد:
<RelatedProducts ProductId="Convert.ToInt32(ProductId)" @rendermode="@(new InteractiveServerRenderMode(false))"/>
نحوهی تعریف خواص استاتیک InteractiveServer بکار گرفته شده و یا کلاس InteractiveServerRenderMode را در ادامه مشاهده میکنید. جهت سهولت تعریف این موارد، سطر زیر که یک using static است، به فایل Imports.razor_ اضافه شدهاست:
@using static Microsoft.AspNetCore.Components.Web.RenderMode
public static class RenderMode { public static InteractiveServerRenderMode InteractiveServer { get; } = new InteractiveServerRenderMode(); public static InteractiveWebAssemblyRenderMode InteractiveWebAssembly { get; } = new InteractiveWebAssemblyRenderMode(); public static InteractiveAutoRenderMode InteractiveAuto { get; } = new InteractiveAutoRenderMode(); } public class InteractiveServerRenderMode : IComponentRenderMode { public InteractiveServerRenderMode() : this(true) { } public InteractiveServerRenderMode(bool prerender) => this.Prerender = prerender; public bool Prerender { get; } }
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید: Blazor8x-Server-Normal.zip
تا اینجا یک پروژهی خالی ASP.NET Core 1.0 را به مرحلهی فعال سازی ASP.NET MVC و تنظیمات مسیریابیهای اولیهی آن رساندهایم. مرحلهی بعد، افزودن Viewها، نمایش اطلاعاتی به کاربران و دریافت اطلاعات از آنها است و همانطور که پیشتر نیز عنوان شد، برای «ارتقاء» نیاز است «15 مورد» ابتدایی مطالب ASP.NET MVC سایت را پیش از ادامهی این سری مطالعه کنید.
معرفی فایل جدید ViewImports
پروژهی خالی ASP.NET Core 1.0 فاقد پوشهی Views به همراه فایلهای آغازین آن است. بنابراین ابتدا در ریشهی پروژه، پوشهی جدید Views را ایجاد کنید.
فایلهای آغازین این پوشه هم در مقایسهی با نگارشهای قبلی ASP.NET MVC اندکی تغییر کردهاند. برای مثال در نگارش قبلی، فایل web.config ایی در ریشهی پوشهی Views قرار داشت و چندین مقصود را فراهم میکرد:
الف) در آن تنظیم شده بود که هر نوع درخواستی به فایلهای موجود در پوشهی Views، برگشت خورده و قابل پردازش نباشند. این مورد هم از لحاظ مسایل امنیتی اضافه شده بود و هم اینکه در ASP.NET MVC، برخلاف وب فرمها، شروع پردازش یک درخواست، از فایلهای View شروع نمیشود. به همین جهت است که درخواست مستقیم آنها بیمعنا است.
در ASP.NET Core، فایل web.config از این پوشه حذف شدهاست؛ چون دیگر نیازی به آن نیست. اگر مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 4 - فعال سازی پردازش فایلهای استاتیک» را به خاطر داشته باشید، هر پوشهای که توسط میان افزار Static Files عمومی نشود، توسط کاربران برنامه قابل دسترسی نخواهد بود و چون پوشهی Views هم به صورت پیش فرض توسط این میان افزار عمومی نمیشود، نیازی به فایل web.config، جهت قطع دسترسی به فایلهای موجود در آن وجود ندارد.
ب) کاربرد دیگر این فایل web.config، تعریف فضاهای نام پیش فرضی بود که در فایلهای View مورد استفاده قرار میگرفتند. برای مثال چون فضای نام HTML Helperهای استاندارد ASP.NET MVC در این فایل web.config قید شده بود، دیگر نیازی به تکرار آن در تمام فایلهای View برنامه وجود نداشت. در ASP.NET Core، برای جایگزین کردن این قابلیت، فایل جدیدی را به نام ViewImports.cshtml_ معرفی کردهاند، تا دیگر نیازی به ارث بری از فایل web.config وجود نداشته باشد.
برای مثال اگر میخواهید بالای Viewهای خود، مدام ذکر using مربوط به فضای نام مدلها برنامه را انجام ندهید، این سطر تکراری را به فایل جدید view imports منتقل کنید:
و این فضاهای نام به صورت پیش فرض برای تمام viewها مهیا هستند و نیازی به تعریف مجدد، ندارند:
• System
• System.Linq
• System.Collections.Generic
• Microsoft.AspNetCore.Mvc
• Microsoft.AspNetCore.Mvc.Rendering
افزودن یک View جدید
در نگارشهای پیشین ASP.NET MVC، اگر بر روی نام یک اکشن متد کلیک راست میکردیم، در منوی ظاهر شده، گزینهی Add view وجود داشت. چنین گزینهای در نگارش RTM اول ASP.NET Core وجود ندارد و مراحل ایجاد یک View جدید را باید دستی طی کنید. برای مثال اگر نام کلاس کنترلر شما PersonController است، پوشهی Person را به عنوان زیر پوشهی Views ایجاد کرده و سپس بر روی این پوشه کلیک راست کنید، گزینهی add new item را انتخاب و سپس واژهی view را جستجو کنید:
البته یک دلیل این مساله میتواند امکان سفارشی سازی محل قرارگیری این پوشهها در ASP.NET Core نیز باشد که در ادامه آنرا بررسی خواهیم کرد (و ابزارهای از پیش تعریف شده عموما با مکانهای از پیش تعریف شده کار میکنند).
امکان پوشه بندی بهتر فایلهای یک پروژهی ASP.NET Core نسبت به مفهوم Areas در نگارشهای پیشین ASP.NET MVC
حالت پیش فرض پوشه بندی فایلهای اصلی برنامههای ASP.NET MVC، مبتنی بر فناوریها است؛ برای مثال پوشههای views و Controllers و امثال آن تعریف شدهاند.
روش دیگری را که برای پوشه بندی پروژههای ASP.NET MVC پیشنهاد میکنند (که Area توکار آن نیز زیر مجموعهی آن محسوب میشود)، اصطلاحا Feature Folder Structure نام دارد. در این حالت برنامه بر اساس ویژگیها و قابلیتهای مختلف آن پوشه بندی میشود؛ بجای اینکه یک پوشهی کلی کنترلرها را داشته باشیم و یک پوشهی کلی views را که پس از مدتی، ارتباط دادن بین اینها واقعا مشکل میشود.
هرکسی که مدتی با ASP.NET MVC کار کرده باشد حتما به این مشکل برخوردهاست. درحال پیاده سازی قابلیتی هستید و برای اینکار نیاز خواهید داشت مدام بین پوشههای مختلف برنامه سوئیچ کنید؛ از پوشهی کنترلرها به پوشهی ویووها، به پوشهی اسکریپتها، پوشهی اشتراکی ویووها و غیره. پس از رشد برنامه به جایی خواهید رسید که دیگر نمیتوانید تشخیص دهید این فایلی که اضافه شدهاست ارتباطش با سایر قسمتها چیست؟
ایدهی «پوشه بندی بر اساس ویژگیها»، بر مبنای قرار دادن تمام نیازهای یک ویژگی، درون یک پوشهی خاص آن است:
همانطور که مشاهده میکنید، در این حالت تمام اجزای یک ویژگی، داخل یک پوشه قرار گرفتهاند؛ از کنترلر مرتبط با Viewهای آن تا فایلهای css و js خاص آن.
برای پیاده سازی آن:
1) نام پوشهی Views را به Features تغییر دهید.
2) پوشهای را به نام StartupCustomizations به برنامه اضافه کرده و سپس کلاس ذیل را به آن اضافه کنید:
حالت پیش فرض ASP.NET MVC، یافتن فایلها در مسیرهای Views/{1}/{0}.cshtml و Views/Shared/{0}.cshtml است؛ که در اینجا {0} نام view است و {1} نام کنترلر. این ساختار هم در اینجا حفظ شدهاست؛ اما اینبار به پوشهی جدید Features اشاره میکند.
RazorViewEngine برنامه، بر اساس وهلهی پیش فرضی از اینترفیس IViewLocationExpander، محل یافتن Viewها را دریافت میکند. با استفاده از پیاه سازی فوق، این پیش فرضها را به پوشهی features هدایت کردهایم.
3) در ادامه به کلاس آغازین برنامه مراجعه کرده و پس از فعال سازی ASP.NET MVC، این قابلیت را فعال سازی میکنیم:
4) اکنون تمام فایلهای مرتبط با یک ویژگی را به پوشهی خاص آن انتقال دهید. منظور از این فایلها، کنترلر، فایلهای مدل و ویوومدل، فایلهای ویوو و فایلهای js و css هستند و نه مورد دیگری.
5) اکنون باید پوشهی Controllers خالی شده باشد. این پوشه را کلا حذف کنید. از این جهت که کنترلرها بر اساس پیش فرضهای ASP.NET MVC (کلاس ختم شدهی به کلمهی Controller واقع در اسمبلی که از وابستگیهای ASP.NET MVC استفاده میکند) در هر مکانی که تعریف شده باشند، یافت خواهند شد و پوشهی واقع شدن آنها مهم نیست.
6) در آخر به فایل project.json مراجعه کرده و قسمت publish آنرا جهت درج نام پوشهی Features اصلاح کنید (تا در حین توزیع نهایی استفاده شود):
در اینجا نیز یک نمونهی دیگر استفادهی از این روش بسیار معروف را مشاهده میکنید.
امکان ارائهی برنامه بدون ارائهی فایلهای View آن
ASP.NET Core به همراه یک EmbeddedFileProvider نیز هست. حالت پیش فرض آن PhysicalFileProvider است که بر اساس تنظیمات IViewLocationExpander توکار (و یا نمونهی سفارشی فوق در مبحث پوشهی ویژگیها) کار میکند.
برای راه اندازی آن ابتدا نیاز است بستهی نیوگت ذیل را به فایل project.json اضافه کرد:
سپس تنظیمات متد ConfigureServices کلاس آغازین برنامه را به صورت ذیل جهت معرفی EmbeddedFileProvider تغییر میدهیم:
و در آخر در فایل project.json، در قسمت build options، گزینهی embed را مقدار دهی میکنیم:
در اینجا چند نکته را باید مدنظر داشت:
1) اگر نام پوشهی Views را به Features تغییر دادهاید، نیاز به ثبت ViewLocationExpanders آنرا دارید (وگرنه، خیر).
2) در اینجا جهت مثال و بررسی اینکه واقعا این فایلها از اسمبلی برنامه خوانده میشوند، متد options.FileProviders.Clear فراخوانی شدهاست. این متد PhysicalFileProvider پیش فرض را حذف میکند. کار PhysicalFileProvider خواندن فایلهای ویووها از فایل سیستم به صورت متداول است.
3) کار قسمت embed در تنظیمات build، افزودن فایلهای cshtml به قسمت منابع اسمبلی است (به همین جهت دیگر نیازی به توزیع آنها نخواهد بود). اگر صرفا **/Features را ذکر کنید، تمام فایلهای موجود را پیوست میکند. همچنین اگر نام پوشهی Views را تغییر ندادهاید، این مقدار همان Views/**/*.cshtml خواهد بود و یا **/Views
4) در EmbeddedFileProvider میتوان هر نوع اسمبلی را ذکر کرد. یعنی میتوان برنامه را به صورت چندین و چند ماژول تهیه و سپس سرهم و یکپارچه کرد (options.FileProviders یک لیست قابل تکمیل است). در اینجا ذکر baseNamespace نیز مهم است. در غیر اینصورت منبع مورد نظر از اسمبلی یاد شده، قابل استخراج نخواهد بود (چون نام اسمبلی، قسمت اول نام هر منبعی است).
فعال سازی کامپایل خودکار فایلهای View در ASP.NET Core 1.0
این قابلیت به زودی جهت یافتن مشکلات موجود در فایلهای razor پیش از اجرای آنها، اضافه خواهد شد. اطلاعات بیشتر
معرفی فایل جدید ViewImports
پروژهی خالی ASP.NET Core 1.0 فاقد پوشهی Views به همراه فایلهای آغازین آن است. بنابراین ابتدا در ریشهی پروژه، پوشهی جدید Views را ایجاد کنید.
فایلهای آغازین این پوشه هم در مقایسهی با نگارشهای قبلی ASP.NET MVC اندکی تغییر کردهاند. برای مثال در نگارش قبلی، فایل web.config ایی در ریشهی پوشهی Views قرار داشت و چندین مقصود را فراهم میکرد:
الف) در آن تنظیم شده بود که هر نوع درخواستی به فایلهای موجود در پوشهی Views، برگشت خورده و قابل پردازش نباشند. این مورد هم از لحاظ مسایل امنیتی اضافه شده بود و هم اینکه در ASP.NET MVC، برخلاف وب فرمها، شروع پردازش یک درخواست، از فایلهای View شروع نمیشود. به همین جهت است که درخواست مستقیم آنها بیمعنا است.
در ASP.NET Core، فایل web.config از این پوشه حذف شدهاست؛ چون دیگر نیازی به آن نیست. اگر مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 4 - فعال سازی پردازش فایلهای استاتیک» را به خاطر داشته باشید، هر پوشهای که توسط میان افزار Static Files عمومی نشود، توسط کاربران برنامه قابل دسترسی نخواهد بود و چون پوشهی Views هم به صورت پیش فرض توسط این میان افزار عمومی نمیشود، نیازی به فایل web.config، جهت قطع دسترسی به فایلهای موجود در آن وجود ندارد.
ب) کاربرد دیگر این فایل web.config، تعریف فضاهای نام پیش فرضی بود که در فایلهای View مورد استفاده قرار میگرفتند. برای مثال چون فضای نام HTML Helperهای استاندارد ASP.NET MVC در این فایل web.config قید شده بود، دیگر نیازی به تکرار آن در تمام فایلهای View برنامه وجود نداشت. در ASP.NET Core، برای جایگزین کردن این قابلیت، فایل جدیدی را به نام ViewImports.cshtml_ معرفی کردهاند، تا دیگر نیازی به ارث بری از فایل web.config وجود نداشته باشد.
برای مثال اگر میخواهید بالای Viewهای خود، مدام ذکر using مربوط به فضای نام مدلها برنامه را انجام ندهید، این سطر تکراری را به فایل جدید view imports منتقل کنید:
@using MyProject.Models
و این فضاهای نام به صورت پیش فرض برای تمام viewها مهیا هستند و نیازی به تعریف مجدد، ندارند:
• System
• System.Linq
• System.Collections.Generic
• Microsoft.AspNetCore.Mvc
• Microsoft.AspNetCore.Mvc.Rendering
افزودن یک View جدید
در نگارشهای پیشین ASP.NET MVC، اگر بر روی نام یک اکشن متد کلیک راست میکردیم، در منوی ظاهر شده، گزینهی Add view وجود داشت. چنین گزینهای در نگارش RTM اول ASP.NET Core وجود ندارد و مراحل ایجاد یک View جدید را باید دستی طی کنید. برای مثال اگر نام کلاس کنترلر شما PersonController است، پوشهی Person را به عنوان زیر پوشهی Views ایجاد کرده و سپس بر روی این پوشه کلیک راست کنید، گزینهی add new item را انتخاب و سپس واژهی view را جستجو کنید:
البته یک دلیل این مساله میتواند امکان سفارشی سازی محل قرارگیری این پوشهها در ASP.NET Core نیز باشد که در ادامه آنرا بررسی خواهیم کرد (و ابزارهای از پیش تعریف شده عموما با مکانهای از پیش تعریف شده کار میکنند).
امکان پوشه بندی بهتر فایلهای یک پروژهی ASP.NET Core نسبت به مفهوم Areas در نگارشهای پیشین ASP.NET MVC
حالت پیش فرض پوشه بندی فایلهای اصلی برنامههای ASP.NET MVC، مبتنی بر فناوریها است؛ برای مثال پوشههای views و Controllers و امثال آن تعریف شدهاند.
Project - Controllers - Models - Services - ViewModels - Views
هرکسی که مدتی با ASP.NET MVC کار کرده باشد حتما به این مشکل برخوردهاست. درحال پیاده سازی قابلیتی هستید و برای اینکار نیاز خواهید داشت مدام بین پوشههای مختلف برنامه سوئیچ کنید؛ از پوشهی کنترلرها به پوشهی ویووها، به پوشهی اسکریپتها، پوشهی اشتراکی ویووها و غیره. پس از رشد برنامه به جایی خواهید رسید که دیگر نمیتوانید تشخیص دهید این فایلی که اضافه شدهاست ارتباطش با سایر قسمتها چیست؟
ایدهی «پوشه بندی بر اساس ویژگیها»، بر مبنای قرار دادن تمام نیازهای یک ویژگی، درون یک پوشهی خاص آن است:
همانطور که مشاهده میکنید، در این حالت تمام اجزای یک ویژگی، داخل یک پوشه قرار گرفتهاند؛ از کنترلر مرتبط با Viewهای آن تا فایلهای css و js خاص آن.
برای پیاده سازی آن:
1) نام پوشهی Views را به Features تغییر دهید.
2) پوشهای را به نام StartupCustomizations به برنامه اضافه کرده و سپس کلاس ذیل را به آن اضافه کنید:
using System.Collections.Generic; using Microsoft.AspNetCore.Mvc.Razor; namespace Core1RtmEmptyTest.StartupCustomizations { public class FeatureLocationExpander : IViewLocationExpander { public void PopulateValues(ViewLocationExpanderContext context) { context.Values["customviewlocation"] = nameof(FeatureLocationExpander); } public IEnumerable<string> ExpandViewLocations( ViewLocationExpanderContext context, IEnumerable<string> viewLocations) { return new[] { "/Features/{1}/{0}.cshtml", "/Features/Shared/{0}.cshtml" }; } } }
RazorViewEngine برنامه، بر اساس وهلهی پیش فرضی از اینترفیس IViewLocationExpander، محل یافتن Viewها را دریافت میکند. با استفاده از پیاه سازی فوق، این پیش فرضها را به پوشهی features هدایت کردهایم.
3) در ادامه به کلاس آغازین برنامه مراجعه کرده و پس از فعال سازی ASP.NET MVC، این قابلیت را فعال سازی میکنیم:
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.Configure<RazorViewEngineOptions>(options => { options.ViewLocationExpanders.Add(new FeatureLocationExpander()); });
5) اکنون باید پوشهی Controllers خالی شده باشد. این پوشه را کلا حذف کنید. از این جهت که کنترلرها بر اساس پیش فرضهای ASP.NET MVC (کلاس ختم شدهی به کلمهی Controller واقع در اسمبلی که از وابستگیهای ASP.NET MVC استفاده میکند) در هر مکانی که تعریف شده باشند، یافت خواهند شد و پوشهی واقع شدن آنها مهم نیست.
6) در آخر به فایل project.json مراجعه کرده و قسمت publish آنرا جهت درج نام پوشهی Features اصلاح کنید (تا در حین توزیع نهایی استفاده شود):
"publishOptions": { "include": [ "wwwroot", "Features", "appsettings.json", "web.config" ] },
در اینجا نیز یک نمونهی دیگر استفادهی از این روش بسیار معروف را مشاهده میکنید.
امکان ارائهی برنامه بدون ارائهی فایلهای View آن
ASP.NET Core به همراه یک EmbeddedFileProvider نیز هست. حالت پیش فرض آن PhysicalFileProvider است که بر اساس تنظیمات IViewLocationExpander توکار (و یا نمونهی سفارشی فوق در مبحث پوشهی ویژگیها) کار میکند.
برای راه اندازی آن ابتدا نیاز است بستهی نیوگت ذیل را به فایل project.json اضافه کرد:
{ "dependencies": { //same as before "Microsoft.Extensions.FileProviders.Embedded": "1.0.0" },
services.AddMvc(); services.Configure<RazorViewEngineOptions>(options => { options.ViewLocationExpanders.Add(new FeatureLocationExpander()); var thisAssembly = typeof(Startup).GetTypeInfo().Assembly; options.FileProviders.Clear(); options.FileProviders.Add(new EmbeddedFileProvider(thisAssembly, baseNamespace: "Core1RtmEmptyTest")); });
"buildOptions": { "emitEntryPoint": true, "preserveCompilationContext": true, "embed": "Features/**/*.cshtml" },
1) اگر نام پوشهی Views را به Features تغییر دادهاید، نیاز به ثبت ViewLocationExpanders آنرا دارید (وگرنه، خیر).
2) در اینجا جهت مثال و بررسی اینکه واقعا این فایلها از اسمبلی برنامه خوانده میشوند، متد options.FileProviders.Clear فراخوانی شدهاست. این متد PhysicalFileProvider پیش فرض را حذف میکند. کار PhysicalFileProvider خواندن فایلهای ویووها از فایل سیستم به صورت متداول است.
3) کار قسمت embed در تنظیمات build، افزودن فایلهای cshtml به قسمت منابع اسمبلی است (به همین جهت دیگر نیازی به توزیع آنها نخواهد بود). اگر صرفا **/Features را ذکر کنید، تمام فایلهای موجود را پیوست میکند. همچنین اگر نام پوشهی Views را تغییر ندادهاید، این مقدار همان Views/**/*.cshtml خواهد بود و یا **/Views
4) در EmbeddedFileProvider میتوان هر نوع اسمبلی را ذکر کرد. یعنی میتوان برنامه را به صورت چندین و چند ماژول تهیه و سپس سرهم و یکپارچه کرد (options.FileProviders یک لیست قابل تکمیل است). در اینجا ذکر baseNamespace نیز مهم است. در غیر اینصورت منبع مورد نظر از اسمبلی یاد شده، قابل استخراج نخواهد بود (چون نام اسمبلی، قسمت اول نام هر منبعی است).
فعال سازی کامپایل خودکار فایلهای View در ASP.NET Core 1.0
این قابلیت به زودی جهت یافتن مشکلات موجود در فایلهای razor پیش از اجرای آنها، اضافه خواهد شد. اطلاعات بیشتر
پس از آشنایی با «روش کار با فایلهای ایستا در برنامههای React»، اکنون اگر این فایلها ایستا نباشند و توسط یک برنامهی ASP.NET Core بازگشت داده شوند، چطور میتوان از آنها در برنامههای React استفاده کرد؟
برپایی پروژههای مورد نیاز
ابتدا یک پوشهی جدید را مانند DownloadFilesSample، ایجاد کرده و در داخل آن دستور زیر را اجرا میکنیم:
در مورد این قالب که امکان تجربهی توسعهی یکپارچهی ASP.NET Core و React را میسر میکند، در مطلب «روش یکی کردن پروژههای React و ASP.NET Core» بیشتر بحث کردیم.
سپس در این پوشه، پوشهی ClientApp پیشفرض آنرا حذف میکنیم؛ چون کمی قدیمی است. همچنین فایلهای کنترلر و سرویس آب و هوای پیشفرض آنرا به همراه پوشهی صفحات Razor آن، حذف میکنیم.
به علاوه بجای تنظیم پیش فرض زیر در فایل کلاس آغازین برنامه:
از تنظیم زیر استفاده کردهایم تا با هر بار تغییری در کدهای پروژهی ASP.NET، یکبار دیگر از صفر npm start اجرا نشود:
بدیهی است در این حالت باید از طریق خط فرمان به پوشهی clientApp وارد شد و دستور npm start را یکبار به صورت دستی اجرا کرد، تا این وب سرور بر روی پورت 3000، راه اندازی شود.
اکنون در ریشهی پروژهی ASP.NET Core ایجاد شده، دستور زیر را صادر میکنیم تا پروژهی کلاینت React را با فرمت جدید آن ایجاد کند:
سپس وارد این پوشهی جدید شده و بستههای زیر را نصب میکنیم:
توضیحات:
- برای استفاده از شیوهنامههای بوت استرپ، بستهی bootstrap نیز در اینجا نصب میشود که برای افزودن فایل bootstrap.css آن به پروژهی React خود، ابتدای فایل clientapp\src\index.js را به نحو زیر ویرایش خواهیم کرد:
این import به صورت خودکار توسط webpack ای که در پشت صحنه کار bundling & minification برنامه را انجام میدهد، مورد استفاده قرار میگیرد.
- برای دریافت فایلها از سمت سرور، از کتابخانهی معروف axios استفاده خواهیم کرد.
کدهای سمت سرور دریافت فایلهای پویا
در اینجا کدهای سمت سرور برنامه، یک فایل PDF ساده را بازگشت میدهند. این محتوای باینری میتواند حاصل اجرای یک گزارش اکسل، PDF و یا کلا هر نوع فایلی باشد:
فایل بازگشتی فوق که در این مثال در مسیر wwwroot\app_data\sample.pdf برنامهی وب کپی شدهاست، در نهایت با آدرس api/Reports/GetPdfReport در سمت کلاینت قابل دسترسی خواهد بود.
روش دریافت محتوای باینری در برنامههای React
برای دریافت یک محتوای باینری از سرور توسط axios مانند تصاویر، فایلهای PDF و اکسل و غیره، مهمترین نکته، تنظیم ویژگی responseType آن به blob است:
ساخت URL برای دسترسی به اطلاعات باینری
تمام مرورگرهای جدید از ایجاد URL برای اشیاء Blob دریافتی از سمت سرور، توسط متد توکار URL.createObjectURL پشتیبانی میکنند. این متد، شیء URL را از شیء window جاری دریافت میکند و سپس اطلاعات باینری را دریافت کرده و آدرسی را جهت دسترسی موقت به آن تولید میکند. حاصل آن، یک URL ویژهاست مانند blob:https://localhost:5001/03edcadf-89fd-48b9-8a4a-e9acf09afd67 که گشودن آن در مرورگر، یا سبب نمایش آن تصویر و یا دریافت مستقیم فایل خواهد شد.
در ادامه کدهای تبدیل blob دریافت شدهی از سرور را به این URL ویژه، مشاهده میکنید:
توضیحات:
- توسط useEffect Hook و بدون ذکر وابستگی خاصی در آن، سبب شبیه سازی رویداد componentDidUpdate شدهایم. به این معنا که متد getResults فراخوانی شدهی در آن، پس از رندر کامپوننت در DOM فراخوانی میشود و بهترین محلی است که از آن میتوان برای ارسال درخواستهای Ajaxای به سمت سرور و دریافت اطلاعات از backend، استفاده کرد و سپس setState را با اطلاعات جدید فراخوانی نمود. معادل setState در اینجا نیز، همان شیء حالتی است که توسط useState Hook و متد setBlobInfo آن تعریف کردهایم.
- پس از دریافت headers و data از سرور، با استفاده از متد createObjectURL، آنرا تبدیل به یک blob URL کردهایم.
- همچنین در سمت سرور، پارامتر fileDownloadName را نیز تنظیم کردهایم. این نام در سمت کلاینت، توسط هدری با کلید content-disposition ظاهر میشود:
بنابراین میتوان آنرا تجزیه کرد و سپس filename را از آن استخراج نمود.
- اکنون که نام فایل و URL دسترسی به دادهی فایل باینری دریافتی از سرور را استخراج و ایجاد کردهایم. با فراخوانی متد setBlobInfo، سبب تنظیم متغیر حالت blobInfo خواهیم شد. این مورد، رندر مجدد UI را سبب شده و توسط آن میتوان برای مثال فایل PDF دریافتی را نمایش داد.
نمایش فایل PDF دریافتی از سرور، به همراه دکمههای دریافت، چاپ و بازکردن آن در برگهای جدید
در ادامه کدهای کامل قسمت رندر این کامپوننت را مشاهده میکنید:
که چنین خروجی را ایجاد میکند:
در اینجا با انتساب مستقیم blob URL ایجاد شده، به خواص src و یا data اشیائی مانند iframe ،object و یا embed، میتوان سبب نمایش فایل pdf دریافتی از سرور شد. این نمایش نیز توسط قابلیتهای توکار مرورگر صورت میگیرد و نیاز به نصب افزونهی خاصی را ندارد.
در ادامه کدهای مرتبط با سه دکمهی چاپ، دریافت و بازکردن فایل دریافتی از سرور را مشاهده میکنید.
مدیریت دکمهی چاپ PDF
پس از اینکه به blobUrl دسترسی یافتیم، اکنون میتوان یک iframe مخفی را ایجاد کرد، سپس src آنرا به این آدرس ویژه تنظیم نمود و در آخر متد print آنرا فراخوانی کرد که سبب نمایش خودکار دیالوگ چاپ مرورگر میشود:
مدیریت دکمهی نمایش فایل PDF در یک برگهی جدید
اگر علاقمند بودید تا این فایل PDF را به صورت تمام صفحه و در برگهای جدید نمایش دهید، میتوان از متد window.open استفاده کرد:
مدیریت دکمهی دریافت فایل PDF
بجای نمایش فایل PDF میتوان دکمهای را بر روی صفحه قرار داد که با کلیک بر روی آن، این فایل توسط مرورگر به صورت متداولی جهت دریافت به کاربر ارائه شود:
در اینجا یک anchor جدید به صورت مخفی به صفحه اضافه میشود که href آن به blobUrl تنظیم شدهاست و همچنین از فایل fileName استخراجی نیز در اینجا جهت ارائهی نام اصلی فایل دریافتی از سرور، کمک گرفته شدهاست. سپس متد click آن فراخوانی خواهد شد. این روش در مورد تدارک دکمهی دریافت تمام blobهای دریافتی از سرور کاربرد دارد و منحصر به فایلهای PDF نیست.
اگر خواستید عملیات axios.get و دریافت فایل، با هم یکی شوند، میتوان متد handleDownloadPdf را پس از پایان کار await axios.get، فراخوانی کرد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: DownloadFilesSample.zip
برای اجرای آن، پس از صدور فرمان dotnet restore که سبب بازیابی وابستگیهای سمت کلاینت نیز میشود، ابتدا به پوشهی clientapp مراجعه کرده و فایل run.cmd را اجرا کنید. با اینکار react development server بر روی پورت 3000 شروع به کار میکند. سپس به پوشهی اصلی برنامهی ASP.NET Core بازگشته و فایل dotnet_run.bat را اجرا کنید. این اجرا سبب راه اندازی وب سرور برنامه و همچنین ارائهی برنامهی React بر روی پورت 5001 میشود.
برپایی پروژههای مورد نیاز
ابتدا یک پوشهی جدید را مانند DownloadFilesSample، ایجاد کرده و در داخل آن دستور زیر را اجرا میکنیم:
> dotnet new react
سپس در این پوشه، پوشهی ClientApp پیشفرض آنرا حذف میکنیم؛ چون کمی قدیمی است. همچنین فایلهای کنترلر و سرویس آب و هوای پیشفرض آنرا به همراه پوشهی صفحات Razor آن، حذف میکنیم.
به علاوه بجای تنظیم پیش فرض زیر در فایل کلاس آغازین برنامه:
spa.UseReactDevelopmentServer(npmScript: "start");
spa.UseProxyToSpaDevelopmentServer("http://localhost:3000");
اکنون در ریشهی پروژهی ASP.NET Core ایجاد شده، دستور زیر را صادر میکنیم تا پروژهی کلاینت React را با فرمت جدید آن ایجاد کند:
> create-react-app clientapp
> cd clientapp > npm install --save bootstrap axios
- برای استفاده از شیوهنامههای بوت استرپ، بستهی bootstrap نیز در اینجا نصب میشود که برای افزودن فایل bootstrap.css آن به پروژهی React خود، ابتدای فایل clientapp\src\index.js را به نحو زیر ویرایش خواهیم کرد:
import "bootstrap/dist/css/bootstrap.css";
- برای دریافت فایلها از سمت سرور، از کتابخانهی معروف axios استفاده خواهیم کرد.
کدهای سمت سرور دریافت فایلهای پویا
در اینجا کدهای سمت سرور برنامه، یک فایل PDF ساده را بازگشت میدهند. این محتوای باینری میتواند حاصل اجرای یک گزارش اکسل، PDF و یا کلا هر نوع فایلی باشد:
using Microsoft.AspNetCore.Mvc; namespace DownloadFilesSample.Controllers { [Route("api/[controller]")] public class ReportsController : Controller { [HttpGet("[action]")] public IActionResult GetPdfReport() { return File(virtualPath: "~/app_data/sample.pdf", contentType: "application/pdf", fileDownloadName: "sample.pdf"); } } }
روش دریافت محتوای باینری در برنامههای React
برای دریافت یک محتوای باینری از سرور توسط axios مانند تصاویر، فایلهای PDF و اکسل و غیره، مهمترین نکته، تنظیم ویژگی responseType آن به blob است:
const getResults = async () => { const { headers, data } = await axios.get(apiUrl, { responseType: "blob" }); }
ساخت URL برای دسترسی به اطلاعات باینری
تمام مرورگرهای جدید از ایجاد URL برای اشیاء Blob دریافتی از سمت سرور، توسط متد توکار URL.createObjectURL پشتیبانی میکنند. این متد، شیء URL را از شیء window جاری دریافت میکند و سپس اطلاعات باینری را دریافت کرده و آدرسی را جهت دسترسی موقت به آن تولید میکند. حاصل آن، یک URL ویژهاست مانند blob:https://localhost:5001/03edcadf-89fd-48b9-8a4a-e9acf09afd67 که گشودن آن در مرورگر، یا سبب نمایش آن تصویر و یا دریافت مستقیم فایل خواهد شد.
در ادامه کدهای تبدیل blob دریافت شدهی از سرور را به این URL ویژه، مشاهده میکنید:
import axios from "axios"; import React, { useEffect, useState } from "react"; export default function DisplayPdf() { const apiUrl = "https://localhost:5001/api/Reports/GetPdfReport"; const [blobInfo, setBlobInfo] = useState({ blobUrl: "", fileName: "" }); useEffect(() => { getResults(); }, []); const getResults = async () => { try { const { headers, data } = await axios.get(apiUrl, { responseType: "blob" }); console.log("headers", headers); const pdfBlobUrl = window.URL.createObjectURL(data); console.log("pdfBlobUrl", pdfBlobUrl); const fileName = headers["content-disposition"] .split(";") .find(n => n.includes("filename=")) .replace("filename=", "") .trim(); console.log("filename", fileName); setBlobInfo({ blobUrl: pdfBlobUrl, fileName: fileName }); } catch (error) { console.log(error); } };
- توسط useEffect Hook و بدون ذکر وابستگی خاصی در آن، سبب شبیه سازی رویداد componentDidUpdate شدهایم. به این معنا که متد getResults فراخوانی شدهی در آن، پس از رندر کامپوننت در DOM فراخوانی میشود و بهترین محلی است که از آن میتوان برای ارسال درخواستهای Ajaxای به سمت سرور و دریافت اطلاعات از backend، استفاده کرد و سپس setState را با اطلاعات جدید فراخوانی نمود. معادل setState در اینجا نیز، همان شیء حالتی است که توسط useState Hook و متد setBlobInfo آن تعریف کردهایم.
- پس از دریافت headers و data از سرور، با استفاده از متد createObjectURL، آنرا تبدیل به یک blob URL کردهایم.
- همچنین در سمت سرور، پارامتر fileDownloadName را نیز تنظیم کردهایم. این نام در سمت کلاینت، توسط هدری با کلید content-disposition ظاهر میشود:
ontent-disposition: "attachment; filename=sample.pdf; filename*=UTF-8''sample.pdf"
- اکنون که نام فایل و URL دسترسی به دادهی فایل باینری دریافتی از سرور را استخراج و ایجاد کردهایم. با فراخوانی متد setBlobInfo، سبب تنظیم متغیر حالت blobInfo خواهیم شد. این مورد، رندر مجدد UI را سبب شده و توسط آن میتوان برای مثال فایل PDF دریافتی را نمایش داد.
نمایش فایل PDF دریافتی از سرور، به همراه دکمههای دریافت، چاپ و بازکردن آن در برگهای جدید
در ادامه کدهای کامل قسمت رندر این کامپوننت را مشاهده میکنید:
import axios from "axios"; import React, { useEffect, useState } from "react"; export default function DisplayPdf() { // ... const { blobUrl } = blobInfo; return ( <> <h1>Display PDF Files</h1> <button className="btn btn-info" onClick={handlePrintPdf}> Print PDF </button> <button className="btn btn-primary ml-2" onClick={handleShowPdfInNewTab}> Show PDF in a new tab </button> <button className="btn btn-success ml-2" onClick={handleDownloadPdf}> Download PDF </button> <section className="card mb-5 mt-3"> <div className="card-header"> <h4>using iframe</h4> </div> <div className="card-body"> <iframe title="PDF Report" width="100%" height="600" src={blobUrl} type="application/pdf" ></iframe> </div> </section> <section className="card mb-5"> <div className="card-header"> <h4>using object</h4> </div> <div className="card-body"> <object data={blobUrl} aria-label="PDF Report" type="application/pdf" width="100%" height="100%" ></object> </div> </section> <section className="card mb-5"> <div className="card-header"> <h4>using embed</h4> </div> <div className="card-body"> <embed aria-label="PDF Report" src={blobUrl} type="application/pdf" width="100%" height="100%" ></embed> </div> </section> </> ); }
در اینجا با انتساب مستقیم blob URL ایجاد شده، به خواص src و یا data اشیائی مانند iframe ،object و یا embed، میتوان سبب نمایش فایل pdf دریافتی از سرور شد. این نمایش نیز توسط قابلیتهای توکار مرورگر صورت میگیرد و نیاز به نصب افزونهی خاصی را ندارد.
در ادامه کدهای مرتبط با سه دکمهی چاپ، دریافت و بازکردن فایل دریافتی از سرور را مشاهده میکنید.
مدیریت دکمهی چاپ PDF
پس از اینکه به blobUrl دسترسی یافتیم، اکنون میتوان یک iframe مخفی را ایجاد کرد، سپس src آنرا به این آدرس ویژه تنظیم نمود و در آخر متد print آنرا فراخوانی کرد که سبب نمایش خودکار دیالوگ چاپ مرورگر میشود:
const handlePrintPdf = () => { const { blobUrl } = blobInfo; if (!blobUrl) { throw new Error("pdfBlobUrl is null"); } const iframe = document.createElement("iframe"); iframe.style.display = "none"; iframe.src = blobUrl; document.body.appendChild(iframe); if (iframe.contentWindow) { iframe.contentWindow.print(); } };
مدیریت دکمهی نمایش فایل PDF در یک برگهی جدید
اگر علاقمند بودید تا این فایل PDF را به صورت تمام صفحه و در برگهای جدید نمایش دهید، میتوان از متد window.open استفاده کرد:
const handleShowPdfInNewTab = () => { const { blobUrl } = blobInfo; if (!blobUrl) { throw new Error("pdfBlobUrl is null"); } window.open(blobUrl); };
مدیریت دکمهی دریافت فایل PDF
بجای نمایش فایل PDF میتوان دکمهای را بر روی صفحه قرار داد که با کلیک بر روی آن، این فایل توسط مرورگر به صورت متداولی جهت دریافت به کاربر ارائه شود:
const handleDownloadPdf = () => { const { blobUrl, fileName } = blobInfo; if (!blobUrl) { throw new Error("pdfBlobUrl is null"); } const anchor = document.createElement("a"); anchor.style.display = "none"; anchor.href = blobUrl; anchor.download = fileName; document.body.appendChild(anchor); anchor.click(); };
اگر خواستید عملیات axios.get و دریافت فایل، با هم یکی شوند، میتوان متد handleDownloadPdf را پس از پایان کار await axios.get، فراخوانی کرد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: DownloadFilesSample.zip
برای اجرای آن، پس از صدور فرمان dotnet restore که سبب بازیابی وابستگیهای سمت کلاینت نیز میشود، ابتدا به پوشهی clientapp مراجعه کرده و فایل run.cmd را اجرا کنید. با اینکار react development server بر روی پورت 3000 شروع به کار میکند. سپس به پوشهی اصلی برنامهی ASP.NET Core بازگشته و فایل dotnet_run.bat را اجرا کنید. این اجرا سبب راه اندازی وب سرور برنامه و همچنین ارائهی برنامهی React بر روی پورت 5001 میشود.
نحوهی نصب و راه اندازی برنامههای ASP.NET Core را در IIS، پیشتر در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 22 - توزیع برنامه توسط IIS» بررسی کردیم. در این مطلب میخواهیم به تعدادی از خطاهای ممکن در حین راه اندازی اولیهی این نوع برنامهها بپردازیم.
خطای 500.19
این خطا زمانی رخ میدهد که ماژول هاستینگ ASP.NET Core، توسط IIS شناسایی نشده باشد. نصب مجدد آن این مشکل را برطرف میکند.
لیست تمام ماژولهای هاستینگ را همواره در اینجا میتوانید پیدا کنید.
خطای 502.5 و یا گاهی از اوقات 502
باید دقت داشته باشید که اگر تنظیم disableStartUpErrorPage در IIS فعال باشد (قابل افزودن به تگ aspNetCore تنظیمات وب کانفیگ ذیل)، صرفا خطای 502 را دریافت میکنید.
این خطا به معنای شکست در اجرای ماژول هاستینگ ASP.NET Core است و ممکن است به یکی از دلایل ذیل ایجاد شده باشد:
الف) در حین اجرای برنامهی شما، استثنایی در کدهای فایل آغازین startup.cs برنامه، رخ دادهاست.
ب) پورت مورد استفادهی برنامه، توسط پروسهی دیگری در حال استفاده است.
ج) برنامهی شما برای SDK با نگارش 1.1.2 تنظیم و کامپایل شدهاست؛ اما بر روی سرور حداکثر، SDK نگارش 1.1.1 نصب شدهاست.
د) ممکن است پروسهی IIS قادر به یافتن و حتی اجرای dotnet.exe نباشد.
برای لاگ کردن مورد «الف»، باید لاگ کردن خطاهای برنامه را در web.config آن فعالسازی کنید:
چند نکته:
- اگر این مورد به مسیر logs\stdout\. تنظیم شدهاست، باید پوشهی logs را در ریشهی پروژه به صورت دستی ایجاد کنید؛ و گرنه IIS آنرا به صورت خودکار ایجاد نخواهد کرد.
- کاربر App Pool برنامه (با نام پیشفرض « IIS AppPool\DefaultAppPool») باید دسترسی نوشتن در این پوشه را داشته باشد؛ وگرنه فایل لاگی در آن ایجاد نخواهد شد.
- همچنین اگر با رعایت تمام این موارد، محتوای این فایل تولید شده باز هم خالی بود، یکبار IIS را ریاستارت کنید. ممکن است IIS کار نوشتن در فایل لاگ را تمام نکرده باشد و با این کار مجبور به تکمیل و بستن فایل میشود.
- برای حالت «ب» قبل از هر تغییری، یکبار کل سرور را ریاستارت کنید.
- برای مورد «ج» نیز باید آخرین SDK هاستینگ را بر روی سرور نصب کنید.
لیست تمام SDKهای نصب شدهی بر روی سیستم را در مسیر «C:\Program Files\dotnet\sdk» میتوانید مشاهده کنید. همچنین دستور «dotnet --list-sdks» نیز لیست SDKهای نصب شده را نمایش میدهد.
- برای رفع حالت «د»، نیاز است این موارد را بررسی کنید:
1- «Load User Profile» را به true تنظیم کنید.
برای اینکار به قسمت Application pools مراجعه کرده و تنظیمات پیشرفتهی App pool مورد استفاده را ویرایش کنید (تصویر فوق).
این تنظیم برای دائمی کردن کلیدهای رمزنگاری برنامههای ASP.NET Core نیز ضروری است و باید جزو چک لیست نصب برنامههای ASP.NET Core قرار گیرد.
2- مورد «د» حتی میتواند به علت عدم تعریف مسیر «C:\Program Files\dotnet\» در path ویندوز باشد. برای این منظور دستور env:path$ را در power shell اجرا کنید و بررسی کنید که آیا این مسیر در خروجی آن موجود است یا خیر؟ اگر نبود، پس از اضافه کردن آن به path ویندوز، باید یکبار IIS را هم ریست کنید تا این تنظیمات جدید را بخواند.
3- مورد «د» ممکن است به علت اشتباه تنظیم پوشهی اصلی برنامه در IIS نیز باشد. یعنی dotnet.exe قادر به یافتن اسمبلیهای برنامه نیست.
4- برای رفع مورد «د» دو دسترسی دیگر را نیز باید بررسی کنید:
الف) آیا کاربر Application pool برنامه به پوشهی برنامه دسترسی read & execute را دارد یا خیر؟
ب) آیا کاربر Application pool برنامه به پوشهی C:\Program Files\dotnet دسترسی read & execute را دارد یا خیر؟
اگر خیر، نحوهی دسترسی دادن به آنها به صورت زیر است:
خطای 502.3 و یا گاهی از اوقات 500
این خطا به صورت خلاصه به معنای «Bad Gateway: Forwarder Connection Error» است و زمانی رخ میدهد که پروسهی dotnet.exe به درخواست رسیده شده یا پاسخی ندادهاست (مشاهده خطای 0x80072EE2 یا ERROR_WINHTTP_TIMEOUT) و یا بیش از اندازه این پاسخ دهی طول کشیدهاست (این تنظیمات را در configuration editor میتوانید مشاهده کنید که در حقیقت همان تگ aspNetCore در تنظیمات وب کانفیگ فوق است).
برای دیباگ بهتر این مورد نیاز است علاوه بر تنظیم web.config فوق، به فایل appsettings.json مراجعه کرده و سطح پیش فرض لاگ کردن اطلاعات را که warning است به information تغییر دهید:
در این حالت درخواستی که پردازش نشدهاست نیز در لاگها حضور خواهد داشت و ممکن است این درخواست به علت عدم تنظیم CORS بدون پاسخ باقی مانده باشد.
و یا اگر پردازشی دارید که بیش از 2 دقیقه طول میکشد (مطابق تنظیمات تصویر فوق)، میتوانید مقدار request time out را بیشتر کنید.
خطای 0x80004005 : 80008083
خطای 0x80008083 به معنای تداخل نگارشها است و خطای 0x80004005 به معنای مفقود بودن یک فایل یا عدم دسترسی به آن است.
این خطا زمانی رخ میدهد که برنامهی خود را ارتقاء داده باشید، اما ماژول هاستینگ ASP.NET Core را بر روی سرور به روز رسانی نکرده باشید.
خطای 500.19
اگر برنامهی شما از امکانات URL Rewrite خود IIS استفاده میکند، عدم نصب بودن آن بر روی سرور، این خطا را سبب خواهد شد.
برای اینکار ابتدا IIS را متوقف کنید. سپس SDK جدید را نصب و پس از آن IIS را مجددا راه اندازی نمائید.
خطای 503
برنامه اجرا نشده و سطر ذیل در Event Viewer ویندوز قابل مشاهده است:
اگر اخیرا سیستم عامل را ارتقاء دادهاید، ممکن است این خطا را دریافت کنید. راه حل آن نصب مجدد ماژول هاستینگ ASP.NET Core است تا نصب قبلی تعمیر شود.
خطای 500.19
این خطا زمانی رخ میدهد که ماژول هاستینگ ASP.NET Core، توسط IIS شناسایی نشده باشد. نصب مجدد آن این مشکل را برطرف میکند.
لیست تمام ماژولهای هاستینگ را همواره در اینجا میتوانید پیدا کنید.
خطای 502.5 و یا گاهی از اوقات 502
باید دقت داشته باشید که اگر تنظیم disableStartUpErrorPage در IIS فعال باشد (قابل افزودن به تگ aspNetCore تنظیمات وب کانفیگ ذیل)، صرفا خطای 502 را دریافت میکنید.
این خطا به معنای شکست در اجرای ماژول هاستینگ ASP.NET Core است و ممکن است به یکی از دلایل ذیل ایجاد شده باشد:
الف) در حین اجرای برنامهی شما، استثنایی در کدهای فایل آغازین startup.cs برنامه، رخ دادهاست.
ب) پورت مورد استفادهی برنامه، توسط پروسهی دیگری در حال استفاده است.
ج) برنامهی شما برای SDK با نگارش 1.1.2 تنظیم و کامپایل شدهاست؛ اما بر روی سرور حداکثر، SDK نگارش 1.1.1 نصب شدهاست.
د) ممکن است پروسهی IIS قادر به یافتن و حتی اجرای dotnet.exe نباشد.
برای لاگ کردن مورد «الف»، باید لاگ کردن خطاهای برنامه را در web.config آن فعالسازی کنید:
<system.webServer> <handlers> <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified" /> </handlers> <aspNetCore processPath="dotnet" arguments=".\MyApp.dll" stdoutLogEnabled="true" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="true" /> </system.webServer>
- اگر این مورد به مسیر logs\stdout\. تنظیم شدهاست، باید پوشهی logs را در ریشهی پروژه به صورت دستی ایجاد کنید؛ و گرنه IIS آنرا به صورت خودکار ایجاد نخواهد کرد.
- کاربر App Pool برنامه (با نام پیشفرض « IIS AppPool\DefaultAppPool») باید دسترسی نوشتن در این پوشه را داشته باشد؛ وگرنه فایل لاگی در آن ایجاد نخواهد شد.
- همچنین اگر با رعایت تمام این موارد، محتوای این فایل تولید شده باز هم خالی بود، یکبار IIS را ریاستارت کنید. ممکن است IIS کار نوشتن در فایل لاگ را تمام نکرده باشد و با این کار مجبور به تکمیل و بستن فایل میشود.
- برای حالت «ب» قبل از هر تغییری، یکبار کل سرور را ریاستارت کنید.
- برای مورد «ج» نیز باید آخرین SDK هاستینگ را بر روی سرور نصب کنید.
لیست تمام SDKهای نصب شدهی بر روی سیستم را در مسیر «C:\Program Files\dotnet\sdk» میتوانید مشاهده کنید. همچنین دستور «dotnet --list-sdks» نیز لیست SDKهای نصب شده را نمایش میدهد.
- برای رفع حالت «د»، نیاز است این موارد را بررسی کنید:
1- «Load User Profile» را به true تنظیم کنید.
برای اینکار به قسمت Application pools مراجعه کرده و تنظیمات پیشرفتهی App pool مورد استفاده را ویرایش کنید (تصویر فوق).
این تنظیم برای دائمی کردن کلیدهای رمزنگاری برنامههای ASP.NET Core نیز ضروری است و باید جزو چک لیست نصب برنامههای ASP.NET Core قرار گیرد.
2- مورد «د» حتی میتواند به علت عدم تعریف مسیر «C:\Program Files\dotnet\» در path ویندوز باشد. برای این منظور دستور env:path$ را در power shell اجرا کنید و بررسی کنید که آیا این مسیر در خروجی آن موجود است یا خیر؟ اگر نبود، پس از اضافه کردن آن به path ویندوز، باید یکبار IIS را هم ریست کنید تا این تنظیمات جدید را بخواند.
3- مورد «د» ممکن است به علت اشتباه تنظیم پوشهی اصلی برنامه در IIS نیز باشد. یعنی dotnet.exe قادر به یافتن اسمبلیهای برنامه نیست.
4- برای رفع مورد «د» دو دسترسی دیگر را نیز باید بررسی کنید:
الف) آیا کاربر Application pool برنامه به پوشهی برنامه دسترسی read & execute را دارد یا خیر؟
ب) آیا کاربر Application pool برنامه به پوشهی C:\Program Files\dotnet دسترسی read & execute را دارد یا خیر؟
اگر خیر، نحوهی دسترسی دادن به آنها به صورت زیر است:
Right click on the folder -> Properties -> Security tab -> Click at Edit button -> Enter `IIS AppPool\DefaultAppPool` user (IIS AppPool\<app_pool_name>) -> Click at Check names -> OK -> Then give it `read & execute` or other permissions.
خطای 502.3 و یا گاهی از اوقات 500
این خطا به صورت خلاصه به معنای «Bad Gateway: Forwarder Connection Error» است و زمانی رخ میدهد که پروسهی dotnet.exe به درخواست رسیده شده یا پاسخی ندادهاست (مشاهده خطای 0x80072EE2 یا ERROR_WINHTTP_TIMEOUT) و یا بیش از اندازه این پاسخ دهی طول کشیدهاست (این تنظیمات را در configuration editor میتوانید مشاهده کنید که در حقیقت همان تگ aspNetCore در تنظیمات وب کانفیگ فوق است).
برای دیباگ بهتر این مورد نیاز است علاوه بر تنظیم web.config فوق، به فایل appsettings.json مراجعه کرده و سطح پیش فرض لاگ کردن اطلاعات را که warning است به information تغییر دهید:
"Console": { "LogLevel": { "Default": "Information" } }
و یا اگر پردازشی دارید که بیش از 2 دقیقه طول میکشد (مطابق تنظیمات تصویر فوق)، میتوانید مقدار request time out را بیشتر کنید.
خطای 0x80004005 : 80008083
Application ‘<IIS path>’ with physical root ‘<Application path>’ failed to start process with commandline ‘”dotnet” .\MyApp.dll’, ErrorCode = ‘0x80004005 : 80008083.
این خطا زمانی رخ میدهد که برنامهی خود را ارتقاء داده باشید، اما ماژول هاستینگ ASP.NET Core را بر روی سرور به روز رسانی نکرده باشید.
خطای 500.19
HTTP Error 500.19 - Internal Server Error The requested page cannot be accessed because the related configuration data for the page is invalid.
برای اینکار ابتدا IIS را متوقف کنید. سپس SDK جدید را نصب و پس از آن IIS را مجددا راه اندازی نمائید.
خطای 503
برنامه اجرا نشده و سطر ذیل در Event Viewer ویندوز قابل مشاهده است:
The Module DLL C:\WINDOWS\system32\inetsrv\aspnetcore.dll failed to load. The data is the error.
پیشنیاز این بحث مطالعهی مطلب «صفحه بندی و مرتب سازی خودکار اطلاعات به کمک jqGrid در ASP.NET MVC» است و در اینجا جهت کوتاه شدن بحث، صرفا به تغییرات مورد نیاز جهت اعمال بر روی مثال اول اکتفاء خواهد شد.
تغییرات مورد نیاز سمت کلاینت جهت فعال سازی جستجو در jqGrid
در سمت کلاینت، در حین تعریف ستونها، ابتدا باید توسط مقدار دهی خاصیت search، ستونهای مشارکت کنندهی در حین جستجو را مشخص کرد:
- برای نمونه در اینجا search: true، جهت دو ستون نام محصول و نام تولید کننده، تنظیم شدهاند.
- stype، روش مقایسهی مقادیر را مشخص میکند. مقدار پیش فرض آن text است و مقادیری مانند int، integer، float، number، numeric، date و datetime را میپذیرد.
- searchoptions برای تنظیم جزئیات نحوهی جستجوی بر روی فیلدها بکار میرود. توسط sopt میتوان آرایهای با مقادیر ذیل را مقدار دهی کرد:
eq به معنای مساوی است، ne، مساوی نیست، lt کمتر و به همین ترتیب.
البته باید دقت داشت که آرایه فوق کاملترین حالت ممکن است و ضرورتی ندارد تمام حالات را برای یک فیلد تعریف کرد. چون برای مثال جستجو cn یا contains برای مقادیر رشتهای معنا دارد و نه سایر حالات.
- searchoptions گزینههای دیگری را نیز میتواند شامل شود. برای مثال در حین نمایش جستجوی داخل ردیفی یا صفحهی دیالوگ مخصوص آن، قصد داریم فیلد نام تولید کننده را توسط یک drop down نمایش دهیم و نه یک text box پیش فرض. برای این منظور dataUrl مقدار دهی شدهاست.
SuppliersSelect آن به اکشن متد ذیل اشاره میکند که لیست تولید کنندگان را با فرمت لیستی از SelectListItemها به یک partial view تولید کنندهی دراپ داون ارسال میکند:
و محتوای فایل _SelectPartial نیز به صورت ذیل است:
در این حالت با نمایش صفحهی جستجو و انتخاب فیلد نام تولید کننده، به صورت خودکار یک dorp down پویا نمایش داده خواهد شد.
- اگر دقت کرده باشید، نام فیلد تولید کننده با Supplier.Id مقدار دهی شدهاست. علت اینجا است که در زمان استفاده از drop down، مقدار Id آیتم انتخابی، به سرور ارسال میشود. به همین جهت کار کردن پویا با Supplier.Id سادهتر خواهد بود.
- برای نمایش دیالوگ و یا نوار ابزار توکار جستجو، میتوان دکمههایی را به فوتر گرید اضافه کرد:
در اینجا نکات ذیل قابل توجه هستند:
- با استفاده از متد jqGrid و پارامتر navGrid، در ناحیهی pager گرید، تنظیمات جستجو را فعال کردهایم.
multipleSearch به معنای امکان جستجوی همزمان بر روی بیش از یک فیلد است. closeOnEscape سبب بسته شدن صفحهی دیالوگ جستجو با فشردن دکمهی ESC میشود. اگر closeAfterSearch به true تنظیم نشود، صفحهی دیالوگ جستجو پس از جستجو، در صفحه باقی مانده و بسته نخواهد شد.
- این دکمهی جستجو، جزو موارد توکار jqGrid است. اگر قصد داشته باشیم یک دکمهی سفارشی دیگر را نیز اضافه کنیم، مجددا از متد jqGrid با پارامتر navButtonAdd در ناحیهی pager استفاده خواهیم کرد. کلیک بر روی آن سبب اجرای متد toolbarSearching میشود.
در اینجا حداقل سه نوع جستجو را میتوان فعال کرد:
- filterToolbar که سبب نمایش نوار ابزار جستجو، دقیقا بالای ستونهای جدول میشود.
- searchGrid که صفحهی دیالوگ مستقلی را جهت جستجو به صورت پویا تولید میکند. اگر خاصیت multipleSearch آن به true تنظیم نشود، این جستجو هربار تنها بر روی یک فیلد قابل انجام خواهد بود و برعکس.
- در حالت جستجوی نوار ابزاری، اگر خواص searchOperators و stringResult به true تنظیم شوند، مانند تصویر ذیل، به ازای هر ستون میتوان از عملگرهای مختلفی استفاده کرد. در غیراینصورت جستجوی انجام شده بر اساس groupOp و defaultSearch پیش فرض انجام میشود. یعنی And یا Or تمام موارد تنها در حالت مثلا contains یا تساوی و امثال آن و نه حالت پیشرفتهی انتخاب عملگرها توسط کاربر.
یک نکته
اگر میخواهید صفحهی جستجو در وسط صفحه ظاهر شود، میتوانید از تنظیمات CSS ذیل استفاده کنید:
پردازش سمت سرور جستجوی پویای jqGrid
کدهای سمت سرور، با کدهای استفاده از dynamic LINQ مایکروسافت یکی است. با این تفاوت که اینبار قسمت where این کوئری نیز پویا میباشد. پیشتر قسمت order by را پویا پردازش کرده بودیم. برای ساخت where پویا که در dynamic LINQ به خوبی پشتیبانی میشود، باید ابتدا ساختار اطلاعات ارسالی به سرور را آنالیز کنیم:
در اینجا ساختار ارسالی به سرور را در سه حالت مختلف جستجوی پویای jqGrid، ملاحظه میکنید:
- در تمام این حالات پارامتر _search مساوی true است (تفاوت آن با درخواست اطلاعات معمولی).
- در حالت جستجوی نوار ابزاری، اگر گزینههای searchOperators و stringResult به true تنظیم نشوند، حالت toolbar search فوق را شاهد خواهیم بود. در غیراینصورت به حالت multi-field search سوئیچ میشود.
- در حالت جستجوی تک فیلدی توسط صفحه دیالوگ جستجوی jqGrid، فیلد در حال جستجو توسط searchField و مقدار آن توسط searchString به سرور ارسال شدهاند. مابقی پارامترها نال هستند.
- در حالت جستجوی چند فیلدی توسط صفحه دیالوگ جستجوی jqGrid، اینبار filters مقدار دهی شدهاست و سایر پارامترها نال هستند. مقدار filters ارسالی، در حقیقت یک شیء JSON است با ساختار کلی ذیل:
که میتوان چنین ساختاری را برای آن متصور شد:
در اینجا AND و Or کلی مشخص میشود، به همراه فیلدهای ارسالی به سرور، عملگرهای اعمالی بر روی آنها و مقادیر مرتبط.
اگر این موارد را کنار هم قرار دهیم، به متدی عمومی ApplyFilter با امضای ذیل خواهیم رسید.
کدهای کامل آنرا به علت طولانی بودن پردازش سه حالت ذکر شدهی فوق، از پروژهی پیوست میتوانید دریافت کنید.
پس از آن، تغییراتی که در کدهای متد GetProducts باید اعمال شوند به صورت ذیل است:
- ابتدا چند پارامتر اضافهتر به امضای متد اضافه شدهاند، تا فیلدهای جستجو را نیز دریافت کنند.
- نوع متد به HttpPost تغییر کردهاست. این مورد برای ارسال اطلاعات حجیم جستجوها به سرور ضروری است و بهتر است از حالت Get استفاده نشود.
این حالت در سمت کلاینت نیز باید تنظیم شود:
- سایر سطرها مانند قبل است؛ فقط یک سطر ذیل جهت اعمال where پویا به عبارت LINQ ساخته شده، اضافه شدهاست:
برای مطالعه بیشتر
جستجوی تک فیلدی
جستجوی نوار ابزاری
جستجوی چند فیلدی
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید
jqGrid03.zip
تغییرات مورد نیاز سمت کلاینت جهت فعال سازی جستجو در jqGrid
در سمت کلاینت، در حین تعریف ستونها، ابتدا باید توسط مقدار دهی خاصیت search، ستونهای مشارکت کنندهی در حین جستجو را مشخص کرد:
colModel: [ { name: 'Name', index: 'Name', align: 'right', width: 200, search: true, stype: 'text', searchoptions: { sopt: searchOptions } }, { name: 'Supplier.Id', index: 'Supplier.Id', align: 'right', width: 200, search: true, stype: 'select', edittype: 'select', searchOperators: true, searchoptions: { sopt: searchOptions, dataUrl: '@Url.Action("SuppliersSelect","Home")' } } ],
- stype، روش مقایسهی مقادیر را مشخص میکند. مقدار پیش فرض آن text است و مقادیری مانند int، integer، float، number، numeric، date و datetime را میپذیرد.
- searchoptions برای تنظیم جزئیات نحوهی جستجوی بر روی فیلدها بکار میرود. توسط sopt میتوان آرایهای با مقادیر ذیل را مقدار دهی کرد:
var searchOptions = ['eq', 'ne', 'lt', 'le', 'gt', 'ge', 'bw', 'bn', 'in', 'ni', 'ew', 'en', 'cn', 'nc'];
البته باید دقت داشت که آرایه فوق کاملترین حالت ممکن است و ضرورتی ندارد تمام حالات را برای یک فیلد تعریف کرد. چون برای مثال جستجو cn یا contains برای مقادیر رشتهای معنا دارد و نه سایر حالات.
- searchoptions گزینههای دیگری را نیز میتواند شامل شود. برای مثال در حین نمایش جستجوی داخل ردیفی یا صفحهی دیالوگ مخصوص آن، قصد داریم فیلد نام تولید کننده را توسط یک drop down نمایش دهیم و نه یک text box پیش فرض. برای این منظور dataUrl مقدار دهی شدهاست.
SuppliersSelect آن به اکشن متد ذیل اشاره میکند که لیست تولید کنندگان را با فرمت لیستی از SelectListItemها به یک partial view تولید کنندهی دراپ داون ارسال میکند:
public ActionResult SuppliersSelect() { var list = ProductDataSource.LatestProducts; var suppliers = list.Select(x => new SelectListItem { Text = x.Supplier.CompanyName, Value = x.Supplier.Id.ToString(CultureInfo.InvariantCulture) }).ToList(); return PartialView("_SelectPartial", suppliers); }
@model IList<SelectListItem> @Html.DropDownList("srch", Model)
- اگر دقت کرده باشید، نام فیلد تولید کننده با Supplier.Id مقدار دهی شدهاست. علت اینجا است که در زمان استفاده از drop down، مقدار Id آیتم انتخابی، به سرور ارسال میشود. به همین جهت کار کردن پویا با Supplier.Id سادهتر خواهد بود.
- برای نمایش دیالوگ و یا نوار ابزار توکار جستجو، میتوان دکمههایی را به فوتر گرید اضافه کرد:
$(document).ready(function () { $('#list').jqGrid({ // ... مانند قبل و توضیحات فوق }) .jqGrid('navGrid', '#pager', { add: false, edit: false, del: false }, {}, // default settings for edit {}, // default settings for add {}, // delete instead that del:false we need this { // search options multipleSearch: true, closeOnEscape: true, closeAfterSearch: true, ignoreCase: true }) .jqGrid('navButtonAdd', "#pager", { caption: "نوار ابزار جستجو", title: "Search Toolbar", buttonicon: 'ui-icon-search', onClickButton: function () { toolbarSearching(); } }); }); function toolbarSearching() { $('#list').filterToolbar({ groupOp: 'OR', defaultSearch: "cn", autosearch: true, searchOnEnter: true, searchOperators: true, // فعال سازی منوی اپراتورها stringResult : true // وجود این سطر سبب میشود تا اپراتورها به سرور ارسال شوند }); }; function singleSearching() { $('#list').searchGrid({ closeAfterSearch: true }); }; function advancedSearching() { $('#list').searchGrid({ multipleSearch: true, closeAfterSearch: true }); };
- با استفاده از متد jqGrid و پارامتر navGrid، در ناحیهی pager گرید، تنظیمات جستجو را فعال کردهایم.
multipleSearch به معنای امکان جستجوی همزمان بر روی بیش از یک فیلد است. closeOnEscape سبب بسته شدن صفحهی دیالوگ جستجو با فشردن دکمهی ESC میشود. اگر closeAfterSearch به true تنظیم نشود، صفحهی دیالوگ جستجو پس از جستجو، در صفحه باقی مانده و بسته نخواهد شد.
- این دکمهی جستجو، جزو موارد توکار jqGrid است. اگر قصد داشته باشیم یک دکمهی سفارشی دیگر را نیز اضافه کنیم، مجددا از متد jqGrid با پارامتر navButtonAdd در ناحیهی pager استفاده خواهیم کرد. کلیک بر روی آن سبب اجرای متد toolbarSearching میشود.
در اینجا حداقل سه نوع جستجو را میتوان فعال کرد:
- filterToolbar که سبب نمایش نوار ابزار جستجو، دقیقا بالای ستونهای جدول میشود.
- searchGrid که صفحهی دیالوگ مستقلی را جهت جستجو به صورت پویا تولید میکند. اگر خاصیت multipleSearch آن به true تنظیم نشود، این جستجو هربار تنها بر روی یک فیلد قابل انجام خواهد بود و برعکس.
- در حالت جستجوی نوار ابزاری، اگر خواص searchOperators و stringResult به true تنظیم شوند، مانند تصویر ذیل، به ازای هر ستون میتوان از عملگرهای مختلفی استفاده کرد. در غیراینصورت جستجوی انجام شده بر اساس groupOp و defaultSearch پیش فرض انجام میشود. یعنی And یا Or تمام موارد تنها در حالت مثلا contains یا تساوی و امثال آن و نه حالت پیشرفتهی انتخاب عملگرها توسط کاربر.
یک نکته
اگر میخواهید صفحهی جستجو در وسط صفحه ظاهر شود، میتوانید از تنظیمات CSS ذیل استفاده کنید:
/* align center search popup in jqgrid */ .ui-jqdialog { display: none; width: 300px; position: absolute; padding: .2em; font-size: 11px; overflow: visible; left: 30% !important; top: 40% !important; }
پردازش سمت سرور جستجوی پویای jqGrid
کدهای سمت سرور، با کدهای استفاده از dynamic LINQ مایکروسافت یکی است. با این تفاوت که اینبار قسمت where این کوئری نیز پویا میباشد. پیشتر قسمت order by را پویا پردازش کرده بودیم. برای ساخت where پویا که در dynamic LINQ به خوبی پشتیبانی میشود، باید ابتدا ساختار اطلاعات ارسالی به سرور را آنالیز کنیم:
//single field search //_search=true&nd=1403935889318&rows=10&page=1&sidx=Id&sord=asc&searchField=Id&searchString=4444&searchOper=eq&filters= //multi-field search //_search=true&nd=1403935941367&rows=10&page=1&sidx=Id&sord=asc&filters=%7B%22groupOp%22%3A%22AND%22%2C%22rules%22%3A%5B%7B%22field%22%3A%22Id%22%2C%22op%22%3A%22eq%22%2C%22data%22%3A%2244%22%7D%2C%7B%22field%22%3A%22SupplierID%22%2C%22op%22%3A%22eq%22%2C%22data%22%3A%221%22%7D%5D%7D&searchField=&searchString=&searchOper= // filters -> {"groupOp":"AND","rules":[{"field":"All","op":"cn","data":"fffff"},{"field":"Price","op":"bn","data":"ffff"}]} //toolbar search //_search=true&nd=1403935593036&rows=10&page=1&sidx=Id&sord=asc&Id=2&Name=333&SupplierID=1&CategoryID=1&Price=44
- در تمام این حالات پارامتر _search مساوی true است (تفاوت آن با درخواست اطلاعات معمولی).
- در حالت جستجوی نوار ابزاری، اگر گزینههای searchOperators و stringResult به true تنظیم نشوند، حالت toolbar search فوق را شاهد خواهیم بود. در غیراینصورت به حالت multi-field search سوئیچ میشود.
- در حالت جستجوی تک فیلدی توسط صفحه دیالوگ جستجوی jqGrid، فیلد در حال جستجو توسط searchField و مقدار آن توسط searchString به سرور ارسال شدهاند. مابقی پارامترها نال هستند.
- در حالت جستجوی چند فیلدی توسط صفحه دیالوگ جستجوی jqGrid، اینبار filters مقدار دهی شدهاست و سایر پارامترها نال هستند. مقدار filters ارسالی، در حقیقت یک شیء JSON است با ساختار کلی ذیل:
{ "groupOp": "AND", "groups" : [ { "groupOp": "OR", "rules": [ { "field": "name", "op": "eq", "data": "England" }, { "field": "id", "op": "le", "data": "5"} ] } ], "rules": [ { "field": "name", "op": "eq", "data": "Romania" }, { "field": "id", "op": "le", "data": "1"} ] }
public class SearchFilter { public string groupOp { set; get; } public List<SearchGroup> groups { set; get; } public List<SearchRule> rules { set; get; } } public class SearchRule { public string field { set; get; } public string op { set; get; } public string data { set; get; } public override string ToString() { return string.Format("'{0}' {1} '{2}'", field, op, data); } } public class SearchGroup { public string groupOp { set; get; } public List<SearchRule> rules { set; get; } }
اگر این موارد را کنار هم قرار دهیم، به متدی عمومی ApplyFilter با امضای ذیل خواهیم رسید.
public IQueryable<T> ApplyFilter<T>(IQueryable<T> query, bool _search, string searchField, string searchString, string searchOper, string filters, NameValueCollection form)
پس از آن، تغییراتی که در کدهای متد GetProducts باید اعمال شوند به صورت ذیل است:
[HttpPost] public ActionResult GetProducts(string sidx, string sord, int page, int rows, bool _search, string searchField, string searchString, string searchOper, string filters) { var list = ProductDataSource.LatestProducts; var pageIndex = page - 1; var pageSize = rows; var totalRecords = list.Count; var totalPages = (int)Math.Ceiling(totalRecords / (float)pageSize); var productsQuery = list.AsQueryable(); productsQuery = new JqGridSearch().ApplyFilter(productsQuery, _search, searchField, searchString, searchOper, filters, this.Request.Form); var productsList = productsQuery.OrderBy(sidx + " " + sord) .Skip(pageIndex * pageSize) .Take(pageSize) .ToList(); var productsData = new JqGridData { Total = totalPages, Page = page, Records = totalRecords, Rows = (productsList.Select(product => new JqGridRowData { Id = product.Id, RowCells = new List<string> { product.Id.ToString(CultureInfo.InvariantCulture), product.Name, product.Supplier.CompanyName, product.Category.Name, product.Price.ToString(CultureInfo.InvariantCulture) } })).ToArray() }; return Json(productsData, JsonRequestBehavior.AllowGet); }
- نوع متد به HttpPost تغییر کردهاست. این مورد برای ارسال اطلاعات حجیم جستجوها به سرور ضروری است و بهتر است از حالت Get استفاده نشود.
این حالت در سمت کلاینت نیز باید تنظیم شود:
$('#list').jqGrid({ //url access method type mtype: 'POST',
productsQuery = new JqGridSearch().ApplyFilter(productsQuery, _search, searchField, searchString, searchOper, filters, this.Request.Form);
برای مطالعه بیشتر
جستجوی تک فیلدی
جستجوی نوار ابزاری
جستجوی چند فیلدی
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید
jqGrid03.zip