مطالب
تبدیل بلوک‌های یونیکد در زیرنویس برای نمایش در تلویزیون‌ها و پلیرها
مقدمه
موقعی که سینمای ناطق کار خود را آغاز کرد، بسیاری از مردم از آن استقبال کردند و بسیاری از سینماگران که این استقبال را دیدند، رفته رفته به سمت سینمای ناطق کشیده شدند. ولی در این بین یک مشکلی ایجاد شده بود؛ اینکه ناشنوایان دیگر مانند قدیم یعنی دوران صامت نمی‌توانستند فیلم‌ها را تماشا کنند، پس نیاز بود این مشکل به نحوی رفع شود. از اینجا بود که ایده‌ی زیرنویس شکل گرفت و این مشکل را رفع نمود. بعدها فیلم‌ها انتقال دهنده‌ی فرهنگ و پیوند دهنده‌ی مردم با فرهنگ‌های مختلف شدند ولی تفاوت در زبان باعث می‌شد که این امر به خوبی صورت نگیرد. به همین علت زیرنویس، وظیفه‌ی دیگری را هم پیدا کرد و آن رساندن پیام فیلم با زبان خود مخاطب بود. امروزه تهیه‌ی زیرنویس‌ها توسط بسیاری از افراد که با زبان انگلیسی (آشنایی با یک زبان میانی برای ترجمه زیرنویس) آشنایی دارند رواج پیدا کرده و روزانه نزدیک به صد زیرنویس یا گاها بیشتر با زبان‌های مختلف بر روی اینترنت قرار می‌گیرند. بزرگترین سایتی که در حال حاضر با شهرت جهانی در این زمینه فعالیت دارد سایت  subscene.com  است.

آشنایی با انواع زیرنویس‌ها
زیرنویس‌ها فرمت‌های مختلفی دارند مانند srt,sub idx,smi و ... ولی در حال حاضر معروف‌ترین و معتبرترین فرمت در بین همه‌ی فرمت‌ها Subrip  با پسوند SRT می‌باشد که قالب متنی به صورت زیر دارد:
203
00:16:38,731 --> 00:16:41,325
<i>Happy Christmas, your arse
I pray God it's our last</i>
که باعث میشود حجم بسیار کمی در حد چند کیلوبایت داشته باشد.

بررسی مشکل ما با زیرنویس در تلویزیون‌ها
یکی از مشکلاتی که ما در اجرای زیرنویس‌ها بر روی تلویزیون‌ها داریم این است که حروف فارسی را به خوبی نمی‌شناسند و در هنگام نمایش با مشکل مواجه می‌شوند که البته در اکثر مواقع با تبدیل زیرنویس از ANSI به Unicode یا UTF-8 مشکل حل می‌شود. ولی در بعضی مواقع تلویزیون یا پلیرها از پشتیبانی زبان فارسی سرباز می‌زنند و زیرنویس را به شکل زیر نمایش می‌دهند.
سلام = م ا ل س
به این جهت ما از یک برنامه به اسم srttouni استفاده می‌کنیم که با استفاده یک روش جایگزینی و معکوس سازی، مشکل ما را حل می‌کند. ولی باز هم این برنامه مشکلاتی دارد و از آنجا که برنامه نویس این برنامه که واقعا کمال تشکر را از ایشان، دارم مشخص نیست، مجبور شدم به جای گزارش، خودم این مشکلات را حل کنم. 
مشکلات این برنامه :
  • عدم حذف تگ‌ها ، گاها برنامه نویس‌ها از تگ هایی چون Bold,italic,underline,color استفاده می‌کنند که معدود برنامه‌هایی آن را پشتیبانی کرده و تلویزیون و پلیرها هم که اصلا پشتیبانی نمی‌کنند و باعث میشود که متن روی تلویزیون مثل کد html ظاهر شود
  • بعضی جملات دوبار روی صفحه ظاهر می‌شوند.
  • تنها یک فایل را در هر زمان تبدیل می‌کند. مثلا اگر یک سریال چند قسمته داشته باشید، برای هر قسمت باید زیرنویس را انتخاب کرده و تبدیل کنید، در صورتی که میتوان دستور داد تمام زیرنویس‌های داخل دایرکتوری را تبدیل کرد یا چند زیرنویس را برای این منظور انتخاب کرد.

نحوه‌ی خواندن زیرنویس با کدنویسی
با تشکر از دوست عزیز ما در این صفحه می‌توان گفت یک کد تقریبا خوب و جامعی را برای خواندن این قالب داریم. بار دیگر نگاهی به قالب یک دیالوگ در زیرنویس می‌اندازیم و آن را بررسی می‌کنیم:
203
00:16:38,731 --> 00:16:41,325
<i>Happy Christmas, your arse
I pray God it's our last</i>
اولین خط شامل شماره‌ی خط است که از یک آغاز می‌گردد تا به تعداد دیالوگ‌ها، خط دوم، زمان آغاز و پایان دیالوگ مورد نظر است، موقعی که دیالوگ روی صفحه ظاهر میشود تا موقعی که دیالوگ از روی صفحه محو شود که به ترتیب بر اساس ساعت:دقیقه:ثانیه و میلی ثانیه می‌باشد. خطوط بعدی هم متن دیالوگ است است و بعد از پایان متن دیالوگ یک خط خالی زیر آن قرار می‌گیرد تا نشان دهد این دیالوگ به پایان رسیده است. اگر همین خط خالی حذف گردد برنامه‌هایی چون Media player classic خطهای زیری را جز متن دیالوگ قبلی به حساب می‌آورند و شماره خط و زمان بندی دیالوگ بعدی به عنوان متن روی صفحه ظاهر می‌گردند و بعضی player‌ها هم قاطی کرده و کلا زیرنویس را نمی‌خوانند یا اون خط رو نشون نمیدن مثل Kmplayer و هر کدام رفتار خاص خودشان را بروز می‌دهند.
کد زیر در کلاس SubRipServices وظیفه‌ی خواندن محتوای فایل srt را بر اساس عبارتی که دادیم دارد:
private readonly static Regex regex_srt = new Regex(@"(?<sequence>\d+)\r\n(?<start>\d{2}\:\d{2}\:\d{2},\d{3}) --\> " +
            @"(?<end>\d{2}\:\d{2}\:\d{2},\d{3})\r\n(?<text>[\s\S]*?)\r\n\r\n", RegexOptions.Compiled);

 public string ToUnicode(string lines)
        {

        string subtitle= regex_srt.Replace(lines,delegate(Match m)
             {
                 string text = m.Groups["text"].Value;
                 //1.remove tags
                 text = CleanScriptTags(text);

                 //2.replace letters
                 PersianReshape reshaper = new PersianReshape();
                 text = reshaper.reshape(text);
                 string[] splitedlines = text.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);
                 text = "";
                 foreach (string line in splitedlines)
                 {
                     //3.reverse tags
                     text += ReverseText(reshaper.reshape(line))+Environment.NewLine ;
                 }
                 return
                     string.Format("{0}\r\n{1} --> {2}\r\n", m.Groups["sequence"], m.Groups["start"].Value,
                         m.Groups["end"]) + text + Environment.NewLine+Environment.NewLine ;
             }
            );

            return subtitle;
        }
در اولین خط ما یک Regular Expersion یا یک عبارت با قاعده تعریف کردیم که در اینجا میتوانید با خصوصیات آن آشنا شوید. ما برای این کلاس یک الگو ایجاد کردیم و بر حسب این الگو، متن یک زیرنویس را خواهد گشت و خطوطی را که با این تعریف جور در می‌آیند و معتبر هستند، برای ما باز می‌گرداند.
عبارتهایی که به صورت <name>? تعریف شده‌اند در واقع یک نامگذاری برای هر قسمت از الگوی ما هستند تا بعدا این امکان برای ما فراهم شود که خطوط برگشتی را تجزیه کنیم که مثلا فقط قسمت متن را دریافت کنیم، یا فقط قسمت زمان شروع یا پایان را دریافت کنیم و ...
متد tounicode یک آرگومان متنی دارد (lines) که شامل محتویات فایل  زیرنویس است. متد Replace در شی regex_srt با هر بار پیدا کردن یک متن بر اساس الگو در رشته lines دلیگیتی را فرا می‌خواند که در اولین پارامتر آن که از نوع matchEvaluator است، شامل اطلاعات متنی است که بر اساس الگو، یافت شده است. خروجی آن از نوع string می‌باشد که با متن پیدا شده بر اساس الگو جابجا خواهد کرد و در نهایت بعد از چندین بار اجرا شدن، کل متن‌های تعویض شده، به داخل متغیر subtitle ارسال خواهند شد.
کاری که ما در اینجا می‌کنیم این است که هر دیالوگ داخل زیرنویس را بر اساس الگو، یافته و متن آن را تغییر داده و متن جدید را جایگزین متن قبلی می‌کنیم. اگر زیرنویس ما 800 دیالوگ داشته باشد این دلیگیت 800 مرتبه اجرا خواهد شد.
از آنجا که ما تنها می‌خواهیم متن زیرنویس را تغییر دهیم، در اولین خط فرامین این دلیگیت تعریف شده، متن مورد نظر را بر اساس همان گروه‌هایی که تعریف کرده‌ایم دریافت می‌کنیم و در متغیر text قرار می‌دهیم:
m.Groups["text"].Value
در مرحله‌ی بعدی ما اولین مشکلمان (حذف تگ‌ها)  را با تابعی به اسم CleanScriptTags برطرف میکنیم که کد آن به شرح زیر است:
 private static readonly Regex regex_tags = new Regex("<.*?>", RegexOptions.Compiled);
 private  string CleanScriptTags(string html)
        {
            return regex_tags.Replace(html, string.Empty);
        }
کد بالا از یک regular Expression دیگر جهت پیدا کردن تگ‌ها استفاده می‌کند و به جای آن‌ها عبارت "" را جایگزین می‌کند. این کد قبلا در سایت جاری در این صفحه توضیح داده شده است. خروجی این تابع را مجددا در text قرار می‌دهیم و به مرحله‌ی دوم، یعنی تعویض کاراکترها می‌رویم:
 PersianReshape reshaper = new PersianReshape();
                 text = reshaper.reshape(text);
                 string[] splitedlines = text.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);
                 text = "";
                 foreach (string line in splitedlines)
                 {
                     //3.reverse tags
                     text += ReverseText(reshaper.reshape(line))+Environment.NewLine ;
                 }
برای اینکه دقیقا متوجه شویم قرار است چکاری انجام شود بیاید دو گروه یا بلوک مختلف در یونیکد را بررسی کنیم. هر بلوک کد در یونیکد شامل محدوده‌ای از کد پوینت هاست که نامی منحصرفرد برای خود دارد و هیچ کدام از کدپوینت‌ها در هر بلوک یا گروه، اشتراکی با بقیه‌ی بلوک‌ها ندارد. سایت codetable از آن دست سایت‌هایی است که اطلاعات خوبی در مورد کدهای یونیکد دارد. در قسمت Unicode Groups دو گروه برای زبان عربی وجود دارند که در جدول این گروه، هر سطر آن یکی از کدها را به صورت دسیمال، هگزا دسیمال و نام و نماد آن، نمایش می‌دهد.
^  ,   ^   Arabic Presentation Forms-A 
^^  Arabic Presentation Forms-B 
بلوک اول طبق گفته‌ی ویکی پدیا دسته‌ی متنوعی از حروف مورد نیاز برای زبان فارسی، اردو، پاکستانی و تعدادی از زبان‌های آسیای مرکزی است.
بلوک دوم شامل نمادها و نشانه‌های زبان عربی است و در حال حاضر برای کد کردن استفاده نمی‌شوند و دلیل حضور آن برای سازگاری با سیستم‌های قدیمی است.
اگر خوب به مشکلی که در بالا برای زیرنویس‌ها اشاره کردیم دقت کنید، گفتیم حروف از هم جدا نشان داده می‌شوند و اگر به بلوک دوم در لینک‌های داده شده نگاه کنید می‌بینید که حروف متصل را داراست. یعنی برای حرف س 4 حرف یا کدپوینت داراست : سـ برای کلماتی مثل سبد، ـس برای کلماتی مثل شانس، ـسـ برای کلماتی مثل بسیار، ولی خود س برای کلمات غیر متصل مثل ناس، البته بعضی حروف یک یا دو حالت می‌طلبند مثل د، ر که فقط دو حالت ـد و د ، ـر و ر را دارند یا مثل آ که یک حالت دارد.
من قبلا یک کلاس به نام lettersTable ایجاد کرده بودم (و دیگر نوشتن آن را ادامه ندادم) که برای هر حرف، یک آیتم در شی‌ءایی از نوع dictionary ساخته بودم و هر کدپوینت بلوک اول را در آن کلید و کد متقابلش را در بلوک دوم، به صورت مقدار ذخیره کرده بودم (گفتیم که هر نماد در بلوک اول، برابر با 4 نماد در بلوک دوم است؛ ولی ما در دیکشنری تنها مقدار اول را ذخیره می‌کنیم. زیرا کد بقیه نمادها دقیقا پشت سر یکدیگر قرار گرفته‌اند که می‌توان با یک جمع ساده از عدد 0 تا 3، به مقدار هر کدام از نمادها رسید. البته ناگفته نماند بعضی نمادها 2 عدد بودند که این هم باید بررسی شود). برای همین هر کاراکتر را با کاراکتر قبل و بعد می‌گرفتم و بررسی می‌کردم و از یک جدول دیکشنری دیگر هم به اسم specialchars هم استفاده کردم تا آن کاراکترهایی که تنها دو نماد یا یک نماد را دارند، بررسی کنم و این کاراکترها همان کاراکترهایی بودند که اگر قبل یک حرف هم بیایند، حرف بعدی به آن‌ها نمی‌چسبد. برای درک بهتر، این عبارت مثال زیر را  برای حرف س در نظر بگیرید:
مستطیل = چون بین هر دو طرف س حر وجود دارد قطعا باید شکل س به صورت ـسـ انتخاب شود ، حالا مثال زیر را در نظر بگیرید:
دست = دـست که اشتباه است و باید باشد دست یعنی شکل سـ باید صدا زده شود، پس این مورد هم باید لحاظ شود.
نمونه‌ای از کد این کلاس:
Dictionary<int ,int>  letters=new Dictionary<int, int>();

   //0=0x0 ,1=1x0 ,2=0x1 ,3=1x1
        private void FillPrimaryTable()
        {
            //آ
            letters.Add(1570, 65153);
            //ا
            letters.Add(1575, 65166);
            //أ
            letters.Add(1571, 65155);
            //ب
            letters.Add(1576, 65167);
            //ت
            letters.Add(1578, 65173);
            //ث
            letters.Add(1579, 65177);
            //ج
            letters.Add(1580, 65181);
.....
}

