در انتهای مطلب «فرمهای مبتنی بر قالبها در Angular - قسمت پنجم - ارسال اطلاعات به سرور» در مورد «بارگذاری اطلاعات drop down از سرور» بحث شد. در اینجا میخواهیم قبل از نمایش اطلاعات این drop down در رابط کاربری، بر روی سطر this.languages = data در VSCode، یک break-point را قرار دهیم و اطلاعات دریافتی از سرور را بررسی کنیم.
پیشنیازها
در اینجا فرض بر این است که موارد ذیل را نصب کردهاید:
- آخرین نگارش مرورگر Chrome
- افزونهی Debugger for Chrome که از آن برای دیباگ کدهای جاوا اسکریپتی و تایپاسکریپتی یکپارچهی با VSCode میتوان استفاده کرد.
تنظیمات VSCode جهت دیباگ برنامههای مبتنی بر Angular CLI
کدهای مطلب «فرمهای مبتنی بر قالبها در Angular - قسمت پنجم - ارسال اطلاعات به سرور» از انتهای بحث آن قابل دریافت هستند. این کدها را دریافت کرده و سپس پوشهی اصلی آنرا در VSCode باز کنید. اگر در ویندوز هستید با کلیک راست بر روی پوشه، گزینهی Open With Code ظاهر میشود و یا حتی میتوان از طریق خط فرمان به پوشهی اصلی برنامه مراجعه کرده و سپس دستور . code را صادر نمود.
در ادامه نیاز است دیباگر VSCode را تنظیم کنیم. اگر پیشتر هیچ تنظیمی را نداشته باشید، پس از مراجعهی به برگهی debug آن، بر روی دکمهی سبز رنگ Start Debugging آن کلیک کنید. در ادامه در منوی باز شده، گزینهی Chrome را انتخاب کنید:
اینکار سبب میشود تا فایل جدید vscode\launch.json. به پوشهی جاری اضافه شده و سپس تنظیمات دیباگر کروم در آن قرار گیرند.
حالت دوم شبیه به حالتی است که برنامهی مورد بحث جاری دارد: پیشتر دیباگری مانند NET Core. در آن تنظیم شدهاست. در این حالت، منوی drop down مربوط به دیباگرهای تنظیم شده را گشوده و سپس گزینهی آخر آن یعنی Add configuration را انتخاب کنید:
در اینجا نیز منوی Intellisense آن ظاهر شده و امکان انتخاب گزینهی Chrome را میدهد:
در نهایت هر دو حالت به ایجاد فایل جدید vscode\launch.json. و یا ویرایش آن منتهی میشوند. در اینجا تنها کاری را که باید انجام داد، تغییر پورت پیش فرض آن است:
- اگر از دستور ng serve استفاده میکنید، این پورت را به 4200 تغییر دهید (پورت پیش فرض این دستور است؛ برای تغییر آن، از پارامتر port-- در دستور ng serve استفاده کنید):
- اگر از دستورات dotnet watch run و سپس ng build -watch استفاده میکنید (اولی وب سرور آزمایشی NET Core. را راه اندازی میکند و دومی کار ساخت پیوستهی برنامهی Angular را انجام میدهد)، این پورت را بر روی 5000 تنظیم کنید:
آزمایش یک break point و بررسی مقادیر دریافتی از سرور
تا اینجا کاری را که انجام دادیم، به افزودن یک قطعه تنظیم جدید به فایل vscode\launch.json. خلاصه میشود.
در ادامه به کامپوننت employee-register.component.ts مراجعه کرده و سطر this.languages = data را تبدیل به یک سطر مستقل درون {} میکنیم تا بتوانیم بر روی آن break point قرار دهیم:
پس از آن از طریق خط فرمان به ریشهی پروژه وارد شده و دو پنجرهی کنسول مجزا را باز کنید. در اولی دستورات
و در دومی دستورات ذیل را اجرا کنید:
سپس به برگهی دیباگ مراجعه کرده و گزینهی جدید Launch Chrome for dotnet build & ng build را انتخاب و سپس بر روی دکمهی سبز رنگ اجرای آن کلیک کنید:
اکنون اگر صفحهی مشاهدهی فرم را در مرورگر کروم باز شده درخواست کنیم، به این break point خواهیم رسید؛ اما ... حاوی اطلاعات data نیست. برای رفع این مشکل نیاز است تنظیمات پیشفرض را به صورت ذیل بهبود بخشید:
- در این تنظیمات تکمیلی وجود runtimeArgs و userDataDir جهت مدیریت داشتن چندین وهلهی از کروم و باز بودن برگههای مختلف آن مفید است.
- تنظیم sourceMaps و همچنین مشخص سازی محل دقیق پوشهی قرارگیری فایلهای نهایی ng build که همان پوشهی wwwroot است در webRoot سبب خواهند شد تا اینبار break point ما حاوی اطلاعات واقعی data دریافتی از سرور باشند (با نزدیک کردن اشارهگر ماوس به data، اطلاعات تکمیلی آن ظاهر میشود):
پیشنیازها
در اینجا فرض بر این است که موارد ذیل را نصب کردهاید:
- آخرین نگارش مرورگر Chrome
- افزونهی Debugger for Chrome که از آن برای دیباگ کدهای جاوا اسکریپتی و تایپاسکریپتی یکپارچهی با VSCode میتوان استفاده کرد.
تنظیمات VSCode جهت دیباگ برنامههای مبتنی بر Angular CLI
کدهای مطلب «فرمهای مبتنی بر قالبها در Angular - قسمت پنجم - ارسال اطلاعات به سرور» از انتهای بحث آن قابل دریافت هستند. این کدها را دریافت کرده و سپس پوشهی اصلی آنرا در VSCode باز کنید. اگر در ویندوز هستید با کلیک راست بر روی پوشه، گزینهی Open With Code ظاهر میشود و یا حتی میتوان از طریق خط فرمان به پوشهی اصلی برنامه مراجعه کرده و سپس دستور . code را صادر نمود.
در ادامه نیاز است دیباگر VSCode را تنظیم کنیم. اگر پیشتر هیچ تنظیمی را نداشته باشید، پس از مراجعهی به برگهی debug آن، بر روی دکمهی سبز رنگ Start Debugging آن کلیک کنید. در ادامه در منوی باز شده، گزینهی Chrome را انتخاب کنید:
اینکار سبب میشود تا فایل جدید vscode\launch.json. به پوشهی جاری اضافه شده و سپس تنظیمات دیباگر کروم در آن قرار گیرند.
حالت دوم شبیه به حالتی است که برنامهی مورد بحث جاری دارد: پیشتر دیباگری مانند NET Core. در آن تنظیم شدهاست. در این حالت، منوی drop down مربوط به دیباگرهای تنظیم شده را گشوده و سپس گزینهی آخر آن یعنی Add configuration را انتخاب کنید:
در اینجا نیز منوی Intellisense آن ظاهر شده و امکان انتخاب گزینهی Chrome را میدهد:
در نهایت هر دو حالت به ایجاد فایل جدید vscode\launch.json. و یا ویرایش آن منتهی میشوند. در اینجا تنها کاری را که باید انجام داد، تغییر پورت پیش فرض آن است:
- اگر از دستور ng serve استفاده میکنید، این پورت را به 4200 تغییر دهید (پورت پیش فرض این دستور است؛ برای تغییر آن، از پارامتر port-- در دستور ng serve استفاده کنید):
{ "version": "0.2.0", "configurations": [ { "type": "chrome", "request": "launch", "name": "Launch Chrome with ng serve", "url": "http://localhost:4200/#", "webRoot": "${workspaceRoot}" } ] }
- اگر از دستورات dotnet watch run و سپس ng build -watch استفاده میکنید (اولی وب سرور آزمایشی NET Core. را راه اندازی میکند و دومی کار ساخت پیوستهی برنامهی Angular را انجام میدهد)، این پورت را بر روی 5000 تنظیم کنید:
{ "version": "0.2.0", "configurations": [ { "type": "chrome", "request": "launch", "name": "Launch Chrome for dotnet build & ng build", "url": "http://localhost:5000/#", "webRoot": "${workspaceRoot}" } ] }
آزمایش یک break point و بررسی مقادیر دریافتی از سرور
تا اینجا کاری را که انجام دادیم، به افزودن یک قطعه تنظیم جدید به فایل vscode\launch.json. خلاصه میشود.
در ادامه به کامپوننت employee-register.component.ts مراجعه کرده و سطر this.languages = data را تبدیل به یک سطر مستقل درون {} میکنیم تا بتوانیم بر روی آن break point قرار دهیم:
ngOnInit() { this.formPoster.getLanguages().subscribe( data => { this.languages = data; }, err => console.log("get error: ", err) ); }
پس از آن از طریق خط فرمان به ریشهی پروژه وارد شده و دو پنجرهی کنسول مجزا را باز کنید. در اولی دستورات
>npm install >ng build --watch
>dotnet restore >dotnet watch run
سپس به برگهی دیباگ مراجعه کرده و گزینهی جدید Launch Chrome for dotnet build & ng build را انتخاب و سپس بر روی دکمهی سبز رنگ اجرای آن کلیک کنید:
اکنون اگر صفحهی مشاهدهی فرم را در مرورگر کروم باز شده درخواست کنیم، به این break point خواهیم رسید؛ اما ... حاوی اطلاعات data نیست. برای رفع این مشکل نیاز است تنظیمات پیشفرض را به صورت ذیل بهبود بخشید:
{ "version": "0.2.0", "configurations": [ { "type": "chrome", "request": "launch", "name": "Launch Chrome for dotnet build & ng build", "url": "http://localhost:5000", "runtimeArgs": [ "--user-data-dir", "--remote-debugging-port=9222", "--disable-session-crashed-bubble" ], "sourceMaps": true, "trace": true, "webRoot": "${workspaceRoot}/wwwroot/", "userDataDir": "${workspaceRoot}/.vscode/chrome" } ] }
- تنظیم sourceMaps و همچنین مشخص سازی محل دقیق پوشهی قرارگیری فایلهای نهایی ng build که همان پوشهی wwwroot است در webRoot سبب خواهند شد تا اینبار break point ما حاوی اطلاعات واقعی data دریافتی از سرور باشند (با نزدیک کردن اشارهگر ماوس به data، اطلاعات تکمیلی آن ظاهر میشود):
- اگر از دستور ng serve استفاده میکنید، در این تنظیمات پورت را به 4200 و محل پوشهی wwwroot را به dist تغییر دهید (مطابق تنظیمات فایل angular-cli.json.).
در بخش اول به معرفی SvelteJs پرداختیم و اولین پروژهی خود را ایجاد کردیم. در ادامه به بررسی جزئیات فایلهای تشکیل شده میپردازیم. قبل از هرچیز پیشنهاد میکنم اگر از vs-code استفاده میکنید Extension Svelte را دانلود و نصب نمایید.
پس از ایجاد پروژه، تعدادی فایل توسط Svelte ایجاد میشوند که در ادامه آنها را بررسی خواهیم کرد.
rollup.config.js :
به طور پیش فرض Svelte از rollup برای ساخت برنامه استفاده میکند که جایگزینی برای webpack است. فعلا نیازی به تغییر و دانستن جزئیاتی در مورد این فایل نداریم؛ چراکه به صورت پیش فرض توسط قالب دریافت شده، تمامی کانفیگهای مورد نظر ما برای توسعه و ساخت نهایی باندل برنامه انجام شدهاست. فقط به چند نکتهی مهم در این فایل اشاره خواهم کرد.
import svelte from 'rollup-plugin-svelte'; import resolve from 'rollup-plugin-node-resolve'; import commonjs from 'rollup-plugin-commonjs'; import livereload from 'rollup-plugin-livereload'; import { terser } from 'rollup-plugin-terser'; const production = !process.env.ROLLUP_WATCH; export default { input: 'src/main.js', output: { sourcemap: true, format: 'iife', name: 'app', file: 'public/bundle.js' }, plugins: [ svelte({ // enable run-time checks when not in production dev: !production, // we'll extract any component CSS out into // a separate file — better for performance css: css => { css.write('public/bundle.css'); } }), // If you have external dependencies installed from // npm, you'll most likely need these plugins. In // some cases you'll need additional configuration — // consult the documentation for details: // https://github.com/rollup/rollup-plugin-commonjs resolve(), commonjs(), // Watch the `public` directory and refresh the // browser on changes when not in production !production && livereload('public'), // If we're building for production (npm run build // instead of npm run dev), minify production && terser() ], watch: { clearScreen: false } };
در خط 12 این فایل، مقدار input برابر با src/main.js است که به rollup، نقطه شروع برنامه را نشان میدهد و مانند سایر فریم ورکها، اسکریپت آغاز کننده برنامه ما میباشد. همینطور در خطوط 15 و 24، فایلهای bundle خروجی برنامه که به صورت پیش فرض در فولدر public قرار میگیرند، مسیرشان مشخص شده است.
package.json :
احتمالا اگر از هر کدام از فریم ورکهای معروفی مثل vue - react - angualr استفاده کرده باشید میدانید این فایل چیست؛ ولی بد نیست توضیح مختصری بدهم و تفاوت مهم packageها در svelte را با سایر فریم ورکها، بیان کنم. این فایل تمام وابستگیهای پروژه و اسکریپتهای مورد نیاز برای ساخت و اجرای برنامه را در خود نگه میدارد.
{ "name": "svelte-app", "version": "1.0.0", "devDependencies": { "npm-run-all": "^4.1.5", "rollup": "^1.10.1", "rollup-plugin-commonjs": "^9.3.4", "rollup-plugin-livereload": "^1.0.0", "rollup-plugin-node-resolve": "^4.2.3", "rollup-plugin-svelte": "^5.0.3", "rollup-plugin-terser": "^4.0.4", "sirv-cli": "^0.4.0", "svelte": "^3.0.0" }, "scripts": { "build": "rollup -c", "autobuild": "rollup -c -w", "dev": "run-p start:dev autobuild", "start": "sirv public", "start:dev": "sirv public --dev" } }
نکته مهمی که در اینجا به چشم میخورد وجود نداشتن بخش dependencies در این فایل است. در بخش dependencies عموما وابستگیهای پروژه در زمان اجرای برنامه قرار میگیرد. همانطور که قبلا اشاره کرده بودم، Svelte یک کامپایلر است. به همین جهت در زمان اجرا، نیاز به هیچ وابستگی اضافهتری ندارد؛ برخلاف سایر فریم ورکها که حداقل نیاز دارند خود اسکریپت فریم ورک، زمان اجرا لود شده و توسط کاربر دانلود شود. بجای آن همانطور که مشاهده میکنید در خط 4, devDependecies وجود دارد که تمام وابستگیهای svelte را دربر میگیرد که فقط قبل از build شدن برنامه مورد نیاز هستند. در خط 15، تگ اسکریپ قرار دارد که برای راحتی ساخت و اجرای برنامه همانطور که در بخش قبل دیدیم میتوانیم از آنها استفاده کنیم (npm run dev ---- npm run build ---- etc)
build | برای ساخت و ایجاد خروجیهای برنامه توسط rollup مورد قرار استفاده میگیرد. |
autobuild | مانند build برای ساخت خروجیهای نهایی برنامه استفاده میشود. ولی تفاوتی که دارد پس از هر تغییر در سورس کد برنامه به صورت خودکار build جدیدی پس از اجرای آن گرفته میشود. |
dev | برنامه را درحالت Developer Mode اجرا میکند که برای مشاهده تغییرات به صورت خودکار در browser، بدون نیاز به رفرش صفحه و همینطور عیب یابی برنامه مناسب است. |
start | از طریق sirv که یک وب سرور سبک برای هاست کردن سایتهای استاتیک است، برنامه را هاست میکند. |
start:dev | مانند start است با این تفاوت که برنامه را در حالت Developer Mode هاست میکند که میتواند برای عیب یابی برنامه از آن استفاده کرد؛ چرا که سورس برنامه از طریق source Map قابل دسترس خواهد بود. |
دو پوشه src و public هم برای ما به صورت پیش فرض ایجاد شدهاند که فولدر public فایلهای نهایی تولید شده برنامه ما را شامل میشود و src، دربرگیرنده تمام سورس کدهای برنامه ما میباشد.
src/App.svelte :
همه بخشهای برنامه در Svelte از کامپوننتها تشکیل میشوند و این فایل کامپوننت اصلی برنامه در Svelte است. همانطور که نام این فایل پیداست پسوند تمام کامپوننتهای Svelte نام این کامپایلر است svelte.
<script> export let name; </script> <style> h1 { color: purple; } </style> <h1>Hello {name}!</h1>
اگر قبلا با vuejs کار کرده باشید، این syntax برای شما آشنا خواهد بود؛ هرچند بسیار شبیه کدنویسی در صفحه html است. در کامپوننتهای svelte شما دو تگ Script و Style دارید و خارج از این دو تگ میتوانید html خود را قرار دهید؛ مانند مثال بالا. در تگ اسکریپت، کدهای جاوا اسکریپتی مرتبط با کامپوننت قرار میگیرد و در تگ Style هم Cssهای مرتبط با کامپوننت. در مثال بالا، در خط 11 ما یک تگ h1 داریم که مقدار hello و یک {name} را نمایش خواهد داد. با استفاده از علامت {} میتوانید کدهای جاوااسکریپتی خود رابه html جاری اضافه کنید که در مثال بالا متغیر name در خط 2 تعریف شده است. در تگ اسکریپت شما امکان ساخت هرگونه متغیر و فانکشنی را که در جاوا اسکریپت معتبر است، خواهید داشت که همینطور میتوان از آنها در صفحه html استفاده کرد. ولی svelte با استفاده از کلمات کلیدی جاوا اسکریپت، چند امکان به این بخش اضافه کرده است. اگر به خط 2 مجددا دقت کنیم، شاید برای شما سؤال ایجاد شود که کلمه World از کجا میآید و چطور به متغیر name نسبت داده شدهاست. نکتهای که در کد بالا وجود دارد، کلمه export قبل از متغیر است. به این معنا که استفاده کننده از این کامپوننت میتواند name را مقدار دهی کند. در svelte به این نوع متغیرها props گفته میشود. در این مثال name توسط اسکریپت آغاز کننده برنامه با به اصطلاح entry point برنامه ما مقدار دهی خواهد شد که در ادامه این فایل را بررسی میکنیم.
src/main.js :
import App from './App.svelte'; const app = new App({ target: document.body, props: { name: 'world' } }); export default app;
اگر به خاطر داشته باشید، ما در کامپوننت App.svelte یک متغیر به نام name را به عنوان یک props (خصیصه) export کرده بودیم و در اینجا مقدار این متغیر را برابر با world قرار دادیم.
در خط آخر (10) هم مانند تمام فایلها و ماژولهای جاوا اسکریپت، این object را برای استفاده export میکنیم.
نکته: پیش نیاز استفاده از svelte، درک نسبی روی مباحث مرتبط با JavaScript و Html و Css است. لذا در این آموزش من به جزئیات مرتبط با این سه مورد وارد نمیشوم و سعی میکنم تمرکز بیشتر بر روی مباحث مرتبط با خود svelte باشد.
در بخش بعدی با ایجاد یک پروژه جدید، با سایر امکانات svelte و همینطور syntax آن بیشتر آشنا خواهیم شد.
فرض کنید میخواهید برای تمام دکمههای حذف، در کل پروژه، قبل از انجام عمل اصلی، یک confirm را به کاربر نشان دهید تا اگر کاربر بر روی کلید تایید، کلیک نمود، عمل مورد نظر انجام شود. برای چنین کاری در یک layout اصلی (و یا یک فایل js ) کدی شبیه به قطعه کد زیر را نوشتهایم:
حال فرض کنید در یکی از صفحات قصد داریم اگر بر دکمهای که قبلا برای آن رویدادی نوشتهایم (منظور کدهای بالا میباشد)، کلیک شد، یکسری عملیات دیگر، جدای از آن عملیات انجام شود. برای اینکار در صفحه مربوطه، کدی شبیه زیر را نوشتهایم :
اگر پروژه را اجرا نمایید و بر روی دکمه مربوطه کلیک نماییم، هر دو Event نوشته شده، اجرا خواهند شد. در چنین حالتی تکلیف چیست و چکار باید کرد؟ جواب: selector را تغییر دهیم :خیر.
برای این کار میتوان رویداد کلیک را برای تگ مورد نظر با استفاده از متد off بازنویسی کنیم:
- روشهایی دیگر برای انجام این کار:
$(document).ready(function () { $(document).on('click', '.confirm', function () { alert("Clicked Me !"); }); });
حال فرض کنید در یکی از صفحات قصد داریم اگر بر دکمهای که قبلا برای آن رویدادی نوشتهایم (منظور کدهای بالا میباشد)، کلیک شد، یکسری عملیات دیگر، جدای از آن عملیات انجام شود. برای اینکار در صفحه مربوطه، کدی شبیه زیر را نوشتهایم :
$(document).ready(function () { $(document).on('click', '.confirm', function () { alert("Clicked Me On nested Page !"); }); });
اگر پروژه را اجرا نمایید و بر روی دکمه مربوطه کلیک نماییم، هر دو Event نوشته شده، اجرا خواهند شد. در چنین حالتی تکلیف چیست و چکار باید کرد؟ جواب: selector را تغییر دهیم :خیر.
برای این کار میتوان رویداد کلیک را برای تگ مورد نظر با استفاده از متد off بازنویسی کنیم:
$(document).off('click', '.confirm');
- روشهایی دیگر برای انجام این کار:
$(".confirm").removeAttr("onclick"); $( ".confim").unbind( "click" );
از زمان Blazor 5x، امکان آپلود فایل به صورت استاندارد به Blazor اضافه شدهاست که نمونهی Blazor Server آنرا پیشتر در مطلب «Blazor 5x - قسمت 17 - کار با فرمها - بخش 5 - آپلود تصاویر» مطالعه کردید. در تکمیل آن، روش آپلود فایلها در برنامههای WASM را نیز بررسی خواهیم کرد. این برنامه از نوع hosted است؛ یعنی توسط دستور dotnet new blazorwasm --hosted ایجاد شدهاست و به صورت خودکار دارای سه بخش Client، Server و Shared است.
معرفی مدل ارسالی برنامه سمت کلاینت
فرض کنید مطابق شکل فوق، قرار است اطلاعات یک کاربر، به همراه تعدادی تصویر از او، به سمت Web API ارسال شوند. برای نمونه، مدل اشتراکی کاربر را به صورت زیر تعریف کردهایم:
ساختار کنترلر Web API دریافت کنندهی مدل برنامه
در این حالت امضای اکشن متد CreateUser واقع در کنترلر Files که قرار است این اطلاعات را دریافت کند، به صورت زیر است:
یعنی در سمت Web API، قرار است اطلاعات مدل User و همچنین لیستی از فایلهای آپلودی (احتمالی و اختیاری) را یکجا و در طی یک عملیات Post، دریافت کنیم. در اینجا نام پارامترهایی را هم که انتظار داریم، دقیقا userModel و inputFiles هستند. همچنین فایلهای آپلودی باید بتوانند ساختار IFormFile استاندارد ASP.NET Core را تشکیل داده و به صورت خودکار به پارامترهای تعریف شده، bind شوند. به علاوه content-type مورد انتظار هم FromForm است.
ایجاد سرویسی در سمت کلاینت، برای آپلود اطلاعات یک مدل به همراه فایلهای انتخابی کاربر
کدهای کامل سرویسی که میتواند انتظارات یاد شده را در سمت کلاینت برآورده کند، به صورت زیر است:
توضیحات:
- کامپوننت استاندارد InputFiles در Blazor Wasm، میتواند لیستی از IBrowserFileهای انتخابی توسط کاربر را در اختیار ما قرار دهد.
- fileParameterName، همان نام پارامتر "inputFiles" در اکشن متد سمت سرور مثال جاری است که به صورت متغیر قابل تنظیم شدهاست.
- model جنریک، برای نمونه وهلهای از شیء User است که به یک فرم Blazor متصل است.
- modelParameterName، همان نام پارامتر "userModel" در اکشن متد سمت سرور مثال جاری است که به صورت متغیر قابل تنظیم شدهاست.
- در ادامه یک MultipartFormDataContent را تشکیل دادهایم. توسط این ساختار میتوان فایلها و اطلاعات یک مدل را به صورت یکجا جمع آوری و به سمت سرور ارسال کرد. به این content ویژه، ابتدای لیستی از new StreamContentها را اضافه میکنیم. این streamها توسط متد OpenReadStream هر IBrowserFile دریافتی از کامپوننت InputFile، تشکیل میشوند. متد OpenReadStream به صورت پیشفرض فقط فایلهایی تا حجم 500 کیلوبایت را پردازش میکند و اگر فایلی حجیمتر را به آن معرفی کنیم، یک استثناء را صادر خواهد کرد. به همین جهت میتوان توسط پارامتر maxAllowedSize آن، این مقدار پیشفرض را تغییر داد.
- در اینجا مدل برنامه به صورت JSON به عنوان یک new StringContent اضافه شدهاست. مزیت کار کردن با JsonSerializer.Serialize استاندارد، ساده شدن برنامه و عدم درگیری با مباحث Reflection و خواندن پویای اطلاعات مدل جنریک است. اما در ادامه مشکلی را پدید خواهد آورد! این رشتهی ارسالی به سمت سرور، به صورت خودکار به یک مدل، Bind نخواهد شد و باید برای آن یک model-binder سفارشی را بنویسیم. یعنی این رشتهی new StringContent را در سمت سرور دقیقا به صورت یک رشته معمولی میتوان دریافت کرد و نه حالت دیگری و مهم نیست که اکنون به صورت JSON ارسال میشود؛ چون MultipartFormDataContent ویژهای را داریم، model-binder پیشفرض ASP.NET Core، انتظار یک شیء خاص را در این بین ندارد.
- تنظیم "form-data" را هم به عنوان Headers.ContentDisposition مشاهده میکنید. بدون وجود آن، ویژگی [FromForm] سمت Web API، از پردازش درخواست جلوگیری خواهد کرد.
- در آخر توسط متد PostAsync، این اطلاعات جمع آوری شده، به سمت سرور ارسال خواهند شد.
پس از تهیهی سرویس ویژهی فوق که میتواند اطلاعات فایلها و یک مدل را به صورت یکجا به سمت سرور ارسال کند، اکنون نوبت به ثبت و معرفی آن به سیستم تزریق وابستگیها در فایل Program.cs برنامهی کلاینت است:
تکمیل فرم ارسال اطلاعات مدل و فایلهای همراه آن در برنامهی Blazor WASM
در ادامه پس از تشکیل IFilesManagerService، نوبت به استفادهی از آن است. به همین جهت همان کامپوننت Index برنامه را به صورت زیر تغییر میدهیم:
در اینجا فیلدهای مورد استفادهی در فرم برنامه مشخص شدهاند:
- SelectedFiles همان لیست فایلهای انتخابی توسط کاربر است.
- UserModel شیءای است که به EditForm جاری متصل خواهد شد.
- توسط isProcessing ابتدا و انتهای آپلود به سرور را مشخص میکنیم.
- UploadErrorMessage، خطای احتمالی انتخاب فایلها مانند «فقط تصاویر را انتخاب کنید» را تعریف میکند.
بر این اساس، فرمی را که در تصویر ابتدای بحث مشاهده کردید، به صورت زیر تشکیل میدهیم:
توضیحات:
- UserModel که وهلهی از شیء اشتراکی User است، به EditForm متصل شدهاست.
- سپس توسط یک InputText و InputNumber، مقادیر خواص نام و سن کاربر را دریافت میکنیم.
- InputFile دارای ویژگی multiple هم امکان دریافت چندین فایل را توسط کاربر میسر میکند. پس از انتخاب فایلها، رویداد OnChange آن، توسط متد OnInputFileChange مدیریت خواهد شد:
- در اینجا امضای متد رویداد گردان OnChange را مشاهده میکنید. توسط متد GetMultipleFiles میتوان لیست فایلهای انتخابی توسط کاربر را دریافت کرد. نیاز است پارامتر maximumFileCount آنرا نیز تنظیم کنیم تا دقیقا مشخص شود چه تعداد فایلی مدنظر است؛ بیش از آن، یک استثناء را صادر میکند.
- در ادامه اگر فایلی انتخاب نشده باشد، یا فایل انتخابی، تصویری نباشد، با مقدار دهی UploadErrorMessage، خطایی را به کاربر نمایش میدهیم.
- در پایان این متد، لیست فایلهای دریافتی را به فیلد SelectedFiles انتساب میدهیم تا در ذیل InputFile، به صورت یک جدول نمایش داده شوند.
مرحلهی آخر تکمیل این فرم، تدارک متد رویدادگردان OnValidSubmit فرم برنامه است:
- در اینجا زمانیکه isProcessing به true تنظیم میشود، دکمهی ارسال اطلاعات، غیرفعال خواهد شد؛ تا از کلیک چندبارهی بر روی آن جلوگیری شود.
- سپس روش استفادهی از متد PostModelWithFilesAsync سرویس FilesManagerService را مشاهده میکنید که اطلاعات فایلها و مدل برنامه را به سمت اکشن متد api/Files/CreateUser ارسال میکند.
- در آخر با وهله سازی مجدد UserModel، به صورت خودکار فرم برنامه را پاک کرده و آمادهی دریافت اطلاعات بعدی میکنیم.
تکمیل کنترلر Web API دریافت کنندهی مدل برنامه
در ابتدای بحث، ساختار ابتدایی کنترلر Web API دریافت کنندهی اطلاعات FilesManagerService.PostModelWithFilesAsync فوق را معرفی کردیم. در ادامه کدهای کامل آنرا مشاهده میکنید:
نکات تکمیلی این کنترلر را در مطلب «بررسی روش آپلود فایلها در ASP.NET Core» میتوانید مطالعه کنید و از این لحاظ هیچ نکتهی جدیدی را به همراه ندارد؛ بجز پارامتر userModel آن:
همانطور که عنوان شد، userModel ارسالی به سمت سرور چون به همراه تعدادی فایل است، به صورت خودکار به شیء User نگاشت نخواهد شد. به همین جهت نیاز است model-binder سفارشی زیر را برای آن تهیه کرد:
در اینجا مقدار رشتهای پارامتر مزین شدهی توسط JsonModelBinder فوق، توسط متد استاندارد JsonSerializer.Deserialize تبدیل به یک شیء شده و به آن پارامتر انتساب داده میشود. اگر نخواهیم از این model-binder سفارشی استفاده کنیم، ابتدا باید پارامتر دریافتی را رشتهای تعریف کنیم و سپس خودمان کار فراخوانی متد JsonSerializer.Deserialize را انجام دهیم:
یک نکته تکمیلی: در Blazor 5x، از نمایش درصد پیشرفت آپلود، پشتیبانی نمیشود؛ از این جهت که HttpClient طراحی شده، در اصل به fetch API استاندارد مرورگر ترجمه میشود و این API استاندارد، هنوز از streaming پشتیبانی نمیکند . حتی ممکن است با کمی جستجو به راهحلهایی که سعی کردهاند بر اساس HttpClient و نوشتن بایت به بایت اطلاعات در آن، درصد پیشرفت آپلود را محاسبه کرده باشند، برسید. این راهحلها تنها کاری را که انجام میدهند، بافر کردن اطلاعات، جهت fetch API و سپس ارسال تمام آن است. به همین جهت درصدی که نمایش داده میشود، درصد بافر شدن اطلاعات در خود مرورگر است (پیش از ارسال آن به سرور) و سپس تحویل آن به fetch API جهت ارسال نهایی به سمت سرور.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: BlazorWasmUpload.zip
معرفی مدل ارسالی برنامه سمت کلاینت
فرض کنید مطابق شکل فوق، قرار است اطلاعات یک کاربر، به همراه تعدادی تصویر از او، به سمت Web API ارسال شوند. برای نمونه، مدل اشتراکی کاربر را به صورت زیر تعریف کردهایم:
using System.ComponentModel.DataAnnotations; namespace BlazorWasmUpload.Shared { public class User { [Required] public string Name { get; set; } [Required] [Range(18, 90)] public int Age { get; set; } } }
ساختار کنترلر Web API دریافت کنندهی مدل برنامه
در این حالت امضای اکشن متد CreateUser واقع در کنترلر Files که قرار است این اطلاعات را دریافت کند، به صورت زیر است:
namespace BlazorWasmUpload.Server.Controllers { [ApiController] [Route("api/[controller]/[action]")] public class FilesController : ControllerBase { [HttpPost] public async Task<IActionResult> CreateUser( [FromForm] User userModel, [FromForm] IList<IFormFile> inputFiles = null)
ایجاد سرویسی در سمت کلاینت، برای آپلود اطلاعات یک مدل به همراه فایلهای انتخابی کاربر
کدهای کامل سرویسی که میتواند انتظارات یاد شده را در سمت کلاینت برآورده کند، به صورت زیر است:
using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Components.Forms; namespace BlazorWasmUpload.Client.Services { public interface IFilesManagerService { Task<HttpResponseMessage> PostModelWithFilesAsync<T>(string requestUri, IEnumerable<IBrowserFile> browserFiles, string fileParameterName, T model, string modelParameterName); } public class FilesManagerService : IFilesManagerService { private readonly HttpClient _httpClient; public FilesManagerService(HttpClient httpClient) { _httpClient = httpClient; } public async Task<HttpResponseMessage> PostModelWithFilesAsync<T>( string requestUri, IEnumerable<IBrowserFile> browserFiles, string fileParameterName, T model, string modelParameterName) { var requestContent = new MultipartFormDataContent(); requestContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data"); if (browserFiles?.Any() == true) { foreach (var file in browserFiles) { var stream = file.OpenReadStream(maxAllowedSize: 512000 * 1000); requestContent.Add(content: new StreamContent(stream, (int)file.Size), name: fileParameterName, fileName: file.Name); } } requestContent.Add( content: new StringContent(JsonSerializer.Serialize(model), Encoding.UTF8, "application/json"), name: modelParameterName); var result = await _httpClient.PostAsync(requestUri, requestContent); result.EnsureSuccessStatusCode(); return result; } } }
- کامپوننت استاندارد InputFiles در Blazor Wasm، میتواند لیستی از IBrowserFileهای انتخابی توسط کاربر را در اختیار ما قرار دهد.
- fileParameterName، همان نام پارامتر "inputFiles" در اکشن متد سمت سرور مثال جاری است که به صورت متغیر قابل تنظیم شدهاست.
- model جنریک، برای نمونه وهلهای از شیء User است که به یک فرم Blazor متصل است.
- modelParameterName، همان نام پارامتر "userModel" در اکشن متد سمت سرور مثال جاری است که به صورت متغیر قابل تنظیم شدهاست.
- در ادامه یک MultipartFormDataContent را تشکیل دادهایم. توسط این ساختار میتوان فایلها و اطلاعات یک مدل را به صورت یکجا جمع آوری و به سمت سرور ارسال کرد. به این content ویژه، ابتدای لیستی از new StreamContentها را اضافه میکنیم. این streamها توسط متد OpenReadStream هر IBrowserFile دریافتی از کامپوننت InputFile، تشکیل میشوند. متد OpenReadStream به صورت پیشفرض فقط فایلهایی تا حجم 500 کیلوبایت را پردازش میکند و اگر فایلی حجیمتر را به آن معرفی کنیم، یک استثناء را صادر خواهد کرد. به همین جهت میتوان توسط پارامتر maxAllowedSize آن، این مقدار پیشفرض را تغییر داد.
- در اینجا مدل برنامه به صورت JSON به عنوان یک new StringContent اضافه شدهاست. مزیت کار کردن با JsonSerializer.Serialize استاندارد، ساده شدن برنامه و عدم درگیری با مباحث Reflection و خواندن پویای اطلاعات مدل جنریک است. اما در ادامه مشکلی را پدید خواهد آورد! این رشتهی ارسالی به سمت سرور، به صورت خودکار به یک مدل، Bind نخواهد شد و باید برای آن یک model-binder سفارشی را بنویسیم. یعنی این رشتهی new StringContent را در سمت سرور دقیقا به صورت یک رشته معمولی میتوان دریافت کرد و نه حالت دیگری و مهم نیست که اکنون به صورت JSON ارسال میشود؛ چون MultipartFormDataContent ویژهای را داریم، model-binder پیشفرض ASP.NET Core، انتظار یک شیء خاص را در این بین ندارد.
- تنظیم "form-data" را هم به عنوان Headers.ContentDisposition مشاهده میکنید. بدون وجود آن، ویژگی [FromForm] سمت Web API، از پردازش درخواست جلوگیری خواهد کرد.
- در آخر توسط متد PostAsync، این اطلاعات جمع آوری شده، به سمت سرور ارسال خواهند شد.
پس از تهیهی سرویس ویژهی فوق که میتواند اطلاعات فایلها و یک مدل را به صورت یکجا به سمت سرور ارسال کند، اکنون نوبت به ثبت و معرفی آن به سیستم تزریق وابستگیها در فایل Program.cs برنامهی کلاینت است:
namespace BlazorWasmUpload.Client { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); // ... builder.Services.AddScoped<IFilesManagerService, FilesManagerService>(); // ... } } }
تکمیل فرم ارسال اطلاعات مدل و فایلهای همراه آن در برنامهی Blazor WASM
در ادامه پس از تشکیل IFilesManagerService، نوبت به استفادهی از آن است. به همین جهت همان کامپوننت Index برنامه را به صورت زیر تغییر میدهیم:
@code { IReadOnlyList<IBrowserFile> SelectedFiles; User UserModel = new User(); bool isProcessing; string UploadErrorMessage;
- SelectedFiles همان لیست فایلهای انتخابی توسط کاربر است.
- UserModel شیءای است که به EditForm جاری متصل خواهد شد.
- توسط isProcessing ابتدا و انتهای آپلود به سرور را مشخص میکنیم.
- UploadErrorMessage، خطای احتمالی انتخاب فایلها مانند «فقط تصاویر را انتخاب کنید» را تعریف میکند.
بر این اساس، فرمی را که در تصویر ابتدای بحث مشاهده کردید، به صورت زیر تشکیل میدهیم:
@page "/" @using System.IO @using BlazorWasmUpload.Shared @using BlazorWasmUpload.Client.Services @inject IFilesManagerService FilesManagerService <h3>Post a model with files</h3> <EditForm Model="UserModel" OnValidSubmit="CreateUserAsync"> <DataAnnotationsValidator /> <div> <label>Name</label> <InputText @bind-Value="UserModel.Name"></InputText> <ValidationMessage For="()=>UserModel.Name"></ValidationMessage> </div> <div> <label>Age</label> <InputNumber @bind-Value="UserModel.Age"></InputNumber> <ValidationMessage For="()=>UserModel.Age"></ValidationMessage> </div> <div> <label>Photos</label> <InputFile multiple disabled="@isProcessing" OnChange="OnInputFileChange" /> @if (!string.IsNullOrWhiteSpace(UploadErrorMessage)) { <div> @UploadErrorMessage </div> } @if (SelectedFiles?.Count > 0) { <table> <thead> <tr> <th>Name</th> <th>Size (bytes)</th> <th>Last Modified</th> <th>Type</th> </tr> </thead> <tbody> @foreach (var selectedFile in SelectedFiles) { <tr> <td>@selectedFile.Name</td> <td>@selectedFile.Size</td> <td>@selectedFile.LastModified</td> <td>@selectedFile.ContentType</td> </tr> } </tbody> </table> } </div> <div> <button disabled="@isProcessing">Create user</button> </div> </EditForm>
- UserModel که وهلهی از شیء اشتراکی User است، به EditForm متصل شدهاست.
- سپس توسط یک InputText و InputNumber، مقادیر خواص نام و سن کاربر را دریافت میکنیم.
- InputFile دارای ویژگی multiple هم امکان دریافت چندین فایل را توسط کاربر میسر میکند. پس از انتخاب فایلها، رویداد OnChange آن، توسط متد OnInputFileChange مدیریت خواهد شد:
private void OnInputFileChange(InputFileChangeEventArgs args) { var files = args.GetMultipleFiles(maximumFileCount: 15); if (args.FileCount == 0 || files.Count == 0) { UploadErrorMessage = "Please select a file."; return; } var allowedExtensions = new List<string> { ".jpg", ".png", ".jpeg" }; if(!files.Any(file => allowedExtensions.Contains(Path.GetExtension(file.Name), StringComparer.OrdinalIgnoreCase))) { UploadErrorMessage = "Please select .jpg/.jpeg/.png files only."; return; } SelectedFiles = files; UploadErrorMessage = string.Empty; }
- در ادامه اگر فایلی انتخاب نشده باشد، یا فایل انتخابی، تصویری نباشد، با مقدار دهی UploadErrorMessage، خطایی را به کاربر نمایش میدهیم.
- در پایان این متد، لیست فایلهای دریافتی را به فیلد SelectedFiles انتساب میدهیم تا در ذیل InputFile، به صورت یک جدول نمایش داده شوند.
مرحلهی آخر تکمیل این فرم، تدارک متد رویدادگردان OnValidSubmit فرم برنامه است:
private async Task CreateUserAsync() { try { isProcessing = true; await FilesManagerService.PostModelWithFilesAsync( requestUri: "api/Files/CreateUser", browserFiles: SelectedFiles, fileParameterName: "inputFiles", model: UserModel, modelParameterName: "userModel"); UserModel = new User(); } finally { isProcessing = false; SelectedFiles = null; } }
- سپس روش استفادهی از متد PostModelWithFilesAsync سرویس FilesManagerService را مشاهده میکنید که اطلاعات فایلها و مدل برنامه را به سمت اکشن متد api/Files/CreateUser ارسال میکند.
- در آخر با وهله سازی مجدد UserModel، به صورت خودکار فرم برنامه را پاک کرده و آمادهی دریافت اطلاعات بعدی میکنیم.
تکمیل کنترلر Web API دریافت کنندهی مدل برنامه
در ابتدای بحث، ساختار ابتدایی کنترلر Web API دریافت کنندهی اطلاعات FilesManagerService.PostModelWithFilesAsync فوق را معرفی کردیم. در ادامه کدهای کامل آنرا مشاهده میکنید:
using System.IO; using Microsoft.AspNetCore.Mvc; using BlazorWasmUpload.Shared; using Microsoft.AspNetCore.Hosting; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using System.Collections.Generic; using Microsoft.Extensions.Logging; using System.Text.Json; using BlazorWasmUpload.Server.Utils; using System.Linq; namespace BlazorWasmUpload.Server.Controllers { [ApiController] [Route("api/[controller]/[action]")] public class FilesController : ControllerBase { private const int MaxBufferSize = 0x10000; private readonly IWebHostEnvironment _webHostEnvironment; private readonly ILogger<FilesController> _logger; public FilesController( IWebHostEnvironment webHostEnvironment, ILogger<FilesController> logger) { _webHostEnvironment = webHostEnvironment; _logger = logger; } [HttpPost] public async Task<IActionResult> CreateUser( //[FromForm] string userModel, // <-- this is the actual form of the posted model [ModelBinder(BinderType = typeof(JsonModelBinder)), FromForm] User userModel, [FromForm] IList<IFormFile> inputFiles = null) { /*var user = JsonSerializer.Deserialize<User>(userModel); _logger.LogInformation($"userModel.Name: {user.Name}"); _logger.LogInformation($"userModel.Age: {user.Age}");*/ _logger.LogInformation($"userModel.Name: {userModel.Name}"); _logger.LogInformation($"userModel.Age: {userModel.Age}"); var uploadsRootFolder = Path.Combine(_webHostEnvironment.WebRootPath, "Files"); if (!Directory.Exists(uploadsRootFolder)) { Directory.CreateDirectory(uploadsRootFolder); } if (inputFiles?.Any() == true) { foreach (var file in inputFiles) { if (file == null || file.Length == 0) { continue; } var filePath = Path.Combine(uploadsRootFolder, file.FileName); using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, MaxBufferSize, useAsync: true); await file.CopyToAsync(fileStream); _logger.LogInformation($"Saved file: {filePath}"); } } return Ok(); } } }
[ModelBinder(BinderType = typeof(JsonModelBinder)), FromForm] User userModel,
using System; using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.ModelBinding; namespace BlazorWasmUpload.Server.Utils { public class JsonModelBinder : IModelBinder { public Task BindModelAsync(ModelBindingContext bindingContext) { if (bindingContext == null) { throw new ArgumentNullException(nameof(bindingContext)); } var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName); if (valueProviderResult != ValueProviderResult.None) { bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult); var valueAsString = valueProviderResult.FirstValue; var result = JsonSerializer.Deserialize(valueAsString, bindingContext.ModelType); if (result != null) { bindingContext.Result = ModelBindingResult.Success(result); return Task.CompletedTask; } } return Task.CompletedTask; } } }
[HttpPost] public async Task<IActionResult> CreateUser( [FromForm] string userModel, // <-- this is the actual form of the posted model [FromForm] IList<IFormFile> inputFiles = null) { var user = JsonSerializer.Deserialize<User>(userModel);
یک نکته تکمیلی: در Blazor 5x، از نمایش درصد پیشرفت آپلود، پشتیبانی نمیشود؛ از این جهت که HttpClient طراحی شده، در اصل به fetch API استاندارد مرورگر ترجمه میشود و این API استاندارد، هنوز از streaming پشتیبانی نمیکند . حتی ممکن است با کمی جستجو به راهحلهایی که سعی کردهاند بر اساس HttpClient و نوشتن بایت به بایت اطلاعات در آن، درصد پیشرفت آپلود را محاسبه کرده باشند، برسید. این راهحلها تنها کاری را که انجام میدهند، بافر کردن اطلاعات، جهت fetch API و سپس ارسال تمام آن است. به همین جهت درصدی که نمایش داده میشود، درصد بافر شدن اطلاعات در خود مرورگر است (پیش از ارسال آن به سرور) و سپس تحویل آن به fetch API جهت ارسال نهایی به سمت سرور.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: BlazorWasmUpload.zip
امکان تبدیل رخدادهای توکار مرورگرها به دایرکتیوهای Blazor در Blazor6x
یکسری دایرکتیو مانند onclick@ و امثال آن، از پیش در Blazor تعریف شدهاند که امکان مدیریت رویدادهای جاوااسکریپتی را در کدهای سیشارپ میسر میکنند. اما تعداد اینها زیاد نیست. برای مثال تعداد رویدادهای قابل تعریف و پشتیبانی شدهی توسط مرورگرها قابل ملاحظهاست. در Blazor 6x روشی جهت دسترسی سادهتر به این رویدادها ارائه شدهاست که شامل این مراحل است. برای نمونه فرض کنید میخواهیم به رویداد paste مرورگر دسترسی پیدا کنیم و یک دایرکتیو سفارشی oncustompaste@ را برای آن تهیه کنیم:
<input @oncustompaste="HandleCustomPaste" />
<script> Blazor.registerCustomEventType('custompaste', { browserEventName: 'paste', createEventArgs: event => { // This example only deals with pasting text, but you could use arbitrary JavaScript APIs // to deal with users pasting other types of data, such as images return { eventTimestamp: new Date(), pastedData: event.clipboardData.getData('text') }; } }); </script>
پس از اینکار، معادل دو پارامتر بازگشت داده شده را به صورت زیر در کدهای سیشارپ تهیه میکنیم:
namespace BlazorCustomEventArgs.CustomEvents { [EventHandler("oncustompaste", typeof(CustomPasteEventArgs), enableStopPropagation: true, enablePreventDefault: true)] public static class EventHandlers { // This static class doesn't need to contain any members. It's just a place where we can put // [EventHandler] attributes to configure event types on the Razor compiler. This affects the // compiler output as well as code completions in the editor. } public class CustomPasteEventArgs : EventArgs { // Data for these properties will be supplied by custom JavaScript logic public DateTime EventTimestamp { get; set; } public string PastedData { get; set; } } }
یک نکته: در اینجا نام oncustompaste به همان نام custompaste کدهای جاوااسکریپتی اشاره میکند. نام تعریف شدهی در قسمت سیشارپ، یک on در ابتدا اضافهتر دارد. اینکار سبب میشود که اکنون بتوان یک رویدادگردان oncustompaste@ سفارشی را که قابل مدیریت در کدهای سیشارپ است، داشت:
@page "/" <p>Try pasting into the following text box:</p> <input @oncustompaste="HandleCustomPaste" /> <p>@message</p> @code { string message; void HandleCustomPaste(CustomPasteEventArgs eventArgs) { message = $"At {eventArgs.EventTimestamp.ToShortTimeString()}, you pasted: {eventArgs.PastedData}"; } }
در مقاله قبلی با یکی از کتابخانههای مدیریت دیتابیس sqlite آشنا شدیم و و یاد گرفتیم که چگونه یک دیتابیس جدید را بسازیم و اطلاعات را از آن دریافت کنیم. در این مقاله قصد داریم، بیشتر در مورد دستورات این کتابخانه بدانیم و بفهمیم که چگونه باید آنها را به کار بست.
دستورات بدون خروجی:
یک سری از دستورات هستند که خروجی ندارند و رکوردی را باز نمیگردانند و برای اجرای دستوراتی چون افزودن، به روزرسانی و حذف بسیار مناسبند. اجرای این دستورات را ما به متدی به نام run میسپاریم. در دفعه قبل که از این دستور استفاده کردیم، پارامتری برای تعیین کردن نداشت؛ ولی در این مقاله، دستور با پارامتر آن را اجرا میکنیم:
ابتدا کدهای زیر را به فایل html، برای درج رکورد جدید اضافه میکنیم:
دستورات بدون خروجی:
یک سری از دستورات هستند که خروجی ندارند و رکوردی را باز نمیگردانند و برای اجرای دستوراتی چون افزودن، به روزرسانی و حذف بسیار مناسبند. اجرای این دستورات را ما به متدی به نام run میسپاریم. در دفعه قبل که از این دستور استفاده کردیم، پارامتری برای تعیین کردن نداشت؛ ولی در این مقاله، دستور با پارامتر آن را اجرا میکنیم:
ابتدا کدهای زیر را به فایل html، برای درج رکورد جدید اضافه میکنیم:
First Name:<br/> <input type="text" id="txtfname" /><br/> Last Name:<br/> <input type="text" id=txtlname /><br/> Number:<br/> <input type="tel" id="txttel" /><br/> <button id="btnsubmit">Save</button><br/>
function GetValues() { let fname=$("#txtfname").val(); let lname=$("#txtlname").val() let tel=$("#txttel").val(); let row= { fname:fname, lname:lname, number:tel }; return row; }
$("#btnsubmit").click((e)=>{ e.preventDefault(); let row=GetValues(); //save in db //get last id let statement==db.prepare("select id from numbers order by id desc limit 1"); let lastRecord=statement.getAsObject({}).id; row.id=lastRecord++; let count=db.prepare("select count(*) as count from numbers order by id desc").getAsObject({}).count; statement.free(); let insertCommand="insert into numbers values(?,?,?,?)"; db.run(insertCommand,[row.id,row.fname,row.lname,String(row.number)]) let newcount=db.prepare("select count(*) as count from numbers order by id desc").getAsObject({}).count; SaveChanges(); //show in the table if(count<newcount) { AddToTable(row); } }); });
var statement= db.prepare("SELECT * FROM NUMBERS WHERE fname=@fname AND lname=@lname"); var result = statement .getAsObject({'@fname' :'ali', '@lname' : 'yeganeh'});
statement.bind(['hossein','yeganeh']);
متد step همانند متدهای next در cursor یا read در datareader عمل میکند و با هر بار صدا زدن، یک رکورد، به سمت جلو حرکت میکند. دریافت هر رکورد جاری توسط متد get و نوع خروجی آرایه انجام میشود:
while(statement.step()) { var rec=statement.get(); }
بعد از اینکه کارمان با آن تمام شد، برای پاکسازی حافظه از متد free استفاده میکنیم. در دستورات بعد، شیء statement را مستقیما مورد استفاده قرار دادهایم و توسط آن تعداد رکوردها را دریافت کردهایم. سپس با استفاده از متد run دستور درج را دادهایم. اینبار این متد را به شکل متفاوتی استفاده کردیم و به آن پارامتری هم دادیم. نحوه ارائه پارامتر به این متد، باید به صورت آرایه و به ترتیب علامتهای ؟ باشد. نهایتا با دریافت تعداد رکوردهای جاری و مقایسه با تعداد رکوردهای سابق متوجه میشویم که آیا رکوردی اضافه شده است یا خیر؟ در صورتی که اضافه شده است، باید رکورد جدید در جدول، توسط جی کوئری نمایش داده شود و تغییرات دیتابیس هم روی دیسک سخت ذخیره شوند. چون دیتابیس مورد استفاده به صورت in-memory یعنی مقیم در حافظه مورد استفاده قرار میگیرد، باید کل دیتابیس، بر روی دیسک سخت رونویسی شود. متد SaveChanges شامل کد زیر است که حاوی کد ارسال پیام به Main Thread یا Main Process میباشد تا در دیسک سخت بنویسد:
const {ipcRenderer} = require('electron'); function SaveChanges() { ipcRenderer.send("SaveToDb"); }
const {ipcMain} = require('electron'); ipcMain.on("SaveToDb", (event, arg) => { SaveToDb(); }); function SaveToDb() { var data=db.export(); var buffer=new Buffer(data); fs.writeFileSync(dbPath,buffer); }
ویرایش رکورد
ابتدا template string سطر جدول را به شکل زیر تغییر میدهیم:
function AddToTable(row) { let tableBody=$("#people"); let rowTemplate=`<tr><td>${row.fname}</td><td>${row.lname}</td><td>${row.number}</td><td><button class= "btn btn-success btnupdate" data-id="${row.id}" >Edit</button></td></tr>`; tableBody.append(rowTemplate); }
سپس در تگ اسکریپت، در رویداد ready جی کوئری، این دستورات را اضافه میکنیم:
$("#people").on('click','.btnupdate',function(e) { e.preventDefault(); row.id=$(this).data("id"); let row=GetValues(); db.run("UPDATE NUMBERS SET FNAME=?,LNAME=?,NUMBER=? WHERE ID=?",[row.fname,row.lname,row.number,row.id]); SaveChanges(); tr=$(this).closest("tr"); let column=0; tr.find("td").each(function(index) { oldRow=$(this); switch(column) { case 0: //fname oldRow.text(row.fname); break; case 1: //lname oldRow.text(row.lname); break; case 2: //number oldRow.text(row.number); break; } column++; }); });
ابتدای id ذخیره شده در المان و مقادیر جدید را دریافت میکنیم. با استفاده از متد run کوئری به روزرسانی را به همراه پارامترها ارسال میکنیم و نتیجه را بر روی دیسک سخت ذخیره میکنیم. از اینجا به بعد نقش جی کوئری پر رنگتر میشود و به خوبی میتوانیم اهمیت آن را درک کنیم. سطر دکمه جاری را پیدا میکنیم و مقادیر جدید را ستون به ستون تغییر میدهیم.
خواندن و بازگردانی رکوردها
در مقاله قبلی با دستور each آشنا شدیم که یک متد غیرهمزمان بود و نتیجه هر رکورد را با یک callback به ما بازگشت میداد. در اصل این متد شامل 4 پارامتر است: پارامتر اول آن، کوئری ارسالی است. پارامتر دوم آن، پارامتر کوئریها ، پارامتر سوم، تابع callback که به ازای هر رکورد اجرا میشود و پارامتر چهارم، تابع done می باشد. یعنی زمانی که کلیه رکوردها بازگشت داده شدند. شکل کامل آن به این صورت است:
db.each("SELECT name,age FROM users WHERE age >= $majority", {$majority:18}, function(row){console.log(row.name)}, function(){console.log("done");} );
در این مقاله با متد دیگری به نام exec نیز آشنا میشویم که بازگردانی مقادیر در آن به صورت همزمان صورت میگیرد و توانایی آن را دارد که چندین دستور select را بازگردانی کند. به عنوان مثال دستور زیر را در نظر بگیرید:
SELECT ID FROM NUMBERS;SELECT FNAME,LNAME FROM NUMBERS
[ {columns: ['id'], values:[[1],[2],[3]]}, {columns: ['fname','lname'], values:[['ali','yeganeh'],['hossein','yeganeh'],['mohammad','yeganeh']]} ]
var records=db.exec("select * from numbers"); let values=records[0].values; let length=values.length; for(let i=0;i<length;i++) { let object=values[i]; let row={ id:object[0], fname:object[1], lname:object[2], number:object[3] }; AddToTable(row); }
پیشنیازها:
چگونه با استفاده از لوسین مطالب را ایندکس کنیم؟
چگونه از افزونه jQuery Auto-Complete استفاده کنیم؟
نحوه استفاده صحیح از لوسین در ASP.NET
اگر به جستجوی سایت دقت کرده باشید، قابلیت ارائه پیشنهاداتی به کاربر توسط یک Auto-Complete به آن اضافه شدهاست. در مطلب جاری به بررسی این مورد به همراه دو مثال Web forms و MVC پرداخته خواهد شد.
قسمت عمده مطلب جاری با پیشنیازهای یاد شده فوق یکی است. در اینجا فقط به ذکر تفاوتها بسنده خواهد شد.
الف) دریافت لوسین
از طریق NuGet آخرین نگارش را دریافت و به پروژه خود اضافه کنید. همچنین Lucene.NET Contrib را نیز به همین نحو دریافت نمائید.
ب) ایجاد ایندکس
کدهای این قسمت با مطلب برجسته سازی قسمتهای جستجو شده، یکی است:
تنها تفاوت آن اضافه شدن titleField.Boost = 3 میباشد. توسط Boost به لوسین خواهیم گفت که اهمیت عبارات ذکر شده در عناوین مطالب، بیشتر است از اهمیت متون آنها.
ج) تهیه قسمت منبع داده Auto-Complete
توضیحات:
برای نمایش Auto-Complete نیاز به منبع داده داریم که نحوه ایجاد آنرا در کدهای فوق ملاحظه میکنید. در اینجا توسط جستجوی سریع لوسین و امکانات PrefixQuery آن، به تعدادی مشخص (maxItems)، رکوردهای یافت شده را بازگشت خواهیم داد. خروجی حاصل لیستی است از SearchResultها شامل عنوان مطلب و Id آن. عنوان را به کاربر نمایش خواهیم داد؛ از Id برای هدایت او به مطلبی مشخص استفاده خواهیم کرد.
د) نمایش Auto-Complete در ASP.NET MVC
توضیحات:
- ابتدا ارجاعاتی را به jQuery، افزونه Auto-Complete و اسکریپت سفارشی تهیه شده، در فایل layout پروژه تعریف خواهیم کرد.
در اینجا سه قسمت را مشاهده میکنید: کدهای کنترلر، View متناظر و اسکریپتی که Auto-Complete را فعال خواهد ساخت.
- قسمت مهم کدهای کنترلر، دو سطر زیر هستند:
مطابق نیاز افزونه انتخاب شده در مثال جاری، فرمت خروجی مدنظر باید شامل سطرهایی حاوی متن قابل نمایش به همراه یک Id (یا در اینجا یک آدرس مشخص) باشد. البته ذکر این Id اختیاری بوده و در اینجا جهت تکمیل بحث ارائه شده است.
return Content هم سبب بازگشت این اطلاعات به افزونه خواهد شد.
- کدهای View متناظر بسیار ساده هستند. تنها نام TextBox تعریف شده مهم میباشد که در متد جاوا اسکریپتی EnableSearchAutocomplete استفاده شده است. به علاوه، نحوه مقدار دهی آدرس دسترسی به اکشن متد ScoredTerms نیز مهم میباشد.
- در متد EnableSearchAutocomplete نحوه فراخوانی افزونه autocomplete را ملاحظه میکنید.
جهت آن، به راست به چپ تنظیم شده است. با 2 کاراکتر ورودی فعال خواهد شد با وقفهای کوتاه. نیازی نیست تا انتخاب کاربر از لیست ظاهر شده حتما با عبارت جستجو شده صد در صد یکی باشد. حداکثر 20 آیتم در لیست ظاهر خواهند شد. اسکرول بار لیست را حذف کردهایم. عرض آن به 300 تنظیم شده است و نحوه فرمت دهی نمایشی آنرا نیز ملاحظه میکنید. برای این منظور از متد formatItem استفاده شده است. آرایه row در اینجا در برگیرنده اعضای Title و Id ارسالی به افزونه است. اندیس صفر آن به عنوان دریافتی اشاره میکند.
همچنین نحوه نشان دادن عکس العمل به عنصر انتخابی را هم ملاحظه میکنید (در متد result مقدار دهی شده). window.location را به عنصر دوم آرایه row هدایت خواهیم کرد. این عنصر دوم مطابق کدهای اکشن متد تهیه شده، به آدرس یک صفحه اشاره میکند.
ه) نمایش Auto-Complete در ASP.NET WebForms
قسمت عمده مطالب فوق با وب فرمها نیز یکی است. خصوصا توضیحات مرتبط با متد EnableSearchAutocomplete ذکر شده.
در اینجا بجای Controller از یک Generic handler استفاده شده است (Search.ashx).
در آن، عنوان مطالب یافت شده به همراه یک آدرس مشخص، تهیه و در Response نوشته خواهند شد.
کدهای کامل مثال فوق را از اینجا میتوانید دریافت کنید:
همچنین باید دقت داشت که پروژه MVC آن از نوع MVC4 است (VS2010) و فرض براین میباشد که IIS Express 7.5 را نیز پیشتر نصب کردهاید.
کلمه عبور فایل: dotnettips91
چگونه با استفاده از لوسین مطالب را ایندکس کنیم؟
چگونه از افزونه jQuery Auto-Complete استفاده کنیم؟
نحوه استفاده صحیح از لوسین در ASP.NET
اگر به جستجوی سایت دقت کرده باشید، قابلیت ارائه پیشنهاداتی به کاربر توسط یک Auto-Complete به آن اضافه شدهاست. در مطلب جاری به بررسی این مورد به همراه دو مثال Web forms و MVC پرداخته خواهد شد.
قسمت عمده مطلب جاری با پیشنیازهای یاد شده فوق یکی است. در اینجا فقط به ذکر تفاوتها بسنده خواهد شد.
الف) دریافت لوسین
از طریق NuGet آخرین نگارش را دریافت و به پروژه خود اضافه کنید. همچنین Lucene.NET Contrib را نیز به همین نحو دریافت نمائید.
ب) ایجاد ایندکس
کدهای این قسمت با مطلب برجسته سازی قسمتهای جستجو شده، یکی است:
using System.Collections.Generic; using System.IO; using Lucene.Net.Analysis.Standard; using Lucene.Net.Documents; using Lucene.Net.Index; using Lucene.Net.Store; using LuceneSearch.Core.Model; using LuceneSearch.Core.Utils; namespace LuceneSearch.Core { public static class CreateIndex { static readonly Lucene.Net.Util.Version _version = Lucene.Net.Util.Version.LUCENE_30; public static Document MapPostToDocument(Post post) { var postDocument = new Document(); postDocument.Add(new Field("Id", post.Id.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED)); var titleField = new Field("Title", post.Title, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS); titleField.Boost = 3; postDocument.Add(titleField); postDocument.Add(new Field("Body", post.Body.RemoveHtmlTags(), Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS)); return postDocument; } public static void CreateFullTextIndex(IEnumerable<Post> dataList, string path) { var directory = FSDirectory.Open(new DirectoryInfo(path)); var analyzer = new StandardAnalyzer(_version); using (var writer = new IndexWriter(directory, analyzer, create: true, mfl: IndexWriter.MaxFieldLength.UNLIMITED)) { foreach (var post in dataList) { writer.AddDocument(MapPostToDocument(post)); } writer.Optimize(); writer.Commit(); writer.Close(); directory.Close(); } } } }
ج) تهیه قسمت منبع داده Auto-Complete
namespace LuceneSearch.Core.Model { public class SearchResult { public int Id { set; get; } public string Title { set; get; } } }
using System.Collections.Generic; using System.IO; using Lucene.Net.Index; using Lucene.Net.Search; using Lucene.Net.Store; using LuceneSearch.Core.Model; using LuceneSearch.Core.Utils; namespace LuceneSearch.Core { public static class AutoComplete { private static IndexSearcher _searcher; /// <summary> /// Get terms starting with the given prefix /// </summary> /// <param name="prefix"></param> /// <param name="maxItems"></param> /// <returns></returns> public static IList<SearchResult> GetTermsScored(string indexPath, string prefix, int maxItems = 10) { if (_searcher == null) _searcher = new IndexSearcher(FSDirectory.Open(new DirectoryInfo(indexPath)), true); var resultsList = new List<SearchResult>(); if (string.IsNullOrWhiteSpace(prefix)) return resultsList; prefix = prefix.ApplyCorrectYeKe(); var results = _searcher.Search(new PrefixQuery(new Term("Title", prefix)), null, maxItems); if (results.TotalHits == 0) { results = _searcher.Search(new PrefixQuery(new Term("Body", prefix)), null, maxItems); } foreach (var doc in results.ScoreDocs) { resultsList.Add(new SearchResult { Title = _searcher.Doc(doc.Doc).Get("Title"), Id = int.Parse(_searcher.Doc(doc.Doc).Get("Id")) }); } return resultsList; } } }
برای نمایش Auto-Complete نیاز به منبع داده داریم که نحوه ایجاد آنرا در کدهای فوق ملاحظه میکنید. در اینجا توسط جستجوی سریع لوسین و امکانات PrefixQuery آن، به تعدادی مشخص (maxItems)، رکوردهای یافت شده را بازگشت خواهیم داد. خروجی حاصل لیستی است از SearchResultها شامل عنوان مطلب و Id آن. عنوان را به کاربر نمایش خواهیم داد؛ از Id برای هدایت او به مطلبی مشخص استفاده خواهیم کرد.
د) نمایش Auto-Complete در ASP.NET MVC
using System.Text; using System.Web.Mvc; using LuceneSearch.Core; using System.Web; namespace LuceneSearch.Controllers { public class HomeController : Controller { static string _indexPath = HttpRuntime.AppDomainAppPath + @"App_Data\idx"; public ActionResult Index(int? id) { if (id.HasValue) { //todo: do something } return View(); //Show the page } public virtual ActionResult ScoredTerms(string q) { if (string.IsNullOrWhiteSpace(q)) return Content(string.Empty); var result = new StringBuilder(); var items = AutoComplete.GetTermsScored(_indexPath, q); foreach (var item in items) { var postUrl = this.Url.Action(actionName: "Index", controllerName: "Home", routeValues: new { id = item.Id }, protocol: "http"); result.AppendLine(item.Title + "|" + postUrl); } return Content(result.ToString()); } } }
@{ ViewBag.Title = "جستجو"; var scoredTermsUrl = Url.Action(actionName: "ScoredTerms", controllerName: "Home"); var bulletImage = Url.Content("~/Content/Images/bullet_shape.png"); } <h2> جستجو</h2> <div align="center"> @Html.TextBox("term", "", htmlAttributes: new { dir = "ltr" }) <br /> جهت آزمایش lu را وارد نمائید </div> @section scripts { <script type="text/javascript"> EnableSearchAutocomplete('@scoredTermsUrl', '@bulletImage'); </script> }
function EnableSearchAutocomplete(url, img) { var formatItem = function (row) { if (!row) return ""; return "<img src='" + img + "' /> " + row[0]; } $(document).ready(function () { $("#term").autocomplete(url, { dir: 'rtl', minChars: 2, delay: 5, mustMatch: false, max: 20, autoFill: false, matchContains: false, scroll: false, width: 300, formatItem: formatItem }).result(function (evt, row, formatted) { if (!row) return; window.location = row[1]; }); }); }
- ابتدا ارجاعاتی را به jQuery، افزونه Auto-Complete و اسکریپت سفارشی تهیه شده، در فایل layout پروژه تعریف خواهیم کرد.
در اینجا سه قسمت را مشاهده میکنید: کدهای کنترلر، View متناظر و اسکریپتی که Auto-Complete را فعال خواهد ساخت.
- قسمت مهم کدهای کنترلر، دو سطر زیر هستند:
result.AppendLine(item.Title + "|" + postUrl); return Content(result.ToString());
return Content هم سبب بازگشت این اطلاعات به افزونه خواهد شد.
- کدهای View متناظر بسیار ساده هستند. تنها نام TextBox تعریف شده مهم میباشد که در متد جاوا اسکریپتی EnableSearchAutocomplete استفاده شده است. به علاوه، نحوه مقدار دهی آدرس دسترسی به اکشن متد ScoredTerms نیز مهم میباشد.
- در متد EnableSearchAutocomplete نحوه فراخوانی افزونه autocomplete را ملاحظه میکنید.
جهت آن، به راست به چپ تنظیم شده است. با 2 کاراکتر ورودی فعال خواهد شد با وقفهای کوتاه. نیازی نیست تا انتخاب کاربر از لیست ظاهر شده حتما با عبارت جستجو شده صد در صد یکی باشد. حداکثر 20 آیتم در لیست ظاهر خواهند شد. اسکرول بار لیست را حذف کردهایم. عرض آن به 300 تنظیم شده است و نحوه فرمت دهی نمایشی آنرا نیز ملاحظه میکنید. برای این منظور از متد formatItem استفاده شده است. آرایه row در اینجا در برگیرنده اعضای Title و Id ارسالی به افزونه است. اندیس صفر آن به عنوان دریافتی اشاره میکند.
همچنین نحوه نشان دادن عکس العمل به عنصر انتخابی را هم ملاحظه میکنید (در متد result مقدار دهی شده). window.location را به عنصر دوم آرایه row هدایت خواهیم کرد. این عنصر دوم مطابق کدهای اکشن متد تهیه شده، به آدرس یک صفحه اشاره میکند.
ه) نمایش Auto-Complete در ASP.NET WebForms
قسمت عمده مطالب فوق با وب فرمها نیز یکی است. خصوصا توضیحات مرتبط با متد EnableSearchAutocomplete ذکر شده.
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="LuceneSearch.WebForms.Default" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width" /> <title>جستجو</title> <link href="Content/Site.css" rel="stylesheet" type="text/css" /> <script src="Scripts/jquery-1.7.1.min.js" type="text/javascript"></script> <script src="Scripts/jquery.autocomplete.js" type="text/javascript"></script> <script src="Scripts/custom.js" type="text/javascript"></script> </head> <body dir="rtl"> <h2> جستجو</h2> <form id="form1" runat="server"> <div align="center"> <asp:TextBox runat="server" dir="ltr" ID="term"></asp:TextBox> <br /> جهت آزمایش lu را وارد نمائید </div> </form> <script type="text/javascript"> EnableSearchAutocomplete('Search.ashx', 'Content/Images/bullet_shape.png'); </script> </body> </html>
using System.Text; using System.Web; using LuceneSearch.Core; namespace LuceneSearch.WebForms { public class Search : IHttpHandler { static string _indexPath = HttpRuntime.AppDomainAppPath + @"App_Data\idx"; public void ProcessRequest(HttpContext context) { string q = context.Request.QueryString["q"]; if (string.IsNullOrWhiteSpace(q)) { context.Response.Write(string.Empty); context.Response.End(); } var result = new StringBuilder(); var items = AutoComplete.GetTermsScored(_indexPath, q); foreach (var item in items) { var postUrl = "Default.aspx?id=" + item.Id; result.AppendLine(item.Title + "|" + postUrl); } context.Response.ContentType = "text/plain"; context.Response.Write(result.ToString()); context.Response.End(); } public bool IsReusable { get { return false; } } } }
در اینجا بجای Controller از یک Generic handler استفاده شده است (Search.ashx).
result.AppendLine(item.Title + "|" + postUrl); context.Response.Write(result.ToString());
کدهای کامل مثال فوق را از اینجا میتوانید دریافت کنید:
همچنین باید دقت داشت که پروژه MVC آن از نوع MVC4 است (VS2010) و فرض براین میباشد که IIS Express 7.5 را نیز پیشتر نصب کردهاید.
کلمه عبور فایل: dotnettips91
نصب: پکیجهای متنوعی از breeze وجود دارند. برای ما بستهی زیر بهترین انتخاب میباشد. با نصب پکیج زیر، breeze در سمت سرور و کلاینت، به همراه ASP.NET Web API 2.2 and Entity Framework 6 نصب میشود:
Install-Package Breeze.WebApi2.EF6
var instance = breeze.config.initializeAdapterInstance("ajax", "angular"); instance.setHttp($http);
Install-Package Breeze.Angular
قلب تپندهی breezejs در کلاینت EntityManager است که نقش data context را در کلاینت، بازی میکند. به برخی از خصوصیات آن میپردازیم:
var manager = new breeze.EntityManager({ dataService: dataService, metadataStore: metadataStore, saveOptions: new breeze.SaveOptions({ allowConcurrentSaves: true, tag: [{}] }) });
var dataService = new breeze.DataService({ serviceName: "/breeze/"+ "Automobile", hasServerMetadata: false, namingConvention: breeze.NamingConvention.camelCase }); var metadataStore = new breeze.MetadataStore({});
- serviceName: نام سرویس دهنده یا کنترلر سمت سرور میباشد. درمورد کنترلر سمت سرور کمی جلوتر بحث میکنیم.
- metadataStore: اطلاعاتی را در مورد تمام آبجکتها (جداول دیتابیس) میدهد. مثل نام فیلدها، نوع فیلدها و...
برای کار با متادیتا دو راه وجود دارد:
1- متا دیتا را خودتان در سمت کلاینت ایجاد نمایید:
var myMetadataStore = new breeze.MetadataStore(); myMetadataStore.addEntityType({...});
var customer = function () { this.City = ""; }; myMetadataStore.registerEntityTypeCtor("Customer", customer);
2- اطلاعات را از سرور دریافت نمایید. در این صورت کنترلر شما باید دارای متد Metadata باشد. بنابراین کنترلی را در سرور به نام Automobile و با محتویات زیر ایجاد نمایید. همانطور که مشاهده میکنید، این کنترلر از ApiController مشتق شده است که تفاوت خاصی با Apiهای دیگر ندارد و تنها به BreezeController مزین شده است. این attribute به NET WebApi کمک میکند که فیلترینگ و مرتب سازی با فرمت oData را فراهم کند و همچنین درک صحیح فرمت json را نیز به کنترلر میدهد.
EFContextProvider: کامپوننتی که تعامل بین کنترلر breeze با Entity Framework را سادهتر میکند و در واقع یک wrapper بر روی دیتاکانتکس یا آبجکت کانتکس میباشد. یکی از وظایف آن ارسال متا دیتا، برای کلاینتهای breeze است.
[BreezeController] public class AutomobileController : ApiController { readonly EFContextProvider<ApplicationDbContext> _contextProvider = new EFContextProvider<ApplicationDbContext>(); [HttpGet] public string Metadata() { return _contextProvider.Metadata(); } [HttpGet] public IQueryable<Customer> Customers() { return _contextProvider.Context.Customers; } [System.Web.Http.HttpPost] public SaveResult SaveChanges(JObject saveBundle) { _contextProvider.BeforeSaveEntitiesDelegate = BeforeSaveEntities; _contextProvider.AfterSaveEntitiesDelegate = afterSaveEntities; return _contextProvider.SaveChanges(saveBundle); } protected Dictionary<Type, List<EntityInfo>> BeforeSaveEntities(Dictionary<Type, List<EntityInfo>> saveMap) { } private void afterSaveEntities(Dictionary<Type, List<EntityInfo>> saveMap, List<KeyMapping> keyMappings) { } }
- saveOptions: نحوهی چگونگی برخورد با ذخیره کردن اطلاعات را مشخص میکند. با ذخیره سازی تغییرات، متد SaveChanges سمت سرور فراخوانی میشود. در breeze میتوان به قبل و بعد از ذخیره سازی اطلاعات دسترسی داشت. یکی از موارد رایج کاربرد آن، اعمال چک کردن دسترسیها، قبل از ذخیره سازی میباشد.
برای ذخیره سازی تغییرات:
manger.saveChanges().then(function success() { }, function failer(e) { });
manger.rejectChanges()
کوئری:بعد از تعریف Entity Manger میتوانیم کوئری خود را اجرا نماییم. کوئری ما شامل گرفتن اطلاعات از جدول Customer، با مرتب سازی بر روی فیلد آیدی میباشد و با اجرا کردن کوئری میتوانیم موفقیت یا عدم موفقیت آنرا بررسی نماییم.
var query = breeze.EntityQuery .from("Customer") .orderBy("Id"); var result= manager.executeQuery(query); result.then(querySucceeded) .fail(queryFailed); query = query.where("Id", "==", 1)
var predicate = new breeze.Predicate("Id", "==", false); query = query.where(predicate) var p1 = new breeze.Predicate("IsArchived", "==", false); var p2 = breeze.Predicate("IsDone", "==", false); var predicate = p1.and(p2); query = query.where(predicate).orderBy("Id")
?$filter=IsArchived eq false&IsDone eq false &$orderby=Id
اعتبارسنجی :اعتبارسنجی در breeze، هم در سمت کلاینت و هم در سمت سرور امکان پذیر میباشد که در مثالی، در قسمت بعدی، validator سفارشی خودمان را خواهیم ساخت و به entity مورد نظر اعمال خواهیم کرد.
breeze دارای یک سری Validator در سطح پراپرتیها است:
- برای انواع اقسام dataType ها مانند Int,string,..
- برای نیازهای رایجی چون: emailAddress,creditCard,maxLength,phone,regularExpression,required,url
هم چنین در breeze امکان تغییر دادن اعتبارسنجیهای پیش فرض نیز وجود دارند. برای مثال برای اینکه در فیلدهای required بتوان متن خالی هم وارد کرد، از دستور زیر میتوان استفاده کرد:
breeze.Validator.required({ allowEmptyStrings: true });
ردیابی تغییرات: هر آیتم Entity دارای EntityAspect است که وضعیت آنرا مشخص میکند و میتواند یکی از وضعیتهای Added،Modified،Deleted،Detached،Unchanged باشد. با مشخص کردن حالت هر آیتم، با فراخوانی SaveChanges تغییرات بر روی دیتابیس اعمال میگردد.
ایجاد آیتم جدید:
manager.createEntity('Customer', jsonValue);
manager.createEntity("Customer", jsonValue, breeze.EntityState.Modified, breeze.MergeStrategy.OverwriteChanges)
manager.createEntity("Customer", item, breeze.EntityState.Deleted)
برای اشنایی بیشتر با امکانات Breeze، قصد داریم یک سایت ایجاد آگهی را راه اندازی کنیم. پیش نیازهای ضروری این بخش typescript ،angularjs ،requirejs هستند. قصد داریم سایتی را برای آگهیهای خرید و فروش خودرو، مشابه با سایت باما ایجاد نماییم:
امکانات این سایت:
- ثبت نام کاربران
- ثبت آگهی توسط کاربران
- ایجاد برچسبهای آگهیها
- امتیاز دهی به آگهیها
- جستجوی آگهیها
- و....
ابتدا نصب پکیجهای زیر
Install-Package angularjs Install-Package angularjs.TypeScript.DefinitelyTyped Install-Package bootstrap Install-Package bootstrap.TypeScript.DefinitelyTyped Install-Package jQuery Install-Package jquery.TypeScript.DefinitelyTyped Install-Package RequireJS Install-Package requirejs.TypeScript.DefinitelyTyped bower install angularAMD
مدلهای برنامه:
ایجاد کلاس BaseEntity
public class BaseEntity { public int Id { get; set; } public bool Status { get; set; } public DateTime CreatedDateTime { get; set; } }
public class Ad : BaseEntity { public string Title { get; set; } public float Price { get; set; } public double Rating { get; set; } public int? RatingNumber { get; set; } public string UserId { get; set; } public DateTime ModifieDateTime { get; set; } public string Description { get; set; } public virtual ICollection<Comment> Comments { get; set; } public virtual IdentityUser User { get; set; } public virtual ICollection<AdLabel> Labels { get; set; } public virtual ICollection<AdMedia> Medias { get; set; } }
ایجاد جدول برچسب
public class Label { public int Id { get; set; } public string Title { get; set; } public int? ParentId { get; set; } public virtual Label Parent { get; set; } public virtual ICollection<Label> Items { get; set; } }
ایجاد جدول مدیا
public class Media { public int Id { get; set; } public string Name { get; set; } public string MimeType { get; set; } }
public class AdLabel { public int Id { get; set; } public virtual Ad Ad { get; set; } public virtual Label Label { get; set; } [Index("IX_AdLabel", 1, IsUnique = true)] public int AdId { get; set; } [Index("IX_AdLabel", 2, IsUnique = true)] public int LabelId { get; set; } public string Value { get; set; } }
public class AdMedia { public int Id { get; set; } public virtual Ad Ad { get; set; } public virtual Media Media { get; set; } [Index("IX_AdMedia", 1, IsUnique = true)] public int AdId { get; set; } [Index("IX_AdMedia", 2, IsUnique = true)] public int MediaId { get; set; } }
public class Comment : BaseEntity { public string Body { get; set; } public double Rating { get; set; } public int? RatingNumber { get; set; } public string EntityName { get; set; } public string UserId { get; set; } public int? ParentId { get; set; } public int? AdId { get; set; } public virtual Comment Parent { get; set; } public virtual Ad Ad { get; set; } public virtual ICollection<Comment> Items { get; set; } public virtual IdentityUser User { get; set; } }
public class Customer:BaseEntity { public string UserId { get; set; } public virtual string DisplayName { get; set; } public virtual string BirthDay { get; set; } public string City { get; set; } public string Address { get; set; } public int? MediaId { get; set; } public bool? NewsLetterSubscription { get; set; } public string PhoneNumber { get; set; } public virtual IdentityUser User { get; set; } public virtual Media Media { get; set; } }
public class Rating { public int Id { get; set; } public string UserId { get; set; } public Double Rate { get; set; } public string EntityName { get; set; } public int DestinationId { get; set; } }
اضافه کردن مدلهای برنامه به ApplicationDbContext
public class ApplicationDbContext : IdentityDbContext<ApplicationUser> { public ApplicationDbContext() : base("DefaultConnection", throwIfV1Schema: false) { } public DbSet<Ad> Ads { get; set; } public DbSet<AdLabel> AdLabels { get; set; } public DbSet<AdMedia> AdMedias { get; set; } public DbSet<Comment> Comments { get; set; } public DbSet<Label> Labels { get; set; } public DbSet<Media> Medias { get; set; } public static ApplicationDbContext Create() { return new ApplicationDbContext(); } }
لود کردن فایل main.js در فایل layout.cshtml ترجیحا در انتهای body
<script src="~/Scripts/require.js" data-main="/app/main"></script>
RequireJS کتابخانهی جاوااسکریپتی برای بارگزاری فایلها در صورت نیاز میباشد. تنها کاری که ما باید انجام بدهیم این است که کدهای خود را داخل moduleها قرار دهیم (در فایلهای جداگانه) و RequireJS در صورت نیاز آنها را load خواهد کرد. همچنین RequireJS وابستگی بین moduleها را نیز مدیریت میکند.
ایجاد فایل main.ts
path: مسیر فایلهای جاوا اسکریپتی
shim: وابستگیهای فایلها(ماژول ها) و export کردن آنها را مشخص میکند.
requirejs.config({ paths: { "app": "app", "angularAmd":"/Scripts/angularAmd", "angular": "/Scripts/angular", "bootstrap": "/Scripts/bootstrap", "angularRoute": "/Scripts/angular-route", "jquery": "/Scripts/jquery-2.2.2", }, waitSeconds: 0, shim: { "angular": { exports: "angular" }, "angularRoute": { deps: ["angular"] }, "bootstrap": { deps: ["jquery"] }, "app": { deps: ["bootstrap","angularRoute"] } } }); require(["app"]);
ایجاد فایل app.ts: کارهایی که در فایل app انجام دادهایم:
ایجاد کنترلر SecurityCtrl و اعمال آن به تگ body
<body ng-controller="SecurityCtrl"> ... </body>
"use strict"; module AdApps { class SecurityCtrl { private $scope: Interfaces.IAdvertismentScope; constructor($scope: Interfaces.IAdvertismentScope) { // security check this.$scope = $scope; } } define(["angularAmd", "angular"], (angularAmd, ng) => { angularAmd = angularAmd.__proto__; var app = ng.module("AngularTypeScript", ['ngRoute']); var viewPath = "app/views/"; var controllerPath = "app/controller/"; app.config(['$routeProvider', $routeProvider => { $routeProvider .when("/", angularAmd.route({ templateUrl: viewPath + "home.html", controllerUrl: controllerPath + "home .js" })) .otherwise({ redirectTo: '/' }); } ]); app.controller('SecurityCtrl', ['$scope', SecurityCtrl]); return angularAmd.bootstrap(app); })}
Page یا «صفحه» در Razor، یکی از ویژگیهای جدید در ASP.NET Core MVC است که تمرکز کدنویسی را بر روی صفحات قرار میدهد و این موجب راحتی کدنویسی و بالارفتن راندمان میشود. این «صفحات» نیازمند استفاده از نسخۀ ASP.NET Core 2.0.0 و نسخههای بعد از آن هستند که در Visual Studio 2017 Update 3 و نسخههای بعدی در دسترس است.
«صفحات» Razor بهطور پیشفرض در MVC در دسترس است و کافیست در فایل Startup.cs، «صفحات» Razor فعال شود:
public class Startup
{
public void ConfigureServices(IServiceCollections services)
{
services.AddMvc(); // موجب فعال شدن «صفحات» و کنترلرها میشود
}
public void Configure(IApplicationBuilder app)
{
app.UseMvc();
}
}
«صفحۀ» زیر را در نظر بگیرید:
@page @{ var message = "Hello, World!"; } <html> <body> <p>@message</p> </body> </html>
این کد، بسیار شبیه «صفحات» ویوی Razor معمولی است؛ اما آنچه آن را متفاوت میسازد، استفاده از دایرکتیو page@ است. با استفاده از page@، درواقع این فایل نقش اکشن متد MVC را نیز انجام میدهد و بدین معناست که میتواند بهطورمستقیم، درخواستها را بدون وساطت هیچ کنترلری مدیریت کند. دایرکتیو page@، باید در ابتدای صفحۀ Razor قرار بگیرد. این دایرکتیو رفتار اصلی سایر Razorها را تحت تأثیر قرار میدهد.
ارتباط میان مسیرهای URL با صفحات، از طریق محل قرارگیری «صفحات» در فایل سیستم، اتفاق میافتد. جدول زیر نحوۀ ارتباط میان مسیر «صفحات» Razor و URL متناظر آن را نشان میدهد:
URL متناظر | نام فایل و مسیر آن |
/ یا /Index | /Pages/Index.cshtml |
/Contact | /Pages/Store/Contact.cshtml |
/Store/Contact | /Pages/Store/Contact.cshtml |
برنامه هنگام اجرا، بهطور پیشفرض، برای یافتن فایل «صفحات» Razor در پوشۀ «صفحات» جستجو میکند.
ویژگیهای جدید «صفحات» Razor بدین منظور طراحی شدهاند تا الگوهای عمومی بهکار رفته در پیمایشگر صفحات وب را آسانتر کنند. صفحۀ «تماس با ما» را در نظر بگیرید که در آن مدل Contact پیادهسازی شده است. فایل MyApp/Contact.cs بدین شکل است:
using System.ComponentModel.DataAnnotations; namespace MyApp { public class Contact { [Required] public string Name { get; set; } [Required] public string Email { get; set; } } }
@page @using MyApp @using Microsoft.AspNetCore.Mvc.RazorPages @addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers" @inject ApplicationDbContext Db @functions { [BindProperty] public Contact Contact { get; set; } public async Task<IActionResult> OnPostAsync() { if (ModelState.IsValid) { Db.Contacts.Add(Contact); await Db.SaveChangesAsync(); return RedirectToPage(); } return Page(); } } <html> <body> <p>فرم زیر را پر کنید تا در اسرع وقت، کارشناسان ما با شما بگیرند</p> <div asp-validation-summary="All"></div> <form method="POST"> <div>Name: <input asp-for="Contact.Name" /></div> <div>Email: <input asp-for="Contact.Email" /></div> <input type="submit" /> </form> </body> </html>
این «صفحه»، هندلری با عنوان OnPostAsync دارد که هنگام ارسال درخواستهای POST (هنگامیکه کاربری فرم را ثبت میکند) اجرا میشود. البته میتوان برای هر نوع تقاضای HTTP، هندلری را اضافه کرد که معمولاً از OnGet برای هرگونه تقاضای نشان دادن یک صفحۀ HTML استفاده میشود و از OnPost برای ارسال اطلاعات از طریق فرم استفاده میشود و همانطور که میدانیم پسوند Async اختیاری است. اما اغلب بهطور قراردادی بهکار برده میشود. کد نوشته شده داخل OnPostAsync بسیار شبیه چیزی است که بهطور معمول در داخل یک کنترلر نوشته میشود. این الگویی است برای صفحاتی که در آنها بیشتر اصول اوّلیۀ MVC از قبیل بایند کردن مدلها، اعتبارسنجی و نتایج اکشنها قابل استفاده است.
روند اصلی OnPostAsync بهشکل زیر است:
کنترل خطاهای اعتبارسنجی.
- اگر خطایی نبود، اطلاعات ذخیره شده و به صفحۀ دیگر ریدایرکت میشود.
- درغیراینصورت، صفحه را دوباره بههمراه خطاهای اعتبار سنجی نمایش میدهد.
- اگر اطلاعات موفقتآمیز وارد شوند، آنگاه متد هندلر OnPostAsync، هلپر RedirctToPage را برای برگرداندن نمونهای از RedirectToPageResult فراخوانی میکند. این یک نوع جدید بازگشتی برای اکشن متد است که شبیه RedirectToAction یا RedirectToRoute است؛ با این تفاوت که مخصوص صفحات طراحی شده است.
هنگامیکه فرم ثبت شده، خطای اعتبارسنجی داشته باشد، آنگاه متد هندلر OnPostAsync هلپر متد Page را فراخوانی میکند. این هلپر یک نمونه از PageResult را برمیگرداند. این شبیه کاری است که اکشنها در کنترلرها، یک ویو را برگردانند. PageResult خروجی پیشفرض یک هندلر متد است و هندلر متدی که نوع void را برگرداند «صفحۀ» جاری را رندر میکند.
خصیصۀ Contact از attribute جدید [BindProperty] برای مقید کردن مدل استفاده میکند. «صفحات» بهطور پیشفرض، ویژگیهایی را که GET نیستند، مقید میکنند. مقیدکردن به خواص، موجب کاهش کدی میشود که شما باید در فرم مورد نظر خود بیاورید؛ مثلاً بهراحتی میتوان نوشت ( <input asp-for="Contacts.Name" /> ) که با این کار مقیدسازی فیلد و خصیصۀ مورد نظر بهطور خودکار انجام میشود.
بهجای استفاده از دایرکتیو model@ در اینجا، از ویژگی جدید «صفحات» استفاده میکنیم. بهطور پیشفرض دایرکتیو کلاس Page همان مدل است و ویژگیهایی شبیه مقیدکردن مدلها، تگ هلپرها و هلپرهای HTML، همگی فقط با ویژگیهایی که درون functions@ نوشته شدهاند، کار میکنند. هنگام استفاده از «صفحات» بهطور خودکار به ویومدل دسترسی وجود دارد.
دایرکتیو inject@ قبل از شروع هندلر متد، قرار گرفته و برای تزریق وابستگی در «صفحات» استفاده میشود که همانند Razor ویوهای قبل کار میکند. متغیر Db در اینجا از نوع ApplicationDbContext است که به «صفحه» تزریق میشود.
در اینجا نیازی به نوشتن هیچ دستوری برای اعتبارسنجی anti-forgery نیست. تولید و اعتبارسنجی anti-forgery بهطور خودکار در «صفحات» انجام میشود و برای دستیابی به این ویژگی امنیتی نیاز به تنظیمات اضافهتری نیست.
میتوان ویوی MyApp/Pages/Contact.chsml را از مدل آن به شکل زیر جدا کرد:
@page @using MyApp @using MyApp.Pages @using Microsoft.AspNetCore.Mvc.RazorPages @addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers" @model ContactModel <html> <body> <p>فرم زیر را پر کنید تا در اسرع وقت، کارشناسان ما با شما بگیرند </p> <div asp-validation-summary="All"></div> <form method="POST"> <div>Name: <input asp-for="Contact.Name" /></div> <div>Email: <input asp-for="Contact.Email" /></div> <input type="submit" /> </form> </body> </html>
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; namespace MyApp.Pages { public class ContactModel : PageModel { public ContactModel(ApplicationDbContext db) { Db = db; } [BindProperty] public Contact Contact { get; set; } private ApplicationDbContext Db { get; } public async Task<IActionResult> OnPostAsync() { if (ModelState.IsValid) { Db.Contacts.Add(Contact); await Db.SaveChangesAsync(); return RedirectToPage(); } return Page(); } } }
با استفاده از PageModel میتوان از الگوی آزمون واحد بهره برد؛ اما مستلزم نوشتن صریح کلاس و سازندۀ آن است. مزیت «صفحات» بدون فایل PageModel این است که از کامپایل در زمان اجرا پشتیبانی میکنند که این قابلیت در هنگام کدنویسی مفید است.
ادامه دارد...