نظرات مطالب
طراحی گردش کاری با استفاده از State machines - قسمت سوم
- مورد استفاده در هر شرکتی با بیش از یک نفر کارمند.
مانند گردش کاری درخواست:
  1. مساعده
  2. مرخصی
  3. ماموریت
  4. تائید ساعات کاری
  5. درخواست و تائید تشکیل یک جلسه
  6. پر کردن و تائید تایم شیت
و ... تمام کارهای یک سازمان یا شرکت
-  هدف آشنایی بصری شما با نحوه حل مسایل چند مرحله‌ای که در ابتدا ساده به نظر می‌رسند، اما 10 مرحله که به آن اضافه شود، مدیریت آن به روش‌های متداول طاقت فرسا خواهد شد.
- وجود این تصاویر، تولید کننده کد و امثال آن صرفا برای ساده کردن توضیح انبوهی متن، به همراه روشی برای حل آن بود.
نظرات مطالب
نحوه اجباری کردن استفاده از WWW در ASP.NET MVC
- در حالت کلی هیچ نیازی به ریدایرکشن فایل‌های استاتیک نیست که در مطلب جاری مشکلی به شمار روند. نه تاثیری در اعتبار سنجی دارند و نه به صورت پیش فرض وارد موتور اجرایی ASP.NET می‌شوند. به هر آدرسی که اشاره می‌کنند تا زمانیکه قابل یافت شدن در سایت باشند، جهت نمایش صحیح سایت کافی هستند؛ حتی اگر به یک آدرس خارج از سایت مانند مخزن کدی در کدپلکس اشاره کنند (مانند خیلی از سایت‌ها و وبلاگ‌ها که فایل‌های تصاویر، css، js و غیره خود را در سایت‌های ثالث هاست می‌کنند).
- امکان اجرای یک سایت MVC4 با IIS6 وجود دارد و نیاز به یک دست کردن آدرس‌های ورود به سایت جهت رفع سوء تفاهم توضیح داده شده در مقدمه مطلب برآورده خواهد شد.
مطالب
فرمت مناسب تصاویر جهت استفاده در iTextSharp

عموما هنگام تهیه یک مستند یا گزارش، هرچقدر حجم نهایی کمتر باشد، توزیع آن ساده‌تر خواهد بود. در اینجا اینطور به نظر می‌رسد که اگر مثلا از تصاویری با فرمت jpg یا png استفاده کنیم، کمترین حجم نهایی را می‌توان بدست آورد. اما حین استفاده از iTextSharp شما با استفاده از تصاویری با فرمت BMP بهترین نتیجه را خواهید گرفت: کمترین حجم و بهترین کیفیت! البته یک نکته‌ی ریز دارد که باید رعایت شود:


using (var pdfDoc = new Document(PageSize.A4))  

{
var pdfWriter = PdfWriter.GetInstance(pdfDoc, new FileStream("tpn.pdf", FileMode.Create));
pdfWriter.SetPdfVersion(new PdfName("1.5"));
pdfWriter.CompressionLevel = PdfStream.BEST_COMPRESSION;
//...

}


در اینجا pdf version و همچنین compression level باید تنظیم شوند. پس از آن فشرده سازی تصاویر BMP به صورت خودکار حین تهیه فایل نهایی انجام خواهد شد.

مطالب
منابع مطالعاتی بیشتر در مورد iTextSharp

  • آشنایی با صفحه بندی در iTextSharp : [+]
  • تعریف هدر و فوتر: [+]
  • افزودن متن ساده در iTextSharp: [+]
  • کار با فونت‌های مختلف در iTextSharp: [+]
  • نحوه‌ی افزودن جدول در iTextSharp: [+]
  • ترسیم اشکال گرافیکی با iTextSharp: [+]
  • کار با تصاویر در iTextSharp: [+] و [+]
  • امکان تبدیل HTML به PDF در iTextSharp: [+]، [+]، [+] و [+]
  • نحوه‌ی تعریف لینک در iTextSharp: [+]
  • نحوه‌ی تعریف لیست در iTextSharp: [+]
  • افزودن نمودار به کمک کنترل‌های چارت مایکروسافت در iTextSharp: [+]
  • امکان تعریف بارکد در iTextSharp: [+]
  • یک سری مثال: [+]
  • یکی کردن چند فایل پی دی اف موجود با هم توسط iTextSharp: [+]