Dictionary<int,byte> specialchars=new Dictionary<int, byte>();

  private void SetSpecialChars()
        {
            //آ
            specialchars.Add(1570, 0);
            //ا
            specialchars.Add(1575, 0);
            //د2
            specialchars.Add(1583, 1);
            //ذ2
            specialchars.Add(1584, 1);
            //ر2
            specialchars.Add(1585, 1);
            //ز2
            specialchars.Add(1586, 1);
            //ژ
            specialchars.Add(1688, 1);
            //و2
            specialchars.Add(1608, 1);
            //أ
            specialchars.Add(1571, 1);

        }
کلاس بالا تنها برای ذخیره‌ی کدپوینت‌ها بود، ولی یک کلاس دیگر هم به اسم lettersCrawler نوشته بودم که متد آن وظیفه‌ی تبدیل را به عهده داشت.

در آن متد هر بار یک حرف را انتخاب می‌کرد و حرف قبلی و بعدی آن را ارسال می‌کرد تا تابع CalculateIncrease آن را محاسبه کرده و کاراکتر نهایی را باز گرداند و به متغیر finalText اضافه می‌کرد. ولی در حین نوشتن، زمانی را به یاد آوردم که اندروید به تازگی آمده بود و هنوز در آن زمان از زبان فارسی پشتیبانی نمی‌کرد و حروف برنامه‌هایی که می‌نوشتیم به صورت جدا از هم بود و همین مشکل را داشت که ما این مشکل را با استفاده از یک کلاس جاوا که دوست عزیزی آن را در اینجا به اشتراک گذاشته بود، حل می‌کردیم. پس به این صورت بود که از ادامه‌ی نوشتن کلاس انصراف دادم و از یک کلاس دقیق‌تر و آماده استفاده کردم.
در واقع این کلاس همین کار بالا را با روشی بهتر انجام می‌دهد. همه‌ی نمادها به طور دقیق‌تری کنترل می‌شوند حتی تنوین‌ها و دیگر علائم، همه نمادها با کدهای متناظر در یک آرایه ذخیره شده‌اند که ما در بالا از نوع Dictionary استفاده کرده بودیم.
تنها کاری که نیاز بود، باید این کد به سی شارپ تبدیل میشد و از آنجایی که این دو زبان خیلی شبیه به هم هستند، حدود ده دقیقه‌ای برای ویرایش کد وقت برد که می‌توانید کلاس نهایی را از اینجا دریافت کنید.
پس خط زیر در متد ToUnicode کار تبدیل اصلی را صورت می‌دهد:
  PersianReshape reshaper = new PersianReshape();
                 text = reshaper.reshape(text);
بنابراین مرحله‌ی دوم انجام شد. این تبدیل در بسیاری از سیستم‌ها همانند اندروید کافی است؛ ولی ما گفتیم که تلویزیون یا پلیر به غیر از جدا جدا نشان دادن حروف، آن‌ها را معکوس هم نشان می‌دهند. پس باید در مرحله‌ی بعد آن‌ها را معکوس کنیم که اینکار با خط زیر و صدا زدن تابع ReverseText انجام میگیرد
 //3.reverse tags
                 text = ReverseText(text);
از آنجا که یک دیالوگ ممکن است چند خطی باشد، این معکوس سازی برای ما دردسر می‌شد و ترتیب خطوط هم معکوس می‌شد. پس ما با استفاده از کد زیر هر یک خط را شکسته و هر کدام را جداگانه معکوس می‌کنیم و سپس به یکدیگر می‌چسبانیم:
string[] splitedlines = text.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);
                 text = "";
                 foreach (string line in splitedlines)
                 {
                     //3.reverse tags
                     text += ReverseText(reshaper.reshape(line))+Environment.NewLine ;
                 }
همه‌ی ما معکوس سازی یک رشته را بلدیم، یکی از روش‌ها این است که رشته را خانه به خانه از آخر به اول با یک for بخوانیم یا اینکه رشته را به آرایه‌ای از کارکاکترها، تبدیل کنیم و سپس با Array.Reverse آن را معکوس کرده و خانه به خانه به سمت جلو بخوانیم و خیلی از روش‌های دیگر. ولی این معکوس سازی‌ها برای ما یک عیب هم دارد و این هست که این معکوس سازی روی نمادهایی چون . یا ! و  غیره که در ابتدا و انتهای رشته آمده‌اند و حروف انگلیسی، نباید اتفاق بیفتند. پس می‌بینیم که تابع معکوس سازی هم باز باید ویژه‌تر باشد. ابتدا قسمت‌های ابتدا و انتها را جدا کرده و از آن حذف می‌کنیم. سپس رشته را معکوس می‌کنیم. ولی ممکن هست و احتمال دارد که بین حروف فارسی هم حروف انگلیسی یا اعداد به کار رود که آن‌ها هم معکوس می‌شوند. برای همین بعد از معکوس سازی یکبار هم باید آن‌ها را با یک عبارت با قاعده یافته و سپس هر کدام را جداگانه معکوس کرده و سپس مثل روش بالا Replace کنیم و رشته‌های جدا شده را به ابتدا و انتهای آن، سر جای قبلیشان می‌چسبانیم.
این دو تابع برای معکوس کردن عادی یک رشته به کار می‌روند:
    private string Reverse(string text)
        {
            return Reverse(text,0,text.Length);
        }

        private string Reverse(string text,int start,int end)
        {
            if (end < start)
                return text;
            string reverseText = "";

            for (int i = end-1; i >=start; i--)
            {
                reverseText += text[i];
            }
            return reverseText;
        }
ولی این تابع ReverseText جمعی از عملیات معکوس سازی ویژه‌ی ماست؛ مرحله اول، مرحله دریافت و ذخیره‌ی حروف خاص در ابتدای رشته به اسم پیشوند prefix است:
  private string ReverseText(string text)
        {
            char[] chararray = text.ToCharArray();
            string reverseText = "";
            bool prefixcomp = false;
            bool postfixcomp = false;
            string prefix = "";
            string postfix = "";

            #region get prefix symbols
            for (int i = 0; i < chararray.Length; i++)
            {
                if (!prefixcomp)
                {
                    char ch =(char) chararray.GetValue(i) ;
                    if (ch< 130)
                    {
                        prefix += chararray.GetValue(i);
                    }
                    else
                    {
                        prefixcomp = true;
                        break;
                    }
                }
            }
            #endregion
}
مرحله‌ی دوم هم دریافت و ذخیره‌ی حروف خاص در انتهای رشته به اسم پسوند postfix است که به این تابع اضافه می‌کنیم:
 #region get postfix symbols
            for (int i = chararray.Length - 1; i >-1 ; i--)
            {
                if (!postfixcomp && prefix.Length!=text.Length)
                {
                    char ch = (char)chararray.GetValue(i);
                    if (ch < 130)
                    {
                        postfix += chararray.GetValue(i);
                    }
                    else
                    {
                        postfixcomp = true;
                        break;
                    }
                }
            }
            #endregion
مرحله‌ی سوم عملیات معکوس سازی روی رشته است و سپس با استفاده از یک Regular Expression حروف انگلیسی و اعداد بین حروف فارسی را یافته و یک معکوس سازی هم روی آن‌ها انجام می‌دهیم تا به حالت اولشان برگردند. کل عملیات معکوس سازی در اینجا به پایان می‌رسد:
  #region reverse text

            reverseText = Reverse(text, prefix.Length, text.Length-postfix.Length);

        
            reverseText = unTagetdLettersRegex.Replace(reverseText, delegate(Match m)
            {
                return Reverse(m.Value);
            });
            #endregion
تعریف عبارت با قاعده‌ی بالا به اسم unTargetedLetters:
private static readonly Regex unTagetdLettersRegex = new Regex(@"[A-Za-z0-9]+", RegexOptions.Compiled);
آخر سر هم رشته را به‌علاوه پیشوند و پسوند جدا شده بر می‌گردانیم:
return prefix+ reverseText+postfix;
کد کامل تابع بدین شکل در می‌آید:
private static readonly Regex unTagetdLettersRegex = new Regex(@"[A-Za-z0-9]+", RegexOptions.Compiled);
private string ReverseText(string text)
        {
            char[] chararray = text.ToCharArray();
            string reverseText = "";
            bool prefixcomp = false;
            bool postfixcomp = false;
            string prefix = "";
            string postfix = "";

            #region get prefix symbols
            for (int i = 0; i < chararray.Length; i++)
            {
                if (!prefixcomp)
                {
                    char ch =(char) chararray.GetValue(i) ;
                    if (ch< 130)
                    {
                        prefix += chararray.GetValue(i);
                    }
                    else
                    {
                        prefixcomp = true;
                        break;
                    }
                }
            }
            #endregion

            #region get postfix symbols
            for (int i = chararray.Length - 1; i >-1 ; i--)
            {
                if (!postfixcomp && prefix.Length!=text.Length)
                {
                    char ch = (char)chararray.GetValue(i);
                    if (ch < 130)
                    {
                        postfix += chararray.GetValue(i);
                    }
                    else
                    {
                        postfixcomp = true;
                        break;
                    }
                }
            }
            #endregion

            #region reverse text

            reverseText = Reverse(text, prefix.Length, text.Length-postfix.Length);

        
            reverseText = unTagetdLettersRegex.Replace(reverseText, delegate(Match m)
            {
                return Reverse(m.Value);
            });
            #endregion

          

            return prefix+ reverseText+postfix;
        }
در نهایت، خط آخر دلیگت همه چیز را طبق فرمت یک دیالوگ srt چینش کرده و بر می‌گردانیم.
return
                     string.Format("{0}\r\n{1} --> {2}\r\n", m.Groups["sequence"], m.Groups["start"].Value,
                         m.Groups["end"]) + text + Environment.NewLine+Environment.NewLine ;
رشته subtitle را به صورت srt ذخیره کرده و انکودینگ را هم Unicode انتخاب کنید و تمام.

نمایی از برنامه‌ی نهایی


اجرای زیرنویس تبدیل شده روی کامپیوتر


روی پلیر یا تلویزیون



  نکته‌ی نهایی: هنگام تست زیرنویس روی فیلم متوجه شدم پلیر خطوط بلند را که در صفحه‌ی نمایش جا نمی‌شود، می‌شکند و به دو خط تقسیم می‌کند. ولی نکته‌ی خنده دار اینجا بود که خط اول را پایین می‌اندازد و خط دوم را بالا. برای همین این تکه کد را نوشتم و به طور جداگانه در گیت هاب هم قرار داده‌ام.
 
این تکه کد را هم بعد از
//1.remove tags
                 text = CleanScriptTags(text);
 به برنامه اضافه می‌کنیم:
  text =StringUtils.ConvertToMultiLine(text);
از این پس خطوط به طولی بین 30 کاراکتر تا چهل کاراکتر  شکسته خواهند شد و مشکل خطوط بلند هم نخواهیم داشت.
کد متد ConvertToMultiline:
namespace Utils
{
    public static class StringUtils
    {
        public static string ConvertToMultiLine(String text, int min = 30, int max = 40)
        {
            if (text.Trim() == "")
                return text;

            string[] words = text.Split(new string[] { " " }, StringSplitOptions.None);

            string text1 = "";
            string text2 = "";
            foreach (string w in words)
            {
                if (text1.Length < min)
                {
                    if (text1.Length == 0)
                    {
                        text1 = w;
                        continue;
                    }

                    if (w.Length + text1.Length <= max)
                        text1 += " " + w;
                }
                else
                    text2 += w + " ";

            }
            text1 = text1.Trim();
            text2 = text2.Trim();
            if (text2.Length > 0)
            {
                text1 += Environment.NewLine + ConvertToMultiLine(text2, min, max);
            }
            return text1;
        }
      
    }
}
آرگومان‌های min و max که به طور پیش فرض 30 و 40 هستند، سعی می‌کنند که هر خط را در نهایت به طور حدودی بین 30 تا 40 کاراکتر نگه دارند.
نکته پایانی : خوشحال میشم دوستان در این پروژه مشارکت داشته باشند و اگر جایی نیاز به اصلاح، بهبود یا ایجاد امکانی جدید دارد  کمک حال باشند و سعی کنند تا آنجا که می‌شود برنامه را روی net frame work 2. نگه دارند و بالاتر نبرند. چون استفاده کننده‌های این برنامه کاربران عادی و گاها با دانش پایین هستند و خیلی از آن‌ها هنوز از ویندوز xp استفاده می‌کنند تا در اجرای برنامه خیلی دچار مشکل نشده و راحت برای بسیاری از آن‌ها اجرا شود.

برنامه مورد نظر را به طور کامل می‌توانید از اینجا  یا اینجا به صورت فایل نهایی و هم سورس دریافت کنید. 
مطالب
تولید هدرهای Content Security Policy توسط ASP.NET Core برای برنامه‌های Angular
پیشتر مطلب «افزودن هدرهای Content Security Policy به برنامه‌های ASP.NET» را در این سایت مطالعه کرده‌اید. در اینجا قصد داریم معادل آن‌را برای ASP.NET Core تهیه کرده و همچنین نکات مرتبط با برنامه‌های Angular را نیز در آن لحاظ کنیم.


تهیه میان افزار افزودن هدرهای Content Security Policy

کدهای کامل این میان افزار را در ادامه مشاهده می‌کنید:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;

namespace AngularTemplateDrivenFormsLab.Utils
{
    public class ContentSecurityPolicyMiddleware
    {
        private readonly RequestDelegate _next;

        public ContentSecurityPolicyMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public Task Invoke(HttpContext context)
        {
            context.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN");
            context.Response.Headers.Add("X-Xss-Protection", "1; mode=block");
            context.Response.Headers.Add("X-Content-Type-Options", "nosniff");

            string[] csp =
            {
              "default-src 'self'",
              "style-src 'self' 'unsafe-inline'",
              "script-src 'self' 'unsafe-inline' 'unsafe-eval'",
              "font-src 'self'",
              "img-src 'self' data:",
              "connect-src 'self'",
              "media-src 'self'",
              "object-src 'self'",
              "report-uri /api/CspReport/Log" //TODO: Add api/CspReport/Log
            };
            context.Response.Headers.Add("Content-Security-Policy", string.Join("; ", csp));
            return _next(context);
        }
    }

