مطالب
Angular CLI - قسمت پنجم - ساخت و توزیع برنامه
ساخت و توزیع برنامه‌های Angular یکی از مهم‌ترین و بحث برانگیزترین قسمت‌های نگارش‌های جدید آن است و به ازای هر پروژه و قالبی که برای آن توسط گروه‌های مختلف ارائه شده‌است، روش‌های متفاوتی را شاهد خواهید بود. در ادامه روش توصیه شده‌ی توسط تیم Angular را که مبتنی است بر webpack و به صورت خودکار توسط Angular CLI مدیریت می‌شود، بررسی خواهیم کرد.


ساخت (Build) برنامه‌های Angular

Angular CLI کار ساخت و کامپایل برنامه را به صورت خودکار انجام داده و خروجی را در مسیری مشخص درج می‌کند. در اینجا می‌توان گزینه‌هایی را بر اساس نوع کامپایل مدنظر مانند کامپایل برای حالت توسعه و یا کامپایل برای حالت توزیع نهایی، انتخاب کرد. همچنین مباحث bundling و یکی کردن تعداد بالای ماژول‌های برنامه در آن لحاظ می‌شوند تا برنامه در حالت توزیع نهایی، سبب 100ها رفت و برگشت به سرور برای دریافت ماژول‌های مختلف آن نشود. به علاوه مباحث uglification (به نوعی obfuscation کدهای جاوا اسکریپتی نهایی) و tree-shaking (حذف کدهایی که در برنامه استفاده نشده‌اند؛ یا کدهای مرده) نیز پیاده سازی می‌شوند. با انجام tree-shaking‌، نه تنها اندازه‌ی توزیع نهایی به کاربر کاهش پیدا می‌کند، بلکه مرورگر نیز حجم کمتری از کدهای جاوااسکریپتی را باید تفسیر کند.
برای شروع می‌توان از دستور ذیل برای مشاهده‌ی تمام گزینه‌های مهیای ساخت برنامه استفاده کرد:
> ng build --help
ذکر تنهای دستور ng build‌، بدون هیچ گزینه‌ای، برای حالت «توسعه‌ی» برنامه بسیار ایده‌آل است (و دقیقا به معنای صدور دستور ng build --dev است). در این حالت خروجی کامپایل شده‌ی برنامه در پوشه‌ی dist تولید می‌شود. اگر از قسمت دوم این سری به خاطر داشته باشید، نام این پوشه‌ی خروجی، جزئی از تنظیمات فایل angular-cli.json. است:
"apps": [
{
   "outDir": "dist",
زمانیکه دستور ng build‌  صادر شود، این فایل‌ها را در پوشه‌ی dist خواهید یافت:

فایل 
توضیح 
 inline.bundle.js   WebPack runtime
از آن برای بارگذاری ماژول‌های برنامه و چسباندن قسمت‌های مختلف به یکدیگر استفاده می‌شود. 
 main.bundle.js   شامل تمام کدهای ما است. 
 polyfills.bundle.js   Polyfills - جهت پشتیبانی از مرورگرهای مختلف.
 styles.bundle.js    شامل بسته بندی تمام شیوه نامه‌های برنامه است 
vendor.bundle.js  کدهای کتابخانه‌های ثالث مورد استفاده و همچنین خود Angular، در اینجا بسته بندی می‌شوند. 
 

روشی برای بررسی محتوای bundleهای تولید شده

تولید bundleها در جهت کاهش رفت و برگشت‌های به سرور و بالا بردن کارآیی برنامه ضروری هستند؛ اما دقیقا این بسته بندی‌ها شامل چه اطلاعاتی می‌شوند؟ این اطلاعات را می‌توان از فایل‌های source map تولیدی استخراج کرد و برای این منظور می‌توان از برنامه‌ی source-map-explorer استفاده کرد.

روش نصب عمومی آن:
 > npm install -g source-map-explorer
روش اجرا:
 > source-map-explorer dist/main.bundle.js
پس از آن یک گزارش HTML ایی از محتوای bundle مدنظر تولید می‌شود.


یک مثال: ساخت برنامه‌ی مثال قسمت چهارم - تنظیمات مسیریابی در حالت dev

در ادامه، کار Build همان مثالی را که در قسمت قبل توضیح داده شد، بررسی می‌کنیم. برای این منظور از طریق خط فرمان به ریشه‌ی پوشه‌ی اصلی پروژه وارد شده و دستور ng build را صادر کنید. یک چنین خروجی را مشاهده خواهید کرد:
 D:\Prog\angular-routing>ng build
Hash: 123cae8bd8e571f44c31
Time: 33862ms
chunk {0} polyfills.bundle.js, polyfills.bundle.js.map (polyfills) 158 kB {4} [initial] [rendered]
chunk {1} main.bundle.js, main.bundle.js.map (main) 14.7 kB {3} [initial] [rendered]
chunk {2} styles.bundle.js, styles.bundle.js.map (styles) 9.77 kB {4} [initial] [rendered]
chunk {3} vendor.bundle.js, vendor.bundle.js.map (vendor) 2.34 MB [initial] [rendered]
chunk {4} inline.bundle.js, inline.bundle.js.map (inline) 0 bytes [entry] [rendered]
و اگر فایل index.html تولیدی آن‌را بررسی کنید، تنها الحاق همین 4 فایل js تولیدی را مشاهده می‌نمائید:
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>AngularRouting</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <app-root>Loading...</app-root>
<script type="text/javascript" src="inline.bundle.js">
</script><script type="text/javascript" src="polyfills.bundle.js">
</script><script type="text/javascript" src="styles.bundle.js">
</script><script type="text/javascript" src="vendor.bundle.js">
</script><script type="text/javascript" src="main.bundle.js"></script>
</body>
</html>

یک نکته: زمانیکه دستور ng serve -o صادر می‌شود، در پشت صحنه دقیقا همین دستور ng build صادر شده و اطلاعات را درون حافظه تشکیل می‌دهد. اما اگر کار ng build را دستی انجام دهیم، اینبار ng serve -o اطلاعات را از پوشه‌ی dist دریافت می‌کند. بنابراین در حین کار با ng serve -o نیازی به build دستی پروژه نیست.

سؤال: چرا حجم فایل endor.bundle.js اینقدر بالا است و شامل چه اجزایی می‌شود؟
نکته‌ای که در اینجا وجود دارد، حجم بالای فایل vendor.bundle.js آن است که 2.34 MB می‌باشد:


چون دستور ng build بدون پارامتری ذکر شده‌است، برنامه را برای حالت توسعه Build می‌کند و به همین جهت هیچگونه بهینه سازی در این مرحله صورت نخواهد گرفت. برای بررسی محتوای این فایل می‌توان دستور ذیل را در ریشه‌ی اصلی پروژه صادر کرد:
 > source-map-explorer dist/vendor.bundle.js
پس از اجرای این دستور، بلافاصله مرورگر پیش فرض سیستم اجرا شده و گزارشی را ارائه می‌دهد.


همانطور که مشاهده می‌کنید، در حالت بهینه سازی نشده و Build برای توسعه، کامپایلر Angular حدود 41 درصد حجم فایل vendor.bundle.js را تشکیل می‌دهد. به علاوه ماژول‌ها و قسمت‌هایی را ملاحظه می‌کنید که اساسا برنامه‌ی فعلی مثال ما از آن‌ها استفاده نمی‌کند؛ مانند http، فرم‌ها و غیره.


سفارشی سازی Build برای محیط‌های مختلف

اگر به پروژه‌ی تولید شده‌ی توسط Angular CLI دقت کنید، حاوی پوشه‌ای است به نام src\environments


هدف از فایل‌های environment برای نمونه تغییر آدرس توزیع برنامه در حالت توسعه و ارائه نهایی است.
همچنین در اینجا می‌توان نحوه‌ی بهینه سازی فایل‌های تولیدی را توسط Build Targets مشخص کرد و اینکار توسط ذکر پرچم prod-- (مخفف production) صورت می‌گیرد.
در ادامه، تفاوت‌های دستورهای ng build و ng build --prod را ملاحظه می‌کنید:
- با اجرای ng build، از فایل environment.ts استفاده می‌شود؛ برخلاف حالت اجرای ng build --prod که از فایل environment.prod.ts استفاده می‌کند.
- Cache-busting در حالت ارائه‌ی نهایی، به تمام اجزای پروژه اعمال می‌شود؛ اما در حالت توسعه فقط برای تصاویر قید شده‌ی در فایل‌های css.
- فایل‌های source map فقط برای حالت توسعه تولید می‌شوند.
- در حالت توسعه، cssها داخل فایل‌های js تولیدی قرار می‌گیرند؛ اما در حالت ارائه‌ی نهایی به صورت فایل‌های css بسته بندی می‌شوند.
- در حالت توسعه برخلاف حالت ارائه‌ی نهایی، کار uglification انجام نمی‌شود.
- در حالت توسعه برخلاف حالت ارائه‌ی نهایی، کار tree-shaking یا حذف کدهای مرده و بدون ارجاع، انجام نمی‌شود.
- در حالت توسعه برخلاف حالت ارائه‌ی نهایی، کار AOT انجام نمی‌شود. در اینجا AOT به معنای Ahead of time compilation است.
- در هر دو حالت توسعه و ارائه‌ی نهایی کار bundling و دسته بندی فایل‌ها انجام خواهد شد.

به همین جهت است که ng build سریع است؛ اما حجم بالاتری را هم تولید می‌کند. چون بسیاری از بهینه سازی‌های حالت ارائه‌ی نهایی را به همراه ندارد.


دستورات build برای حالت توسعه و ارائه‌ی نهایی

برای حالت توسعه، هر 4 دستور ذیل یک مفهوم را دارند و به همین جهت مورد ng build متداول‌تر است:
>ng build --target=development --environment=dev
>ng build --dev -e=dev
>ng build --dev
>ng build

برای حالت ارائه‌ی نهایی، هر 3 دستور ذیل یک مفهوم را دارند و به همین جهت مورد ng build --prod متداول‌تر است:
>ng build --target=production --environment=prod
>ng build --prod -e=prod
>ng build --prod

همچنین هر کدام از این دستورات را توسط پرچم‌های ذیل نیز می‌توان سفارشی سازی کرد:

 پرچم  مخفف  توضیح
 sourcemap--  sm-  تولید سورس‌مپ
aot--    Ahead of Time compilation 
watch--  w-  تحت نظر قرار دادن فایل‌ها و ساخت مجدد
environment--  e-  محیط ساخت
 target--  t-  نوع ساخت
 dev--    مخفف نوع ساخت جهت توسعه
 prod--     مخفف نوع ساخت جهت ارائه نهایی

برای مثال در حالت prod، سورس‌مپ‌ها تولید نخواهند شد. اگر علاقمندید تا این فایل‌ها نیز تولید شوند، پرچم souremap را نیز ذکر کنید.
و یا اگر برای حالت dev می‌خواهید AOT را فعالسازی کنید، پرچم aot-- را در آنجا قید کنید.


یک مثال: ساخت برنامه‌ی مثال قسمت چهارم - تنظیمات مسیریابی در حالت prod

تا اینجا خروجی حالت dev ساخت برنامه‌ی قسمت چهارم را بررسی کردیم. در ادامه دستور ng build --prod را در ریشه‌ی پروژه صادر می‌کنیم:
 D:\Prog\angular-routing>ng build --prod
Hash: f5bd7fd555a85af8a86f
Time: 39932ms
chunk {0} polyfills.18173234f9641113b9fe.bundle.js (polyfills) 158 kB {4} [initial] [rendered]
chunk {1} main.c6958def7c5f51c45261.bundle.js (main) 50.3 kB {3} [initial] [rendered]
chunk {2} styles.d41d8cd98f00b204e980.bundle.css (styles) 69 bytes {4} [initial] [rendered]
chunk {3} vendor.b426ba6883193375121e.bundle.js (vendor) 1.37 MB [initial] [rendered]
chunk {4} inline.8cec210370dd3af5f1a0.bundle.js (inline) 0 bytes [entry] [rendered]


همانطور که ملاحظه می‌کنید، اینبار نه تنها حجم فایل‌ها به میزان قابل ملاحظه‌ای کاهش پیدا کرده‌اند، بلکه این نام‌ها به همراه یک سری hash هم هستند که کار cache-busting (منقضی کردن کش مرورگر، با ارائه‌ی نگارشی جدید) را انجام می‌دهند.

در ادامه اگر بخواهیم مجددا برنامه‌ی source-map-explorer را جهت بررسی محتوای فایل‌های js اجرا کنیم، به خطای عدم وجود sourcemapها خواهیم رسید (چون در حالت prod، به صورت پیش فرض غیرفعال هستند). به همین‌جهت برای این مقصود خاص نیاز است از پرچم فعالسازی موقت آن استفاده کرد:
> ng build --prod --sourcemap
> source-map-explorer dist/vendor.b426ba6883193375121e.bundle.js


همانطور که در تصویر نیز مشخص است، اینبار کامپایلر Angular به همراه تمام ماژول‌هایی که در برنامه ارجاعی به آن‌ها وجود نداشته‌است، حذف شده‌اند و کل حجم بسته‌ی Angular به 366 KB کاهش یافته‌است.


بررسی دستور ng serve

تا اینجا برای اجرای برنامه در حالت dev از دستور ng serve -o استفاده کرده‌ایم. کار ارائه‌ی برنامه توسط این دستور، از محتوای کامپایل شده‌ی درون حافظه با مدیریت webpack انجام می‌شود. به همین جهت بسیار سریع بوده و قابلیت live reload را ارائه می‌دهد (نمایش آنی تغییرات در مرورگر، با تغییر فایل‌ها).
همانند تمام دستورات دیگر، اطلاعات بیشتری را در مورد این دستور، از طریق راهنمای آن می‌توان به دست آورد:
 > ng serve --help

که شامل این موارد هستند (علاوه بر تمام مواردی را که در حالت ng build می‌توان مشخص کرد؛ مثلا ng serve --prod -o):

 پرچم مخفف
توضیح
 open-- o-
بازکردن خودکار مرورگر پیش فرض.
حالت پیش فرض آن گشودن مرورگر توسط خودتان است و سپس مراجعه‌ی دستی به آدرس برنامه. 
 port--  p-  تغییر پورت پیش فرض مانند ng server -p 8626 
 live-reload--  lr-   فعال است مگر اینکه آن‌را با false مقدار دهی کنید.
 ssl--    ارائه به صورت HTTPS
 proxy-config--  pc-  Proxy configuration file 


استخراج فایل تنظیمات webpack از Angular CLI

Angular CLI برای مدیریت build، در پشت صحنه از webpack استفاده می‌کند. فایل تنظیمات آن نیز جزئی از فایل‌های توکار این ابزار است و قرار نیست به صورت پیش فرض و مستقیم توسط پروژه‌ی جاری ویرایش شود. به همین جهت آن‌را در ساختار پروژه‌ی تولید شده، مشاهده نمی‌کنید.
اگر علاقمند به سفارشی سازی بیشتر این تنظیمات پیش فرض باشید، ابتدا باید آن‌را اصطلاحا eject کنید و سپس می‌توان آن‌را ویرایش کرد:
 > ng eject
Ejection was successful.

To run your builds, you now need to do the following commands:
- "npm run build" to build.
- "npm run test" to run unit tests.
- "npm start" to serve the app using webpack-dev-server.
- "npm run e2e" to run protractor.

Running the equivalent CLI commands will result in an error.
============================================
Some packages were added. Please run "npm install".
همانطور که مشاهده می‌کنید عنوان کرده‌است که از این پس خودتان باید بسیاری از مسایل را به صورت دستی مدیریت کنید و Angular CLI دیگر آن‌ها را به صورت خودکار مدیریت نمی‌کند و دیگر دستورات ng build و ng serve کار نخواهند کرد (این تغییرات در فایل package.json درج می‌شوند).
در این حالت است که فایل webpack.config.js به ریشه‌ی پروژه جهت سفارشی سازی شما اضافه خواهد شد. همچنین فایل‌های .angular-cli.json، package.json نیز جهت درج این تغییرات ویرایش می‌شوند.

و اگر در این لحظه پشیمان شده‌اید (!) فقط کافی است تا این مرحله‌ی جدید commit شده‌ی به مخزن کد را لغو کنید و باز هم به همان Angular CLI قبلی می‌رسید.
نظرات مطالب
تبدیل زیرنویس‌های خاص پلورال‌سایت به فرمت SRT
ظاهراً ساختار عوض شده به این شکل (البته در اینجا data-s حذف شده و مقدار آن به صورت رشته ایی در انتهای مقدار ng-click اضافه شده است به صورت start=39.796) :
<li class="transcript-clip">
<a href="javascript:void(0)" ng-click="launchPlayerWindow('http://pluralsight.com/training', 'author=scott-allen&amp;name=mvc4-building-m1-intro&amp;mode=live&amp;clip=0&amp;course=mvc4-building');">Introduction</a><br>
    <div>
<a href="javascript:void(0)" ng-click="launchPlayerWindow('http://pluralsight.com/training', 'author=scott-allen&amp;name=mvc4-building-m1-intro&amp;mode=live&amp;clip=0&amp;course=mvc4-building&amp;start=39.796');">and also have an understanding of the design goals of the MVC framework.</a>
        <a href="javascript:void(0)" ng-click="launchPlayerWindow('http://pluralsight.com/training', 'author=scott-allen&amp;name=mvc4-building-m1-intro&amp;mode=live&amp;clip=0&amp;course=mvc4-building&amp;start=43.796');">So, let's get started.</a>
    </div>
</li>

مطالب
jQuery Tips #1
چگونگی تغییر سایز فونت  صفحه با استفاده از jQuery

کد زیر را در نظر بگیرید
 $(function () {
            // اندازه واقعی فونت
            var originalFontSize = $('#test').css('font-size');
            $(".resetFont").click(function () {
                $('#test').css('font-size', originalFontSize);
            });
            // افزایش اندازه فونت
            $(".increaseFont").click(function () {
                var currentFontSize = $('#test').css('font-size');
                var currentFontSizeNum = parseFloat(currentFontSize);
                var newFontSize = currentFontSizeNum + 5;
                $('#test').css('font-size', newFontSize);
                return false;
            });
            // کاهش اندازه فونت
            $(".decreaseFont").click(function () {
                var currentFontSize = $('#test').css('font-size');
                var currentFontSizeNum = parseFloat(currentFontSize);
                var newFontSize = currentFontSizeNum - 5;
                $('#test').css('font-size', newFontSize);
                return false;
            });
        });
       

  و کد HTML زیر را
 <div id="test">
        jQuery Tips By Mohsen Bahrzadeh
    </div>
    <a href="#">decreaseFont</a>
    <a href="#">Increase</a>
    <a href="#">resetFont</a>
 در این کد ابتدا اندازه فونت را درون متغیر originalFontSize ذخیره و سپس 3 متد تعریف کرده‌ایم، که اولین متد اندازه فونت را به اندازه فونت اولیه بر می‌گرداند و در متد دوم می‌خواهیم اندازه فونت تگی با ID=test را افزایش دهیم.
برای این کار ابتدا اندازه جاری تگ را گرفته و درون متغیر currentFontSize قرار داده سپس مقدار متغیر currentFontSize را به Float تبدیل کرده و 5 واحد به آن اضافه  کرده ایم و سپس عدد بدست آمده را به عنوان اندازه فونت جدید در نظر گرفته ایم. برای متد سوم دقیقاً همین اتفاق می‌افتد فقط به جای + از - استفاده شده است

نظرات مطالب
به اشتراک گذاری داده ها بین کنترلرها در AngularJs
بله. اگر متدی را به کنترلر 2 اضافه کنید و سپس یک تگ لینک در view برای فراخوانی این متد، به صورت زیر :
sampleApp.controller('controller2', function ($scope, BookData) {
    $scope.books = BookData;
    $scope.change = function(){
         BookData[0].name = 'change this book';
    }
});

 و view :
<div ng-controller="controller2">
        <p>Data from controller2</p>
        <table>
            <tr ng-repeat="book in books">
                <td>
                    {{book.code}}
                </td>
                <td>
                    {{book.name}}
                </td>
            </tr>
        </table>
<a href="javascript:void(0)" ng-click="change();" >Click here to change book1</a>
    </div>
میبینید که در کنترلر شماره 1 هم تغییر میکند.
مطالب
کار با شیوه‌نامه‌های تصاویر در بوت استرپ 4
بوت استرپ 4 به همراه کلاس‌های کمکی مفیدی برای کار با تصاویر نیز می‌باشد؛ مانند:
- کلاس img-fluid که سبب ایجاد تصاویر واکنشگرا می‌شود. به این معنا که اگر این کلاس به تصویری انتساب داده شد، عرض آن با عرض container آن، تطابق پیدا می‌کند.
- کلاس rounded-dir برای گرد کردن گوشه‌های تصاویر کاربرد دارد. در اینجا ذکر dir اختیاری است و می‌تواند مقادیر زیر را داشته باشد:
top, right, bottom, left, circle
همچنین اگر تصویری دارای گوشه‌های گرد است (توسط کلاس css دیگری تنظیم شده‌است)، می‌توان با اعمال کلاس rounded-0، آن‌را لغو کرد.
- کلاس img-thumbnail نیز تصویری را با گوشه‌های گرد و همچنین با حاشیه‌ای به ضخامت 1px، ایجاد می‌کند.
- کلاس‌های float-left و float-right حالت پیش‌فرض نمایش inline تصاویر را لغو کرده و آن‌ها را در جهت‌های دلخواهی نمایش می‌دهند.
- اگر حالت نمایش تصویر inline است، می‌توان مانند متون با استفاده از کلاس text-center، آن‌ها را در میانه‌ی container، نمایش داد.
- اگر تصویر به صورت block نمایش داده می‌شود (display:block)، کلاس mx-auto می‌تواند آن‌را در میانه‌ی container نمایش دهد.


مثالی از تصاویر واکنشگرا

<body>
    <div class="container">
        <section class="content" id="testimonials">
            <h2>Testimonials</h2>
            <figure>
                <img class="img-fluid" src="images/testimonial-johnb.jpg" alt="John B Photo">
                <figcaption>The staff at Wisdom worked tirelessly to determine
                    why our three-year old Golden Retriever, Roxie, started
                    going into sudden kidney failure. They stabilized her and
                    provided fluids until her kidneys were again functioning
                    normally.</figcaption>
            </figure>
        </section>
    </div>
</body>
در اینجا با اعمال کلاس img-fluid به یک تصویر، سبب خواهیم شد تا عرض آن با عرض container تطابق پیدا کند:


همانطور که مشاهده می‌کنید، این کلاس، یک تصویر بزرگ را متناسب با عرض دربرگیرنده‌ی آن، نمایش می‌دهد.


مثال تصویر با گوشه‌های گرد

در همین مثال اگر بخواهیم گوشه‌های تصویر را گرد کنیم، فقط کافی است کلاس rounded را نیز به آن اضافه کنیم:
<img class="img-fluid rounded" src="images/testimonial-johnb.jpg" alt="John B Photo">
با این خروجی:



مثال تصویر با گوشه‌های گرد در جهتی خاص

و یا اگر جهتی را نیز به آن اعمال کنیم:
<img class="img-fluid rounded-top" src="images/testimonial-johnb.jpg" alt="John B Photo">
برای مثال فقط  گوشه‌های بالای آن گرد می‌شوند:


و یا نتیجه‌ی اعمال جهت دایره‌ای به تصویر:
<img class="img-fluid rounded-circle" src="images/testimonial-johnb.jpg" alt="John B Photo">
بیضی شکلی به صورت زیر است:



مثال نمایش تصویر در گوشه‌ای خاص از صفحه

اگر بخواهیم تصویر را برای مثال در سمت چپ صفحه نمایش دهیم:
<img class="img-fluid rounded-circle float-left" width="200px"
                    src="images/testimonial-lorraines.jpg" alt="Lorraine Photo">
خروجی آن به صورت زیر خواهد بود:


برای نمایش تصویر در وسط صفحه:
<body>
    <div class="container">
        <section class="content" id="testimonials">
            <h2>Testimonials</h2>
            <figure class="text-center">
                <img class="img-fluid rounded-circle" src="images/testimonial-mcphersons.jpg"
                    alt="McPhersons Photo" width="200px">
                <figcaption>When Samantha, our Siamese cat, began sleeping all
                    the time and urinating excessively, we brought her to see
                    the specialists at Wisdom. Now, two years later, Samantha
                    is still free from any complications of diabetes, and her
                    blood sugar regularly tests normal.</figcaption>
            </figure>
        </section>
    </div>
</body>
در اینجا با استفاده از کلاس text-center، تصویر را به میانه‌ی صفحه منتقل کرده‌ایم.



کار با عناصر figure جهت نمایش بهتر تصاویر و متون ذیل آن‌ها

برای کار بهتر با عناصر figure در بوت استرپ، ابتدا کلاس figure را به آن‌ها انتساب می‌دهیم. سپس می‌توان کلاس figure-img را به تصاویر داخل آن‌ها افزود. این مورد سبب می‌شود تا تصاویر با اندکی فاصله نسبت به متن داخل آن نمایش داده شوند. در آخر می‌توان به figcaption، کلاس figure-caption را افزود تا رنگ و فاصله‌ی مناسبی را به آن اعمال کند:
<body>
    <div class="container">
        <section class="content" id="testimonials">
            <h2>Testimonials</h2>
            <figure class="figure">
                <img class="img-fluid figure-img rounded" src="images/testimonial-lorraines.jpg"
                    alt="Lorraine Photo">
                <figcaption class="figure-caption">Wisdom Pet Medicine is the
                    only clinic around that
                    will even book pet fish for appointments. When our 13-year
                    old Comet goldfish, McAllister, turned from silvery white
                    to an angry red, we called around, urgently trying to find
                    a veterinarian who could help. Wisdom not only got us in
                    the same day, but also was able to diagnose McAllister as
                    having a severe case of septicemia.</figcaption>
            </figure>
        </section>
    </div>
</body>
با این خروجی:


کلاس  figure-img سبب شده‌است تا تصویر، با متن، اندکی فاصله پیدا کند. همچنین کلاس figure-caption، متن ذیل تصویر را اندکی روشن‌تر نمایش می‌دهد.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: Bootstrap4_03.zip
نظرات مطالب
ارائه‌ی قالبی عمومی برای استفاده از تقویم‌های جاوااسکریپتی در Blazor

یک نکته‌ی تکمیلی: استفاده از این DatePicker در برنامه‌های Blazor SSR

اگر علاقمند باشید تا از این DatePicker در برنامه‌های Blazor SSR به‌صورت یک کامپوننت استفاده کنید، روش کار به صورت زیر است:

  • نیاز به نسخه‌ی اصلاح شده‌ی آن خواهید داشت.
  • سپس هر المان ورودی که مزین به ویژگی data-dnt-date-picker بود، یافت شده و این DatePicker به آن متصل می‌شود.
  • همچنین از این جهت که در برنامه‌های Blazor SSR، ویژگی enhanced navigation و نمایش Ajax ای صفحات با fetch، فعال است، باید حالت enhancedload را تحت نظر قرار داده و این کوئری گرفتن یافتن عناصر با ویژگی data-dnt-date-picker و اتصال مجددا به آن‌ها را مدیریت کرد.
  • تا همینجا و با این تنظیمات، نمایش این DatePicker فعال می‌شود و ... کار می‌کند. اگر علاقمند به تبدیل آن به یک کامپوننت مخصوص Blazor SSR هم هستید، می‌توانید از کامپوننت DntInputPersianDatePicker.razor و کدهای آن،‌ ایده بگیرید که جزئیات آن پیشتر در مطلب جاری بررسی شده‌است؛ هرچند این مطلب بیشتر به سایر نگارش‌های Blazor می‌پردازد.

مطالب
قابل ویرایش کننده‌ی فوق العاده x-editable ؛ قسمت دوم
در قسمت قبلی با نحوه‌ی اجرا و ویژگی‌های فنی و خصوصیات کدنویسی x-editable آشنا شدیم. غیر از این خصوصیات، خصوصیات دیگری هم هستند که فقط مختص نوع کنترلی هست که در قسمت type مشخص کرده‌اید.

کنترلهای زیر جهت ورود اطلاعات در ویرایشگر پشتیبانی می‌شوند:
  • text
  • textarea
  • select
  • date
  • datetime
  • dateui
  • combodate
  • html5types
  • checklist
  • wysihtml5
  • typeahead
  • typeaheadjs
  • select2 
text
 clear دکمه‌ای جهت حذف محتوای کادر متنی است. مقدار پیش فرض آن true است.
 escape  برای دفاع در برابر کدهای مخرب html به کار میرود و کاراکترهای مدنظر را در صورت true بودن غیرفعال می‌کند. البته اگر از خاصیت display استفاده کنید این گزینه تاثیرش را از دست خواهد داد.
 inputclass یک کلاس css را به کادر متنی اعمال می‌کند.
placeholder
مقدار داده شده را در صورتی که کادر متنی خالی باشد، نشان می‌دهد.
 tpl به معنی یک قالب. شما می‌توانید کد html تگ input خود را وارد کنید؛ ولی توصیه نمی‌شود.


 TextArea

همان خاصیت‌های قبلی را دارد بعلاوه rows که نمایانگر مقدار ارتفاع آن است.

select

خاصیت‌های escape,input,class و tpl را دارد به‌علاوه خاصیت‌های زیر:

 prepend  همانند گزینه پایینی است ولی قبل از آن داده‌های خود را اضافه می‌کند.
 source از آنجا که یک لیست، لیستی از آیتم‌ها را دارد و کاربر یکی از آن‌ها را انتخاب می‌کند، این بخش، منبع آیتم‌ها را معرفی می‌کند. این خاصیت چهار نوع داده می‌پذیرد: آرایه یا شیء‌ایی از مقادیر. تابعی که بعد از انجام هر عملی، اطلاعات به آن پاس می‌شوند و یا از نوع رشته که این رشته یک آدرس سمت سرور است که با درخواست از آن آدرس، اطلاعات را دریافت می‌کند.
 sourceCache
 اگه خاصیت بالا با آدرسی پر شده باشد که از سمت سرور بخواند، در دفعات بعدی مقدار دریافتی را از کش خواهد خواند.
 sourceError  یک پیام خطا هنگام بارگزاری اطلاعات
 sourceOptions  در صورتیکه قصد اضافه کردن پارامتری را به درخواست ایجکسی دارید. یک شیء از پارامترها را به آن نسبت می‌دهیم و برای رونویسی پارامترها از یک تابع استفاده می‌کنیم که نحوه‌ی تغییرات را قبلا در جدول شماره یک دیده‌اید.
 
date
خاصیت‌های مشترک قبلی : tpl,input,class,escape و clear است.
 datepicker  پیکربندی تقویم را بر عهده دارد. برای اطلاعات بیشتر در مورد پیکربندی تقویم به این لینک مراجعه فرمایید.
{ weekStart: 0, startView: 0, minViewMode: 0, autoclose: false }
 format قالب بندی فرمت تاریخ جهت ارسال به سرور\ حالت پیش فرض yyyy-mm-dd
مقادیری که میتوان به کار برد: yy   yyyy mm   m  dd   d
 viewformat  این فرمت هنگام نمایش به کار می‌آید و در صورتیکه مقدار عنصر در این قالب نباشد، آن را تبدیل می‌کند.


 datetime در بوت استراپ

کاملا مشترک با مورد قبلی.


dateUI

مختص JqueryUI است و کاملا مشترک با مورد قبلی.


combodate

موارد مشترک قبلی را دارد ولی به جای خاصیت datepicker از combodate استفاده می‌شود که پیکربندی آن در این لینک قرار دارد.


نوع‌های HTML 5

شامل موارد زیر است:

  • password
  • email
  • url
  • tel
  • number
  • range
  • time 
html5 شامل عناصر زیادی است که ویژگی‌های جالبی را مد نظر دارند؛ ولی ممکن است بعضی المان‌ها در بعضی مرورگرها کارآیی مناسبی نداشته باشند که در این صفحه سازگاری مرورگرها با این نوع المان‌ها ذکر شده است.
خاصیت‌های ذکر شده در مورد نوع text، در مورد آن‌ها نیز صدق می‌کند.

checklist
همانند نوع select است؛ فقط خاصیت separator را دارد که کارش جدا کردن مقادیر است و مقدار پیش فرض آن علامت ',' است.


wysihtml5
سورس و دمو ی این نوع ادیتور که بر پایه‌ی بوت استرپ بنا شده است و زحمت اضافه کردن کتابخانه‌ها به صفحه، بر عهده شماست.
مداخل زیر را به طور دستی به صفحه اضافه کنید:
<link href="js/inputs-ext/wysihtml5/bootstrap-wysihtml5-0.0.2/bootstrap-wysihtml5-0.0.2.css" rel="stylesheet" type="text/css"></link>  
<script src="js/inputs-ext/wysihtml5/bootstrap-wysihtml5-0.0.2/wysihtml5-0.3.0.min.js"></script>  
<script src="js/inputs-ext/wysihtml5/bootstrap-wysihtml5-0.0.2/bootstrap-wysihtml5-0.0.2.min.js"></script>
و همچنین اسکریپت x-editable برای کار با این عنصر را هم اضافه کنید:
<script src="js/inputs-ext/wysihtml5/wysihtml5.js"></script>
این فایل در بسته‌ای که دانلود کرده‌اید موجود است. شامل خاصیت‌های escape,inputclass,placeholder,tpl است و خاصیت wysihtml5 شامل تنظیمات و پیکربندی ادیتور است که پیکریندی آن را می‌توانید در اینجا مطالعه بفرمایید.

typeahead
این گزینه فقط مختص بوت استرپ 2 است و یک کنترل autocomplete به شمار می‌آید. منبع داده‌های آن از طریق خاصیت source به دو صورت آرایه و object تامین می‌گردد.
['text1', 'text2', 'text3' ...]

//or

[{value: 1, text: "text1"}, {value: 2, text: "text2"}, ...]
شامل خاصیت‌های clear,escape,prepend,source,sourceOptions,sourceError,sourceCache,inputclass,tpl است و شامل خاصیت typeahead جهت پیکربندی آن می‌شود.

typeaheadjs
همانند قبلی است و بر اساس twitterBootstrap است و شامل همان خصوصیات قبلی است. تنها خصوصیت typeahead آن است که باید از این پیکربندی استفاده کنید.

Select2
این المان بر اساس این کتابخانه  سورس باز ایجاد می‌شود. و مستندات آن شامل جزئیات و پیکربندی آن می‌شود. برای معرفی آن فایل‌های زیر را به صفحه معرفی کنید.
<link href="select2/select2.css" rel="stylesheet" type="text/css"></link>  
<script src="select2/select2.js"></script>
برای دریافت استایل بوت استرپی آن این فایل را صدا بزنید:
<link href="select2-bootstrap.css" rel="stylesheet" type="text/css"></link>
نکته: در حال حاضر خاصیت autotext روی این المان جواب نمی‌دهد و می‌توانید از خاصیت data-value به جای آن استفاده کنید.

شامل خاصیت‌های inputclass , escape , placeolder , source , tpl می‌باشد و از select2 برای دریافت پیکربندی‌های کنترل استفاده می‌کند و علامت جدا کننده آن توسط viewseperator صورت می‌گیرد.


قالبی نو برای ویرایشگر

ویرایشگر فعلی ما به خصوص در بوت استرپ، ظاهر فوق العاده‌ای دارد. ولی اگر بازهم مایل به تغییر و رونویسی هستید، این امکان فراهم شده است.

fn.editableform.template$
 مقدار پیش فرض آن که حتما باید شامل تگ فرم و کلاس‌های مدنظر باشد:
    <form>
        <div>
             <div><div></div><div></div></div>
             <div></div>
        </div> 
    </form>
در صورتی که قصد تغییر کلاس‌های آن را دارید باید کلاس‌های زیر را رونویسی کنید:
  • control-group
  • editable-input
  • editable-buttons
  • editable-error-block

fn.editableform.buttons$ 
    <button type="submit">ok</button>
    <button type="button">cancel</button>
کلاس‌های editable-sumit و editable-cancel به طور خودکار به کلاس editable-buttons تزریق می‌شوند.
و نهایتا جهت تغییر loading

fn.editableform.loading$  
<div></div>

گاهی اوقات نیاز است که خصوصیات این ویرایشگر را در شرایط متغیر صفحه کنترل کنیم، برای مثال گاهی پیش می‌آید که بخواهید در یک شرایط خاص ویرایشگر یک المان خاص را غیرفعال کنید. کد زیر مثال این تغییرات است.
$('#favsite').editable('option', 'disabled', false);

متدها و رویدادها


متدهایی که روی آن قابل اجراست:
editable
ویرایشگر را بر اساس مقادیر اولیه روی عنصر مشخص شده فعال می‌کند.
() activate  فوکوس را به input ویرایشگر باز می‌گرداند.
() destory  حذف ویژگی ویرایش از روی عنصر
() disable  غیرفعال کردن ویرایشگر
() enable  فعال سازی آن
 ()getvalue
باعث بازگردانی مقدار جاری همه عناصر توسط شیء جفت کلید مقدار می‌شود و عناصری که شامل متن یا مقداری نیستند، از آن حذف می‌شوند. در صورتیکه قصد دارید مقدار تنها یک عنصر قابل دریافت باشد، با خاصیت isSingle آن را true کنید.
    $('#username, #fullname').editable('getValue');
    //result:
    {
    username: "superuser",
    fullname: "John"
    }
    //isSingle = true
    $('#username').editable('getValue', true);
    //result "superuser"
 ()hide  مخفی کردن تگ فرم ویرایشگر
(option(key,value
 تغییر خصوصیات یک عنصر که در بالا هم نمونه کد آن را دیدیم.
(setvalue(value,convertStr  ست کردن مقدار جدید کنترل و پارامتر دوم وضعیت تبدیل این مقدار به فرمت داخلی است که برای آن تعریف شده است مثل date
() show  نمایش ویرایشگر
( submit(options  در صورتی که خاصیت ارسال خودکار به سمت سرور را غیر فعال کرده باشید، با این گزینه می‌توانید همه اطلاعات و تغییرات را ارسال کنید. برای ایجاد فرم بر اساس ویرایشگرها و ارسال اطلاعات با کلیک بر روی دکمه submit کاربرد دارد. یک مثال در این زمینه .
پارامترهای options به شرح زیر هستند:
url
data
ajaxoptions
(error(obj
(success(obj,config

از نسخه 1.5.1 میتوان این گزینه را به راحتی روی یک المان خاص هم صدا زد:
$('#username').editable('submit')
() toggle  کدی که صدا زده می‌شود بین دو وضعیت show و hide سوئیچ می‌کند.
() toggleDisabled  تغییر وضعیت بین دو حالت enable و disable
() validate  انجام اعتبارسنجی بر روی همه کنترل ها.
    $('#username, #fullname').editable('validate');
    // possible result:
    {
      username: "username is required",
      fullname: "fullname should be minimum 3 letters length"
    }


رویدادها

 hidden این رویداد زمانی رخ می‌دهد که ویرایشگر دیگر قابل مشاهده نیست و شامل دو پارامتر event و reason است. reason دلیل اینکه چرا ویرایشگر از دید خارج شده است را با یکی از گزینه‌های زیر مشخص می‌کند.
save
cancel
onblur
nochange
manual

    $('#username').on('hidden', function(e, reason) {
        if(reason === 'save' || reason === 'cancel') {
            //auto-open next editable
            $(this).closest('tr').next().find('.editable').editable('show');
        } 
    });
init
موقعی صدا زده میشود که متد editable روی عنصر صدا زده می‌شود و به یاد داشته باشید که این رویداد باید قبل از آن ست شده باشد.
    $('#username').on('init', function(e, editable) {
        alert('initialized ' + editable.options.name);
    });
    $('#username').editable();
save
 موقعی که مقدار جدید، با موفقیت تایید می‌شود. دو پارامتر event و params را باز می‌گرداند که params شامل دو خصوصیت newValue و response است که به ترتیب مقدار جدید و اطلاعات برگشت داده شده از درخواست آژاکس است.
    $('#username').on('save', function(e, params) {
        alert('Saved value: ' + params.newValue);
    });
shown
موقعیکه ویرایشگر نمایش می‌یابد و فرم با موفقیت رندر شده است. برای اشیایی چون select باید صبر کنید تا مقادیر آن‌ها بارگذاری شوند.
    $('#username').on('shown', function(e, editable) {
        editable.input.$input.val('overwriting value of input..');
    });
 

حل مشکل این ابزار در کندو

موقعیکه من این ابزار را بر روی treeview قرار دادم، به این مشکل برخوردم که اطراف پنجره باز شده، توسط حاشیه‌های treeview محدود شده است و مطابق شکل زیر قسمت‌هایی از آن دیده نمی‌شود. به همین علت css‌های کندو را به اندازه یک خط ویرایش کردم.


برای حل این مشکل فایل kendo.common-xxx را باز کنید. xxx بر اساس قالبی که برای کندو انتخاب کرده‌اید، می‌تواند متفاوت باشد. در مثال‌های کندو عموما این xxx به نام default شناخته می‌شود یا برای مثال من، bootstrap بود.
بعد از اینکه باز کردید، به دنبال چنین استایلی بگردید:
div.k-treeview{
border-width: 0px;
background: transparent none repeat scroll 0px center;
overflow: auto;
white-space: nowrap;
}
خط زیر را از آن حذف کنید تا مشکل حل شود.
overflow: auto;

نکته بعدی اینکه وقتی ویرایشگر در حالت popup قرار می‌گیرد، مقدار خاصیت title نمایش می‌یابد که عموما با مضامینی چون "کلمه جدید را وارد نمایید" و ... پر می‌شود که به طور پیش فرض سمت چپ قرار گرفته است. کد زیر را در صفحه وارد کنید تا متن در سمت راست قرار بگیرد:
  .popover-title {
        text-align: right;
    }

مطالب
نمایش اخطارها و پیام‌های بوت استرپ به کمک TempData در ASP.NET MVC
در بوت استرپ برای نمایش اعلانی به کاربر، از کلاس alert می‌توان استفاده کرد. برای نمایش این اعلان کافی است محتوای خود را درون یک div با کلاس alert قرار دهیم: 
<div class="alert">
    نمایش اعلانات
</div>

تعدادی کلاس دیگر نیز جهت استفاده از رنگ‌های مختلف نیز توسط بوت استرپ ارائه شده است: 

همچنین اگر مایل بودید می‌توانید با افزودن یک دکمه با کلاس close و ویژگی data-dismiss مساوی alert، امکان بستن پیام را در اختیار کاربر قرار دهید: 

<div class="alert alert-warning alert-dismissable">
  <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
  نمایش اعلان
</div>

در ادامه قصد داریم این پیام را بعد از ثبت اطلاعات، به کاربر نمایش دهیم. یعنی در داخل کد، امکان صدا زدن این نوع پیام‌ها را داشته باشیم.

ابتدا کلاس‌های زیر را تعریف می‌کنیم:

کلاس Alert

public class Alert
{
        public const string TempDataKey = "TempDataAlerts";
        public string AlertStyle { get; set; }
        public string Message { get; set; }
        public bool Dismissable { get; set; }
}

در کلاس فوق خصوصیت یک alert را تعریف کرده‌ایم (از خاصیت TempDataKey جهت پاس دادن alert‌ها به view استفاده می‌کنیم).

  کلاس AlertStyles
public class AlertStyles
{
        public const string Success = "success";
        public const string Information = "info";
        public const string Warning = "warning";
        public const string Danger = "danger";
}
کلاس فوق نیز جهت نگهداری اسامی کلاس‌های alert، مورد استفاده قرار می‌گیرد. قدم بعدی، استفاده از کلاس‌های فوق و انتقال alerts توسط TempData به داخل viewها می‌باشد. برای جلوگیری از زیاد شدن حجم کدهای تکراری داخل کنترلرها و همچنین به عنوان یک Best practice، یک کنترلر Base را برای اینکار تعریف می‌کنیم و متدهای موردنیاز را داخل آن می‌نویسیم:
public class BaseController : Controller
{
        public void Success(string message, bool dismissable = false)
        {
            AddAlert(AlertStyles.Success, message, dismissable);
        }

        public void Information(string message, bool dismissable = false)
        {
            AddAlert(AlertStyles.Information, message, dismissable);
        }

        public void Warning(string message, bool dismissable = false)
        {
            AddAlert(AlertStyles.Warning, message, dismissable);
        }

        public void Danger(string message, bool dismissable = false)
        {
            AddAlert(AlertStyles.Danger, message, dismissable);
        }

        private void AddAlert(string alertStyle, string message, bool dismissable)
        {
            var alerts = TempData.ContainsKey(Alert.TempDataKey)
                ? (List<Alert>)TempData[Alert.TempDataKey]
                : new List<Alert>();

            alerts.Add(new Alert
            {
                AlertStyle = alertStyle,
                Message = message,
                Dismissable = dismissable
            });

            TempData[Alert.TempDataKey] = alerts;
        }
}
از متدهایی که به صورت عمومی تعریف شده‌اند جهت ارسال پیام به view استفاده می‌کنیم. متد AddAlert نیز جهت ایجاد لیستی از پیام‌ها(Alert) مورداستفاده قرار می‌گیرد؛ زیرا ممکن است بخواهید هم زمان از چندین متد عمومی فوق استفاده کنید، یعنی چندین پیام را به کاربر نمایش دهید. 

نکته: در کد فوق از TempData جهت پاس دادن شیء alerts استفاده کرده‌ایم. TempData به صورت short-lived عمل می‌کند به دو دلیل: 1- بلافاصله بعد از خوانده شدن، حذف خواهد شد. 2- پس از پایان درخواست از بین خواهد رفت. از TempData جهت پاس دادن داده‌ها از درخواست فعلی به درخواست بعدی (redirect از یک صفحه به صفحه دیگر) استفاده می‌شود. یعنی در زمان redirect سعی می‌کند داده‌های بین redirectها را در خود نگه دارد. اگر از ViewBag و ViewData استفاده می‌کردیم داده‌های داخل آنها بلافاصله بعد از redirect شدن null می‌شدند.
به طور مثال اکشن متد زیر را در نظر بگیرید:
public ActionResult Index()
        {
            var userInfo = new
            {
                Name = "Sirwan",
                LastName = "Afifi",
            };
 
            ViewData["User"] = userInfo;
            ViewBag.User = userInfo;
            TempData["User"] = userInfo;

            return RedirectToAction("About");
        }
view :
@{
    ViewBag.Title = "About";
}

<h1>Tempdata</h1><p>@TempData["User"]</p>
<h1>ViewData</h1><p>@ViewData["User"]</p>
<h1>ViewBag</h1><p>@ViewBag.User</p>
اگر کد فوق را تست کنید خواهید دید که در خروجی تنها اطلاعات داخل TempData نمایش داده می‌شود.
معمولاً برای ارسال داده‌های خطاها از TempData استفاده می‌شود.

اکنون در هر کنترلری که می‌خواهید پیامی را به صورت alert، پس از ثبت اطلاعات به کاربر نمایش دهید، باید از کنترلر BaseController  ارث‌بری کنید:
public class NewsController : BaseController
{
readonly INewsService _newsService;
        readonly IUnitOfWork _uow;
        public NewsController(INewsService newsService, IUnitOfWork uow)
        {
            _newsService = newsService;
            _uow = uow;
        }
        [HttpPost]
        [ValidateAntiForgeryToken]
        [ValidateInput(false)]
        public ActionResult Create(News news)
        {
            if (ModelState.IsValid)
            {
                _newsService.AddNews(news);
                _uow.SaveChanges();
                Success(string.Format("خبر با عنوان  <b>{0}</b> با موفقیت ذخیره گردید!", news.Title), true);
                return RedirectToAction("Index");
            }
            Danger("خطا در هنگام ثبت اطلاعات ");
            return View(news);
        }
        [HttpPost]
        public ActionResult Delete(int id)
        {
            _newsService.DeleteNewsById(id);
            _uow.SaveChanges();
            Danger("اطلاعات مورد نظر با موفقیت حذف گردید!", true);
            return RedirectToAction("Index");
        }
}

نمایش پیام‌ها 
برای نمایش پیام‌ها یک partial view با نام _Alerts در مسیر Views\Shared ایجاد می‌کنیم: 
@{
    var alerts = TempData.ContainsKey(Alert.TempDataKey)
                ? (List<Alert>)TempData[Alert.TempDataKey]
                : new List<Alert>();

    if (alerts.Any())
    {
        <hr />
    }

    foreach (var alert in alerts)
    {
        var dismissableClass = alert.Dismissable ? "alert-dismissable" : null;
        <div class="alert alert-@alert.AlertStyle @dismissableClass">
            @if (alert.Dismissable)
            {
                <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
            }
            @Html.Raw(alert.Message)
        </div>
    }
}
در کد فوق، ابتدا شیء alerts را از TempData دریافت کرده‌ایم و توسط یک حلقه foreach، داخل آن به ازای هر آیتم، آن را پیمایش می‌کنیم و در نهایت کد html متناظر با هر alert را در خروجی نمایش می‌دهیم.
اکنون جهت استفاده از partial view فوق در جایی که می‌خواهید پیام نمایش داده شود partial view فوق را فراخوانی کنید (به عنوان مثال داخل فایل Layout): 
<div>
                    @{ Html.RenderPartial("_Alerts"); }
                    @RenderBody()
</div>

مطالب
سفارشی کردن صفحه بندی WebGrid در ASP.NET MVC
بعد از استفاده از گرید‌های Grid.mvc , JQGrid, Kendo و مشکلاتی که با هر کدام از آنها داشتم، در نهایت به WebGrid که به صورت توکار وجود دارد، برای استفاده جهت نمایش اطلاعات رسیدم؛ از این جهت که به کتابخانه‌ی جانبی نیازی ندارد و از نظر سرعت و لود شدن بهینه می‌باشد، البته با اضافه کردن یکسری کدهای css.
برای آشنایی بیشتر با این helper توصیه میکنم ابتدا این مقاله را مطالعه نمایید.
به صورت پیش فرش WbebGrid صفحه بندی را به صورت خیلی ساده فقط با نمایش اعداد و جهت نماهای جلو و عقب، نشان می‌دهد که برای پروژه‌های رسمی تا حدودی جالب نیست.

در این مطلب قصد داریم از کتابخانه‌ی bootstrap جهت صفحه بندی استفاده کنیم و در نهایت به صفحه بندی زیر برسیم:


برای اینکار، ابتدا قبل از هر چیزی به یک متد الحاقی برای انجام صفحه بندی سفارشی سازی شده، نیاز داریم که کدهای این متد به صورت زیر خواهد بود:

public static class WebGridExtensions
{
    public static HelperResult PagerList(
        this WebGrid webGrid,
        WebGridPagerModes mode = WebGridPagerModes.NextPrevious | WebGridPagerModes.Numeric,
        string firstText = null,
        string previousText = null,
        string nextText = null,
        string lastText = null,
        int numericLinksCount = 5)
    {
        return PagerList(webGrid, mode, firstText, previousText, nextText, lastText, numericLinksCount, explicitlyCalled: true);
    }

    private static HelperResult PagerList(
        WebGrid webGrid,
        WebGridPagerModes mode,
        string firstText,
        string previousText,
        string nextText,
        string lastText,
        int numericLinksCount,
        bool explicitlyCalled)
    {
        
        int currentPage = webGrid.PageIndex;
        int totalPages = webGrid.PageCount;
        int lastPage = totalPages - 1;

        var ul = new TagBuilder("ul");
        var li = new List<TagBuilder>();

        
        if (ModeEnabled(mode, WebGridPagerModes.FirstLast)) {
            if (String.IsNullOrEmpty(firstText)) {
                firstText = "اولین";
            }

            var part = new TagBuilder("li") {
                InnerHtml = GridLink(webGrid, webGrid.GetPageUrl(0), firstText)
            };

            if (currentPage == 0) {
                part.MergeAttribute("class", "disabled");
            }

            li.Add(part);

        }
        
        if (ModeEnabled(mode, WebGridPagerModes.NextPrevious)) {
            if (String.IsNullOrEmpty(previousText)) {
                previousText = "قبلی";
            }

            int page = currentPage == 0 ? 0: currentPage - 1;

            var part = new TagBuilder("li") {
                InnerHtml = GridLink(webGrid, webGrid.GetPageUrl(page), previousText)
            };
            
            if (currentPage == 0) {
                part.MergeAttribute("class", "disabled");
            }

            li.Add(part);

        }


       if (ModeEnabled(mode, WebGridPagerModes.Numeric) && (totalPages > 1)) {
            int last = currentPage + (numericLinksCount / 2);
            int first = last - numericLinksCount + 1;
            if (last > lastPage) {
                first -= last - lastPage;
                last = lastPage;
            }
            if (first < 0) {
                last = Math.Min(last + (0 - first), lastPage);
                first = 0;
            }
            for (int i = first; i <= last; i++) {

                var pageText = (i + 1).ToString(CultureInfo.InvariantCulture);
                var part = new TagBuilder("li") {
                    InnerHtml = GridLink(webGrid, webGrid.GetPageUrl(i), pageText)
                };

                if (i == currentPage) {
                    part.MergeAttribute("class", "active");
                }
                
                li.Add(part);

            }
        }
        
        if (ModeEnabled(mode, WebGridPagerModes.NextPrevious)) {
            if (String.IsNullOrEmpty(nextText)) {
                nextText = "بعدی";
            }
            
            int page = currentPage == lastPage ? lastPage: currentPage + 1;

            var part = new TagBuilder("li") {
                InnerHtml = GridLink(webGrid, webGrid.GetPageUrl(page), nextText)
            };
            
            if (currentPage == lastPage) {
                part.MergeAttribute("class", "disabled");
            }

            li.Add(part);

        }
        
        if (ModeEnabled(mode, WebGridPagerModes.FirstLast)) {
            if (String.IsNullOrEmpty(lastText)) {
                lastText = "آخرین";
            }

            var part = new TagBuilder("li") {
                InnerHtml = GridLink(webGrid, webGrid.GetPageUrl(lastPage), lastText)
            };
            
            if (currentPage == lastPage) {
                part.MergeAttribute("class", "disabled");
            }

            li.Add(part);

        }

        ul.InnerHtml = string.Join("", li);

        var html = "";
        if (explicitlyCalled && webGrid.IsAjaxEnabled) {
            var span = new TagBuilder("span");
            span.MergeAttribute("data-swhgajax", "true");
            span.MergeAttribute("data-swhgcontainer", webGrid.AjaxUpdateContainerId);
            span.MergeAttribute("data-swhgcallback", webGrid.AjaxUpdateCallback);

            span.InnerHtml = ul.ToString();
            html = span.ToString();

        } else {
            html = ul.ToString();
        }

        return new HelperResult(writer => {
            writer.Write(html);         
        });
    }

    private static String GridLink(WebGrid webGrid, string url, string text) 
    {
        TagBuilder builder = new TagBuilder("a");
        builder.SetInnerText(text);
        builder.MergeAttribute("href", url);
        if (webGrid.IsAjaxEnabled) {
            builder.MergeAttribute("data-swhglnk", "true");
        }
        return builder.ToString(TagRenderMode.Normal);
    }


    private static bool ModeEnabled(WebGridPagerModes mode, WebGridPagerModes modeCheck)
    {
        return (mode & modeCheck) == modeCheck;
    }

}
کلاس فوق باید در پوشه‌ی App_Code قرار گیرد.
پس از آن در View یی که اطلاعات را نمایش می‌دهید، فقط لازم است کد زیر را اضافه نمایید:
<div>
@grid.PagerList(mode: WebGridPagerModes.All)
</div>
تا اینجا متد مورد نظر برای انجام صفحه بندی گرید پیاده سازی شد. ادامه‌ی کار هم مشخص است؛ داشتن یک PartialView جهت نمایش لیست اطلاعات، پاس دادن دیتا به Partial و تمام.

در ادامه برای تکمیل بحث مثالی را از نحوه‌ی نمایش اطلاعات و صفحه بندی سفارشی نشان خواهیم داد:

PartialView لازم برای نمایش اطلاعات
تنظیمات لازم گرید :
@{  
    WebGrid grid = new WebGrid(Model,
                               rowsPerPage: 10,
                               ajaxUpdateContainerId: "grid");
    
    var rowIndex = ((grid.PageIndex + 1) * grid.RowsPerPage) - (grid.RowsPerPage - 1);
}
تعیین فیلد‌های گرید :
    @grid.Table(
                            tableStyle: "table table-striped table-hover",
                    headerStyle: "webgrid-header",
          
            alternatingRowStyle: "webgrid-alternating-row",
            selectedRowStyle: "webgrid-selected-row",
            rowStyle: "webgrid-row-style",
            columns: grid.Columns(
                
                  grid.Column(columnName: "Name", header: "نام استان", style: "myfont"),
                  grid.Column(columnName: "NameEn", header: "نام استان ( انگلیسی )", style: "myfont"),
                  grid.Column(header: "", format: item => @Html.ActionLink("مدیریت شهرها", actionName: MVC.Admin.City.ActionNames.Index, controllerName: MVC.Admin.City.Name, routeValues: new {Code=item.Code },htmlAttributes:null)),
                 grid.Column(header: "",
                 style: "text-align-center-col smallcell",
                             format: item => @Html.ActionLink(linkText: "ویرایش", actionName: "Edit",
                             controllerName: "Province", routeValues: new { area = "Admin", code = item.Code },
                             htmlAttributes: new { @class = "btn-sm btn-info vertical-center" }))
                 ) )

<div>
    @grid.PagerList(mode: WebGridPagerModes.All)
</div>

خروجی حاصل به صورت زیر خواهد بود :


اگر طبق توضیحات بالا عمل کرده باشید، در نهایت صفحه بندی شما به صورت عمودی نمایش داده می‌شود؛ یعنی هر کدام از شماره صفحات در یک سطر. دلیل آن هم این است که تگ ul، کلاس .pagination  را ندارد. در کدهای بوت استراپ تعریف شده است که تمام li هایی که به صورت مستقیم داخل کلاس .pagination هستند خصوصیات مورد نظر را بگیرند.

برای این کار دو راه حل وجود دارد :

راه حل اول: تغییر کدهای css

کدهای  نوشته شده برای صفحه بندی در بوت استراپ را از حالت زیر:

.pagination > li
به حالت زیر تغییر دهید:
.pagination li

یادآوری : علامت < در CSS یعنی به صورت مستقیم و در شاخه‌ی اول.

راه حل دوم - افزودن کلاس .pagination  به تگ ul:
ابتدا کلاس .pagination  را از تگ div حذف نمایید:
<div >
    @grid.PagerList(mode: WebGridPagerModes.All)
</div>

و در کدهایی کلاس WebGridExtensions،  در قسمتی که تگ ul اصافه می‌شود، کلاس مورد نظر را به آن اضافه می‌کنیم:
  var ul = new TagBuilder("ul");
        ul.AddCssClass("pagination");


دانلود کدهای این مثال


نکته‌ای در مورد Webgrid
اگر نیاز داشتید به یکباره تمام اطلاعات را در گرید لود نکنید و به صورت n تاn تا رکورد‌ها را نمایش دهید، در این حالت پس از پاس دادن لیستی از اطلاعات به View مورد نظر لازم است تعداد کل رکورد‌ها را در یک متغییر به سمت View بفرستید. این کار به این دلیل می‌باشد که بتوان صفحه بندی را تولید کرد. برای این کار در بخش تنظیمات Webgrid مقدار source را برابر null قرار دهید و از قطعه کد زیر جهت بایند کردن گرید، بعد از کدهای تنظیمات WebGrid استفاده نمایید:
grid.Bind(Model, rowCount: (int)ViewBag.PageCount);
مطالب
آشنایی با Saltarelle کامپایلر قدرتمند #C به جاوااسکریپت

شاید ساده‌ترین تعریف برای  Saltarelle  این باشد که «کامپایلریست که کد‌های C# را به جاوا اسکریپت تبدیل می‌کند». محاسن زیادی را می‌توان برای اینگونه کامپایلر‌ها نام برد؛ مخصوصا در پروژه‌های سازمانی که نگهداری از کد‌های جاوا اسکریپت بسیار سخت و گاهی خارج از توان است و این شاید مهمترین عامل ظهور ابزارهای جدید از قبیل Typescript باشد.

در هر صورت اگر حوصله و وقت کافی برای تجهیز تیم نرم افزاری، به دانش یک زبان جدید مانند Typescript نباشد، استفاده از توان و دانش تیم تولید، از زبان C# ساده‌ترین راه حل است و اگر ابزاری مطمئن برای استفاده از حداکثر قدرت JavaScript همراه با امکانات نگهداری و توسعه کد‌ها وجود داشته باشد، بی شک Saltarelle یکی از بهترین‌های آنهاست.

قبلا کامپایلر هایی از این دست مانند  Script# وجود داشتند، اما فاقد همه امکانات C# بوده وعملا قدرت کامل C# در کد نویسی وجود نداشت. اما با توجه به ادعای توسعه دهندگان این کامپایلر سورس باز در استفاده‌ی حداکثری از کلیه ویژگی‌های C# 5 و با وجود Library ‌های متعدد می‌توان Saltarelle  را عملا یک کامپایلر موفق در این زمینه دانست.

برای استفاده از Saltarelle در یک برنامه وب ساده باید یک پروژه Console Application به Solution اضافه کرد و پکیج Saltarelle.Compiler را از nuget نصب نمایید. بعد از نصب این پکیج، کلیه Reference ‌ها از پروژه حدف می‌شوند و هر بار Build توسط کامپایلر Saltarelle  انجام می‌شود. البته با اولین Build، مقداری Error را خواهید دید که برای از بین بردنشان نیاز است پکیج Saltarelle.Runtime را نیز در این پروژه نصب نمایید:

PM> Install-Package Saltarelle.Compiler
PM> Install-Package Saltarelle.Runtime

در صورتیکه کماکان Build  نهایی با Error همرا بود، یکبار این پروژه را Unload  و سپس مجددا Load نمایید



UI یک پروژه وب MVC است و Client یک Console Application که پکیج‌های مورد نیاز Saltarelle  روی آن نصب شده است.

در صورتیکه پروژه را Build نماییم و نگاهی به پوشه‌ی Debug بیاندازیم، یک فایل JavaScript همنام پروژه وجود دارد:


برای اینکه بعد از هر بار Build ، فایل اسکریپت به پوشه‌ی مربوطه در پروژه UI منتقل شود کافیست کد زیر را در Post Build  پروژه Client بنویسیم: 

copy "$(TargetDir)$(TargetName).js" "$(SolutionDir)SalratelleSample.UI\Scripts"

اکنون پس از هر بار Build ، فایل اسکریپت مورد نظر در پوشه‌ی Scripts پروژه UI  آپدیت می‌شود:


در ادامه کافیست فایل اسکریپت را به layout اضافه کنیم. 

<script src="~/Scripts/SaltarelleSample.Client.js"></script>

در پوشه‌ی Saltarelle.Runtime در پکیج‌های نصب شده، یک فایل اسکریپت به نام mscorlib.min.js نیز وجود دارد که حاوی اسکریپت‌های مورد نیاز Saltarelle در هنگام اجراست. آن را به پوشه اسکریپت‌های پروژه UI کپی نمایید و سپس به Layout  اضافه کنید. 

<script src="~/Scripts/mscorlib.min.js"></script>
<script src="~/Scripts/SaltarelleSample.Client.js"></script>

حال نوبت به اضافه نمودن library‌های مورد نیازمان است. برای دسترسی به آبجکت هایی از قبیل document, window, element و غیره در جاوااسکریپت می‌توان پکیج Saltarelle.Web را در پروژه‌ی Client نصب نمود و برای دسترسی به اشیاء و فرمانهای jQuery، پکیج Salratelle.jQuery را نصب نمایید. 

> Install-Package Saltarelle.Web
> Install-Package Saltarelle.jQuery

به این library‌ها imported library می‌گویند. در واقع، در زمان کامپایل، برای این library‌ها فایل اسکریپتی تولید نمی‌شود و فقط آبجکت‌های #C هستند که که هنگام کامپایل تبدیل به کدهای ساده اسکریپت می‌شوند که اگر اسکریپت مربوط به آنها به صفحه اضافه نشده باشد، اجرای اسکریپت با خطا مواجه می‌شود.

به طور ساده‌تر وقتی از jQuery library استفاده می‌کنید هیچ فایل اسکریپت اضافه‌ای تولید نمی‌شود، اما باید اسکریپت jQuery به صفحه شما اضافه شده باشد.

<script src="~/Scripts/jquery-1.10.2.min.js"></script>

مثال ما یک اپلیکیشن ساده برای خواندن فید‌های همین سایت است. ابتدا کد‌های سمت سرور را در پروژه UI  می نویسیم.

کلاس‌های مورد نیاز ما برای این فید ریدر: 

public class Feed
    {
        public string FeedId { get; set; }
        public string Title { get; set; }
        public string Address { get; set; }

    }
    public class Item
    {
        public string Title { get; set; }
        public string Link { get; set; }
        public string Description { get; set; }
    }

و یک کلاس برای مدیریت منطق برنامه 

 public class SiteManager
    {
        private static List<Feed> _feeds;
        public static List<Feed> Feeds
        {
            get
            {
                if (_feeds == null)
                    _feeds = CreateSites();
                return _feeds;
            }
        }
        private static List<Feed> CreateSites()
        {
            return new List<Feed>() { 
                new Feed(){
                    FeedId = "1",
                    Title = "آخرین تغییرات سایت",
                    Address = "https://www.dntips.ir/rss.xml"
                },
                 new Feed(){
                    FeedId = "2",
                    Title = "مطالب سایت",
                    Address = "https://www.dntips.ir/feeds/posts"
                },
                 new Feed(){
                    FeedId = "3",
                    Title = "نظرات سایت",
                    Address = "https://www.dntips.ir/feeds/comments"
                },
                 new Feed(){
                    FeedId = "4",
                    Title = "خلاصه اشتراک ها",
                    Address = "https://www.dntips.ir/feed/news"
                },
            };
        }

        public static IEnumerable<Item> GetNews(string id)
        {
            XDocument feedXML = XDocument.Load(Feeds.Find(s=> s.FeedId == id).Address);
            var feeds = from feed in feedXML.Descendants("item")
                        select new Item
                        {
                            Title = feed.Element("title").Value,
                            Link = feed.Element("link").Value,
                            Description = feed.Element("description").Value
                        };
            return feeds;
        }

    }

کلاس SiteManager فقط یک لیست از فید‌ها دارد و متدی که با گرفتن شناسه‌ی فید ، یک لیست از آیتم‌های موجود در آن فید ایجاد می‌کند.

حال دو ApiController برای دریافت داده‌ها ایجاد می‌کنیم

public class FeedController : ApiController  
{
        // GET api/<controller>
        public IEnumerable<Feed> Get()
        {
            return SiteManager.Feeds;
        }
    }

public class ItemsController : ApiController
    {
        // GET api/<controller>/5
        public IEnumerable<Item> Get(string id)
        {
            return SiteManager.GetNews(id);
        }
    }

در View پیش‌فرض که Index از کنترلر Home  است،  یک Html ساده برای فرم  صفحه اضافه می‌کنیم 

<div>
    <div>
        <h2>Feeds</h2>
        <ul id="Feeds">
           
        </ul>
    </div>
    <div>
        <h2>Items</h2>
        <p id="FeedItems">
        </p>
    </div>
   
</div>

در المنت Feeds لیست فید‌ها را قرار می‌دهیم و در FeedItems آیتم‌های مربوط به هر فید. حال به سراغ کد‌های سمت کلاینت می‌رویم و به جای جاوا اسکریپت از Saltarelle استفاده می‌کنیم.

کلاس Program را از پروژه Client باز می‌کنیم و متد Main را به شکل زیر تغییر می‌دهیم:

static void Main()
        {
            jQuery.OnDocumentReady(() => {
                FillFeeds();
            });
        }

بعد از کامپایل شدن، کد #C شارپ بالا به صورت زیر در می‌آید: 

$SaltarelleSample_Client_$Program.$main = function() {
$(function() {
$SaltarelleSample_Client_$Program.$fillFeeds();
});
};
$SaltarelleSample_Client_$Program.$main();

و این همان متد معروف jQuery است که Saltarelle.jQuery برایمان ایجاد کرده است.

متد FillFeeds را به شکل زیر پیاده سازی می‌کنیم

private static void FillFeeds()
        {
            jQuery.Ajax(new jQueryAjaxOptions()
            {
                Url = "/api/feed",
                Type = "GET",
                Success = (d,t,r) => {

                    // Fill 
                    var ul = jQuery.Select("#Feeds");
                    jQuery.Each((List<Feed>)d, (idx,i) => {
                        var li = jQuery.Select("<li>").Text(i.Title).CSS("cursor", "pointer");
                        li.Click(eve => {
                            FillData(i.FeedId);
                        });
                        ul.Append(li);
                    });
                }
            });
        }

آبجکت jQuery، متدی به نام Ajax دارد که یک شی از کلاس jQueryAjaxOptions را به عنوان پارامتر می‌پذیرد. این کلاس کلیه خصوصیات متد Ajax در jQuery را پیاده سازی می‌کند. نکته شیرین آن توانایی نوشتن lambda برای Delegate هاست.

خاصیت Success یک Delegate است که 3 پارامتر ورودی را می‌پذیرد.

public delegate void AjaxRequestCallback(object data, string textStatus, jQueryXmlHttpRequest request);

data همان مقداریست که api باز می‌گرداند که یک لیست از Feed هاست. برای زیبایی کار، من یک کلاس Feed در پروژه Client اضافه می‌کنم که خصوصیاتی مشترک با کلاس اصلی سمت سرور دارد و مقدار برگشی Ajax را به آن تبدیل می‌کنم.

کلاس Feed و Item

 [PreserveMemberCase()]
    public class Feed
    {
        //[ScriptName("FeedId")]
        public string FeedId;

        //[ScriptName("Title")]
        public string Title;

        //[ScriptName("Address")]
        public string Address;

    }

    [PreserveMemberCase()]
    public class Item
    {
        // [ScriptName("Title")]
        public string Title;

        // [ScriptName("Link")]
        public string Link;

        // [ScriptName("Description")]
        public string Description;
    }
Attrubute‌های زیادی در Saltarelle وجود دارند و از آنجایی که کامپایلر اسم فیلد‌ها را camelCase کامپایل می‌کند من برای جلوگیری از آن از PreserveMemberCase  بر روی هر کلاس استفاده کردم. می‌توانید اسم هر فیلد را سفارشی کامپایل نمایید. 
jQuery.Each((List<Feed>)d, (idx,i) => {
                        var li = jQuery.Select("<li>").Text(i.Title).CSS("cursor", "pointer");
                        li.Click(eve => {
                            FillData(i.FeedId);
                        });
                        ul.Append(li);
                    });

به ازای هر آیتمی که در شیء بازگشتی وجود دارد، با استفاد از متد each در jQuery یک li ایجاد می‌کنیم. همان طور که می‌بینید کلیه خواص، به شکل Fluent قابل اضافه شدن می‌باشد. سپس برای li یک رویداد کلیک که در صورت وقوع، متد FillData را با شناسه فید کلیک شده فراخوانی می‌کند و در آخر li را به المنت ul اضافه می‌کنیم.

برای هر کلیک هم مانند مثال بالا api را با شناسه‌ی فید مربوطه فراخوانی کرده و به ازای هر آیتم، یک سطر ایجاد می‌کنیم.

private static void FillData(string p)
        {
            jQuery.Ajax(new jQueryAjaxOptions()
            {
                Url = "/api/items/" + p,
                Type = "GET",
                Success = (d, t, r) => {
                    var content = jQuery.Select("#FeedItems");
                    content.Html("");
                    foreach (var item in (List<Item>)d)
                    {
                        var row = jQuery.Select("<div>").AddClass("row").CSS("direction", "rtl");
                        var link = jQuery.Select("<a>").Attribute("href", item.Link).Text(item.Title);
                        row.Append(link);
                        content.Append(row);
                    }
                }
            });
        }
خروجی برنامه به شکل زیر است: 

در این مثال ما از Saltarelle.jQuery برای استفاده از jQuery.js استفاده نمودیم. library‌های متعددی برای Saltarelle  از قبیل  linq,angular,knockout,jQueryUI,nodeJs ایجاد شده و همچنین قابلیت‌های زیادی برای نوشتن imported library‌های سفارشی نیز وجود دارد. 

مطمئنا استفاده از چنین کامپایلرهایی راه حلی سریع برای رهایی از مشکلات متعدد کد نویسی با جاوا اسکریپت در نرم افزارهای بزرگ مقیاس است. اما مقایسه آنها با ابزارهایی از قبیل typescript احتیاج به زمان و تجربه کافی در این زمینه دارد.