مطالب
آموزش سیلورلایت 4 - قسمت‌های 11 تا 20

فصل عنوان فایل مرتبط
11 آشنایی با MVVM Light Toolkit +
12 اعمال قالب‌های متفاوت به برنامه‌های Silverlight +
13 استفاده از Web Services در Silverlight +
14 آشنایی با مفاهیم مرتبط با شیء Application و مدیریت آن +
15 امنیت در Silverlight +
16 روش‌های تعیین اعتبار ورودی کاربر در Silverlight +
17 استفاده از تصاویر و فایل‌های چند رسانه‌ای در Silverlight +
18 بررسی جامع توانایی‌های کنترل DataGrid +
19 معرفی سایر امکانات و ویژگی‌های اختصاصی Silverlight 4 +
20 بررسی اجرای خارج از مرورگر برنامه‌های Silverlight +


مطالب
کش کردن اطلاعات غیر پویا در ASP.Net - قسمت سوم

در دو قسمت قبل در مورد IIS7 و IIS6 صحبت شد (+ و +).
در تکمیل قسمت دوم، یک مورد هم جزو قابلیت‌های ذاتی IIS6 و همچنین IIS5 است که می‌توان آن‌را فعال نمود (اگر دسترسی به سرور دارید) :




تنظیم مدت زمان content expiration ، بدون نیاز به برنامه نویسی خاصی، کار اضافه کردن هدر مربوط به مدت زمان کش شدن سمت کلاینت را به محتویات غیرپویای سایت شما مانند تصاویر ، فایل‌های CSS و غیره انجام می‌دهد. آمارها نشان می‌دهند که این تنظیم، زمان بارگذاری بعدی را بین 50 تا 70 درصد کاهش می‌دهد.
تنظیم این قابلیت را می‌توانید به چک لیست نصب IIS خود اضافه نمائید.

مطالب
Pipeها در Angular 2 – قسمت دوم – ساخت Pipe سفارشی
در قسمت قبل، مقدماتی از Pipeها را مورد برسی قرار دادیم؛ از جمله کاربرد Pipeها، نحوه استفاده از آنها، معرفی یکسری Pipe از پیش ساخته شده Angular، نحوه ارسال پارامتر به آنها و همچنین نحوه استفاده از آنها را در داخل Typescript، فراگرفتیم. در این قسمت نحوه ساخت Pipeهای سفارشی و همچنین نکات تکمیلی در مورد آنها را مورد بحث و بررسی قرار می‌دهیم.

نحوه ساخت Pipe سفارشی

علاوه بر استفاده از Pipeهای از پیش ساخته شده Angular، شما می‌توانید Pipeهای سفارشی خود را نیز تعریف و استفاده کنید. به عنوان مثال می‌خواهیم Pipe ای را با نام perNumber تعریف کنیم، تا تمامی اعداد موجود در عبارت ورودی Pipe را به صورت اعداد فارسی نمایش دهد. یعنی با اعمال این Pipe به عدد 123456 خروجی ۱۲۳۴۵۶ مورد انتظار است. برای ایجاد Pipe سفارشی مراحل زیر را انجام دهید.


قدم اول - ساخت یک فایل با نام دلخواه

طبق Style Guide در Angular.io نام این فایل را per-number.pipe.ts انتخاب می‌کنیم.


قدم دوم – افزودن ماژولهای مورد نیاز

داخل فایل ایجاد شده ماژول‌های Pipe و PipeTransform را با استفاده از دستور import از angular/core@ اضافه می‌کنیم.
 import { Pipe, PipeTransform } from '@angular/core';


قدم سوم – ساخت کلاس و مزین کردن آن به Pipe@

یک کلاس با نام دلخواه را مثلا به نام PerNumberPipe ایجاد می‌کنیم. این کلاس علاوه بر اینکه PipeTransform را پیاده سازی خواهد کرد، مزین به متادیتای Pipe@ نیز می‌باشد. متادیتای Pipe@ هنگام تزئین کلاس، یک نام را دریافت می‌کند. این نام قرار است به عنوان نام نهایی Pipe برای اعمال بر روی Template expressions مورد استفاده قرار بگیرد.
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({name: 'perNumber'}) export class PerNumberPipe implements PipeTransform {

}