    public static class ContentSecurityPolicyMiddlewareExtensions
    {
        /// <summary>
        /// Make sure you add this code BEFORE app.UseStaticFiles();,
        /// otherwise the headers will not be applied to your static files.
        /// </summary>
        public static IApplicationBuilder UseContentSecurityPolicy(this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<ContentSecurityPolicyMiddleware>();
        }
    }
}
که نحوه‌ی استفاده از آن در کلاس آغازین برنامه به صورت ذیل خواهد بود:
public void Configure(IApplicationBuilder app)
{
   app.UseContentSecurityPolicy();

توضیحات تکمیلی

افزودن X-Frame-Options
 context.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN");
از هدر X-FRAME-OPTIONS، جهت منع نمایش و رندر سایت جاری، در iframeهای سایت‌های دیگر استفاده می‌شود. ذکر مقدار SAMEORIGIN آن، به معنای مجاز تلقی کردن دومین جاری برنامه است.


افزودن X-Xss-Protection
 context.Response.Headers.Add("X-Xss-Protection", "1; mode=block");
تقریبا تمام مرورگرهای امروزی قابلیت تشخیص حملات XSS را توسط static analysis توکار خود دارند. این هدر، آنالیز اجباری XSS را فعال کرده و همچنین تنظیم حالت آن به block، نمایش و رندر قسمت مشکل‌دار را به طور کامل غیرفعال می‌کند.


افزودن X-Content-Type-Options
 context.Response.Headers.Add("X-Content-Type-Options", "nosniff");
وجود این هدر سبب می‌شود تا مرورگر، حدس‌زدن نوع فایل‌ها، درخواست‌ها و محتوا را کنار گذاشته و صرفا به content-type ارسالی توسط سرور اکتفا کند. به این ترتیب برای مثال امکان لینک کردن یک فایل غیرجاوا اسکریپتی و اجرای آن به صورت کدهای جاوا اسکریپت، چون توسط تگ script ذکر شده‌است، غیرفعال می‌شود. در غیراینصورت مرورگر هرچیزی را که توسط تگ script به صفحه لینک شده باشد، صرف نظر از content-type واقعی آن، اجرا خواهد کرد.


افزودن Content-Security-Policy
string[] csp =
            {
              "default-src 'self'",
              "style-src 'self' 'unsafe-inline'",
              "script-src 'self' 'unsafe-inline' 'unsafe-eval'",
              "font-src 'self'",
              "img-src 'self' data:",
              "connect-src 'self'",
              "media-src 'self'",
              "object-src 'self'",
              "report-uri /api/CspReport/Log" //TODO: Add api/CspReport/Log
            };
context.Response.Headers.Add("Content-Security-Policy", string.Join("; ", csp));
وجود این هدر، تزریق کدها و منابع را از دومین‌های دیگر غیرممکن می‌کند. برای مثال ذکر self در اینجا به معنای مجاز بودن الصاق و اجرای اسکریپت‌ها، شیوه‌نامه‌ها، تصاویر و اشیاء، صرفا از طریق دومین جاری برنامه است و هرگونه منبعی که از دومین‌های دیگر به برنامه تزریق شود، قابلیت اجرایی و یا نمایشی نخواهد داشت.

در اینجا ذکر unsafe-inline و unsafe-eval را مشاهده می‌کنید. برنامه‌های Angular به همراه شیوه‌نامه‌های inline و یا بکارگیری متد eval در مواردی خاص هستند. اگر این دو گزینه ذکر و فعال نشوند، در کنسول developer مرورگر، خطای بلاک شدن آن‌ها را مشاهده کرده و همچنین برنامه از کار خواهد افتاد.

یک نکته: با فعالسازی گزینه‌ی aot-- در حین ساخت برنامه، می‌توان unsafe-eval را نیز حذف کرد.


استفاده از فایل web.config برای تعریف SameSite Cookies

یکی از پیشنهادهای اخیر ارائه شده‌ی جهت مقابله‌ی با حملات CSRF و XSRF، قابلیتی است به نام  Same-Site Cookies. به این ترتیب مرورگر، کوکی سایت جاری را به همراه یک درخواست ارسال آن به سایت دیگر، پیوست نمی‌کند (کاری که هم اکنون با درخواست‌های Cross-Site صورت می‌گیرد). برای رفع این مشکل، با این پیشنهاد امنیتی جدید، تنها کافی است SameSite، به انتهای کوکی اضافه شود:
 Set-Cookie: sess=abc123; path=/; SameSite

نگارش‌های بعدی ASP.NET Core، ویژگی SameSite را نیز به عنوان CookieOptions لحاظ کرده‌اند. همچنین یک سری از کوکی‌های خودکار تولیدی توسط آن مانند کوکی‌های anti-forgery به صورت خودکار با این ویژگی تولید می‌شوند.
اما مدیریت این مورد برای اعمال سراسری آن، با کدنویسی میسر نیست (مگر اینکه مانند نگارش‌های بعدی ASP.NET Core پشتیبانی توکاری از آن صورت گیرد). به همین جهت می‌توان از ماژول URL rewrite مربوط به IIS برای افزودن ویژگی SameSite به تمام کوکی‌های تولید شده‌ی توسط سایت، کمک گرفت. برای این منظور تنها کافی است فایل web.config را ویرایش کرده و موارد ذیل را به آن اضافه کنید:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <rewrite>
      <outboundRules>
        <clear />
        <!-- https://scotthelme.co.uk/csrf-is-dead/ -->
        <rule name="Add SameSite" preCondition="No SameSite">
          <match serverVariable="RESPONSE_Set_Cookie" pattern=".*" negate="false" />
          <action type="Rewrite" value="{R:0}; SameSite=lax" />
          <conditions></conditions>
        </rule>
        <preConditions>
          <preCondition name="No SameSite">
            <add input="{RESPONSE_Set_Cookie}" pattern="." />
            <add input="{RESPONSE_Set_Cookie}" pattern="; SameSite=lax" negate="true" />
          </preCondition>
        </preConditions>
      </outboundRules>
    </rewrite>
  </system.webServer>
</configuration>


لاگ کردن منابع بلاک شده‌ی توسط مرورگر در سمت سرور

اگر به هدر Content-Security-Policy دقت کنید، گزینه‌ی آخر آن، ذکر اکشن متدی در سمت سرور است:
   "report-uri /api/CspReport/Log" //TODO: Add api/CspReport/Log
با تنظیم این مورد، می‌توان موارد بلاک شده را در سمت سرور لاگ کرد. اما این اطلاعات ارسالی به سمت سرور، فرمت خاصی را دارند:
{
  "csp-report": {
    "document-uri": "http://localhost:5000/untypedSha",
    "referrer": "",
    "violated-directive": "script-src",
    "effective-directive": "script-src",
    "original-policy": "default-src 'self'; style-src 'self'; script-src 'self'; font-src 'self'; img-src 'self' data:; connect-src 'self'; media-src 'self'; object-src 'self'; report-uri /api/Home/CspReport",
    "disposition": "enforce",
    "blocked-uri": "eval",
    "line-number": 21,
    "column-number": 8,
    "source-file": "http://localhost:5000/scripts.bundle.js",
    "status-code": 200,
    "script-sample": ""
  }
}
به همین جهت ابتدا نیاز است توسط JsonProperty کتابخانه‌ی JSON.NET، معادل این خواص را تولید کرد:
    class CspPost
    {
        [JsonProperty("csp-report")]
        public CspReport CspReport { get; set; }
    }

    class CspReport
    {
        [JsonProperty("document-uri")]
        public string DocumentUri { get; set; }

        [JsonProperty("referrer")]
        public string Referrer { get; set; }

        [JsonProperty("violated-directive")]
        public string ViolatedDirective { get; set; }

        [JsonProperty("effective-directive")]
        public string EffectiveDirective { get; set; }

        [JsonProperty("original-policy")]
        public string OriginalPolicy { get; set; }

        [JsonProperty("disposition")]
        public string Disposition { get; set; }

        [JsonProperty("blocked-uri")]
        public string BlockedUri { get; set; }

        [JsonProperty("line-number")]
        public int LineNumber { get; set; }

        [JsonProperty("column-number")]
        public int ColumnNumber { get; set; }

        [JsonProperty("source-file")]
        public string SourceFile { get; set; }

        [JsonProperty("status-code")]
        public string StatusCode { get; set; }

        [JsonProperty("script-sample")]
        public string ScriptSample { get; set; }
    }
اکنون می‌توان بدنه‌ی درخواست را استخراج و سپس به این شیء ویژه نگاشت کرد:
namespace AngularTemplateDrivenFormsLab.Controllers
{
    [Route("api/[controller]")]
    public class CspReportController : Controller
    {
        [HttpPost("[action]")]
        [IgnoreAntiforgeryToken]
        public async Task<IActionResult> Log()
        {
            CspPost cspPost;
            using (var bodyReader = new StreamReader(this.HttpContext.Request.Body))
            {
                var body = await bodyReader.ReadToEndAsync().ConfigureAwait(false);
                this.HttpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body));
                cspPost = JsonConvert.DeserializeObject<CspPost>(body);
            }

            //TODO: log cspPost

            return Ok();
        }
    }
}
در اینجا نحوه‌ی استخراج Request.Body را به صورت خام را مشاهده می‌کنید. سپس توسط متد DeserializeObject کتابخانه‌ی JSON.NET، این رشته به شیء CspPost نگاشت شده‌است.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
مطالب
استفاده از چند Routing در یک پروژه ASP.NET MVC بدون درد و خونریزی

کار کردن با مسیریابی برای یک پروژه ساده ، نیاز به طراحی پیچیده ندارد. مسیریابی پیش فرض موجود در فایل RoutConfig.cs برای کارهای ابتدایی کافیست. اما اگر کمی کار پیچیده شود و صفحات مختلفی با منطق‌های متفاوتی ایجاد کنیم، ممکن است با مشکل روبرو شویم. در MVC5 به کمک دخالت ویژگی‌ها در مسیریابی، کار ساده شده است اما در MVC4 و قبل از آن چه باید کرد؟ پیش از بسط مساله، ابتدا این سوال را پاسخ میدهیم که چگونه صفحه‌ی start پروژه انتخاب میشود؟ 

مسیریابی پیش فرض یک پروژه MVC به شکل زیر است : 

routes.MapRoute(
           name: "Default",
           url: "{controller}/{action}/{id}",
           defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
       );

وقتی یک پروژه MVC را بررسی کنید، مشاهده می‌کنید که در شاخه‌ی اصلی آن، فایل index یا default وجود ندارد و اصولا منطق کار با اکشن‌ها و صدا زدن آنها و دیدن پاسخ‌ها توسط View، این اتفاق را توجیه میکند. پس وقتی درخواستی به سمت شاخه‌ی اصلی ما فرستاده میشود، مسیریابی وارد عمل می‌شود. وقتی پروژه را اجرا میکنید، متد RegisterRoutes از شیءایی که از کلاس RoutingConfig.cs ساخته شده (به فایل global.asax نگاه کنید) فراخوانی می‌شود و شیء Routes از " قالب آدرس " هایی که ما تعیین کرده ایم پُر می‌شود. بخشی از فایل RoutConfig.cs را در تصویر زیر می‌بینیم. 

به Url کد فوق نگاه کنید که در حقیقت آدرسی نسبی است. آدرس ما به طور کامل به شکل زیر قابل تعریف است: 

http://mydomain.com/{controller}/{action}/{id}

واضح است که درخواست اولیه به سایت ما دارای بخش‌های controller و action و بخش اختیاری id نیست. پس به شکل پیشفرض بر اساس آنچه در جلوی خصوصیت defaults نوشته شده است، فراخوانی خواهد شد. یعنی اکشن Index از داخل کنترلر Home صدا زده می‌شود و چون id نداریم، هیچ نوع id به متد (اکشن) index ارسال نخواهد شد.

یک روتینگ دیگر به شکل زیر در بالای کد فوق اضافه کنید : 

routes.MapRoute(
          name: "Default1",
          url: "{controller}/{action}/{id}",
          defaults: new { controller = "News", action = "Index", id = UrlParameter.Optional }
      );

حتما متوجه شدید که اینبار با اجرای پروژه، متد(اکشن) Index از کنترلر News فراخوانی خواهد شد. در حقیقت اولین روتینگ ساخته شده همان چیزیست که Asp.NET MVC پس از دریافت ریکوئست به آن رجوع میکند. دقت کنید که مقدار name در دو MapRouting فوق متفاوت است. 

اما این اسم‌ها(name ها) به چه دردی می‌خورند؟

 قبل از توضیح در این مورد، default1 را از پروژه حذف میکنیم و با همان MapRouting پیش فرض به کار خود ادامه می‌دهیم. ابتدا مروری بر عملکرد MapRouting انجام میدهیم. 

همانطور که می‌دانید برای ایجاد یک لینک از طریق ویوی Razor از چیزی شبیه دستور زیر می‌توانیم استفاده کنیم: 

<a href="@Url.Action("post", "News", new { id = @item.PostName }, null)"> @item.PostTitle </a>

فرض کنید یک سایت خبری داریم و گروه‌های مختلف آن شامل مطالب متفاوتی هستند. کنترلری به نام News ایجاد کرده‌ایم، که دارای یک اکشن به نام post است که نام مطلب را دریافت کرده و یک View به ما برمی‌گرداند. بخش هایی از صفحه اول را در تصاویر زیر می‌بینید. تصویر آخر، بخش پایین صفحه اخبار است، که بوسیله لینک (لینک های) موجود صفحات جابجا میشوند. 

وقتی کد فوق در یک صفحه فراخوانی میشود، کالکشن Routes بررسی می‌شود و لینک مورد نظر، با توجه به ورودی‌های Url.Action ساخته می‌شود. ما یک MapRouting به نام Default در کالکشن Routes داریم، پس Url.Action لینک زیر را می‌سازد: 

http://mydomain.com/news/post/نام-مقاله

استفاده از MapRouting مثل یک جعبه است که ورودی آن مقادیر موجود در Url.Action می‌باشد و آنچه به ما بر می‌گرداند یک آدرس برای فراخوانی اکشن مورد نیاز است. (آخرین پارامتر، null قرار داده شده که برای تعیین نوع پروتکل استفاده میشود که http یا https میتواند باشد.)

برای فراخوانی صفحه‌ی اخبار (لیست اخبار) می‌توانید دستور زیر را بنویسید : 

@Html.ActionLink("اخبار", "Index", "News", null, new { @class="btn btn-success"})

این دستور تگ anchor را نیز می‌سازد و به کمک MapRouting  آدرسی شبیه آدرس زیر را به ما بر می‌گرداند. (به تفاوت دو دستور دقت کنید): 

<a href="/News">اخبار</a>

واضح است که چون MapRouting یک حالت پیش فرض درونی دارد که دارای اکشن دیفالت index است، پس ActionLink اگر ببیند لینکش در صفحه قرار است به شکل /news/index تعریف شود خود بخود بخش index را حذف میکند.

فرض کنید بعد از کلیک روی لینک فوق، اکشن index ، یک آبجکتِ لیستی با 10 خبر آخر، ایجاد میکند. پس ما میخواهیم قابلیت صفحه بندی را برای لیست اخبار فعال کنیم و در هر صفحه 10 مطلب را نمایش دهیم. مشکل از همینجا آغاز میشود. MapRouting فعلی جوابگوی ما نخواهد بود و آدرس را به شکل زیر نمایش میدهد. 

<a href="/News/index?pid=2">صفحه بعد</a>

و آنچه ما در View نوشته‌ایم چیزی شبیه کد زیر است :

@Html.ActionLink("صفحه بعد", "index", "News", new { pid = …. }, null)

مشکلی در ارجاع به صفحات وجود ندارد و با کلیک روی لینک "صفحه بعد" مقدار عدد 2 به اکشن index ارسال میشود و اگر کد نویسی را برای take و skip کردن لیست، درست انجام شده باشد، نتیجه مورد نظر نمایش داده خواهد شد. اما آدرس فوق آدرس زیبایی نیست. اولین فکری که به ذهن برنامه نویس میرسد، ایجاد یک مسیریابی دیگر است. فکر درستیست؛ اما اگر چند بار دیگر این اتفاق بیفتد و در بخش هایی از برنامه نیاز به روتینگ پیدا کنید و روتینگ‌های جدید ایجاد کنید متوجه خواهید شد که مدیریت این MapRouting ‌ها کار خسته کننده و طاقت فرسایی خواهد شد، مخصوصا اگر بدانید که فقط مجاز به استفاده از یک  پارامتر optional در هر MapRouting هستید! دست شما کاملا بسته است. لینک‌های بالای سایت را اصلاح میکنید ولی لینک‌های پایین سایت خراب میشوند و بالعکس.

به هر حال MapRouting زیر را به RoutConfig.cs اضافه میکنیم : 

           routes.MapRoute("PostPaging", "{controller}/{action}/{id}/{pid}",
            defaults: new
            {
                controller = "News",
                action = "Index",
                id = "all",
                pid = UrlParameter.Optional

            }
            );
-  اسم این MapRouting ، دیگر Default نیست.
-  یک پارامتر pid اضافه‌تر از MapRouting اولی دارد.
-   pid به عنوان  یک پارامتر اختیاری تعریف شده است، پس "قالب آدرس" بسیار شبیه مپ روتینگ قبلی است.
-  مقدار id اختیاری نیست، چون قرار است در آینده بتوانیم گروه‌های مختلف موجود در بخش اخبار را صفحه بندی کنیم و قرار نیست پشت سر هم MapRouting ایجاد کنیم و کافیست به جای id اسم گروه را بنویسیم. در حالتیکه اسمی از گروه درلینکهایمان نبرده باشیم به شکل پیشفرض all قرار داده میشود که یعنی کل اخبار مد نظر است. (در اکشن مربوطه باید این تصمیمات را لحاظ کنیم)
-  حتما این MapRouting را بعد از MapRouting اولیه بنویسید، کمی پیشتر، علت این امر توضیح داده شد و گفته شد اولین چیزی که MVC پس از درخواست ما میبیند به عنوان Routing بررسی می‌کند (درخواست اولیه) و چون ساختار  MapRouting فوق تا اندازه ای شبیه ساختار Default MapRouting است ممکن است با فراخوانی سایت مشکل ایجاد شود.
- میتوانید MapRouting
را کمی خاص‌تر هم بنویسیم : 
routes.MapRoute("NewsPaging", "News/index/{id}/{pid}",
            defaults: new
            {
                controller = "News",
                action = "Index",
                id = "all",
                pid = UrlParameter.Optional
            }
            );
اینکار بستگی به پروژه‌ی شما دارد، مپ روتینگ فوق این مزیت را دارد که با مپ روتینگ‌های دیگر به سختی قاطی میشود! یعنی حتا اگر قبل از مپ روتینگ دیفالت نوشته شود برنامه با مشکل مواجه نخواهد شد، چون اصلا شکل درخواست اولیه به سایت، چیزی شبیه این آدرس نیست. اما خاص بودن آن و همچنین نوع بهره گیری از آن با کمک Action یا ActioLink شاید شما را سردرگم خواهد کند.  
اما مشکل این MapRouting ‌ها چیست؟
درخواست به سایت آمده و قرار است سایت بارگذاری شود؛ ترتیب زیر در شیء routes ثبت شده است :  

در صفحه اول ما لینکی به شکل زیر گذاشته ایم : 
@Html.ActionLink("اخبار", "Index", "News", null, new { @class="btn btn-success"})
مشکلی پیش نخواهد آمد. روند فوق تکرار میشود و ActionLink برای ساختن لینک نهایی از MapRouting اول استفاده میکند (بدون اینکه به MapRouting دوم نگاه کند).

در صفحه اخبار، لینک "صفحه بعد" وجود دارد ، آیا این لینک به شکل صحیح نمایش داده میشود؟ خیر! نتیجه کار را ببینید : 
<a href="/News/index?pid=2">صفحه بعد</a>
 علت واضح است، ActionLink اصلا MapRouting دومی را نمی‌بیند و با همان MapRouting اولی لینک فوق را ساخته است. جای MapRouting ‌ها را عوض کنیم؟ مطمئنید با اینکار درخواست مربوط به صفحه اول شما سلامت به مقصد میرسد؟ مطمئنید با اینکار بقیه Action ‌ها و ActionLink ‌های موجود در صفحه اول شما به درستی تبدیل میشوند؟
به نظر میرسد یک طرح دقیق برای آدرس دهی شاید این مسائل را حل کند ولی نه وقت داریم و نه اعصاب. 


دقت کنید ما برای کار با روتینگ با دو مساله مواجه هستیم :
1. ساخته شدن لینک‌ها توسط هلپر‌ها (که از مپ روتینگ‌های ثبت شده ما پیروی میکند)
2. واکنش پروژه به درخواست‌های دریافتی و هدایت آن به اکشن‌های مورد نظر که کاملا به ترتیب ثبت مپ روتینگ‌ها در کالکشن Routes بستگی دارد .


 خوشبختانه یک هلپر به شکل زیر در MVC وجود دارد : 
 @Html.RouteLink("صفحه بعد", "PostPaging", new { action = "cat", controller = "News", id =. .., pid = ...})
مقدار اول متن لینک، مقدار دوم نام MapRouting ،مقدار سوم یک آبجکت است که نوع اکشن و کنترلر و پارامترهای مورد نظر MapRouting را تعیین میکند. به همین سادگی! این پاسخ به همان سوالیست که در ابتدای مقاله مطرح شد. "نام گذاری مپ روتینگ‌ها به چه درد میخورد؟"
نتیجه برای لینک موجود در صفحه اخبار چیزی شبیه شکل زیر خواهد شد :
<a href="/News/cat/all/2">صفحه بعد</a>
چند شکل دیگر از این هلپر را ببینید : 
@Html.RouteLink("اخبار", "Default", new { controller = "news" })

@Html.RouteLink("درباره ما", "pages", new RouteValueDictionary(new { controller="Page", action="Index", pagename="درباره-ما"}), new Dictionary<string, Object> { { "data-toggle", "popover" }, { "data-placement", "top" } })

همانطور که میبینید در RoutLink اولی، اخبار را به کمک MapRouting با نام default بازنویسی میکنیم و نتیجه چیزی شبیه کد زیر خواهد شد : 

<a href="/News">اخبار</a>

در RoutLink دومی اولا از یک RoutValueDictionary به جای یک آبجکت ساده استفاده کرده ایم و مقادیر را به شکل فوق به کنترلر و اکشن و ...نسبت داده ایم ثانیا برای بخش HTML نیز پراپرتی‌ها را به کمک یک دیکشنری ارسال میکنیم، به خاطر وجود "-" در یکی از خواص، راه دیگری غیر از اینکار نداریم.

اما دقت کنید که از یک MapRouting جدید استفاده کردیم که نامش pages است، 

 این MapRoutnig را قبل از دیگر Routing ‌ها می‌نویسیم؟ وسط دو MapRouting قبلی مینویسیم؟ آخر MapRouting ‌ها مینویسیم؟ آیا فرقی میکند؟ اگر سریع بگوییم خــیر! اشتباه کرده ایم. واقعا فرق میکند.

دقت کنید موضوع MapRouting فقط ایجاد یک لینک‌تر و تمیز نیست؛ RoutLink یک لینک تمیز بر اساس مپ روتینگی که نامش برده شده ایجاد میکند اما تضمین نمیکند که با کلیک بر روی لینک به هدف برسیم و به خطای 404 برخورد نکنیم! اگر روی لینک کلیک کنید آدرس شروع به تفسیر شدن میکند و این تفسیر اصلا ربطی به نامی که به RoutLink داده ایم ندارد و ترتیب موجود در کالکشن ایجاد شده در RoutConfig تعیین کننده است.(آبجکت Routes ) اگر MapRouting فوق را در انتهای بقیه بگذاریم صفحه اول لود میشود ولی با کلیک روی "درباره ما" صفحه پیغام خطا خواهد داد.
 باید به یاد داشته باشیم برای اجرای درخواست (کلیک روی لینک)، آنچه برای ASP.NET MVC اهمیت دارد، ترتیب قرار گیری MapRouting ‌ها در RouteRegister است  و ما به کمک RoutLink تنها مشکل ساخت لینک‌ها بر اساس قالب MapRouting مورد نظرمان را حل کردیم و این به ما تضمینی برای هدایت آن لینک به مکان درست را نخواهد داد. 

اگر ترتیب به شکل زیر باشد :

1
2
3
باشد. درخواست اولیه برای بالا آمدن سایت به مشکل برخورد نمی‌کند چون همان مپ روتینگ 1 اجرا میشود. اما مشکل فوق به وجود خواهد آمد و خطای 404 با کلیک بر روی "درباره ما" نمایش داده خواهد شد چون با کلیک روی "درباره ما" مپ روتینگ شماره 1 وارد عمل میشود.

اگر ترتیب به شکل زیر باشد :