قدم چهارم – پیاده سازی متد transform

به واسطه اعمال اینترفیس PipeTransform، این کلاس باید متد transform را پیاده سازی کند. این متد در پارامتر اول خود، عبارت ورودی را که قرار است Pipe بر روی آن اعمال شود، دریافت می‌کند و در ادامه تعداد دلخواهی پارامتر ورودی Pipe را که می‌خواهد، می‌تواند دریافت کند.
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({name: 'perNumber'})
export class PerNumberPipe implements PipeTransform {
    transform(value: any, ...args: any[]): any {

    }
}

نکته ۱: نام انتخابی برای Pipe در آذین‌گر Pipe@ بایستی یک شناسه معتبر در JavaScript باشد.
نکته ۲: متد transform برای Pipe اجباری است و حتما بایستی پیاده سازی شود. اینترفیس PipeTransform این متد را برای کلاس اجباری می‌کند؛ هرچند استفاده از این اینترفیس برای کلاس Pipe کاملا اختیاری است.


قدم آخر – نوشتن کد تبدیل اعداد

Pipe مورد نظر ما قرار است یک رشته عددی را از ورودی دریافت کند و تمامی اعداد لاتین آن را به فارسی تبدیل کند. همچنین این Pipe هیچگونه پارامتری را دریافت نمی‌کند. کد زیر نحوه پیاده سازی متد transform را نمایش می‌دهد.
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({name: 'perNumber'}) export class PerNumberPipe implements PipeTransform {
    transform(input: string): string{
        if (input == undefined) return '';
        var str = input.toString().trim();
        if (str === "") return "";
        str = str.replace(/0/g, '۰');
        str = str.replace(/1/g, '۱');
        str = str.replace(/2/g, '۲');
        str = str.replace(/3/g, '۳');
        str = str.replace(/4/g, '۴');
        str = str.replace(/5/g, '۵');
        str = str.replace(/6/g, '۶');
        str = str.replace(/7/g, '۷');
        str = str.replace(/8/g, '۸');
        str = str.replace(/9/g, '۹');
        return str;
    }
}


نحوه معرفی Pipe سفارشی به برنامه

حالا جهت استفاده از Pipe سفارشی در کامپوننت‌های خود کافی است آنرا یکبار در قسمت declarations در AppModule خود  تعریف کنید.
import { PerNumberPipe } from './pipes/per-number.pipe.ts'
...

declarations: [PerNumberPipe]


نحوه استفاده از Pipeهای سفارشی 

نحوه استفاده از Pipeهای سفارشی، دقیقا مشابه Pipeهای از قبل ساخته شده Angular می‌باشد.
<h3>{{'12345679' | perNumber}}</h3>
 

یکسری از Pipeهای مربوط به زبان فارسی در گیت‌هاب بنده پیاده سازی شده است که نیازمند همکاری دوستان است. لطفا جهت همکاری برای ساخت یک ابزار جامع برای زبان فارسی در Angular به این لینک مراجعه کنید.
 
Pipeها و تشخیص تغییرات

Angular برای اعمال Pipe بر روی Template expressions بایستی تمامی رخدادهای برنامه را تحت نظر قرار داده و با مشاهده هر تغییری بر روی عبارت ورودی Pipe، فراخوانی Pipe را آغاز کند. از جمله این رخدادها می‌توان به رخداد mouse move، timer tick، server response و فشرده شدن کلیدهای ماوس و یا کیبورد اشاره کرد. واضح است که بررسی تغییرات عبارت در این همه رخداد می‌تواند مخرب باشد و بر روی کارآئی (Performance) تاثیر منفی خواهد گذاشت. اما Angular برای حل این مشکل و همچنین هنگام مشاهده سریع تغییرات هنگام استفاده از Pipeها، الگوریتم‌های سریع و ساده‌ای در نظر گرفته است.