3
2
1
آیا اصلا صفحه اول سالم لود خواهد شد؟ خیر! درخواست نسبی " / " (یا به طور کامل http://mydomain.com ) شماره 3 را به خیر پشت سر میگذارد، چون اصلا چیزی به نام page در آدرس وجود ندارد که از این MapRouting بخواهد پیروی کند. اما در شماره 2 گیر می‌افتد چون این فرمت را حفظ کرده است :
"{controller}/{action}/{id}/{pid}"
Pid که اختیاریست (بر اساس قوانین تعریف شده در مپ روتینگ شماره 2) بقیه موارد نیز حالت دیفالت دارند، یعنی اگر تعریف نشده باشند (مثل همین درخواست) خودبخود جایگذاری میشوند. پس به طور کلی صفحه اول ما تغییر می‌کند و اکشن index از کنترلر Home اجرا نمیشود و به جایش اکشن Index از کنترلر News اجرا میشود و صفحه اول را از دست میدهیم و صفحه اخبار را میبینیم! نتیجه اینکه نباید هرگز 2 را قبل از 1 قرار دهیم.
اگر ترتیب به شکل زیر باشد :
3
1
2
این همان چیزیست که مد نظر ماست. اولا 1 قبل از 2 است و صفحه اول برای لود شدن به مشکل برخورد نمیکند.
ثانیا وقتی روی "درباره ما" کلیک میکنیم همان شماره 3 فراخوانی میشود و بقیه مپ روتینگ‌ها اعمال نمیشوند.

تنظیم این مقاله با هدف آموزش صورت گرفته است.  حتما با تفکر و به خاطر سپردن نکات با طرح ریزی ساختار صفحات به کمک کمترین تعداد MapRouting  به بهترین نتایج خواهیم رسید. 

مطالب
کار با Docker بر روی ویندوز - قسمت سوم - نصب Docker بر روی ویندوز سرور
در قسمت قبل، Docker for Windows را بر روی ویندوز 10 نصب کردیم تا بتوانیم از هر دوی Linux Containers و Windows Containers استفاده کنیم. در این قسمت، نحوه‌ی نصب Docker را بر روی ویندوز سرور، صرفا جهت اجرای Windows Containers، بررسی می‌کنیم؛ از این جهت که در دنیای واقعی، عموما Linux Containers را بر روی سرورهای لینوکسی و Windows Containers را بر روی سرورهای ویندوزی اجرا می‌کنند.


Docker for Windows چگونه از هر دوی کانتینرهای ویندوزی و لینوکسی پشتیبانی می‌کند؟

زمانیکه docker for windows را اجرا می‌کنیم، سرویسی را ایجاد می‌کند که سبب اجرای پروسه‌ی ویژه‌ای به نام com.docker.proxy.exe می‌شود:


هنگامیکه برای مثال فرمان docker run nginx را توسط Docker CLI اجرا می‌کنیم، Docker CLI از طریق واسط یاد شده، دستورات را به MobyLinuxVM منتقل می‌کند. به این صورت است که امکان اجرای Linux Containers، بر روی ویندوز میسر می‌شوند:


اکنون اگر به Windows Containers سوئیچ کنیم (از طریق کلیک راست بر روی آیکن Docker در قسمت Tray Icons ویندوز)، پروسه‌ی dockerd.exe یا docker daemon شروع به کار خواهد کرد:


اینبار اگر مجددا از Docker CLI برای اجرای مثلا IIS Container استفاده کنیم، دستور ما از طریق واسط‌های com.docker.proxy و dockerd‌، به کانتینر ویندوزی منتقل و اجرا می‌شود:



نگاهی به معماری Docker بر روی ویندوز سرور

داکر بر روی ویندوز سرور، تنها به همراه موتور مدیریت کننده‌ی Windows Containers است:


در اینجا با صدور فرمان‌های Docker CLI، پیام‌ها مستقیما به dockerd یا موتور داکر بر روی ویندوز سرور ارسال شده و سپس کار اجرا و مدیریت یک Windows Container انجام می‌شود.


نصب Docker بر روی ویندوز سرور

جزئیات مفصل و به روز Windows Containers را همواره می‌توانید در این آدرس در سایت مستندات مجازی سازی مایکروسافت مطالعه کنید (قسمت Container Host Deployment - Windows Server آن). پیشنیاز کار با آن نیز نصب حداقل ویندوز سرور 2016 می‌باشد و بهتر است تمام به روز رسانی‌های آن‌را نیز نصب کرده باشید؛ چون تعدادی از بهبودهای کار با کانتینرهای آن، به همراه به روز رسانی‌ها آن ارائه شده‌اند.
برای شروع به نصب، نیاز است کنسول PowerShell ویندوز را با دسترسی Admin اجرا کنید.
سپس اولین دستوراتی را که نیاز است اجرا کنید، کار نصب موتور Docker و CLI آن‌را به صورت خودکار بر روی ویندوز سرور انجام می‌دهند:
Install-Module -Name DockerMsftProvider -Repository PSGallery -Force
Install-Package -Name docker -ProviderName DockerMsftProvider
Restart-Computer -Force
- که پس از نصب و ری‌استارت سیستم، نتیجه‌ی آن‌را در پوشه‌ی c:\Program Files\Docker می‌توانید ملاحظه کنید.
- به علاوه اگر دستور *get-service *docker را در کنسول PowerShell صادر کنید، مشاهده خواهید کرد که سرویس جدیدی را به نام Docker نیز نصب و راه اندازی کرده‌است که به dockerd.exe اشاره می‌کند.
- و یا اگر در کنسول PowerShell دستور docker را صادر کنید، ملاحظه خواهید کرد که CLI آن، فعال و قابل استفاده‌است. برای مثال، دستور docker version را صادر کنید تا بتوانید نگارش docker نصب شده را ملاحظه نمائید.


اجرای Image مخصوص NET Core. بر روی ویندوز سرور

تگ‌های مختلف Image مخصوص NET Core. را در اینجا ملاحظه می‌کنید. در ادامه قصد داریم tag مرتبط با nanoserver آن‌را نصب کنیم (با حجم 802MB):
docker run microsoft/dotnet:nanoserver
زمانیکه این دستور را اجرا می‌کنیم، پس از اجرای آن، ابتدا یک \:C را نمایش می‌دهد و بعد خاتمه یافته و به command prompt بازگشت داده می‌شویم. برای مشاهده‌ی علت آن، اگر دستور docker ps -a را اجرا کنیم، در ستون command آن، قسمتی از دستوری را که اجرا کرده‌است، می‌توان مشاهده کرد. برای مشاهده‌ی کامل این دستور، نیاز است دستور docker ps -a --no-trunc را اجرا کنیم. در اینجا سوئیچ no-trunc به معنای no truncate است یا عدم حذف قسمت انتهایی یک دستور طولانی. در این حالت مشاهده خواهیم کرد که این دستور، کار اجرای cmd.exe واقع در پوشه‌ی ویندوز را انجام می‌دهد (یا همان command prompt معمولی ویندوز). چون دستور docker run فوق به آن متصل نشده‌است، این پروسه ابتدا \:c را نمایش می‌دهد و سپس خاتمه پیدا می‌کند. برای رفع این مشکل، از interactive command که در قسمت قبل توضیح دادیم، استفاده خواهیم کرد:
docker run -it microsoft/dotnet:nanoserver
اینبار اگر این دستور را اجرا کنیم، به command prompt آغاز شده‌ی توسط آن، متصل خواهیم شد. اکنون اگر در همینجا (داخل container در حال اجرا) دستور dotnet --info را صادر کنید، می‌توان مشخصات NET Core SDK. نصب شده را مشاهده کرد. برای خروج از آن نیز دستور exit را صادر کنید.


چرا حجم Image مخصوص NET Core. نگارش nanoserver آن حدود 800 مگابایت است؟

در مثال قبلی، دسترسی به command prompt مجزایی نسبت به command prompt اصلی سیستم، در داخل یک container، شاید اندکی غیر منتظره بود و اکنون این سؤال مطرح می‌شود که یک image، شامل چه چیزهایی است؟
یک image شاید در ابتدای کار صرفا شامل فایل‌های اجرایی یک برنامه‌ی خاص به نظر برسد؛ اما زمانیکه قرار است تبدیل به یک container قابل اجرا شود، شامل بسیاری از فایل‌های دیگر نیز خواهد شد. برای درک این موضوع نیاز است لایه‌های نرم افزاری که یک سیستم را تشکیل می‌دهند، بررسی کنیم:


در این تصویر از پایین‌ترین لایه‌ای را که با سخت افزار ارتباط برقرار می‌کند تا بالاترین لایه‌ی موجود نرم افزاری را مشاهده می‌کنید. دراینجا هر چیزی را که در ناحیه‌ی کرنل قرار نمی‌گیرد، User Space می‌نامند. برنامه‌های قرار گرفته‌ی در User Space برای کار با سخت افزار نیاز است با کرنل ارتباط برقرار کنند و برای این منظور از System Calls استفاده می‌کنند که عموما کتابخانه‌هایی هستند که جزئی از سیستم عامل می‌باشند؛ مانند API ویندوز. برای مثال MongoDB توسط Win32 API و System Calls، فرامینی را به کرنل منتقل می‌کند.
در روش متداول توزیع و نصب نرم افزار، ما عموما همان بالاترین سطح را توزیع و نصب می‌کنیم؛ برای مثال خود MongoDB را. در اینجا نصاب MongoDB فرض می‌کند که در سیستم جاری، تمام لایه‌های دیگر، موجود و آماده‌ی استفاده هستند و اگر اینگونه نباشد، به مشکل برخواهد خورد و اجرا نمی‌شود. برای اجتناب از یک چنین مشکلاتی مانند عدم حضور وابستگی‌هایی که یک برنامه برای اجرا نیاز دارد، imageهای docker، نحوه‌ی توزیع نرم افزارها را تغییر داده‌اند. اینبار یک image بجای توزیع فقط MongoDB، شامل تمام قسمت‌های مورد نیاز User Space نیز هست:


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


به همین جهت بود که برای مثال در قسمت قبل موفق شدیم IIS مخصوص ویندوز سرور با تگ nanoserver را بر روی ویندوز 10 که بسیاری از وابستگی‌های مرتبط را به همراه ندارد، با موفقیت اجرا کنیم.
به علاوه چون یک container صرفا به معنای یک running process از یک image است، هر فایل اجرایی داخل آن image را نیز می‌توان به صورت یک container اجرا کرد؛ مانند cmd.exe داخل image مرتبط با NET Core. که آن‌را بررسی کردیم.


کارآیی Docker Containers نسبت به ماشین‌های مجازی بسیار بیشتر است

مزیت دیگر یک چنین توزیعی این است که اگر چندین container در حال اجرا را داشته باشیم:


 در نهایت تمام آن‌ها فقط با یک لایه‌ی کرنل کار می‌کنند و آن هم کرنل اصلی سیستم جاری است. به همین جهت کارآیی docker containers نسبت به ماشین‌های مجازی بیشتر است؛ چون هر ماشین مجازی، کرنل مجازی خاص خودش را نسبت به یک ماشین مجازی در حال اجرای دیگر دارد. در اینجا برای ایجاد یک لایه ایزوله‌ی اجرای برنامه‌ها، تنها کافی است یک container جدید را اجرا کنیم و در این حالت وارد فاز بوت شدن یک سیستم عامل کامل، مانند ماشین‌های مجازی نمی‌شویم.

شاید مطابق تصویر فوق اینطور به نظر برسد که هرچند تمام این containers از یک کرنل استفاده می‌کنند، اما اگر قرار باشد هر کدام OS Apps & Libs خاص خودشان را در حافظه بارگذاری کنند، با کمبود شدید منابع روبرو شویم. دقیقا مانند حالتیکه چند ماشین مجازی را اجرا کرده‌ایم و دیگر سیستم اصلی قادر به پاسخگویی به درخواست‌های رسیده به علت کمبود منابع نیست. اما در واقعیت، یک image داکر، از لایه‌های مختلفی تشکیل می‌شود که فقط خواندنی هستند و غیرقابل تغییر و زمانیکه docker یک لایه‌ی فقط خواندنی را در حافظه بارگذاری کرد، اگر container دیگری، از همان لایه‌ی تعریف شده‌، در image خود نیز استفاده می‌کند، لایه‌ی بارگذاری شده‌ی فقط خواندنی در حال اجرای موجود را با آن به اشتراک می‌گذارد (مانند تصویر زیر). به این ترتیب میزان مصرف منابع docker containers نسبت به ماشین‌های مجازی بسیار کمتر است:



روش کنترل پروسه‌ای که درون یک کانتینر اجرا می‌شود

با اجرای دستور docker run -it microsoft/dotnet:nanoserver ابتدا به command prompt داخلی و مخصوص این container منتقل می‌شویم و سپس می‌توان برای مثال با NET Core CLI. کار کرد. اما امکان اجرای این CLI به صورت زیر نیز وجود دارد:
docker run -it microsoft/dotnet:nanoserver dotnet --info
این دستور، مشخصات SDK نصب شده را نمایش می‌دهد و سپس مجددا به command prompt سیستم اصلی (که به آن میزبان، host و یا container host نیز گفته می‌شود) بازگشت داده خواهیم شد؛ چون کار NET Core CLI. خاتمه یافته‌است، پروسه‌ی متعلق به آن نیز خاتمه می‌یابد.
بدیهی است در این حالت تمام فایل‌های اجرایی داخل این container را نیز می‌توان اجرا کرد. برای مثال می‌توان کنسول پاورشل داخل این container را اجرا کرد:
docker run -it microsoft/dotnet:nanoserver powershell
زمانیکه به این کنسول دسترسی پیدا کردید، برای مثال دستور get-process را اجرا کنید. به این ترتیب می‌توانید لیست تمام پروسه‌هایی ر که هم اکنون داخل این container در حال اجرا هستند، مشاهده کنید.


هر کانتینر دارای یک File System ایزوله‌ی خاص خود است

تا اینجا دریافتیم که هر image، به همراه فایل‌های user space مورد نیاز خود نیز می‌باشد. به عبارتی هر image یک file system را نیز ارائه می‌دهد که تنها درون همان container قابل دسترسی می‌باشد و از مابقی سیستم جاری ایزوله شده‌است.
برای آزمایش آن، کنسول پاورشل را در سیستم میزبان (سیستم عامل اصلی که docker را اجرا می‌کند)، باز کرده و دستور \:ls c را صادر کنید. به این ترتیب می‌توانید لیست پوشه‌ها و فایل‌های موجود در درایو C میزبان را مشاهده نمائید. سپس دستور docker run -it microsoft/dotnet:nanoserver powershell را اجرا کنید تا به powershell داخل کانتینر NET Core. دسترسی پیدا کنیم. اکنون دستور \:ls c را مجددا اجرا کنید. خروجی آن کاملا متفاوت است نسبت به گزارشی که پیشتر بر روی سیستم میزبان تهیه کردیم؛ دقیقا مانند اینکه هارد درایو یک container متفاوت است با هارد درایو سیستم میزبان.


این تصویر زمانی تهیه شده‌است که دستور docker run یاد شده را صادر کرده‌ایم و درون powershell آن قرار داریم. همانطور که مشاهده می‌کنید یک Disk جدید، به ازای این Container در حال اجرا، به سیستم میزبان اضافه شده‌است. این Disk زمانیکه در powershell داخل container، دستور exit را صادر کنیم، بلافاصله محو می‌شود. چون پروسه‌ی container، به این ترتیب خاتمه یافته‌است.
اگر دستور docker run یاد شده را دو بار اجرا کنیم، دو Disk جدید ظاهر خواهند شد:


یک نکته: اگر بر روی این درایوهای مجازی کلیک راست کرده، گزینه‌ی change drive letter or path را انتخاب نموده و یک drive letter را به آن‌ها نسبت دهید، می‌توانید محتویات داخل آن‌ها را توسط Windows Explorer ویندوز میزبان نیز به صورت یک درایو جدید، مشاهده کنید.


خلاصه‌ای از ایزوله سازی‌های کانتینرها تا به اینجا

تا اینجا یک چنین ایزوله سازی‌هایی را بررسی کردیم:
- ایزوله سازی File System و وجود یک disk مجازی مجزا به ازای هر کانتینر در حال اجرا.

- پروسه‌های کانتینرها از پروسه‌های میزبان ایزوله هستند. برای مثال اگر دستور get-process را داخل یک container اجرا کنید، خروجی آن با خروجی اجرای این دستور بر روی سیستم میزبان یکی نیست. یعنی نمی‌توان از داخل کانتینرها، به پروسه‌های میزبان دسترسی داشت و دخل و تصرفی را در آن‌ها انجام داد که از لحاظ امنیتی بسیار مفید است. هر چند اگر به task manager ویندوز میزبان مراجعه کنید، می‌توان پروسه‌های داخل یک container را توسط Job Object ID یکسان آن‌ها تشخیص دهید (مثال آخر قسمت قبل)، اما یک container، قابلیت شمارش پروسه‌های خارج از مرز خود را ندارد.

- ایزوله سازی شبکه مانند کارت شبکه‌ی مجازی کانتینر IIS که در قسمت قبل بررسی کردیم. برای آزمایش آن دستور ipconfig را در داخل container و سپس در سیستم میزبان اجرا کنید. نتیجه‌ای را که مشاهده خواهید کرد، کاملا متفاوت است. یعنی network stack این دو کاملا از هم مجزا است. شبیه به اینکه به یک سیستم، چندین کارت شبکه را متصل کرده باشید. اینکار در اینجا با تعریف virtual network adaptors انجام می‌شود و لیست آن‌ها را در قسمت «All Control Panel Items\Network Connections» سیستم میزبان می‌توانید مشاهده کنید. یکی از مهم‌ترین مزایای آن این است که اگر در یک container، وب سروری را بر روی پورت 80 آن اجرا کنید، مهم نیست که در سیستم میزبان، یک IIS در حال سرویس دهی بر روی پورت 80 هم اکنون موجود است. این دو پورت با هم تداخل نمی‌کنند.

- در حالت کار با Windows Containers، رجیستری کانتینر نیز از میزبان آن مجزا است و یا متغیرهای محیطی این‌ها یکی نیست. برای مثال دستور \:ls env را در کانتینر و سیستم میزبان اجرا کنید تا environment variables را گزارش گیری کنید. خروجی این دو کاملا متفاوت است. برای مثال حداقل computer name، user name‌های قابل مشاهده‌ی در این گزارش‌ها، متفاوت است و یا دستور \:ls hkcu را در هر دو اجرا کنید تا خروجی رجیستری متعلق به کاربر جاری هر کدام را مشاهده کنید که در هر دو متفاوت است.

- در حالت کار با Linux Containers هر چیزی که ذیل عنوان namespace مطرح می‌شود مانند شبکه، PID، User، UTS، Mount و غیره شامل ایزوله سازی می‌شوند.


دو نوع Windows Containers وجود دارند

در ویندوز، Windows Server Containers و Hyper-V Containers وجود دارند. در این قسمت تمام کارهایی را که بر روی ویندوز سرور انجام دادیم، در حقیقت بر روی Windows Server Containers انجام شدند و تمام Containerهای ویندوزی را که در قسمت قبل بر روی ویندوز 10 ایجاد کردیم، از نوع Hyper-V Containers بودند.
تفاوت مهم این‌ها در مورد نحوه‌ی پیاده سازی ایزوله سازی آن‌ها است. در حالت Windows Server Containers، کار ایزوله سازی پروسه‌ها توسط کرنل اشتراکی بین کانتینرها صورت می‌گیرد اما در Hyper-V Containers، این ایزوله سازی توسط hypervisor آن انجام می‌شود؛ هرچند نسبت به ماشین‌های مجازی متداول بسیار سریع‌تر است، اما بحث به اشتراک گذاری کرنل هاست را که پیشتر در این قسمت بررسی کردیم، در این حالت شاهد نخواهیم بود. ویندوز سرور 2016 می‌تواند هر دوی این ایزوله سازی‌ها را پشتیبانی کند، اما ویندوز 10، فقط نوع Hyper-V را پشتیبانی می‌کند.


روش اجرای Hyper-V Containers بر روی ویندوز سرور

در صورت نیاز برای کار با Hyper-V Containers، نیاز است مانند قسمت قبل، ابتدا Hyper-V را بر روی ویندوز سرور، فعالسازی کرد:
Install-WindowsFeature hyper-v
Restart-Computer -Force
اکنون برای اجرای دستور docker run ای که توسط Hyper-V مدیریت می‌شود، می‌توان به صورت زیر، از سوئیچ isolation استفاده کرد:
docker run -it --isolation=hyperv microsoft/dotnet:nanoserver powershell
در این حالت اگر به disk management سیستم میزبان مراجعه کنید، دیگر حالت اضافه شدن disk مجازی را مشاهده نمی‌کنید. همچنین اگر به task manager ویندوز میزبان مراجعه کنید، دیگر لیست پروسه‌های داخل container را نیز در اینجا نمی‌بینید. علت آن روش ایزوله سازی متفاوت آن با Windows Server Containers است و بیشتر شبیه به ماشین‌های مجازی عمل می‌کند. در کل اگر نیاز به حداکثر و شدیدترین حالت ایزوله سازی را دارید، از این روش استفاده کنید.
مطالب
xamarin.android قسمت اول
در ابتدای کار تشکر و سپاس از استاد دانشمند و پر مایه‌ام جناب مهندس رضا محمد پور که از محضر پر فیض تدریسشان، بهره‌ها برده‌ام.  
هدف از این سری آشنایی با زامارین اندروید میباشدکه آشنایی با سی شارپ پیش نیاز آن میباشد و ورژن ویژوال استودیو 2017 من در حال حاضر 15.7.4 می‌باشد.
 اولین پروژه را با زامارین شروع میکنیم. طبق معمول بعد از نصب ویژوال استودیو از گزینه File گزینه New Project را انتخاب میکنیم.

در ورژن‌های قبلی ویژوال استودیو، در زمان بارگذاری پروژه، احتیاجی به اجرای نرم افزار‌های تحریم گذر نبود؛ همانند ورژن 15.6. ولی در این ورژن که من نصب کردم بدلیل نصب خودکار کتابخانه‌های متریال دیزاین، باید از این گونه نرم افزار‌ها نیز استفاده کرد.

درقسمت بعدی گزینه BlankApp را انتخاب و در قسمت Minimum Android Version که با انتخاب آن میتوانیم ورژن گوشی‌های اندروید برای استفاده از این اپلیکیشن را انتخاب نماییم. به عنوان مثال با انتخاب اندروید 4.4 برنامه ما صرفا برای گوشی‌های اندورید 4.4 به بالا جواب میدهد. بعد از تایید، پروژه باز شده که با این solution روبرو میشویم.

- قسمت Properties را اگر بازکنیم، با دو گزینه روبرو میشویم که یکی فایل android manifest هست و اگر روی properties آن کلیک و ویژگی‌هایی را انتخاب کنیم، بطور خودکار بر روی manifest تاثیر میگذارند. در قسمت‌های بعد در این رابطه جداگانه بحث خواهیم کرد.

- در قسمت Asset که به معنای منابع اندروید می‌باشد، به عنوان مثال صفحات Razor، فونت و یا صفحات HTML و یا عکس و یا ... را می‌توانیم قرار دهیم.

- در قسمت Resource که پوشه‌های آن layout ،mipmap ،values و resource.designer میباشند، در پوشه layout می‌توانیم صفحات استاندارد اندروید را شروع به طراحی کنیم. درقسمت mipmap عکس‌ها و یا فایل‌های xml ایی را که قرار است استفاده کنیم، در پروژه قرار میدهیم. در قسمت value که بیشتر برای انتخاب و تغییر تم یا استفاده از Resource‌ها (همانند Asp.mvc که استفاده میکردیم) است که البته با ساختاری متفاوت در اندروید از آن‌ها استفاده میکنیم، قرار میگیرند.

- در قسمت Resource.Designer که در مطالب بعد با آن آشنا خواهید شد، تمامی آیتم‌های انتخابی از جمله Layout ها  و عکس‌ها و... با ذخیره کردن در این قسمت دخیره میشوند که بعد با رفرنس دادن از طریق resource پروژه میتوانیم از عکس‌ها و لی‌آوت‌ها در کد نویسی استفاده کنیم.

- در انتها جهت معرفی به mainactivity می‌رسیم که یک صفحه است شامل المنت‌ها و اجزای مختلف و کاربر میتواند با آن ارتباط برقرار کند. 

  [Activity(Label = "@string/app_name", Theme = "@style/AppTheme", MainLauncher = true)]
    public class MainActivity : AppCompatActivity
    {
        protected override void OnCreate(Bundle savedInstanceState)
        {
            base.OnCreate(savedInstanceState);
            // Set our view from the "main" layout resource
            SetContentView(Resource.Layout.activity_main);
        }
    }  
  همانطور که مشاهده میکنید اکتیویتی ما از AppCompatactivity ارث بری کرده، همانند ارث بری که در mvc از controller‌ها داشتیم. در قسمت Attribute که به نام اکتیویتی تعریف شده است، برچسب یا همان Lable را که در زمان اجرا به نام اکتیویتی می‌دهد، مشاهده می‌کنید. در قسمت تم، میتوان تمی را که برای آن از قبل نوشته شده‌است و به یک اکتیویتی اختصاص داد، قرار داد. در ضمن این نکته را یاد آوری کنم زمانیکه اکتیویتی ما از Appcompatactivity ارث بری میکند، انتخاب تم اجباری میباشد.

- در گزینه بعدی Mainluncher را میبینیم که تعیین کننده‌ی نقطه شروع اکتیویتی ما در بین اکتیویتی‌های دیگر می‌باشد.


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

  protected override void OnCreate(Bundle savedInstanceState)
        {
            base.OnCreate(savedInstanceState);
            LinearLayout ln;
            Button btn;
            // Set our view from the "main" layout resource
            SetContentView(Resource.Layout.activity_main);
            for (int i = 0; i < 5; i++)
            {
             btn = new Button(this);
             btn.Text = i.ToString();
             ln= FindViewById<LinearLayout>(Resource.Id.linearLayout1);
             btn.Click += Btn_Click;
             ln.AddView(btn);
            }
        }
        private void Btn_Click(object sender, System.EventArgs e)
        {
            Button btntest = sender as Button; 
            btntest.SetBackgroundColor(Android.Graphics.Color.Blue);
        }
    }
در قسمت تعریف دکمه منظور از This این می‌باشد که این دکمه برای اکتیویتی جاری است و Findview by id که می‌بینید، من در قسمت لی‌آوت activity main، یک LinearLayout و یک Id را قرار داده ام که البته id باید منحصر بفرد باشد و با findviewbyid آی‌دی linearlayout را که قرار داده‌ام، پیدا و استفاده کردم که کد‌های آن را در زیر میتوانید مشاهده کنید.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:minWidth="25px"
    android:minHeight="25px">
    <LinearLayout
        android:orientation="vertical"
        android:minWidth="25px"
        android:minHeight="25px"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/linearLayout1" />
</RelativeLayout>
اکنون اولین برنامه را می‌تونید تست و اجرا کنید: AppTrainng-1.zip
مطالب
آشنایی و بررسی ابزار Glimpse
در مطلب MiniProfiler ابزار مانیتور کارآیی وب سایت‌ها را بررسی کردیم. اما ابزار Glimpse هم جزو ابزار‌های حرفه‌ای است که در مطلبی آقای هانسلمن در سایت خود به آن پرداخته بودند. اما دیدم جای یک مطلب فارسی در این رابطه خالی است.


Glimpse چیست؟
glimpse یک ابزار حرفه‌ای برای نمایش زمان اجرای کدها، پیکربندی سرور، درخواست‌های وب، اشکال زدایی و بررسی کارآیی وب سایت‌های MVC و Web Forms می‌باشد. البنه بدون آنکه در کد‌های پروژه شما تغییری ایجاد نماید.
ابتدا در پنجره Nuget عبارت glimpse را جستجو و آن را نصب نمایید:


کتابخانه‌های زیادی برای این ابزار آماده شده‌اند:

  • کتابخانه Glimpse Core
    که هسته اصلی ابزار است، حتما باید نصب شود.
  • کتابخانه Glimpse ASP.NET
    برای بررسی وب سایت‌های نوشته شده با ASP.NET Web Forms استفاده می‌شود. البته بری Mvc هم لازم است.
  • کتابخانه Glimpse Mvc2, Glimpse Mvc3، Glimpse Mvc4
    برای بررسی وب سایت‌های نوشته شده با ASP.NET Mvc
  • کتابخانه Glimpse Ado
    برای بررسی و نمایش زمان کوئری بر روی پایگاه داده
  • کتاخانه Glimpse EF4.3، Glimpse EF5، Glimpse EF6
    برای زمانیکه از نگارش‌های مختلف Entity Framework استفاده می‌نماییم
پس از نصب کتابخانه‌های مورد نیاز، پروژه را rebuild و سپس اجرا نمایید. برای فعال کردن glimpse آدرس http://{your-site}/Glimpse.axd را اجرا کنید تا صفحه تنظیمات آن فعال شوند و سپس بر روی گزینه Turn Glimpse on، کلیک کنید. همچنین با گزینه Turn Glimpse off می‌توانید آن را غیر فعال نمایید.

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


این ابزار شامل سه قسمت است:
  • HTTP
    اطلاعات Request و زمان پاسخ و اطلاعات سرور نمایش داده می‌شود
  • HOST
    اطلاعات صفحه اجرا شده، زمان پاسخ و تعداد کوئری‌های اجرا شده و زمان آن نمایش داده می‌شوند
  • AJAX
    اطلاعات درخواست‌های اجکسی این صفحه و تعداد آن نمایش داده می‌شوند
بر روی هر یک از این قسمت‌ها با حرکت ماوس، جزئیات آن قسمت نمایش داده می‌شود.

اگر بر روی آیکون g ابزار کلیک کنید، همچون developer tools مرورگر‌ها باز شده و دارای زبانه‌های متعددی می‌باشد. مثلا اگر پلاگین ado و ef5 نصب باشند، در زبانه SQL می‌توانید کوئری‌های اجرا شده و زمان مصرف شده آن‌ها را مشاهده نمایید

زبانه دیگر Timeline است که زمان انقیاد اشیاء و رویدادها را بصورت گرافیکی نمایش می‌دهد.

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

  • طراحی دیتابیس یا بانک اطلاعاتی بر پایه چند زبانه بودن و بررسی سناریوهای مختلف.
  • نکاتی که باید در ساخت سایت‌های چند زبانه به آن‌ها دقت کرد.
  • شیوه‌ی تشخیص و تغییر زبان سایت
  • معرفی چند کامپوننت وب، برای مباحث چند زبانه

طراحی مدل دیتابیس

اولین کار برای داشتن یک سایت چند زبانه، این است که یک مدل صحیح و مناسب را برای دیتابیس خود انتخاب کنید. یکی از اولین روش‌هایی که به ذهن هر فردی می‌رسد این است که برای هر ستون متنی که قرار است چند زبانه باشد، به تعداد زبان‌ها برایش یک ستون در نظر بگیریم. یعنی برای جدول مقالات که قرار است در سه زبان فارسی و انگلیسی و عربی باشد، سه ستون برای عنوان مقاله و سه ستون نیز برای متن آن داشته باشیم. تصویر زیر نمونه‌ای از این مدل را نشان می‌دهد.

مزایا:

  1. پیاده سازی آسان

معایب:

  1. در این روش با زیاد شدن هر زبان، تعداد ستون‌ها افزایش می‌یابد که باعث می‌شود طراحی مناسبی نداشته باشد.
  2. در ضمن این مورد باید توسط برنامه نویس مرتبا اضافه گردد یا اینکه برنامه نویس این امکان را در سیستم قرار دهد که مدیر سایت بتواند در پشت صحنه کوئری افزودن ستون را ایجاد کند که باید جدول مرتبا مورد alter گرفتن قرار بگیرد.
  3. ممکن است همیشه برای هر زبانی مطلبی قرار نگیرد و این مورد باعث می‌شود بی جهت فضایی برای آن در نظر گرفته شود.

پی نوشت: با اینکه امروزه بحث فیلدهای sparse Column وجود دارد ولی این فیلد‌ها در هر شرایطی مورد استفاده قرار نمی‌گیرند وبیشتر متعلق به زمانی است که می‌دانیم آن فیلد به شدت کم مورد استفاده قرار می‌گیرد.