در قسمت بعد با انواع Pipeها در Angular و همچنین نحوه پیاده سازی آنها، آشنا خواهیم شد. 
مطالب
Accord.NET #3
در مطلب قبل یک مثال مفهومی درباره کاربرد SVM بیان شد و دیدیم که این الگوریتم، یک روش دودویی است و عموما برای زمانی به کار می‌رود که مجموعه داده ما شامل دو کلاس باشد.
اگر بخواهیم نوع چهار میوه (سیب، گلابی، موز، پرتغال) را که از خط سورتینگ عبور می‌کنند، تشخیص دهیم و یا اینکه بخواهیم تشخیص اعداد دست نویس را داشته باشیم و یا اینکه حتی مطالب این وب سایت را که شامل چندین برچسب هستند، طبقه بندی کنیم، آیا در این تشخیص‌ها SVM به ما کمک می‌کند؟ پاسخ مثبت است.
در فضای نام یادگیری ماشین Accord.NET دو تابع خوب MulticlassSupportVectorLearning و MultilabelSupportVectorMachine برای این گونه از مسائل تعبیه شده است. زمانیکه مسئله‌ی ما شامل مجموعه داده‌هایی بود که در چندین کلاس دسته بندی می‌شوند (مانند دسته بندی میوه، اعداد و ...) روش Multiclass  و زمانیکه عناصر مجموعه داده ما به طور جداگانه شامل چندین برچسب باشند (مانند دسته بندی مطالب با داشتن چندین تگ، ...) روش Multilabel ابزار مفیدی خواهند بود. (+)

با توجه به دودویی بودن ماشین بردار پشتیبان، دو استراتژی برای به کارگیری این الگوریتم برای دسته بندی‌های چند کلاسه وجود دارد:
  • روش یک در مقابل همه - One-against-all : در این روش عملا همان روش دودویی SVM را برای هر یک از کلاس‌ها به صورت جداگانه بررسی می‌کنیم. مثلا برای تشخیص میوه، یک بار دو کلاس سیب و غیر سیب (مابقی) بررسی می‌شوند و به همین ترتیب برای سایر کلاس‌ها و در مجموع صفحات ابرصفحه جدا کننده بین هر کلاس در مقابل سایر کلاس‌ها ایجاد می‌شود.

  • روش یک در مقابل یک - One-against-one (*) : در این روش هر کلاس، با هر یک از کلاس‌های دیگر به صورت تک تک بررسی می‌شود و صفحات ابرصفحه جدا کننده مابین هر جفت کلاس متفاوت ایجاد می‌شود. (بیشتر در +)

*روش "یک در مقابل یک" یا One-against-one اساس کار دسته بندی MulticlassSupportVectorMachine در فضای نام Accord.MachineLearning است.

یک مثال کاربردی :  هدف در این مثال دسته بندی اعداد فارسی به کمک MulticlassSupportVectorMachine است.

به معرفی ابزار کار مورد نیاز می‌پردازیم.

1.مجموعه ارقام دستنویس هدی: مجموعه ارقام دستنویس هدی که اولین مجموعه‌ی بزرگ ارقام دستنویس فارسی است، مشتمل بر ۱۰۲۳۵۳ نمونه دستنوشته سیاه سفید است. این مجموعه طی انجام یک پروژه‏‌ی کارشناسی ارشد درباره بازشناسی فرم‌های دستنویس تهیه شده است. داده‌های این مجموعه از حدود ۱۲۰۰۰ فرم ثبت نام آزمون سراسری کارشناسی ارشد سال ۱۳۸۴ و آزمون کاردانی پیوسته‌‏ی دانشگاه جامع علمی کاربردی سال ۱۳۸۳ استخراج شده است. (اطلاعات بیشتر درباره مجموعه ارقام دستنویس هدی) .

تعداد 1000 نمونه (از هر عدد 100 نمونه) از این مجموعه داده، با فرمت bmp در این پروژه مورد استفاده قرار گرفته که به همراه پروژه در انتهای این مطلب قابل دریافت است.