پی نوشت دوم : در صورتی که فیلد شما مانند متن مقاله که عموما از نوع داده (varchar(max است استفاده می‌کنید و در صورتی که زبان مورد استفاده قرار نگیرد در خیلی از اوقات بی جهت فیلد‌های Blob ساخته اید که بهینه سازی آن را نیز باید در نظر بگیرید.


در مرحله‌ی بعد برای رفع مشکلات بالا یک جدول از زبان‌ها، مانند جدول زیر را ایجاد می‌کنیم:
 
 ID  کد
 Language  زبان
 ISO  کد دو رقمی آن زبان
 Flag  پرچم آن کشور
بعد از آن هر مقاله برای یک زبان ایجاد خواهد شد؛ چیزی مانند تصویر زیر:

مزایا:

  1. پیاده سازی آسان

معایب:

  1. ایجاد رکوردهای تکراری، هر مقاله برای بعضی از اطلاعاتش که چند زبانه نیستند داده‌های تکراری خواهد داشت.
  2. هر مقاله یک مقاله‌ی جدا شناخته می‌شود و ارتباطی میان آنان نخواهد بود. بدین ترتیب توانایی ایجاد گزارش‌هایی چون هر گروه از مقاله و دسته بندی آن‌ها از بین خواهد رفت. در ضمن مدیر عموما در یک سیستم مدیریتی می‌خواهد تنها یک لینک را به یک مقاله بدهد و سایت بنا به تشخیص در زبان مزبور، یکی از این مقالات را به کاربر نمایش دهد؛ نه اینکه مرتبا مدیر برای هر زبان، لینکی را مهیا کند و در این حالت چنین چیزی ممکن نخواهد بود.
  3. در یک سیستم فروشگاهی همانند تصویر بالا کار هم سخت‌تر می‌شود و هر رکورد، یک محصول جدا شناخته می‌شود و ویرایش‌ها هم برای هر کدام باید جداگانه صورت بگیرد که در عمل این طرح را رد می‌کند.


سومین راه حل این است که سه جدول ایجاد کنیم:

یک. جدول زبان‌ها (که بالاتر ایجاد شده بود)

دو . جدول نام مقاله به همراه اطلاعات پایه و فیلدها بی نیاز به چند زبانه بودن

سه : یک جدول که هر دو ستون آن کدهای کلید دو جدول بالا را دارند و فیلدهای چند زبانه در آن وجود دارند.

جدول پایه

 ID  کد
 Name  نام مقاله
 CreationDate  تاریخ ایجاد
 Writer  نویسنده
 Visibilty  وضعیت نمایش
 جدول مقالات
 LanguageCode کد زبان
 ArticleID  کد مقاله
 CreationDate  تاریخ ایجاد
 Visibility  وضعیت نمایش مقاله
Title
عنوان مقاله
ContentText
متن مقاله

 در جدول پایه یک مقاله ایجاد می‌شود که اطلاعات عمومی همه مقالات را دارد و حتی خصوصیت وضعیت نمایش آن، روی همه‌ی مقالات با هر زبانی تاثیر می‌گذارد. در جدول دو، هر مقاله یک رکورد دارد که کد زبان و کد مقاله برای آن یک کلید ترکیبی به حساب می‌آیند. پس، از هر مقاله یک یا چند زبان خواهیم داشت. همچنین دارای فیلدهایی با وضعیت مخصوص به خود هم هستند؛ مثل فیلد وضعیت نمایش مقاله که فقط برای این مقاله با این زبان کاربرد دارد.

مزایا:

  1. گزارش گیری آسان برای هر دسته مقاله با زبان‌های مختلف و ارتباط و یکپارچگی
  2. آسان در افزودن زبان.

معایب:

  1. ایجاد کوئری‌های پیچیده‌تر و جوین دار که به نسبت روش‌های قبلی کوئری‌ها پیچیده‌تر شده اند.
  2. کدنویسی زیادتر.

استفاده از ساختارهای XML یا JSON برای ذخیره سازی اطلاعات چند زبانه مانند ساختارهای زیر:

XML
<Articles>
<Article>
this is english text
</Article>
<Article>
این یک متن فارسی است
</Article>
</Articles>

یا 
<Articles>
<en-us>
this is english text
</en-us>
<fa-ir>
این یک متن فارسی است
</fa-ir>
</Articles>
JSON
"Articles":["en-us':{"title":"this is english text","content":" english content"},"fa-ir":{"title":"متن فارسی","content":"محتوای فارسی"}]
ازSQL Server 2005 به بعد از نوع داده xml پشتیبانی می‌شود و در نسخه‌ی 2016 آن نیز پشتیبانی از Json اضافه شده است که حتی شامل اندیکس‌های اختصاصی هم برای این دو نوع می‌باشد.
از مزایای این روش ذخیره‌ی همه داده‌ها در یک ستون و یک جدول است و نیازی به ستون‌های اضافه یا جداول اضافه نیست ولی معایب این روش استفاده از کوئری‌های پیچیده‌تر جهت ارتباط و خواندن است.

استفاده از بانک‌های اطلاعاتی NO SQL
در این بانک‌ها دیگر درگیر تعداد ستون‌ها و جنس آن‌ها نیستیم و میتوانیم برای هر مقاله یا محصول، هر تعداد زبان و یا فیلد را که می‌خواهیم، در نظر بگیریم و اضافه کنیم. برای آشنایی بیشتر با این نوع بانک‌ها و انواع آن، مقالات مربوط به nosql را در سایت دنبال کنید.

نکاتی که در یک سایت چند زبانه باید به آن‌ها توجه کرد.

یک . زبان آن صفحه را معرفی کنید: این کار هم به موتورهای جست و جو برای ثبت سایت شما کمک می‌کند و هم برای معلولین که از ابزارهای صفحه خوان استفاده می‌کنند، کمک بزرگی است. در این روش، صفحه خوان‌ها و دستگاه‌های خط بریل که زبان صفحه را تشخیص نمی‌دهند با خواندن کد زبان می‌توانند زبان صفحه را تشخیص دهند. با استفاده از خط زیر میتوانید زبان اصلی صفحه‌ی خود را تنظیم نمایید:
<html lang="en">

اگر از XHTML استفاده می‌کنید خاصیت زیر را فراموش نکنید. دریافت W3C Validation بدون آن امکان پذیر نخواهد بود.
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
با تغییر زبان هر صفحه، باید تنظیم زبان آن تغییر یابد:



دو. چند زبانه بودن صفحه: در بالا یاد گرفتیم که چگونه زبان اصلی صفحه را تنظیم کنیم، ولی گاهی اوقات صفحه به غیر از زبان اصلی، شامل زبان‌های دیگر هم می‌شود؛ مثل نقل قول‌ها یا موارد دیگر. برای این‌کار می‌توانید از خصوصیت lang که در اکثر تگ‌ها پشتیبانی می‌شود، استفاده کنید. مثال پایین یک نقل قول فرانسوی است که ما آن را به خصوصیت lang، جهت تایید زبانش مزین کرده‌ایم:
<blockquote lang=”fr”>

<p>Le plus grand faible des hommes, c'est l'amour qu'ils ont de la vie.</p>

</blockquote>

سه. لینک ها : اگر دارید در صفحه‌ای لینک به جایی می‌دهید که متفاوت از زبان شماست، حتما باید زبان صفحه یا سایت مقصد را مشخص کنید. مثلا لینک زیر برای صفحه‌ای است که از یک زبان غیر فرانسوی به یک صفحه‌ی با زبان فرانسوی هدایت می‌شود:
<a href="" hreflang="fr">French</a>

همچنین اگر متن لینک شما هم به زبان فرانسوی باشد خیلی خوب می‌شود که آن را هم بیان کنید و از خاصیت lang و هم hreflang همزمان استفاده کنید:
<a href="" hreflang="fr">Francais</a>

چهار. جهت صفحه: به طور پیش فرض شما این مورد را به خاطر طراحی ظاهر صفحه رعایت می‌کنید ولی خالی از لطف نیست که بگوییم جهت زبان را هم با خصوصیت dir  که مقدار پیش فرضش راست به چپ هست، ذکر کنید. هر چند که عموما وب سایت‌های چند زبانه شامل یک قالب دیگر به صورت راست چین یا چپ چین هم هستند که بر اساس زبان انتخاب شده، قالب خود را تغییر خواهند داد.

پنج. انکودینگ صفحه را مشخص کنید: برای اینکه نحوه‌ی رمزگذاری و رمزگشایی حروف و نمادها مشخص گردد، باید انکودینگ تنظیم شود و حتی برای بعضی از موتورهای جست و جو که ممکن است با وب سایت شما به مشکل بر بخورند. امروزه بیشتر از صفحات یونیکد استفاده می‌شود که سطح وسیعی از کاراکترها را پشتیبانی می‌کند.
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">

HTML5
<meta charset="UTF-8">

شش. اندازه‌ی فونت:
موقعی که یک سایت چند زبانه را طراحی می‌کنید این نکته خیلی مهم هست که بدانید اندازه فونت‌های زبان پیش فرض، برای باقی زبان‌ها مناسب نیستند. به عنوان مثال ممکن است اندازه فونتی برای زبان‌های انگلیسی، فرانسوی و آلمانی مناسب باشد ولی برای زبان‌های فارسی و عربی و چینی و ... مناسب نباشد و خواندن آن سخت شود. به همین جهت یکی از راه‌های حل این مشکل استفاده از قالب css است که وابسته به خصوصیت lang ای است که شما برای صفحه و هر المان یا تگی که از این خصوصیت استفاده می‌کند، تعیین کرده‌اید.
:lang(en) {

font-size: 85%;

font-family: arial, verdana, sans-serif;

}

:lang(zh) {

font-size: 125%;

font-family: helvetica, verdana, sans-serif;

}

خط زیر تعیین میکند که از استایل اول استفاده شود:
<html lang="en">
و خط زیر تعیین می‌کند که از استایل دوم استفاده شود:
<html lang="zh">
البته این کد بالا در مرورگرهای فایرفاکس، اپرا و IE8 به بالا پاسخ می‌دهد. برای سایر مروگرها چون کروم و نسخه‌های پیشین IE باید از شیوه‌ی زیر بهره ببرید:
<body class="english"> or <body class="chinese">

و استایل:
.english {

font-size: 85%;

font-family: arial, verdana, sans-serif;

}

.chinese {

font-size: 125%;

font-family: helvetica, verdana, sans-serif;

}
در این شیوه برای تگ مربوطه یک کلاس با نام آن زبان ایجاد کرده که محتوای آن تنظیمات قلم آن زبان می‌باشد.

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

هشت : زمان را نیز تغییر دهید: یکی از مواردی که در کمتر سایت چند زبانه‌ای به چشم می‌خورد و به نظر بنده می‌تواند بسیار مهم باشد این است که time zone منطقه‌ی هر زبان را بدانید. به عنوان مثال برای مقاله‌ی خود، تاریخ ایجاد را به صورت UTC ذخیره کنید و سپس نمایش را بر اساس زبان یا حتی بهتر و دقیق‌تر از طریق IP کشور مربوطه به دست آورید. برای کاربران ثبت نام شده این تاریخ می‌تواند دقیق‌تر باشد همانند انجمن‌های وی بولتین.


شیوه‌های تشخیص زبان سایت

یکی از راه‌های تشخیص زبان این است که موقعی که برای اولین بار کاربری به سایت مراجعه می‌کند، زبان مورد نظرش را سوال کنید و این اطلاعات را در یک کوکی بدون تاریخ انقضاء ذخیره کنید تا در دفعات بعدی آن را بررسی نمایید.

دومین راه، استفاده از IP کاربر مراجعه کننده است تا بر اساس آن زبان مورد نظر را انتخاب کنید.

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

پی نوشت: فراموش نگردد که امکان تغییر زبان همیشه برای کاربر مهیا باشد و طوری نباشد که کاربر در آینده نتواند زبان سایت را تغییر دهد؛ حتی اگر تشخیص خودکار سایت برای زبان فعال باشد.

پی نوشت: در روش‌های بالا بهتر است همان مرتبه‌ی اول اطلاعات را در یک کوکی ذخیره کنید تا مراحل پیگیری راحت‌تر و آسان‌تر شود.


پلاگین‌ها و ابزارهای مدیریت زبان

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

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

پلاگین بعدی International Telephone Input است که  پیاده سازی پلاگین بالا می‌باشد. برای مواردی مفید است که شما نیاز دارید کد تلفنی کشوری را انتخاب کنید.

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

قسمت بیست و دوم آموزش MVC که مبحث Globalization را دنبال می‌کند.

قسمت اول از شش قسمت مباحث Globalization  که دنباله‌ی آن را می‌توانید در مقاله‌ی خودش دنبال کنید.

نظرات مطالب
Ajax.BeginForm و ارسال فایل به سرور در ASP.NET MVC
ممنون از راهنماییتون ولی مشکل من اینه که نمیخوام از تگ multiple استفاده کنم.
میخوام در همه مرورگرها قابل اجرا باشه و هر فایل مشخصات خودشو داره ...
یعنی مثلا 4 تا input file جداگانه و ثبت با یک کلیک 
مطالب
کار با Docker بر روی ویندوز - قسمت پنجم - ایجاد Imageهای سفارشی
تا اینجا با نحوه‌ی اجرای برنامه‌های مختلف توسط داکر مانند وب سرور لینوکسی nginx و یا IIS ویندوزی آشنا شدیم؛ اما هنوز محتوایی را در آن‌ها هاست نکرده‌ایم. در این قسمت این موضوع را بررسی خواهیم کرد و در طی این فرآیند، با نحوه‌ی ساخت Imageهای سفارشی نیز آشنا خواهیم شد.


روش نگاشت محتوای یک سایت استاتیک در یک Container که وب سرور است

فرض کنید یک سایت استاتیک بوت استرپی را تهیه کرده‌اید و قصد دارید آن‌را توسط وب سرور nginx، هاست کنید. برای این‌کار، چندین گزینه پیش روی ما هستند:
گزینه‌ی اول: دریافت image مربوط به nginx، سپس ایجاد یک container از آن و در آخر با استفاده از «روش به اشتراک گذاری فایل سیستم میزبان با کانتینرها» که در قسمت قبل بررسی کردیم، این وب سایت را آماده‌ی اجرا و دسترسی می‌کنیم.
گزینه‌ی دوم: کپی کردن فایل‌های وب سایت از سیستم میزبان، به درون فایل سیستم خود container.
گزینه‌ی سوم: ایجاد یک image سفارشی که از ابتدا به همراه فایل‌های وب سایت استاتیک ما است و در این حالت تنها کافی است این image را تبدیل به container اجرایی کنیم.


روش اول: به اشتراک گذاری فایل سیستم میزبان با کانتینر وب سرور جهت هاست آن

در قسمت قبل، یک فایل tar ایجاد شده‌ی در سیستم میزبان ویندوزی را با یک کانتینر لینوکسی به اشتراک گذاشتیم تا بتوانیم محتویات آن‌را استخراج کنیم. در اینجا قصد داریم پوشه‌ی وب سایت استاتیک خود را که در سیستم میزبان ویندوزی قرار دارد، با وب سرور nginx که توسط یک container در حال اجرا است، به اشتراک بگذاریم تا آن‌را هاست کند.
فرض کنید وب سایت استاتیک ما در مسیر c:\users\vahid\mysite سیستم میزبان قرار دارد که داخل آن یک فایل index.html و تعدادی فایل css و js آماده‌ی برای هاست شدن، وجود دارند. برای هاست آن توسط nginx، از دستور زیر استفاده خواهیم کرد:
 docker run --rm -it -p 8080:80 -v c:\users\vahid\mysite:/usr/share/nginx/html nginx
در این دستور:
- سوئیچ rm سبب می‌شود تا پس از خاتمه‌ی کار nginx، این container نیز حذف شود.
- از سوئیچ it استفاده شده‌است تا با فشردن ctrl+c، بتوانیم پروسه‌ی container را خاتمه دهیم و پس از آن، برنامه‌ی nginx دیگر در background در حال اجرا نباشد (اجرای آن در foreground).
- سپس پورت 8080 سیستم میزبان، به پورت 80 وب سرور nginx نگاشت شده‌است. چون containerها دارای network stack خاص خودشان هستند (که آن‌را در قسمت سوم بررسی کردیم)، پورت 80 آن‌ها با پورت 80 سیستم میزبان تداخل نمی‌کند و اگر برای مثال بر روی پورت 80 سیستم جاری، IIS در حال اجرا باشد، سبب عدم اجرا شدن وب سرور nginx به دلیل تداخل پورت‌ها نمی‌شود.
- در ادامه روش volume mount را مشاهده می‌کنید که در قسمت قبل بررسی کردیم. مسیر c:\users\vahid\mysite سیستم میزبان، به مسیر ویژه‌ی /usr/share/nginx/html داخل container نگاشت شده‌است. این مسیر، یک مسیر استاندارد بوده و در مستندات docker hub این وب سرور، ذکر شده‌است.
- در آخر هم نام image این وب سرور را ذکر کرده‌ایم.

پس از اجرای این دستور، اگر nginx پیش‌تر دریافت نشده باشد، image آن دریافت شده، یک container بر اساس آن ساخته می‌شود و سپس با پارامترهایی که توضیح دادیم، اجرا خواهد شد. اکنون اگر در سیستم میزبان، مسیر http://localhost:8080 را در مرورگر باز کنید، وب سایت استاتیک خود را مشاهده خواهید کرد.


روش دوم: کپی کردن فایل‌های وب سایت از سیستم میزبان، به درون فایل سیستم خود container

همانطور که در قسمت سوم نیز بررسی کردیم، فایل سیستم مربوط به هاست، به طور کامل از فایل سیستم container، جدا و ایزوله است و بدون volume mount، یک container نمی‌تواند به فایل‌های میزبان خود دسترسی پیدا کند. بنابراین گزینه‌ی دیگری که در اینجا وجود خواهد داشت، کپی کردن فایل‌های میزبان و انتقال آن‌ها به container می‌باشد؛ شبیه به کپی کردن فایل‌ها از یک کامپیوتر موجود در شبکه به کامپیوتر دیگری در آن.
برای این منظور ابتدا nginx را در پس‌زمینه اجرا می‌کنیم:
 docker run -d -p 8080:80 --name nginx nginx
در این دستور، سوئیچ‌های rm و it حذف شده‌اند. علت اینجا است که سوئیچ d، سبب اجرای این دستور در پس‌زمینه می‌شود؛ یعنی بلافاصله سبب بازگشت ما به خط فرمان خواهد شد و در این حالت نمی‌خواهیم که این container حذف شود. همچنین یک نام نیز به آن انتساب داده شده‌است تا بتوان ساده‌تر با آن کار کرد.
پس از اجرای این دستور و بازگشت به command prompt، جهت اطمینان حاصل کردن از اجرای آن در پس زمینه، دستور docker ps را صادر می‌کنیم که لیست آن، حاوی گزارشی از container‌های در حال اجرا است.
اکنون توسط دستور ویژه‌ی docker exec، می‌خواهیم درون یک container در حال اجرا، پروسه‌ای را اجرا کنیم. یعنی با اینکه پروسه‌ی nginx داخل این container در حال اجرا است، برای مثال می‌خواهیم یک shell را نیز داخل آن اجرا کنیم:
 docker exec -it nginx bash
در اینجا دستور docker exec، سبب اجرای bash shell داخل کانتینری با نام nginx می‌شود (همان سوئیچ name در دستور قبلی و نه نام image آن) و چون می‌خواهیم به این shell در foreground دسترسی داشته باشیم، از سوئیچ it نیز استفاده شده‌است. پس از اجرا شدن bash shell، اکنون به فایل سیستم این container دسترسی یافته‌ایم. برای مثال دستور ls را صادر کنید تا لیستی از آن‌را مشاهده نمائید. سپس به کمک آن، به پوشه‌ی ویژه‌ی html این وب سرور وارد می‌شویم:
 cd /usr/share/nginx/html
و برای مثال می‌توان در آن تغییر ایجاد کرد:
ls
mv index.html index2.html
exit
این دستورات سبب می‌شوند تا فایل پیش‌فرض index.html آن، به index2.html تغییر نام یابد و سپس از این shell خارج می‌شویم و به shell سیستم میزبان باز خواهیم گشت. در اینجا دستور docker cp (که در PowerShell سیستم میزبان اجرا می‌شود)، امکان کپی کردن فایل‌ها را از سیستم میزبان به یک container میسر می‌کند.
 docker cp c:\users\vahid\mysite nginx:/usr/share/nginx/html
پس از دستور docker cp ابتدا مسیر مبداء مشخص می‌شود و سپس ابتدا نام container مقصد به همراه یک : و در ادامه مسیر مقصد نهایی کپی در آن container ذکر خواهند شد. به این ترتیب فایل‌های وب سایت استاتیک ما در سیستم میزبان به پوشه‌ی html مخصوص nginx، در کانیتنری که در حال اجرای آن است کپی می‌شوند. برای آزمایش صحت این کپی می‌توان دستور زیر را صادر کرد که لیست فایل‌های این پوشه‌ی html را نمایش می‌دهد:
 docker exec nginx ls /usr/share/nginx/html
اینبار نیز اگر در سیستم میزبان، مسیر http://localhost:8080 را در مرورگر باز کنید، وب سایت استاتیک خود را مشاهده خواهید کرد که فایل‌های آن از داخل خود container تامین می‌شوند و وابستگی به سیستم میزبان ندارند.


روش سوم: ایجاد یک image سفارشی که از ابتدا به همراه فایل‌های وب سایت استاتیک ما است

در روش دوم، موفق شدیم که فایل‌های مدنظر خود را به درون container در حال اجرا کپی کنیم. اکنون می‌خواهیم یک snapshot را از آن تهیه کنیم؛ شبیه به کاری که با ماشین‌های مجازی نیز انجام می‌شود و این روشی است که از آن برای ساخت یک image سفارشی استفاده می‌شود. برای این منظور از دستور docker commit استفاده می‌شود تا تصویری را از وضعیت یک container در حال اجرا، در آن لحظه تهیه کنیم:
 docker commit nginx mysite:nginx
پس از دستور docker commit، نام container ای که می‌خواهیم تصویر وضعیت جاری آن‌را ذخیره کنیم، ذکر می‌شود. پس از آن به صورت اختیاری می‌توان یک نام جدید و همچنین tag ای را برای آن ذکر کرد.
اکنون پس از اجرای این دستور، با استفاده از فرمان docker images می‌توان مشاهده کرد که image جدید mysite، با tag ای معادل nginx، ایجاد شده‌است.
در ادامه برای اجرای این image جدید، می‌توان از دستور زیر استفاده کرد:
 docker run -d -p 8090:80 --name mysite mysite:nginx
روش اجرای آن همانند سایر imageهای موجود است و در اینجا از نام image به همراه tag آن استفاده شده‌است. همچنین پورت نگاشت شده‌ی آن‌را به سیستم میزبان نیز 8090 انتخاب کرده‌ایم. نامی را نیز به آن نسبت داده‌ایم تا بتوان از آن در دستور docker exec استفاده کرد.
اکنون اگر در سیستم میزبان، مسیر http://localhost:8090 را در مرورگر باز کنید، وب سایت استاتیک خود را مشاهده خواهید کرد و یا توسط دستور زیر می‌توانید فایل‌های موجود در پوشه‌ی html وب سرور nginx این container جدید در حال اجرا را ملاحظه نمائید:
 docker exec mysite ls /usr/share/nginx/html
که این فایل‌ها نه از طریق نگاشت فایل سیستم میزبان، به مسیری در container جاری تامین شده‌اند و نه از جائی به داخل آن کپی شده‌اند. بلکه دقیقا از image از پیش آماده شده‌ی آن خوانده شده‌اند.


نگاهی به لایه‌های یک Image در مقایسه با یک Container

زمانیکه خواستیم image جدید و سفارشی خاص خود را ایجاد کنیم، با image اصلی nginx شروع کردیم. اولین لایه‌ی موجود در این image، سیستم عاملی است که می‌تواند آن‌را اجرا کند. برفراز این لایه، لایه‌ی خود nginx قرار گرفته‌است. اگر خواستید تاریخچه‌ی ایجاد یک image را مشاهده کنید، از دستور docker history nginx استفاده نمائید. خروجی آن لیست دستوراتی را نمایش می‌دهد که برای ساخت این image مورد استفاده قرار گرفته‌اند. البته دستور docker history nginx --no-trunc، اطلاعات بیشتری را با نمایش لیست کامل و خلاصه نشده‌ی دستورات، ارائه می‌دهد. این دستورات را در صفحه‌ی docker hub هر image نیز می‌توان مشاهده کرد. در قسمت full description هر image، در ابتدای توضیحات، قسمتی است به نام supported tags and respective dockerfile links. در اینجا هر tag نامبرده شده، در حقیقت لینکی است به یک فایل که دقیقا همین دستورات را لیست کرده‌است. به این فایل، docker file گفته می‌شود که روش ساخت یک image را توضیح می‌دهد. هدف آن، خودکار سازی اجرای دستوراتی است که سبب ساخت یک image می‌شوند.

در ادامه اگر از این image، یک container را ایجاد کنیم، این container هر دو لایه‌ی OS و Framework را به همراه خواهد داشت؛ به علاوه‌ی لایه‌ی دیگری به نام Container/Run که می‌توان فایل‌های آن‌را خواند و یا در آن نوشت. بنابراین لایه‌ای که فایل‌های وب سایت استاتیک ما در آن کپی شدند، دقیقا همین لایه‌است.


و زمانیکه از یک container تصویری تهیه می‌شود، تغییراتی را که به فایل سیستم آن ایجاد کرده‌ایم، به صورت یک لایه‌ی جدید بر روی لایه‌های قبلی آن image، ظاهر و ثبت می‌شود. برای اثبات این موضوع، می‌توان از دستور docker diff nginx استفاده کرد. در اینجا nginx نام container ای است که می‌خواهیم تغییرات آن‌را با image قبلی که بر پایه‌ی آن ایجاد شده‌است، مشاهده کنیم.


تبدیل دستورات docker به یک docker file

تا اینجا یک چنین دستوراتی را برای اجرای کانتینر nginx، کپی فایل‌ها به آن و سپس تهیه‌ی یک تصویر از آن، اجرا کردیم:
docker run -d -p 8080:80 --name nginx nginx
docker cp c:\users\vahid\mysite nginx:/usr/share/nginx/html
docker commit nginx mysite:nginx
برای خودکار سازی آن‌ها هرچند می‌توان این دستورات را در یک اسکریپت نیز قرار داد، اما docker، قابلیت پردازش اسکریپت‌های خاص خود را نیز دارد که به آن Dockerfile گفته می‌شود. برای این منظور سطرهای فوق به صورت زیر تغییر می‌کنند:
بجای سطر اول، تنها نام image ای را که می‌خواهیم کار را بر مبنای آن انجام دهیم، ذکر می‌کنیم:
 FROM nginx
دستور دوم نیز تبدیل به دستور کپی Docker می‌شود:
 COPY mysite /usr/share/nginx/html
این دو سطر را به صورت یک فایل متنی، با نام ویژه‌ی Dockerfile ذخیره می‌کنیم (بدون پسوند) و این Dockerfile را دقیقا در کنار پوشه‌ی mysite قرار می‌دهیم (داخل پوشه‌ی c:\users\vahid) تا کار کپی را از همینجا شروع کند.
سپس برای اجرای این فایل، بجای دستور docker commit آخر، از دستور زیر استفاده می‌کنیم:
 docker build -f Dockerfile -t mysite:nginx-df .
البته می‌توان f Dockerfile- را نیز از این دستور حذف کرد؛ چون مقدار پیش‌فرض آن است (مگر آنکه بخواهیم مسیر خاصی را دقیقا مشخص کنیم):
 docker build -t mysite:nginx-df .
در هر دو دستور آخری که ذکر شدند، در انتهای دستور، یک نقطه نیز قرار دارد که به آن build context گفته می‌شود؛ یا دقیقا همین پوشه‌ای که در آن قرار داریم (c:\users\vahid).
تگ این image را نیز متفاوت با قبلی‌ها انتخاب کرده‌ایم؛ nginx-df بجای مقدار قبلی.
در این حالت اگر دستور آخر را اجرا کنیم، دستور docker images گزارش اضافه شدن این image جدید را ارائه خواهد داد.

مرجع کامل ساخت Dockerfileها را در اینجا می‌توانید مطالعه کنید.


ساخت یک image سفارشی برای هاست یک وب سایت استاتیک در IIS

تا اینجا از وب سرور لینوکسی nginx برای هاست وب سایت استاتیک خود استفاده کردیم. در ادامه می‌خواهیم از وب سرور IIS برای اینکار استفاده نمائیم. بنابراین ابتدا نیاز است یا از ویندوز سرور استفاده کنیم و یا می‌توان با کلیک راست بر روی آیکن Docker در قسمت Tray Icons ویندوز، به Windows Containers سوئیچ کرد و سپس به صورت زیر عمل نمود.
اینبار محتوای Dockerfile ای که کنار پوشه‌ی mysite قرار می‌گیرد، به صورت زیر خواهد بود:
FROM microsoft/iis:nanoserver

COPY mysite c:/inetpub/wwwroot
کار با image اصلی iis با tag مخصوص nanoserver که کم حجم‌تر است، شروع می‌شود. سپس فایل‌های mysite به پوشه‌ی wwwroot این وب سرور کپی خواهد شد.
در ادامه با استفاده از دستور زیر و اجرای فایل Dockerfile، این image جدید را با tag ای به نام iis ایجاد می‌کنیم:
 docker build -t mysite:iis .
پس از آن دستورات docker images و docker ps را جهت مشاهده‌ی وضعیت این image جدید اجرا کنید.


به اشتراک گذاری imageهای سفارشی در Docker Hub

برای به اشتراک گذاری imageهای سفارشی خود در Docker Hub، نیاز است tag آن‌ها را توسط دستور docker tag مطابق فرمت ویژه‌ی docker hub ویرایش کرد:
 docker tag mysite:nginx-df my_user_name/some_name:new_tag_name
در این دستور، Tag فعلی، با ذکر نام کاربری، نام مخزنی جدید در docker hub و سپس یک tag دلخواه، ویرایش می‌شود.
و در آخر برای انتشار آن می‌توان از دستور docker push استفاده کرد:
 docker push my_user_name/some_name:new_tag_name
اگر در اینجا پیام خطای unauthorized را مشاهده کردید، ابتدا دستور docker login را اجرا کنید تا بتوانید به سایت docker hub لاگین کنید (بر اساس مشخصات اکانت خود در داکر هاب) و سپس دستور فوق را اجرا نمائید.
پس از پایان کار اگر به سایت docker hub و مخازن خود مراجعه کنید، این image جدید قابل مشاهده خواهد بود.
اشتراک‌ها
یک ویرایشگر تمام عیار برای طراحان وب!

با کلیک کردن روی دکمه «Launch the Editor» یک محیط ساده و سریع برای ویرایش فایل‌های HTML، Js و Css به همراه امکان نمایش آنلاین نتایج برای شما فراهم می‌شود. نکته جالب دسترسی ساده به بسیاری از کتابخانه‌های معروف جاوااسکریپت است که به صورت خودکار به پروژه‌های شما اضافه می‌شوند. مدیریت نسخه‌های مختلف فایلها و ... 

یک ویرایشگر تمام عیار برای طراحان وب!