2.استخراج ویژگی (Feature extraction ) : در بازشناسی الگو و مفاهیم کلاس بندی، یکی از مهمترین گام‌ها، استخراج ویژگی است. ما موظف هستیم تا اطلاعات مناسبی را به عنوان ورودی برای دسته بندی‌مان معرفی نماییم. روش‌های مختلفی برای استخراج ویژگی وجود دارند. ویژگی‌ها به دو دسته‌ی کلی ویژگی‌های ظاهری (Appearance) و ویژگی‌های توصیف کننده ( Descriptive) قابل تقسیم هستند. در تشخیص حروف و اعداد، ویژگی‌هایی مانند شدت نور نقاط (Intensity)، تعداد حلقه بسته، تعداد خطوط راست، تعداد دندانه، تعداد نقطه (برای حروف) و ... در دسته‌ی اول و ویژگی‌هایی مانند شیب خطوط، گرادیان، میزان افت یا شدت نور یک ناحیه، HOG و ... در دسته دوم قرار می‌گیرند. در این مطلب ما تنها از روش شدت نور نقاط برای استخراج ویژگی‌هایمان استفاده کرده‌ایم.
کد زیر با دریافت یک فایل Bitmap، ابتدا ابعاد را به اندازه 32*32 تغییر می‌دهد و سپس آن‌را به صورت یک بردار 1*1024 را بر می‌گرداند:

        //تابع استخراج ویژگی
        private static double[] FeatureExtractor(Bitmap bitmap)
        {
            bitmap = BitmapResizer(bitmap, 32, 32);

            double[] features = new double[32 * 32];
            for (int i = 0; i < 32; i++)
                for (int j = 0; j < 32; j++)
                    features[i * 32 + j] = (bitmap.GetPixel(j, i).R == 255) ? 0 : 1;

            return features;
        }

        //تابع تغییر دهنده ابعاد عکس
        private static Bitmap BitmapResizer(Bitmap bitmap, int width, int height)
        {
            var newbitmap = new Bitmap(width, height);
            using (Graphics g = Graphics.FromImage((Image)newbitmap))
            {
                g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
                g.DrawImage(bitmap, 0, 0, width, height);
            }
            return newbitmap;
        }
3.ایجاد ورودی‌ها و برچسب : در این مرحله ما باید ورودی‌های دسته بندی SVM را که عملا آرایه‌ای براساس تعداد نمونه‌های مجموعه آموزش (train) است، ایجاد نماییم.

ورودی‌ها (inputs) = با توجه به اینکه تعداد نمونه‌ها 50 مورد از هر عدد (مجموعا 500 نمونه) تعیین شده است و تعداد ویژگی‌های هر نمونه یک بردار با طول 1024 است، ابعاد ماتریس ورودی مان [1024][500] می‌شود.
برچسب‌ها (labels) = تعداد برچسب مسلما به تعداد نمونه هایمان یعنی 500 مورد می‌باشد و مقادیر آن قاعدتا عدد متناظر آن تصویر است.


برای این کار از قطعه کد زیر استفاده می‌کنیم :
            var path = new DirectoryInfo(Directory.GetCurrentDirectory()).Parent.Parent.FullName + @"\dataset\";

            // ایجاد ورودی و برچسب
            int trainingCount = 50;
            double[][] inputs = new double[trainingCount * 10][];
            int[] labels = new int[trainingCount * 10];

            var index = 0;
            var filename = "";
            Bitmap bitmap;
            double[] feature;

            for (int number = 0; number < 10; number++)
            {
                for (int j = 0; j < trainingCount; j++)
                {
                    index = (number * trainingCount) + j;
                    filename = string.Format(@"{0}\{0} ({1}).bmp", number, j + 1);
                    bitmap = new Bitmap(path + filename);

                    feature = FeatureExtractor(bitmap);

                    inputs[index] = feature;
                    labels[index] = number;

                    Console.WriteLine(string.Format("{0}.Create input and label for number {1}", index, number));
                }
            }
4.در نهایت به دسته بندمان که همان MulticlassSupportVectorLearning است، خواهیم رسید. همانطور که در مطلب قبل مطرح شد، پس از تعریف پارامترهای Classifier مان، باید آن را به یک الگوریتم یادگیری که در اینجا هم همان روش SMO است، نسبت دهیم.
        private static double MachineLearning(IKernel kernel, double[][] inputs, int[] labels)
        {
            machine_svm = new MulticlassSupportVectorMachine(1024, kernel, 10);

            // معرفی دسته بندمان به الگوریتم یادگیری SMO
            MulticlassSupportVectorLearning ml = new MulticlassSupportVectorLearning(machine_svm, inputs, labels)
            {
                Algorithm = (svm, classInputs, classOutputs, i, j) => 
                    new SequentialMinimalOptimization(svm, classInputs, classOutputs)
            };

            var error = ml.Run();
            return error;
        }
می‌توانیم پس از اینکه ماشین دسته بندمان آماده شد، برای آزمایش تعدادی از نمونه‌های جدید و دیده نشده (UnSeen) را که در نمونه‌های آموزشی وجود نداشتند، مورد ارزیابی قرار دهیم. برای این کار اعداد 0 تا 9 از مجموعه داده مان را در نظر می‌گیریم و به وسیله کد زیر نتایج را مشاهده می‌کنیم :
            // بررسی یک دسته از ورودی‌ها 
            index = 51;
            for (int number = 0; number < 10; number++)
            {
                filename = string.Format(@"{0}\{0} ({1}).bmp", number, index);
                bitmap = new Bitmap(path + filename);

                feature = FeatureExtractor(bitmap);

                double[] responses;
                int recognizednumber = machine_svm.Compute(feature, out responses);

                Console.WriteLine
                (
                    String.Format
                    (
                        "Recognized number for file {0} is : '{1}' [{2}]",
                        filename,
                        recognizednumber,
                        (recognizednumber == number ? "OK" : "Error")
                    )
                );
                if (!machine_svm.IsProbabilistic)
                {
                    // Normalize responses
                    double max = responses.Max();
                    double min = responses.Min();

                    responses = Accord.Math.Tools.Scale(min, max, 0, 1, responses);
                    //int minIndex = Array.IndexOf(responses, 0);              
                }
            }


مشاهده می‌شود که تنها بازشناسی تصاویر اعداد 4 و 6، به اشتباه انجام شده است که جای نگرانی نیست و می‌توان با افزایش تعداد نمونه‌های آموزشی و یا تغییرات پارامتر‌ها از جمله نوع کرنل و یا الگوریتم آموزنده این خطاها را نیز بر طرف کرد.

همانطور که دیدیم SVM گزینه‌ی بسیار مناسبی برای طبقه بندی خیلی از مسائل دو کلاسه و یا حتی چند کلاسه است. اما آکورد دات نت Classifier‌های خوب دیگری (مانند Naive Bayes و Decision Trees یا درخت تصمیم و ... ) را نیز در چارچوب خود جای داده که در مطالب آینده معرفی خواهند شد.

دریافت پروژه
نظرات مطالب
Blazor 5x - قسمت دهم - مبانی Blazor - بخش 7 - مسیریابی

یک نکته‌ی تکمیلی: امکان تعریف مسیریابی صفحات، با استفاده از ویژگی Route

عموما مسیریابی‌های صفحات Blazor، به صورت زیر تعریف می‌شوند:

@page "/counter"

و اگر نیاز باشد تا این مسیر را در قسمت‌های دیگری هم ذکر کنیم (برای مثال در لینک‌ها و یا متد NavigateTo)، باید دقیقا همین مسیر و عبارت را در چندین قسمت برنامه تکرار کنیم. برای رفع این مشکل، با استفاده از ویژگی Route می‌توان مسیریابی فوق را به صورت زیر بازنویسی کرد:

@attribute [Route(Constants.CounterRoute)]

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

public static class Constants
{
    public const string CounterRoute = "/counter";
}

و اگر در قسمت دیگری از برنامه نیاز به ارجاعی به آن بود، می‌توان همین رشته‌ی ثابت را مجددا مورد استفاده قرار داد:

NavigationManager.NavigateTo(Constants.CounterRoute);

نظرات مطالب
ساخت منوهای چند سطحی در ASP.NET MVC
- منوی چند سطحی از بوت استرپ 3 حذف شده‌است. علت آن هم سازگار نبودن یک چنین طراحی با حالت mobile first و مرور آن در صفحات کوچک‌تر است. به صورت خلاصه طراحی که منوی چند سطحی داشته باشد، برای حالت مرور در موبایل مناسب نیست و نیاز به تغییر طراحی جهت یک سطح کردن آن دارد.
- «رو» و «زیر» توسط z-index در css تنظیم می‌شوند. این‌ها را باید با مثلا فایرباگ به صورت زنده اضافه و بررسی کنید تا به جواب برسید.
- برای بوت استرپ 3، منوی چند سطحی ویژه‌ای طراحی شده که به نظر با صفحات کوچک هم سازگار است: «smartmenus ».