نظرات مطالب
نکات استفاده از افزونه‌ی Web Essentials جهت پردازش LESS
هنگام کار با LESS در Visual Studio توسط افزونه‌ی Web Essentials نیازی به این کار نیست، شما کافی است فایل کامپایل شده CSSتان را داخل Layoutتان قرار دهید بعد از اینکار در هر بار تغییر فایل less و کامپایل مجدد آن(چه در حالت build و چه در حالت save) فایل اضافه شده به Layout به صورت خودکار به روز رسانی می‌گردد.
مطالب
استفاده از Re-Captcha
در اینجا استفاده از re-CAPTCHA برای ASP.Net و در اینجا برای ASP.Net MVC با استفاده از سرویس گوگل نسخه 1 آن آشنا شدید. در این مقاله می‌خواهیم توضیحاتی را در مورد دلیل استفاده و نحوه‌ی ثبت re-CAPTCHA نسخه 2 برای تکنولوژی‌های ASP.Net و ASP.Net MVC ارائه کنیم.

  reCAPTCHA چیست؟

استفاده آسان و امنیت بالا، جمله‌ای می‌باشد که گوگل در سرتیتر تعریف آن جای داده که البته عنوان «من روبات نیستم» در سرویس استفاده شده‌است. reCAPTCHA یک سرویس رایگان برای وب سایت‌های شما در جهت حفظ آن در برابر روبات‌های مخرب است و از موتور تجزیه و تحلیل پیشرفته‌ی تشخیص انسان در برابر روبات‌ها استفاده می‌نماید. reCAPTCHA را میتوان به صورت ماژول در بلاگ و یا فرم‌های ثبت نام و ... جای داد که فقط با یک کلیک هویت سنجی انجام خواهد شد. گاها ممکن است بجای کلیک از شما سوالی پرسیده شود که در این صورت می‌بایستی تصاویر مرتبط با آن سوال را تیک زده باشید.



دلیل استفاده از reCAPTCHA:

  1. گزارش روزانه از وضعیت موفقیت آمیز بودن هویت سنجی
  2. سهولت استفاده برای کاربران
  3. سهولت استفاده جهت برنامه نویسان
  4. دسترسی پذیری مناسب بدلیل وجود سؤالات تصویری و تلفظ و پخش عبارت بصورت صوتی
  5. امنیت بالا 

آیا می‌توان قالب reCAPTCHA را تغییر داد؟

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



زبان‌های پشتیبانی شده در این سرویس:


اضافه نمودن reCAPTCHA به سایت:

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



نام، دامنه سایت و مالک را وارد و ثبت نام نماید.

پس از آنکه بر روی دکمه‌ی ثبت نام کلیک نمودید، برای شما دو کلید جدید را ثبت می‌نماید که منحصر به سایت شماست. Site Key رشته ای را داراست که در کد‌های HTML قرار خواهد گرفت و کلید بعدی Secret Key می‌باشد. ارتباط سایت شما با گوگل می‌بایستی به صورت محرمانه محفوظ بماند.


گام‌های لازم جهت نمایش سرویس در سایت:

  1. دستورات سمت کاربر
  2. دستورات سمت سرور 

دستورات سمت کاربر:

کد زیر را در قبل از بسته شدن تک <head/> قرار دهید:

<script src='https://www.google.com/recaptcha/api.js'></script>
کد زیر را در داخل تگ فرمی که می‌خواهید کپچا نمایش داده شود قرار دهید:
<div data-sitekey="6LdHGgwTAAAAAClKFhGthRrjBXh5AUGd4eWNCQq7"></div>

نکته: مقدار data-sitekey برابر است با رشته Site Key که گوگل برای شما ثبت نمود.



دستورات سمت سرور:

وقتی کاربر فرم حاوی کپچا را که به صورت صحیح هویت سنجی آن انجام شده باشد به سمت سرور ارسال کند، به عنوان بخشی از داده‌ی ارسال شده، یک رشته با نام g-recaptcha-response  با دستور Request دریافت خواهید کرد که به منظور بررسی اینکه آیا گوگل تایید کرده است که کاربر، یک درخواست POST ارسال نمود‌است. با این پارامترها یک مقدار json برگشت داده خواهد شد که می‌بایستی کلاسی متناظر با آن جهت خواندن ساخته شود.

با استفاده از کد زیر مقدار برگشتی Json را در کلاس مپ می‌نمائیم:
using System.Collections.Generic;
using Newtonsoft.Json;

namespace BaseConfig.Security.Captcha
{
    public class RepaptchaResponse
    {
        [JsonProperty("success")]
        public bool Success { get; set; }

        [JsonProperty("error-codes")]
        public List<string> ErrorCodes { get; set; }
    }
}

با استفاده از کلاس زیر درخواستی به گوگل ارسال شده و در صورتیکه با خطا مواجه شود با استفاده از دستور switch به آن دسترسی خواهیم یافت.
using System.Configuration;
using System.Net;
using Newtonsoft.Json;

namespace BaseConfig.Security.Captcha
{
    public class ReCaptcha
    {
        public static string _secret;

        static ReCaptcha()
        {
            _secret = ConfigurationManager.AppSettings["ReCaptchaGoogleSecretKey"];
        }

        public static bool IsValid(string response)
        {
            //secret that was generated in key value pair
            var client = new WebClient();
            var reply = client.DownloadString($"https://www.google.com/recaptcha/api/siteverify?secret={_secret}&response={response}");

            var captchaResponse = JsonConvert.DeserializeObject<RepaptchaResponse>(reply);

            // when response is false check for the error message
            if (!captchaResponse.Success)
            {
                //if (captchaResponse.ErrorCodes.Count <= 0) return View();

                //var error = captchaResponse.ErrorCodes[0].ToLower();
                //switch (error)
                //{
                //    case ("missing-input-secret"):
                //        ViewBag.Message = "The secret parameter is missing.";
                //        break;
                //    case ("invalid-input-secret"):
                //        ViewBag.Message = "The secret parameter is invalid or malformed.";
                //        break;

                //    case ("missing-input-response"):
                //        ViewBag.Message = "The response parameter is missing.";
                //        break;
                //    case ("invalid-input-response"):
                //        ViewBag.Message = "The response parameter is invalid or malformed.";
                //        break;

                //    default:
                //        ViewBag.Message = "Error occured. Please try again";
                //        break;
                //}
                return false;
            }
            // Captcha is valid
            return true;
        }
    }
}

تابع IsValid از نوع برگشتی Boolean بوده و خطایی برگشت داده نخواهد شد و از این جهت به صورت کامنت برای شما گذاشته شده که می‌توان متناظر با کد نویسی آن را تغییر دهید.
در اکشن زیر مقدار response برسی می‌شود تا خالی نباشد و همچنین مقدار آن را می‌توان با استفاده از تابع IsValid در کلاس ReCaptcha به سمت گوگل فرستاد.
        //
        // POST: /Account/Login
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public virtual async Task<ActionResult> Login(LoginPageModel model, string returnUrl)
        {
            var response = Request["g-recaptcha-response"];
            if (response != null && ReCaptcha.IsValid(response))
            {
                // 
            }
         }

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

/**
 * 
 * @param {} data 
 * @returns {} 
 */
function Success(data) {
    grecaptcha.reset();
}

تا اینجا موفق شدیم تا فرم ارسالی همراه کپچا را به سمت سرور ارسال کنیم. اما ممکن است در یک صفحه از چند کپچا استفاده شود که در این صورت می‌بایستی دستورات سمت کاربر تغییر نمایند.

برای این کار دستور
<div data-sitekey="6LdHGgwTAAAAAClKFhGthRrjBXh5AUGd4eWNCQq7"></div>  
که در بالا تعریف شد، به شکل زیر تغییر خواهد کرد:

    <script>
        var recaptcha1;
        var recaptcha2;
        var myCallBack = function () {
            //Render the recaptcha1 on the element with ID "recaptcha1"
            recaptcha1 = grecaptcha.render('recaptcha1', {
                'sitekey': '6Lf9FQwTAAAAAE6XlDqrey24K4xJOPM5nNVBmNO9',
                'theme': 'light'
            });

            //Render the recaptcha2 on the element with ID "recaptcha2"
            recaptcha2 = grecaptcha.render('recaptcha2', {
                'sitekey': '6Lf9FQwTAAAAAE6XlDqrey24K4xJOPM5nNVBmNO9',
                'theme': 'light'
            });

            //Render the recaptcha3 on the element with ID "recaptcha3"
            recaptcha2 = grecaptcha.render('recaptcha3', {
                'sitekey': '6Lf9FQwTAAAAAE6XlDqrey24K4xJOPM5nNVBmNO9',
                'theme': 'light'
            });
        };
    </script>

برای نمایش کپچا، تگ‌های div با id متناظر با recaptcha1, recaptcha2, recaptcha3 ( در این مثال از سه کپچا در صفحه استفاده شده است ) در صفحه قرار خواهند گرفت.

<div id="recaptcha1"></div>
<div id="recaptcha2"></div>
<div id="recaptcha3"></div>

کار ما تمام شد. حال اگر پروژه را اجرا نمائید، در صفحه سه کپچا مشاهده خواهید کرد.


چند زبانه کردن کپچا:

برای چند زبانه کردن کافیست با مراجعه به این لینک و یا استفاده از تصویر بالا ( زبان‌های پشتیبانی ) مقدار آن زبان را برابر با پراپرتی hl که به صورت کوئری استرینگ برای گوگل ارسال می‌گردد، استفاده نمود. کد زیر نمونه‌ی استفاده شده برای زبان‌های انگلیسی و فارسی می‌باشد که با ریسورس مقدار دهی می‌شود.
<script src='https://www.google.com/recaptcha/api.js?hl=@(App_GlobalResources.CP.CurrentAbbrivation)'></script>

در صورتی که از فایل ریسوس استفاده نمی‌کنید می‌توان به صورت مستقیم مقدار دهی نمائید:
<script src='https://www.google.com/recaptcha/api.js?hl=fa'></script>



برای دوستانی که با تکنولوژی ASP.Net کار می‌کنند، این روال هم برای آنها هم صادق می‌باشد.

برای دریافت پروژه اینجا کلیک نمائید.
مطالب
Globalization در ASP.NET MVC - قسمت سوم
قبل از ادامه، بهتر است یک مقدمه کوتاه درباره انواع منابع موجود در ASP.NET ارائه شود تا درک مطالب بعدی آسانتر شود.

نکات اولیه
- یک فایل Resource درواقع یک فایل XML شامل رشته هایی برای ذخیره سازی مقادیر (منابع) موردنیاز است. مثلا رشته هایی برای ترجمه به زبانهای دیگر، یا مسیرهایی برای یافتن تصاویر یا فایلها و ... . پسوند این فایلها resx. است (مثل MyResource.resx).
- این فایلها برای ذخیره منابع از جفت داده‌های کلید-مقدار (key-value pair) استفاده می‌کنند. هر کلید معرف یک ورودی مجزاست. نام این کلیدها حساس به حروف بزرگ و کوچک نیست (Not Case-Sensitive).
- برای هر زبان (مثل fa برای فارسی) یا کالچر موردنظر (مثل fa-IR برای فارسی ایرانی) می‌توان یک فایل Resource جداگانه تولید کرد. عناون زبان یا کالچر باید جزئی از نام فایل Resource مربوطه باشد (مثل MyResource.fa.resx یا MyResource.fa-IR.resx). هر منبع باید دارای یک فایل اصلی (پیش‌فرض) Resource باشد. این فایل، فایلی است که برای حالت پیش‌فرض برنامه (بدون کالچر) تهیه شده است و در عنوان آن از نام زیان یا کالچری استفاده نشده است (مثل MyResource.resx). برای اطلاعات بیشتر به قسمت اول این سری مراجعه کنید.
- تمامی فایل‌های Resource باید دارای کلیدهای یکسان با فایل اصلی Resource باشند. البته لزومی ندارد که این فایل‌ها حاوی تمامی کلیدهای منبع پیش‌فرض باشند. درصورت عدم وجود کلیدی در یک فایل Resource عملیات پیش فرض موجود در دات نت با استفاده از فرایند مشهور به fallback مقدار کلید موردنظر را از نزدیکترین و مناسبترین فایل موجود انتخاب می‌کند (درباره این رفتار در قسمت اول توضیحاتی ارائه شده است).
- در زمان اجرا موتور پیش فرض مدیریت منابع دات نت با توجه به کالچر UI در ثرد جاری اقدام به انتخاب مقدار مناسب برای کلیدهای درخواستی (به همراه فرایند fallback) می‌کند. فرایند نسبتا پیچیده fallback در اینجا شرح داده شده است.

منابع Global و Local
در ASP.NET دو نوع کلی Resource وجود دارد که هر کدام برای موقعیت‌های خاصی مورد استفاده قرار می‌گیرند:

- Resourceهای Global: منابعی کلی هستند که در تمام برنامه در دسترسند. این فایل‌ها در مسیر رزرو شده APP_GlobalResources در ریشه سایت قرار می‌گیرند. محتوای هر فایل resx. موجود در این فولدر دارای دسترسی کلی خواهد بود.

- Resourceهای Local: این منابع همان‌طور که از نامشان پیداست محلی! هستند و درواقع مخصوص همان مسیری هستند که در آن تعبیه شده اند! در استفاده از منابع محلی به ازای هر صفحه وب (aspx. یا master.) یا هر یوزرکنترل (ascx.) یک فایل resx. تولید می‌شود که تنها در همان صفحه یا یوزرکنترل در دسترس است. این فایل‌ها درون فولدر رزرو شده APP_LocalResources در مسیرهای موردنظر قرار می‌گیرند. درواقع در هر مسیری که نیاز به این نوع از منابع باشد، باید فولدری با عنوان App_LocalResources ایجاد شود و فایلهای resx. مرتبط با صفحه‌ها یا یوزرکنترل‌های آن مسیر در این فولدر مخصوص قرار گیرد.
در تصویر زیر چگونگی افزودن این فولدرهای مخصوص به پروژه وب اپلیکیشن نشان داده شده است:

نکته: دقت کنید که تنها یک فولدر App_GlobalResources به هر پروزه می‌توان افزود. همچنین در ریشه هر مسیر موجود در پروژه تنها می‌توان یک فولدر Appp_LocalResources داشت. پس از افزودن هر یک از این فولدرهای مخصوص، منوی فوق به صورت زیر در خواهد آمد:

نکته: البته با تغییر نام یک فولدر معمولی به این نام‌های رزرو شده نتیجه یکسانی بدست خواهد آمد.
 
نکته: در زمان اجرا، عملیات استخراج داده‌های موجود در این نوع منابع، به صورت خودکار توسط ASP.NET انجام می‌شود. این داده‌ها پس از استخراج در حافظه سرور کش خواهند شد.

برای روشن‌تر شدن مطالب اشاره شده در بالا به تصویر فرضی! زیر توجه کنید (اسمبلی‌های تولید شده برای منابع کلی و محلی فرضی است):

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

نکته: نحوه برخورد با این نوع از فایل‌های Resource در پروژه‌های Web Site و Web Application کمی باهم فرق می‌کند. موارد اشاره شده در این مطلب بیشتر درباره Web Applicationها صدق می‌کند.

برای آشنایی بیشتر بهتر است یک برنامه وب اپلیکیشن جدید ایجاد کرده و همانند تصویر زیر یکسری فایل Resource به فولدرهای اشاره شده در بالا اضافه کنید:

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

می‌بینید که خاصیت Build Action آن به Content مقداردهی شده است. این مقدار موجب می‌شود تا این فایل به همین صورت و در همین مسیر مستقیما در پابلیش نهایی برنامه ظاهر شود. در قسمت قبل به خاصیت Buil Action و مقادیر مختلف آن اشاره شده است.
هم‌چنین می‌بینید که مقدار پراپرتی Custom Tool به GlobalResourceProxyGenerator تنظیم شده است. این ابزار مخصوص تولید کلاس مربوط به منابع کلی در ویژوال استودیو است. با استفاده از این ابزار فایل Resource1.Designer.cs که در تصویر قبلی نیز نشان داده شده، تولید می‌شود.
حالا پنجره پراپرتی‌های منبع محلی را باز کنید:

می‌بینید که همانند منبع کلی خاصیت Build Action آن به Content تنظیم شده است. همچنین مقداری برای پراپرتی Custom Tool تنظیم نشده است. این مقدار پیش فرض را تغییر ندهید، چون با تنظیم مقداری برای آن چیز مفیدی عایدتان نمی‌شود! 

نکته: برای به روز رسانی مقادیر کلیدهای منابعی که با توجه به توضیحات بالا به همراه برنامه به صورت فایلهای resx. پابلیش می‌شوند، کافی است تا محتوای فایلهای resx. مربوطه با استفاده از یک ابزار (همانند نمونه ای که در قسمت قبل شرح داده شد) تغییر داده شوند. بقیه عملیات توسط ASP.NET انجام خواهد شد. اما با تغییر محتوای این فایلهای resx. با توجه به رفتار FCN در ASP.NET (که در قسمت قبل نیز توضیح داده شد) سایت Restart خواهد شد. البته این روش تنها برای منابع کلی و محلی درون مسیرهای مخصوص اشاره شده کار خواهد کرد.

استفاده از منابع Local و Global
پس از تولید فایل‌های Resource، می‌توان از آن‌ها در صفحات وب استفاده کرد. معمولا از این نوع منابع برای مقداردهی پراپرتی کنترل‌ها در صفحات وب استفاده می‌شود. برای استفاده از کلیدهای منابع محلی می‌توان از روشی همانند زیر بهره برد:
<asp:Label ID="lblLocal" runat="server" meta:resourcekey="lblLocalResources" ></asp:Label> 
اما برای منابع کلی تنها می‌توان از روش زیر استفاده کرد (یعنی برای منابع محلی نیز می‌توان از این روش استفاده کرد):
<asp:Label ID="lblGlobal" runat="server" Text="<%$ Resources:CommonTerms, HelloText %>" ></asp:Label> 
به این عبارات که با فوت پررنگ مشخص شده اند اصطلاحا «عبارات بومی‌سازی» (Localization Expression) می‌گویند. در ادامه این سری مطالب با نحوه تعریف نمونه‌های سفارشی آن آشنا خواهیم شد.
به نمونه اول که برای منابع محلی استفاده می‌شود نوع ضمنی (Implicit Localization Expression) می‌گویند. زیرا نیازی نیست تا محل کلید موردنظر صراحتا ذکر شود!
به نمونه دوم که برای منابع کلی استفاه می‌شود نوع صریح (Explicit Localization Expression) می‌گویند. زیرا برای یافتن کلید موردنظر باید آدرس دقیق آن ذکر شود!

بومی سازی ضمنی (Implicit Localization) با منابع محلی
عنوان کلید مربوطه در این نوع عبارات همانطور که در بالا نشان داده شده است، با استفاده از پراپرتی مخصوص meta:resoursekey مشخص می‌شود. در استفاده از منابع محلی تنها یک نام برای کل خواص کنترل مربوطه در صفحات وب کفایت می‌کند. زیرا عنوان کلیدهای این منبع باید از طرح زیر پیروی کند:
ResourceKey.Property
ResourceKey.Property.SubProperty    یا    ResourceKey.Property-SubProperty
برای مثال در لیبل بالا که نام کلید Resource آن به lblLocalResources تنظیم شده است، اگر نام صفحه وب مربوطه page1.aspx باشد، برای تنظیم خواص آن در فایل page1.aspx.resx مربوطه باید از کلیدهایی با عناوینی مثل عنوان‌های زیر استفاه کرد:
lblLocalResources.Text
lblLocalResources.BackColor
برای نمونه به تصاویر زیر دقت کنید:


بومی سازی صریح (Explicit Localization)
در استفاده از این نوع عبارات، پراپرتی مربوطه و نام فایل منبع صراحتا در تگ کنترل مربوطه آورده می‌شود. بنابراین برای هر خاصیتی که می‌خواهیم مقدار آن از منبعی خاص گرفته شود باید از عبارتی با طرح زیر استفاده کنیم:
<%$ Resources: Class, ResourceKey %>
در این عبارت، رشته Resources پیشوند (Prefix) نام دارد و مشخص کننده استفاده از نوع صریح عبارات بومی سازی است. Class نام کلاس مربوط به فایل منبع بوده و اختیاری است که تنها برای منابع کلی باید آورده شود. ResourceKey نیز کلید مربوطه را در فایل منبع مشخص می‌کند.
برای نمونه به تصاویر زیر دقت کنید:


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

نکته: به دلیل تولید کلاسی مخصوص منابع کلی (با توجه به توضیحات ابتدای این مطلب راجع به پراپرتی Custom Tool)، امکان استفاده مستقیم از آن درون کد نیز وجود دارد. این کلاسها که به صورت خودکار تولید می‌شوند، به صورت مستقیم از کلاس ResourceManager برای یافتن کلیدهای منابع استفاده می‌کنند. اما روش مستقیمی برای استفاده از کلیدهای منابع محلی درون کد وجود ندارد. 

نکته: درون کلاس System.Web.UI.TemplateControl و نیز کلاس HttpContext دو متد با نامهای GetGlobalResourceObject و GetLocalResourceObject وجود دارد که برای یافتن کلیدهای منابع به صورت غیرمستقیم استفاده می‌شوند. مقدار برگشتی این دو متد از نوع object است. این دو متد به صورت مستقیم از کلاس ResourceManager استفاده نمیکنند! هم‌چنین ازآنجاکه کلاس Page از کلاس TemplateControl مشتق شده است، بنابراین این دو متد در صفحات وب در دسترس هستند.

دسترسی با برنامه نویسی
همانطور که در بالا اشاره شد امکان دستیابی به کلیدهای منابع محلی و کلی ازطریق دو متد GetGlobalResourceObject و GetLocalResourceObject نیز امکان پذیر است. این دو متد با فراخوانی ResourceProviderFactory جاری سعی در یافتن مقادیر کلیدهای درخواستی در منابع موجود می‌کنند. درباره این فرایند در مطالب بعدی به صورت مفصل بحث خواهد شد.

کلاس TemplateControl
این دو متد در کلاس TemplateControl از نوع Instance (غیر استاتیک) هستند. امضای (Signature) این دو متد در این کلاس به صورت زیر است:

متد GetLocalResourceObject:
protected object GetLocalResourceObject(string resourceKey)
protected object GetLocalResourceObject(string resourceKey, Type objType, string propName)
در متد اول، پارامتر resourceKey در متد GetLocalResourceObject معرف کلید منبع مربوطه در فایل منبع محلی متناظر با صفحه جاری است. مثلا lblLocalResources.Text. ازآنجاکه به صورت پیش‌فرض موقعیت فایل منبع محلی مرتبط با صفحات وب مشخص است بنابراین تنها ارائه کلید مربوطه برای یافتن مقدار آن کافی است. مثال:
txtTest.Text = GetLocalResourceObject("txtTest.Text") as string;
متد دوم برای استخراج کلیدهای منبع محلی با مشخص کردن نوع داده محتوا (معمولا برای داده‌های غیر رشته‌ای) و پراپرتی موردنظر به کار می‌رود. در این متد پارامتر objType برای معرفی نوع داده متناظر با داده موجود در کلید resourceKey استفاده می‌شود. از پارامتر propName نیز همانطور که از نامش پیداست برای مشخص کردن پراپرتی موردنظر از این نوع داده معرفی شده استفاده می‌شود.

متد GetGlobalResourceObject:
protected object GetGlobalResourceObject(string className, string resourceKey)
protected object GetGlobalResourceObject(string className, string resourceKey, Type objType, string propName)
در این دو متد، پارامتر className مشخص کننده نام کلاس متناظر با فایل منبع اصلی (فایل منبع اصلی که کلاس مربوطه با نام آن ساخته می‌شود) است. سایر پارامترها همانند دو متد قبلی است. مثال:
TextBox1.Text = GetGlobalResourceObject("Resource1", "String1") as string;

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

متد GetLocalResourceObject: 
public static object GetLocalResourceObject(string virtualPath, string resourceKey)
public static object GetLocalResourceObject(string virtualPath, string resourceKey, CultureInfo culture)
در این دو متد، پارامتر virtualPath مشخص کننده مسیر نسبی صفحه وب متناظر با فایل منبع محلی موردنظر است، مثل "Default.aspx/~". پارامتر resourceKey نیز کلید منبع را تعیین می‌کند و پارامتر culture نیز به کالچر موردنظر اشاره دارد. مثال:
txtTest.Text = HttpContext.GetLocalResourceObject("~/Default.aspx", "txtTest.Text") as string;
 
متد GetGlobalResourceObject:
public static object GetGlobalResourceObject(string classKey, string resourceKey)
public static object GetGlobalResourceObject(string classKey, string resourceKey, CultureInfo culture)
در این دو متد، پارامتر className مشخص کننده نام کلاس متناظر با فایل منبع اصلی (فایل منبع بدون نام زبان که کلاس مربوطه با نام آن ساخته می‌شود) است. سایر پارامترها همانند دو متد قبلی است. مثال:
TextBox1.Text = HttpContext.GetGlobalResourceObject("Resource1", "String1") as string;
 
نکته: بدیهی است که در MVC تنها می‌توان از متدهای کلاس HttpContext استفاده کرد.

روش دیگری که تنها برای منابع کلی در دسترس است، استفاده مستقیم از کلاسی است که به صورت خودکار توسط ابزارهای Visual Studio برای فایل منبع اصلی تولید می‌شود. نمونه‌ای از این کلاس را که برای یک فایل Resource1.resx (که تنها یک ورودی با نام String1 دارد) در پوشه App_GlobalResources تولید شده است، در زیر مشاهده می‌کنید:
//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Runtime Version:4.0.30319.17626
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

namespace Resources {
    using System;
    
    /// <summary>
    ///   A strongly-typed resource class, for looking up localized strings, etc.
    /// </summary>
    // This class was auto-generated by the StronglyTypedResourceBuilder
    // class via a tool like ResGen or Visual Studio.
    // To add or remove a member, edit your .ResX file then rerun ResGen
    // with the /str option or rebuild the Visual Studio project.
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Web.Application.StronglyTypedResourceProxyBuilder", "10.0.0.0")]
    [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
    internal class Resource1 {
        
        private static global::System.Resources.ResourceManager resourceMan;
        
        private static global::System.Globalization.CultureInfo resourceCulture;
        
        [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
        internal Resource1() {
        }
        
        /// <summary>
        ///   Returns the cached ResourceManager instance used by this class.
        /// </summary>
        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
        internal static global::System.Resources.ResourceManager ResourceManager {
            get {
                if (object.ReferenceEquals(resourceMan, null)) {
                    global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Resources.Resource1", global::System.Reflection.Assembly.Load("App_GlobalResources"));
                    resourceMan = temp;
                }
                return resourceMan;
            }
        }
        
        /// <summary>
        ///   Overrides the current thread's CurrentUICulture property for all
        ///   resource lookups using this strongly typed resource class.
        /// </summary>
        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
        internal static global::System.Globalization.CultureInfo Culture {
            get {
                return resourceCulture;
            }
            set {
                resourceCulture = value;
            }
        }
        
        /// <summary>
        ///   Looks up a localized string similar to String1.
        /// </summary>
        internal static string String1 {
            get {
                return ResourceManager.GetString("String1", resourceCulture);
            }
        }
    }
}

نکته: فضای نام پیش‌فرض برای منابع کلی در این کلاس‌ها همیشه Resources است که برابر پیشوند (Prefix) عبارت بومی سازی صریح است.

نکته: در کلاس بالا نحوه نمونه سازی کلاس ResourceManager نشان داده شده است. همانطور که مشاهده می‌کنید تعیین کردن مشخصات فایل اصلی Resource مربوطه که در اسمبلی نهایی تولید و کش می‌شود، اجباری است! در مطلب بعدی با این کلاس بیشتر آشنا خواهیم شد.

نکته: همانطور که قبلا نیز اشاره شد، کار تولید اسمبلی مربوط به فایل‌های منابع کلی و محلی و کش کردن آن‌ها در اسمبلی در زمان اجرا کاملا بر عهده ASP.NET است. مثلا در نمونه کد بالا می‌بینید که کلاس ResourceManager برای استخراج نوع Resources.Resource1 از اسمبلی App_GlobalResources نمونه‌سازی شده است، با اینکه این اسمبلی و نوع مذبور در زمان کامپایل و پابلیش وجود ندارد!

برای استفاده از این کلاس می‌توان به صورت زیر عمل کرد:
TextBox1.Text = Resources.Resource1.String1;

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

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

نکته: فایل تولیدی توسط ویژوال استودیو در فرایند مدیریت منابع ASP.NET تاثیرگذار نیست! باز هم تاکید می‌کنم که کار استخراج کلیدهای Resource از درون فایلهای resx. کاملا به صورت جداگانه و خودکار و در زمان اجرا انجام می‌شود (درباره این فرایند در مطالب بعدی شرح مفصلی خواهد آمد). درواقع شما می‌توانید خاصیت Custom Tool مربوط به منابع کلی را نیز همانند منابع محلی به رشته‌ای خالی مقداردهی کنید و ببینید که خللی در فرایند مربوطه رخ نخواهد داد!

نکته: تنها برای حالتی که بخواهید از روش آخری که در بالا اشاره شد برای دسترسی با برنامه‌نویسی به منابع کلی بهره ببرید (روش مستقیم)، به این کلاس تولیدی توسط ویژوال استودیو نیاز خواهید داشت. دقت کنید که در این کلاس نیز کار اصلی برعهده کلاس ResourceManager است. درواقع می‌توان کلا از این فایل خودکارتولیدشده صرفنظر کرد و کار استخراج کلیدهای منابع را به صورت مستقیم به نمونه‌ای از کلاس ResourceManager سپرد. این روش نیز در قسمت‌های بعدی شرح داده خواهد شد.

نکته: اگر فایل‌های Resource درون اسمبلی‌های جداگانه‌ای باشند (مثلا در یک پروژه جداگانه، همانطور که در قسمت اول این سری مطالب پیشنهاد شده است)، موتور پیش فرض منابع در ASP.NET بدرد نخواهد خورد! بنابراین یا باید از نمونه‌های اختصاصی کلاس ResourceManager استفاده کرد (کاری که کلاس‌های خودکار تولیدشده توسط ابزارهای ویژوال استودیو انجام می‌دهند)، یا باید از پرووایدرهای سفارشی استفاده کرد که در مطالب بعدی نحوه تولید آن‌ها شرح داده خواهد شد.
 
همانطور که در ابتدای این مطلب اشاره شد، این مقدمه در اینجا صرفا برای آشنایی بیشتر با این دونوع Resource آورده شده تا ادامه مطلب روشن‌تر باشد، زیرا با توجه به مطالب ارائه شده در قسمت اول این سری، در پروژه‌های MVC استفاده از یک پروژه جداگانه برای نگهداری این منابع راه حل مناسبتری است.
در مطلب بعدی به شرح نحوه تولید پرووایدرهای سفارشی می‌پردازم.
نظرات اشتراک‌ها
پروژه SmartUnderline
- کاری به ASP.NET MVC سمت سرور ندارد.
- مثال آن در اینجا ارائه شده. روی آن کلیک راست کنید و سورس آن‌را مشاهده کنید. اسکریپت آن باید الحاق شود و متد init دارد در ابتدای کار. همچنین فایل‌های CSS خاصی هم نیاز دارد. برای مشاهده‌ی یکجای فایل‌های CSS آن از افزونه‌ی web developer کمک بگیرید. این افزونه قابلیت نمایش یکجای فایل‌های اسکریپت آن صفحه را هم دارد. کلا برای مهندسی معکوس این نوع صفحات گنگ بسیار مفید است.
مطالب
آشنایی با NHibernate - قسمت چهارم

در این قسمت یک مثال ساده از insert ، load و delete را بر اساس اطلاعات قسمت‌های قبل با هم مرور خواهیم کرد. برای سادگی کار از یک برنامه Console استفاده خواهد شد (هر چند مرسوم شده است که برای نوشتن آزمایشات از آزمون‌های واحد بجای این نوع پروژه‌ها استفاده شود). همچنین فرض هم بر این است که database schema برنامه را مطابق قسمت قبل در اس کیوال سرور ایجاد کرده اید (نکته آخر بحث قسمت سوم).

یک پروژه جدید از نوع کنسول را به solution برنامه (همان NHSample1 که در قسمت‌های قبل ایجاد شد)، اضافه نمائید.
سپس ارجاعاتی را به اسمبلی‌های زیر به آن اضافه کنید:
FluentNHibernate.dll
NHibernate.dll
NHibernate.ByteCode.Castle.dll
NHSample1.dll : در قسمت‌های قبل تعاریف موجودیت‌ها و نگاشت‌ آن‌ها را در این پروژه class library ایجاد کرده بودیم و اکنون قصد استفاده از آن را داریم.

اگر دیتابیس قسمت قبل را هنوز ایجاد نکرده‌اید، کلاس CDb را به برنامه افزوده و سپس متد CreateDb آن‌را به برنامه اضافه نمائید.

using FluentNHibernate;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using NHSample1.Mappings;

namespace ConsoleTestApplication
{
class CDb
{
public static void CreateDb(IPersistenceConfigurer dbType)
{
var cfg = Fluently.Configure().Database(dbType);

PersistenceModel pm = new PersistenceModel();
pm.AddMappingsFromAssembly(typeof(CustomerMapping).Assembly);
var sessionSource = new SessionSource(
cfg.BuildConfiguration().Properties,
pm);

var session = sessionSource.CreateSession();
sessionSource.BuildSchema(session, true);
}
}
}
اکنون برای ایجاد دیتابیس اس کیوال سرور بر اساس نگاشت‌های قسمت قبل، تنها کافی است دستور ذیل را صادر کنیم:

CDb.CreateDb(
MsSqlConfiguration
.MsSql2008
.ConnectionString("Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true")
.ShowSql());

تمامی جداول و ارتباطات مرتبط در دیتابیسی که در کانکشن استرینگ فوق ذکر شده است، ایجاد خواهد شد.

در ادامه یک کلاس جدید به نام Config را به برنامه کنسول ایجاد شده اضافه کنید:

using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using NHibernate;
using NHSample1.Mappings;

namespace ConsoleTestApplication
{
class Config
{
public static ISessionFactory CreateSessionFactory(IPersistenceConfigurer dbType)
{
return
Fluently.Configure().Database(dbType
).Mappings(m => m.FluentMappings.AddFromAssembly(typeof(CustomerMapping).Assembly))
.BuildSessionFactory();
}
}
}
اگر بحث را دنبال کرده باشید، این کلاس را پیشتر در کلاس FixtureBase آزمون واحد خود، به نحوی دیگر دیده بودیم. برای کار با NHibernate‌ نیاز به یک سشن مپ شده به موجودیت‌های برنامه می‌باشد که توسط متد CreateSessionFactory کلاس فوق ایجاد خواهد شد. این متد را به این جهت استاتیک تعریف کرده‌ایم که هیچ نوع وابستگی به کلاس جاری خود ندارد. در آن نوع دیتابیس مورد استفاده ( برای مثال اس کیوال سرور 2008 یا هر مورد دیگری که مایل بودید)، به همراه اسمبلی حاوی اطلاعات نگاشت‌های برنامه معرفی شده‌اند.

اکنون سورس کامل مثال برنامه را در نظر بگیرید:

کلاس CDbOperations جهت اعمال ثبت و حذف اطلاعات:

using System;
using NHibernate;
using NHSample1.Domain;

namespace ConsoleTestApplication
{
class CDbOperations
{
ISessionFactory _factory;

public CDbOperations(ISessionFactory factory)
{
_factory = factory;
}

public int AddNewCustomer()
{
using (ISession session = _factory.OpenSession())
{
using (ITransaction transaction = session.BeginTransaction())
{
Customer vahid = new Customer()
{
FirstName = "Vahid",
LastName = "Nasiri",
AddressLine1 = "Addr1",
AddressLine2 = "Addr2",
PostalCode = "1234",
City = "Tehran",
CountryCode = "IR"
};

Console.WriteLine("Saving a customer...");

session.Save(vahid);
session.Flush();//چندین عملیات با هم و بعد

transaction.Commit();

return vahid.Id;
}
}
}

public void DeleteCustomer(int id)
{
using (ISession session = _factory.OpenSession())
{
using (ITransaction transaction = session.BeginTransaction())
{
Customer customer = session.Load<Customer>(id);
Console.WriteLine("Id:{0}, Name: {1}", customer.Id, customer.FirstName);

Console.WriteLine("Deleting a customer...");
session.Delete(customer);

session.Flush();//چندین عملیات با هم و بعد

transaction.Commit();
}
}
}
}
}
و سپس استفاده از آن در برنامه

using System;
using FluentNHibernate.Cfg.Db;
using NHibernate;
using NHSample1.Domain;

namespace ConsoleTestApplication
{
class Program
{
static void Main(string[] args)
{
//CDb.CreateDb(SQLiteConfiguration.Standard.ConnectionString("data source=sample.sqlite").ShowSql());
//return;

//todo: Read ConnectionString from app.config or web.config
using (ISessionFactory session = Config.CreateSessionFactory(
MsSqlConfiguration
.MsSql2008
.ConnectionString("Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true")
.ShowSql()
))
{
CDbOperations db = new CDbOperations(session);
int id = db.AddNewCustomer();
Console.WriteLine("Loading a customer and delete it...");
db.DeleteCustomer(id);
}

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}
توضیحات:
نیاز است تا ISessionFactory را برای ساخت سشن‌های دسترسی به دیتابیس ذکر شده در تنظمیات آن جهت استفاده در تمام تردهای برنامه، ایجاد نمائیم. لازم به ذکر است که تا قبل از فراخوانی BuildSessionFactory این تنظیمات باید معرفی شده باشند و پس از آن دیگر اثری نخواهند داشت.
ایجاد شیء ISessionFactory هزینه بر است و گاهی بر اساس تعداد کلاس‌هایی که باید مپ شوند، ممکن است تا چند ثانیه به طول انجامد. به همین جهت نیاز است تا یکبار ایجاد شده و بارها مورد استفاده قرار گیرد. در برنامه به کرات از using استفاده شده تا اشیاء IDisposable را به صورت خودکار و حتمی، معدوم نماید.

بررسی متد AddNewCustomer :
در ابتدا یک سشن را از ISessionFactory موجود درخواست می‌کنیم. سپس یکی از بهترین تمرین‌های کاری جهت کار با دیتابیس‌ها ایجاد یک تراکنش جدید است تا اگر در حین اجرای کوئری‌ها مشکلی در سیستم، سخت افزار و غیره پدید آمد، دیتابیسی ناهماهنگ حاصل نشود. زمانیکه از تراکنش استفاده شود، تا هنگامیکه دستور transaction.Commit آن با موفقیت به پایان نرسیده باشد، اطلاعاتی در دیتابیس تغییر نخواهد کرد و از این لحاظ استفاده از تراکنش‌ها جزو الزامات یک برنامه اصولی است.
در ادامه یک وهله از شیء Customer را ایجاد کرده و آن‌را مقدار دهی می‌کنیم (این شیء در قسمت‌های قبل ایجاد گردید). سپس با استفاده از session.Save دستور ثبت را صادر کرده، اما تا زمانیکه transaction.Commit فراخوانی و به پایان نرسیده باشد، اطلاعاتی در دیتابیس ثبت نخواهد شد.
نیازی به ذکر سطر فلاش در این مثال نبود و NHibernate اینکار را به صورت خودکار انجام می‌دهد و فقط از این جهت عنوان گردید که اگر چندین عملیات را با هم معرفی کردید، استفاده از session.Flush سبب خواهد شد که رفت و برگشت‌ها به دیتابیس حداقل شود و فقط یکبار صورت گیرد.
در پایان این متد، Id ثبت شده در دیتابیس بازگشت داده می‌شود.

چون در متد CreateSessionFactory ، متد ShowSql را نیز ذکر کرده بودیم، هنگام اجرای برنامه، عبارات SQL ایی که در پشت صحنه توسط NHibernate تولید می‌شوند را نیز می‌توان مشاهده نمود:



بررسی متد DeleteCustomer :
ایجاد سشن و آغاز تراکنش آن همانند متد AddNewCustomer است. سپس در این سشن، یک شیء از نوع Customer با Id ایی مشخص load‌ خواهد گردید. برای نمونه، نام این مشتری نیز در کنسول نمایش داده می‌شود. سپس این شیء مشخص و بارگذاری شده را به متد session.Delete ارسال کرده و پس از فراخوانی transaction.Commit ، این مشتری از دیتابیس حذف می‌شود.

برای نمونه خروجی SQL پشت صحنه این عملیات که توسط NHibernate مدیریت می‌شود، به صورت زیر است:

Saving a customer...
NHibernate: select next_hi from hibernate_unique_key with (updlock, rowlock)
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 17, @p1 = 16
NHibernate: INSERT INTO [Customer] (FirstName, LastName, AddressLine1, AddressLine2, PostalCode, City, CountryCode, Id) VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7);@p0 = 'Vahid', @p1 = 'Nasiri', @p2 = 'Addr1', @p3 = 'Addr2', @p4 = '1234', @p5 = 'Tehran', @p6 = 'IR', @p7 = 16016
Loading a customer and delete it...
NHibernate: SELECT customer0_.Id as Id2_0_, customer0_.FirstName as FirstName2_0_, customer0_.LastName as LastName2_0_, customer0_.AddressLine1 as AddressL4_2_0_, customer0_.AddressLine2 as AddressL5_2_0_, customer0_.PostalCode as PostalCode2_0_, customer0_.City as City2_0_, customer0_.CountryCode as CountryC8_2_0_ FROM [Customer] customer0_ WHERE customer0_.Id=@p0;@p0 = 16016
Id:16016, Name: Vahid
Deleting a customer...
NHibernate: DELETE FROM [Customer] WHERE Id = @p0;@p0 = 16016
Press a key...
استفاده از دیتابیس SQLite بجای SQL Server در مثال فوق:

فرض کنید از هفته آینده قرار شده است که نسخه سبک و تک کاربره‌ای از برنامه ما تهیه شود. بدیهی است SQL server برای این منظور انتخاب مناسبی نیست (هزینه بالا برای یک مشتری، مشکلات نصب، مشکلات نگهداری و امثال آن برای یک کاربر نهایی و نه یک سازمان بزرگ که حتما ادمینی برای این مسایل در نظر گرفته می‌شود).
اکنون چه باید کرد؟ باید برنامه را از صفر بازنویسی کرد یا قسمت دسترسی به داده‌های آن‌را کلا مورد باز بینی قرار داد؟ اگر برنامه اسپاگتی ما اصلا لایه دسترسی به داده‌ها را نداشت چه؟! همه جای برنامه پر است از SqlCommand و Open و Close ! و عملا استفاده از یک دیتابیس دیگر یعنی باز نویسی کل برنامه.
همانطور که ملاحظه می‌کنید، زمانیکه با NHibernate کار شود، مدیریت لایه دسترسی به داده‌ها به این فریم ورک محول می‌شود و اکنون برای استفاده از دیتابیس SQLite تنها باید تغییرات زیر صورت گیرد:
ابتدا ارجاعی را به اسمبلی System.Data.SQLite.dll اضافه نمائید (تمام این اسمبلی‌های ذکر شده به همراه مجموعه FluentNHibernate ارائه می‌شوند). سپس:
الف) ایجاد یک دیتابیس خام بر اساس کلاس‌های domain و mapping تعریف شده در قسمت‌های قبل به صورت خودکار

CDb.CreateDb(SQLiteConfiguration.Standard.ConnectionString("data source=sample.sqlite").ShowSql());
ب) تغییر آرگومان متد CreateSessionFactory

//todo: Read ConnectionString from app.config or web.config
using (ISessionFactory session = Config.CreateSessionFactory(
SQLiteConfiguration.Standard.ConnectionString("data source=sample.sqlite").ShowSql()
))
{
...

نمایی از دیتابیس SQLite تشکیل شده پس از اجرای متد قسمت الف ، در برنامه Lita :




دریافت سورس برنامه تا این قسمت

نکته:
در سه قسمت قبل، تمام خواص پابلیک کلاس‌های پوشه domain را به صورت معمولی و متداول معرفی کردیم. اگر نیاز به lazy loading در برنامه وجود داشت، باید تمامی کلاس‌ها را ویرایش کرده و واژه کلیدی virtual را به کلیه خواص پابلیک آن‌ها اضافه کرد. علت هم این است که برای عملیات lazy loading ، فریم ورک NHibernate باید یک سری پروکسی را به صورت خودکار جهت کلاس‌های برنامه ایجاد نماید و برای این امر نیاز است تا بتواند این خواص را تحریف (override) کند. به همین جهت باید آن‌ها را به صورت virtual تعریف کرد. همچنین تمام سطرهای Not.LazyLoad نیز باید حذف شوند.

ادامه دارد ...


نظرات مطالب
نحوه اضافه کردن Auto-Complete به جستجوی لوسین در ASP.NET MVC و Web forms
این موارد را باید با اصلاح اسکریپت یا css مربوط به auto-complete مدیریت کنید.
مثلا محل قرارگیری منوی بازشونده به صورت زیر مقدار دهی شده. این را در فایل jquery.autocomplete.js یافته و اصلاح کنید:
left: offset.left - options.width + 125
یا تصویر loading در css به نحو زیر تعیین شده:
.ac_loading
{
background: white url('Images/indicator.gif') left center no-repeat;
}
مطالب
افزونه rtlColResizable جهت تغییر سایز ستون‌های جدول‌های راست به چپ HTML
برای تغییر سایز ستون‌های جداول HTML با استفاده از ماوس، افزونه‌های زیادی تدارک دیده شده است که از جمله مطرح‌ترین آن‌ها می‌توان به colResizable اشاره کرد. حتی اگر از DataGrid‌های مطرح وب هم استفاده کرده باشید، اکثر آن‌ها از تغییر سایز ستون‌ها توسط کاربر پشتیبانی می‌کنند. اما مشکل بزرگی که در همه‌ی آن‌ها مشترک است  این است که فقط از چیدمان‌های چپ به راست پشتیبانی می‌کنند و به محض اینکه شما ساختار راست به چپ را به جدول مورد نظر اعمال کنید، عملکرد تغییر سایز ستون‌ها با اشکال مواجه می‌شود.
به همین جهت برای تغییر سایز ستون‌ها توسط کاربر، افزونه ای برای jQuery تدارک دیدم که با جداول معمول HTML که همان table‌ها هستند سازگار است و به راحتی به هر جدولی می‌توان آن را اعمال کرد.
 
کدهای افزونه‌ی rtlColResizable:
(function ($, undefined) {

    $.fn.extend({
        'rtlColResizable': function (options) {
            var defaults = {
                //Default values for the plugin's options here
            };

            options = $.extend(defaults, options);

            var isMouseButtonPressed;
            var $resizingElement = undefined;
            var resizingElementStartWidth;
            var mouseCursorStartX;
            var isCursorInResizingPosition;

            var addResizingCursorStyle = function ($element) {
                $element.css({
                    'cursor': 'col-resize',
                    'user-select': 'none',
                    '-o-user-select': 'none',
                    '-ms-user-select': 'none',
                    '-moz-user-select': 'none',
                    '-khtml-user-select': 'none',
                    '-webkit-user-select': 'none',
                });
            };

            var removeResizingCursorStyle = function ($element) {
                $element.css({
                    'cursor': 'default',
                    'user-select': 'text',
                    '-o-user-select': 'text',
                    '-ms-user-select': 'text',
                    '-moz-user-select': 'text',
                    '-khtml-user-select': 'text',
                    '-webkit-user-select': 'text',
                });
            };

            var canResize = function (e) {
                return (e.offsetX || e.clientX - $(e.target).offset().left) < 10;
            };

            return this.each(function () {

                var opts = options;
                var tableColumns = $(this).find('th');

                tableColumns.filter(':not(:last-child)').mousedown(function (e) {

                    $resizingElement = $(this);
                    isMouseButtonPressed = true;
                    mouseCursorStartX = e.pageX;
                    resizingElementStartWidth = $resizingElement.width();

                });

                tableColumns.mousemove(function (e) {

                    if (canResize(e)) {
                        addResizingCursorStyle($(e.target));
                        isCursorInResizingPosition = true;

                    } else if (!isMouseButtonPressed) {
                        removeResizingCursorStyle($(e.target));
                        isCursorInResizingPosition = false;
                    }

                    if (isCursorInResizingPosition && isMouseButtonPressed) {

                        $resizingElement.width(resizingElementStartWidth + (mouseCursorStartX - e.pageX));
                    }

                });

                $(document).mouseup(function () {
                    if (isMouseButtonPressed) {
                        removeResizingCursorStyle($resizingElement);
                        isMouseButtonPressed = false;
                    }

                });

            });
        }
    });

})(jQuery);
 
نحوه‌ی استفاده:
این افزونه با تگ Table در HTML سازگار است. فقط تنها موردی که باید رعایت شود این است که در هنگام تعریف ساختار جدول، باید استاندارد تعریف ستون‌ها یا همان Header‌ها را رعایت کنید و از تگ‌های thead و th استفاده کنید.
نمونه ای از نحوه‌ی استفاده از آن را در کدهای زیر می‌بینید:
<!DOCTYPE html>
<html>
<head>
    <title>rtlColResizable Sample</title>
</head>
<body>
    <table id="myTable">
        <thead>
            <tr>
                <th>ستون1</th> <!-- نام ستون‌ها را حتما در این تگ باید تعریف کنید -->
                <th>ستون2</th>
                <th>ستون3</th>
            </tr>
        </thead>
        <tr>
            <td>داده مربوط به ستون 1</td>
            <td>داده مربوط به ستون 2 </td>
            <td>داده مربوط به ستون 3</td>
        </tr>
    </table>

    <script type="text/javascript" src="jquery-1.9.1.min.js"></script>
    <script type="text/javascript" src="jquery.rtlColResizable.js"></script>
    <script>

        $('table#myTable').rtlColResizable();

    </script>
</body>
</html>
     
دریافت نمونه کدی از نحوه‌ی استفاده از این افزونه
 
مطالب
کار با SignalR Core از طریق یک کلاینت Angular
نگارش AspNetCore.SignalR 1.0.0-alpha1-final چند روزی هست که منتشر شده‌است. در این مطلب قصد داریم یک برنامه‌ی وب ASP.NET Core 2.0 را به همراه یک Hub ایجاد کرده و سپس این Hub را در یک کلاینت Angular (2+) مورد استفاده قرار دهیم.


پیشنیازها

برای دنبال کردن این مثال فرض بر این است که NET Core 2.0 SDK. و همچنین Angular CLI را نیز پیشتر نصب کرده‌اید. مابقی بحث توسط خط فرمان و ابزارهای dotnet cli و angular cli ادامه داده خواهند شد و الزامی به نصب هیچگونه IDE نیست و این مثال تنها توسط VSCode پیگیری شده‌است.


تدارک ساختار ابتدایی مثال جاری


ساخت برنامه‌ی وب، توسط dotnet cli
ابتدا یک پوشه‌ی جدید را به نام SignalRCore2Sample ایجاد می‌کنیم. سپس داخل این پوشه، پوشه‌ی دیگری را به نام SignalRCore2WebApp ایجاد خواهیم کرد (تصویر فوق). از طریق خط فرمان به این پوشه وارد شده (در ویندوز، در نوار آدرس، دستور cmd.exe را تایپ و enter کنید) و سپس فرمان ذیل را صادر می‌کنیم:
 dotnet new mvc
این دستور، یک برنامه‌ی جدید ASP.NET Core 2.0 را تولید خواهد کرد.

ساخت برنامه‌ی کلاینت، توسط angular cli
سپس از طریق خط فرمان به پوشه‌ی SignalRCore2Sample بازگشته و دستور ذیل را صادر می‌کنیم:
 ng new SignalRCore2Client
این دستور، یک برنامه‌ی Angular را در پوشه‌ی SignalRCore2Client تولید می‌کند (تصویر فوق).

اکنون که در پوشه‌ی ریشه‌ی SignalRCore2Sample قرار داریم، اگر در خط فرمان، دستور . code را صادر کنیم، VSCode هر دو پوشه‌ی وب و client را با هم در اختیار ما قرار می‌دهد:


تکمیل پیشنیازهای برنامه‌ی وب

پس از ایجاد ساختار اولیه‌ی برنامه‌های وب ASP.NET Core و کلاینت Angular، اکنون نیاز است وابستگی جدید AspNetCore.SignalR را به آن معرفی کنیم. به همین جهت به فایل SignalRCore2WebApp.csproj مراجعه کرده و تغییرات ذیل را به آن اعمال می‌کنیم:
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>netcoreapp2.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
    <PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.0.0-alpha1-final" />
  </ItemGroup>
  <ItemGroup>
    <DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0" />
    <DotNetCliToolReference Include="Microsoft.DotNet.Watcher.Tools" Version="2.0.0" />
  </ItemGroup>
</Project>
در اینجا ابتدا بسته‌ی Microsoft.AspNetCore.SignalR اضافه شده‌است. همچنین Microsoft.DotNet.Watcher.Tools را نیز اضافه کرده‌ایم تا بتوان از مزیت build تدریجی پروژه، به ازای هر تغییر صورت گرفته، استفاده کنیم.
پس از این تغییرات، دستور ذیل را در خط فرمان صادر می‌کنیم تا وابستگی‌های پروژه نصب شوند:
 dotnet restore
البته اگر افزونه‌ی #C مخصوص VSCode را نصب کرده باشید، تغییرات فایل csproj را دنبال کرده و پیام restore را نیز ظاهر می‌کند؛ تا همین دستور فوق را به صورت خودکار اجرا کند.
یک نکته: نگارش فعلی افزونه‌ی #C مخصوص VSCode، با تغییر فایل csproj و restore وابستگی‌های آن نیاز دارد یکبار آن‌را بسته و سپس مجددا اجرا کنید، تا اطلاعات intellisense خود را به روز رسانی کند. بنابراین اگر VSCode بلافاصله کلاس‌های مرتبط با بسته‌های جدید را تشخیص نمی‌دهد، علت صرفا این موضوع است.

پس از بازیابی وابستگی‌ها، به ریشه‌ی پروژه‌ی برنامه‌ی وب وارد شده و دستور ذیل را صادر کنید:
 dotnet watch run
این دستور، پروژه را build کرده و سپس بر روی پورت 5000 ارائه می‌دهد. همچنین به ازای هر تغییری در فایل‌های کدهای برنامه، به صورت خودکار برنامه را build کرده و مجددا ارائه می‌دهد.


تکمیل برنامه‌ی وب جهت ارسال پیام‌هایی به کلاینت‌های متصل به آن

پس از افزودن وابستگی‌های مورد نیاز، بازیابی و build برنامه، اکنون نوبت به تعریف یک Hub است، تا از طریق آن بتوان پیام‌هایی را به کلاینت‌های متصل ارسال کرد. به همین جهت یک پوشه‌ی جدید را به نام Hubs به پروژه‌ی وب افزوده و سپس کلاس جدید MessageHub را به صورت ذیل به آن اضافه می‌کنیم:
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;

namespace SignalRCore2WebApp.Hubs
{
    public class MessageHub : Hub
    {
        public Task Send(string message)
        {
            return Clients.All.InvokeAsync("Send", message);
        }
    }
}
این کلاس از کلاس پایه Hub مشتق می‌شود. سپس در متد Send آن می‌توان پیام‌هایی را به کلاینت‌های متصل به برنامه ارسال کرد.

پس از تعریف این Hub، نیاز است به کلاس Startup مراجعه کرده و دو تغییر ذیل را اعمال کنیم:
الف) ثبت و معرفی سرویس SignalR
ابتدا باید SignalR را فعالسازی کرد. به همین جهت نیاز است سرویس‌های آن‌را به صورت یکجا توسط متد الحاقی AddSignalR در متد ConfigureServices به نحو ذیل معرفی کرد:
public void ConfigureServices(IServiceCollection services)
{
   services.AddSignalR();
   services.AddMvc();
}

ب) ثبت مسیریابی دسترسی به Hub
پس از تعریف Hub، مرحله‌ی بعدی، مشخص سازی نحوه‌ی دسترسی به آن است. به همین جهت در متد Configure، به نحو ذیل Hub را معرفی کرده و سپس یک path را برای آن مشخص می‌کنیم:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
   app.UseSignalR(routes =>
   {
      routes.MapHub<MessageHub>(path: "message");
    });
یعنی اکنون این Hub در آدرس ذیل قابل دسترسی است:
  http://localhost:5000/message
این آدرسی است که در کلاینت Angular، از آن برای اتصال به هاب، استفاده خواهیم کرد.


انتشار پیام‌هایی به تمام کاربران متصل به برنامه

آدرس فوق به تنهایی کار خاصی را انجام نمی‌دهد. از آن جهت اتصال کلاینت‌های برنامه استفاده می‌شود و این کلاینت‌ها پیام‌های رسیده‌ی از طرف برنامه را از این آدرس دریافت خواهند کرد. بنابراین مرحله‌ی بعد، ارسال تعدادی پیام به سمت کلاینت‌ها است. برای این منظور به HomeController برنامه‌ی وب مراجعه کرده و آن‌را به نحو ذیل تغییر می‌دهیم:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using SignalRCore2WebApp.Hubs;

namespace SignalRCore2WebApp.Controllers
{
    public class HomeController : Controller
    {
        private readonly IHubContext<MessageHub> _messageHubContext;
        public HomeController(IHubContext<MessageHub> messageHubContext)
        {
            _messageHubContext = messageHubContext;
        }

        public IActionResult Index()
        {
            return View(); // show the view
        }

        [HttpPost]
        public async Task<IActionResult> Index(string message)
        {
            await _messageHubContext.Clients.All.InvokeAsync("Send", message);
            return View();
        }
    }
}
برای دسترسی به Hubهای تعریف شده می‌توان از سیستم تزریق وابستگی‌ها استفاده کرد. برای این منظور تنها کافی است Hub مدنظر را به عنوان آرگومان جنریک IHubContext تعریف کرد. سپس از طریق آن می‌توان به این context‌، در قسمت‌های مختلف برنامه دسترسی یافت و برای مثال پیام‌هایی را به کاربران ارائه داد.
در این مثال ابتدا View ذیل نمایش داده می‌شود:
@{
    ViewData["Title"] = "Home Page";
}

<form method="post"
      asp-action="Index"
      asp-controller="Home"
      role="form">
  <div class="form-group">
     <label label-for="message">Message: </label>
     <input id="message" name="message" class="form-control"/>
  </div>
  <button class="btn btn-primary" type="submit">Send</button>
</form>
کار آن فرستادن یک پیام به متد Index است. سپس این متد، به کمک context تزریق شده‌ی Hub پیام‌ها، این پیام را به تمام کلاینت‌های متصل ارسال می‌کند.


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

تا اینجا ساختار ابتدایی برنامه‌ی Angular را توسط Angular CLI ایجاد کردیم. اکنون نیاز است وابستگی سمت کلاینت SignalR Core را نصب کنیم. به همین جهت از طریق خط فرمان به پوشه‌ی SignalRCore2Client وارد شده و دستور ذیل را صادر کنید:
 npm install @aspnet/signalr-client --save
پرچم save آن سبب خواهد شد تا این وابستگی علاوه بر نصب، در فایل package.json نیز درج شود.
کلاینت رسمی signalr، هم جاوا اسکریپتی است و هم تایپ‌اسکریپتی. به همین جهت به سادگی توسط یک برنامه‌ی تایپ اسکریپتی Angular قابل استفاده است. کلاس‌های آن‌را در مسیر node_modules\@aspnet\signalr-client\dist\src می‌توانید مشاهده کنید.
در ابتدا، فایل app.component.ts را به نحو ذیل تغییر می‌دهیم:
import { Component, OnInit } from "@angular/core";
import { HubConnection } from "@aspnet/signalr-client";

@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"]
})
export class AppComponent implements OnInit {
  hubPath = "http://localhost:5000/message";
  messages: string[] = [];

  ngOnInit(): void {
    const connection = new HubConnection(this.hubPath);
    connection.on("send", data => {
      this.messages.push(data);
    });
    connection.start().then(() => {
      // connection.invoke("send", "Hello");
      console.log("connected.");
    });
  }
}
در اینجا در ابتدا، کلاس HubConnection از ماژول aspnet/signalr-client@ دریافت شده‌است. سپس بر این اساس در ngOnInit، یک وهله از آن که به مسیر Hub تعریف شده‌ی برنامه اشاره می‌کند، ایجاد خواهد شد. هر زمانیکه پیامی از سمت سرور دریافت گردید، این پیام را به لیست messages، که یک آرایه است اضافه می‌کنیم. در آخر برای راه اندازی این اتصال، متد start آ‌ن‌را فراخوانی خواهیم کرد. در اینجا می‌توان یک متد سمت سرور را فراخوانی کرد و یا برقراری اتصال را در کنسول developers مرورگر نمایش داد.
آرایه‌ی messages را به نحو ذیل توسط یک حلقه در قالب این کامپوننت نمایش خواهیم داد:
<div>
  <h1>
    The messages from the server:
  </h1>
  <ul>
    <li *ngFor="let message of messages">
      {{message}}
    </li>
  </ul>
</div>
پس از آن به ریشه‌ی پروژه‌ی کلاینت مراجعه کرده و دستور ذیل را صادر می‌کنیم تا برنامه‌ی Angular ساخته شده و در مرورگر پیش فرض سیستم نمایش داده شود:
  ng serve -o
در این حالت برنامه در آدرس  http://localhost:4200/ قابل دسترسی خواهد بود.


همانطور که مشاهده می‌کنید، پیام خطای ذیل را صادر کرده‌است:
 Failed to load http://localhost:5000/message: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:4200' is therefore not allowed access.
علت اینجا است که برنامه‌ی Angular بر روی پورت 4200 کار می‌کند و برنامه‌ی وب ما بر روی پورت 5000 تنظیم شده‌است. به همین جهت نیاز است CORS را در برنامه‌ی وب تنظیم کرد تا امکان یک چنین دسترسی صادر شود.
برای این منظور به فایل آغازین برنامه‌ی وب مراجعه کرده و سرویس‌های AddCors را به مجموعه‌ی سرویس‌های برنامه اضافه می‌کنیم:
public void ConfigureServices(IServiceCollection services)
{
    services.AddSignalR();
    services.AddCors(options =>
            {
                options.AddPolicy("CorsPolicy",
                    builder => builder
                        .AllowAnyOrigin()
                        .AllowAnyMethod()
                        .AllowAnyHeader()
                        .AllowCredentials());
            });
    services.AddMvc();
}
پس از آن در متد Configure، این سیاست دسترسی باید مورد استفاده قرار گیرد؛ و گرنه این تنظیمات کار نخواهد کرد. محل قرارگیری آن نیز باید پیش از سایر تنظیمات باشد:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
   app.UseCors(policyName: "CorsPolicy");
اکنون اگر مجددا برنامه‌ی Angular را Refresh کنیم، در console توسعه دهندگان مرورگر، مشاهده خواهیم کرد که اتصال برقرار شده‌است:


در آخر برای آزمایش برنامه، به آدرس http://localhost:5000 یا همان برنامه‌ی وب، مراجعه کرده و پیامی را ارسال کنید. بلافاصله مشاهده خواهید کرد که این پیام توسط کلاینت Angular دریافت شده و نمایش داده می‌شود:



کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید: SignalRCore2Sample.zip
برای اجرا آن، ابتدا به پوشه‌ی SignalRCore2WebApp مراجعه کرده و دو فایل bat آن‌را به ترتیب اجرا کنید. اولی وابستگی‌ها‌ی برنامه را بازیابی می‌کند و دومی برنامه را بر روی پورت 5000 ارائه می‌دهد.
سپس به پوشه‌ی SignalRCore2Client مراجعه کرده و در آنجا نیز دو فایل bat ابتدایی آن‌را به ترتیب اجرا کنید. اولی وابستگی‌های برنامه‌ی Angular را بازیابی می‌کند و دومی برنامه‌ی Angular را بر روی پورت 4200 اجرا خواهد کرد.
مطالب
شروع به کار با AngularJS 2.0 و TypeScript - قسمت نهم - مسیریابی
یک برنامه، از صفحات و Viewهای مختلفی تشکیل می‌شود و Routing یا مسیریابی، امکان ناوبری بین این Viewها را میسر می‌کند. یک برنامه‌ی AngularJS 2.0، یک برنامه‌ی تک صفحه‌ای وب است. به این معنا که تمام Viewهای برنامه، در یک صفحه نمایش داده می‌شوند؛ که معمولا همان index.html سایت است. هدف از سیستم مسیریابی، مدیریت نحوه‌ی نمایش و قرارگیری این Viewها، درون تک صفحه‌ی برنامه است.


برپایی تنظیمات اولیه‌ی سیستم مسیریابی در AngularJS 2.0

برای کار با سیستم مسیریابی AngularJS 2.0، ابتدا باید اسکریپت‌های آن به صفحه اضافه شوند. در ادامه المان پایه‌ای تعریف شده و سپس باید سرویس پروایدر مسیریابی را رجیستر کرد. جزئیات این موارد را در ادامه بررسی می‌کنیم:

الف) سرویس مسیریابی، جزئی از angular2/core نیست. به همین جهت مدخل اسکریپت متناظر با آن باید به صفحه‌ی اصلی سایت اضافه شود که این مورد، در قسمت اول بررسی پیشنیازهای نصب AngularJS 2.0 صورت گرفته‌است:
 <!-- Required for routing -->
<script src="~/node_modules/angular2/bundles/router.dev.js"></script>
این تعریف در فایل Views\Shared\_Layout.cshtml (و یا index.html) پروژه‌ی جاری موجود است.

ب) افزودن المان base به ابتدای صفحه:
 <!DOCTYPE html>
<html>
<head>
    <base href="/">
بلافاصله پس از تگ head، المان base اضافه می‌شود. این المان به سیستم مسیریابی اعلام می‌کند که چگونه آدرس‌های خود را تشکیل دهد. به صورت پیش فرض، AngularJS 2.0 از آدرس‌هایی با فرمت HTML5 استفاده می‌کند. در این حالت دیگر نیازی به ذکر # برای مشخص سازی مسیریابی‌های محلی نیست.
از آنجائیکه فایل index.html در ریشه‌ی سایت قرار گرفته‌است، مقدار آغازین href آن به / تنظیم شده‌است.

ج) شبیه به حالت ثبت پروایدر HTTP در قسمت قبل، برای ثبت پروایدر مسیریابی نیز به فایل App\app.component.ts مراجعه می‌کنیم:
//same as before ...
import { ROUTER_PROVIDERS } from 'angular2/router';
 
//same as before ... 

@Component({
//same as before …
    providers: [
        ProductService,
        HTTP_PROVIDERS,
        ROUTER_PROVIDERS
    ]
})
//same as before ...
در اینجا سرویس ROUTER_PROVIDERS به خاصیت providers اضافه شده‌است و همچنین import متناظر با آن نیز به ابتدای صفحه اضافه گردیده‌است.
علت ختم شدن نام این سرویس‌ها به PROVIDERS این است که این تعاریف، امکان استفاده‌ی از چندین سرویس زیر مجموعه‌ی آن‌ها را فراهم می‌کنند و صرفا یک سرویس نیستند.


ساخت کامپوننت نمایش جزئیات محصولات

در ادامه می‌خواهیم جزئیات هر محصول را با کلیک بر روی نام آن در لیست محصولات، در آدرسی دیگر به صورتی مجزا مشاهده کنیم. به همین منظور به پوشه‌ی products برنامه مراجعه کرده و دو فایل جدید product-detail.component.ts و product-detail.component.html را ایجاد کنید؛ با این محتوا:
الف) محتوای فایل product-detail.component.html
<div class='panel panel-primary'>
    <div class='panel-heading'>
        {{pageTitle}}
    </div>
</div>
ب) محتوای فایل product-detail.component.ts
import { Component } from 'angular2/core';

@Component({
    templateUrl: 'app/products/product-detail.component.html'
})
export class ProductDetailComponent  {
    pageTitle: string = 'Product Detail'; 
}
در اینجا یک کامپوننت جدید را ایجاد کرده‌ایم که در قالب آن، مقدار خاصیت pageTitle با روش interpolation در آن درج شده‌است. کلاس ProductDetailComponent، قالب خود را از طریق مقدار دهی آدرس آن در خاصیت templateUrl متادیتای خود، پیدا می‌کند.
اگر دقت کنید، این کامپوننت ویژه دارای خاصیت selector نیست. ذکر selector تنها زمانی اجباری است که بخواهیم این کامپوننت را داخل کامپوننتی دیگر قرار دهیم. در اینجا قصد داریم این کامپوننت را به صورت یک View جدید، توسط سیستم مسیریابی نمایش دهیم و نه به صورت جزئی از یک کامپوننت دیگر.


افزودن تنظیمات مسیریابی به برنامه

مسیریابی در AngularJS 2.0، مبتنی بر کامپوننت‌ها است. به همین جهت، ابتدای کار مسیریابی، مشخص سازی تعدادی از کامپوننت‌ها هستند که قرار است به عنوان مقصد سیستم راهبری (navigation) مورد استفاده قرار گیرند و به ازای هر کدام، یک مسیریابی و Route جدید را تعریف می‌کنیم. تعریف هر Route جدید شامل انتساب نامی به آن، تعیین مسیر مدنظر و مشخص سازی کامپوننت مرتبط است:
 { path: '/products', name: 'Products', component: ProductListComponent },
برای مثال جهت تعریف Route کامپوننت لیست محصولات، نام آن‌را Products، مسیر آن‌را products/ و در آخر کامپوننت آن‌را به نام کلاس متناظر با آن، تنظیم می‌کنیم.
این تنظیمات به عنوان یک متادیتای جدید دیگر به کلاس AppComponent، در فایل app.component.ts اضافه می‌شوند:
//same as before …
import { ROUTER_PROVIDERS, RouteConfig } from 'angular2/router';
 
//same as before …
 
@Component({
    //same as before …
})
@RouteConfig([
    { path: '/welcome', name: 'Welcome', component: WelcomeComponent, useAsDefault: true },
    { path: '/products', name: 'Products', component: ProductListComponent }
])
export class AppComponent {
    pageTitle: string = "DNT AngularJS 2.0 APP";
}
در اینجا decorator جدیدی به نام RouteConfig به کلاس AppComponent اضافه شده‌است و همچنین importهای متناظری نیز در ابتدای این فایل تعریف شده‌اند.
همانطور که ملاحظه می‌کنید، یک کلاس می‌تواند بیش از یک decorator داشته باشد.
()RouteConfig@ را به کامپوننتی الصاق می‌کنیم که قصد میزبانی مسیریابی را دارد (Host component). این مزین کننده، آرایه‌ای از اشیاء را قبول می‌کند و هر شیء آن دارای خواصی مانند مسیر، نام و کامپوننت است. باید دقت داشت که نام هر مسیریابی تعریف شده باید pascal case باشد. در غیراینصورت مسیریاب ممکن است این نام را با path اشتباه کند.  
همچنین امکان تعریف خاصیت دیگری به نام useAsDefault نیز در اینجا میسر است. از آن جهت تعریف مسیریابی پیش فرض سیستم، در اولین بار نمایش آن، استفاده می‌شود.

در اینجا نام کامپوننت، رشته‌ای ذکر نمی‌شود و دقیقا اشاره دارد به نام کلاس متناظر. بنابراین هر نام کلاسی که در اینجا اضافه می‌شود، باید به همراه import ماژول آن نیز در ابتدای فایل جاری باشد. به همین جهت اگر تنظیمات فوق را اضافه کنید، ذیل کلمه‌ی WelcomeComponent یک خط قرمز مبتنی بر عدم تعریف آن کشیده می‌شود. برای تعریف آن، پوشه‌ی جدیدی را به ریشه‌ی سایت به نام home اضافه کنید و به آن دو فایل ذیل را اضافه نمائید:
الف) محتوای فایل welcome.component.ts
import { Component } from 'angular2/core';
 
@Component({
    templateUrl: 'app/home/welcome.component.html'
})
export class WelcomeComponent {
    public pageTitle: string = "Welcome";
}
ب) محتوای فایل welcome.component.html
<div class="panel panel-primary">
    <div class="panel-heading">
        {{pageTitle}}
    </div>
    <div class="panel-body">
        <h3 class="text-center">Default page</h3>
    </div>
</div>
کار این کامپوننت، نمایش صفحه‌ی آغازین برنامه است؛ بر اساس تنظیم useAsDefault: true مسیریابی‌های تعریف شده‌.
پس از تعریف این کامپوننت، اکنون باید import ماژول آن‌را به ابتدای فایل app.component.ts اضافه کنیم، تا مشکل عدم شناسایی نام کلاس WelcomeComponent برطرف شود:
 import { WelcomeComponent } from './home/welcome.component';


فعال سازی مسیریابی‌های تعریف شده

روش‌های مختلفی برای دسترسی به اجزای یک برنامه وجود دارند؛ برای مثال کلیک بر روی یک لینک، دکمه و یا تصویر و سپس فعال سازی مسیریابی متناظر با آن. همچنین کاربر می‌تواند آدرس صفحه‌ای را مستقیما در نوار آدرس‌های مرورگر وارد کند. به علاوه امکان کلیک بر روی دکمه‌های back و forward مرورگر نیز همواره وجود دارند. تنظیمات مسیریابی‌های انجام شده، دو مورد آخر را به صورت خودکار مدیریت می‌کنند. در اینجا تنها باید مدیریت اولین حالت ذکر شده را با اتصال مسیریابی‌ها به اعمال کاربران، انجام داد.
به همین جهت منویی را به بالای صفحه‌ی برنامه اضافه می‌کنیم. برای این منظور، فایل app.component.ts را گشوده و خاصیت template کامپوننت AppComponent را به نحو ذیل تغییر می‌دهیم:
@Component({
    //same as before …
    template: `
     <div>
        <nav class='navbar navbar-default'>
            <div class='container-fluid'>
                <a class='navbar-brand'>{{pageTitle}}</a>
                <ul class='nav navbar-nav'>
                    <li><a [routerLink]="['Welcome']">Home</a></li>
                    <li><a [routerLink]="['Products']">Product List</a></li>
                </ul>
            </div>
        </nav>
        <div class='container'>
            <router-outlet></router-outlet>
        </div>
     </div>
    `,
    //same as before …
})
در اینجا یک navigation bar بوت استرپ 3، جهت تعریف منوی بالای صفحه اضافه شده‌است.
سپس جهت تعریف لینک‌های هر آیتم، از یک دایرکتیو توکار AngularJS 2.0 به نام routerLink استفاده می‌کنیم. هر routerLink به یکی از آیتم‌های تنظیم شده‌ی در RouteConfig بایند می‌شود. بنابراین نام‌هایی که در اینجا قید شده‌اند، دقیقا نام‌هایی هستند که در خاصیت name هر کدام از اشیاء تشکیل دهنده‌ی RouteConfig، تعریف و مقدار دهی گردید‌ه‌اند.
اکنون اگر کاربر بر روی یکی از لینک‌های Home و یا Product List کلیک کند، مسیریابی متناظر با آن فعال می‌شود (بر اساس این نام، در لیست عناصر RouteConfig جستجویی صورت گرفته و عنصر معادلی بازگشت داده می‌شود) و سپس View آن کامپوننت نمایش داده خواهد شد.
تا اینجا دایرکتیو جدید routerLink به قالب کامپوننت اضافه شد‌ه‌است؛ اما AngularJS 2.0 نمی‌داند که باید آن‌را از کجا دریافت کند. به همین جهت ابتدا import آن‌را (ROUTER_DIRECTIVES) به ابتدای ماژول جاری اضافه خواهیم کرد:
 import { ROUTER_PROVIDERS, RouteConfig, ROUTER_DIRECTIVES } from 'angular2/router';
و سپس خاصیت دایرکتیوهای کامپوننت ریشه‌ی سایت را نیز با آن مقدار دهی خواهیم کرد:
 directives: [ROUTER_DIRECTIVES],
علت جمع بود نام این دایرکتیو این است که routerLink استفاده شده، یکی از اعضای مجموعه‌ی دایرکتیوهای مسیریابی توکار AngularJS 2.0 است.

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

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

اکنون اگر برنامه را اجرا کنیم، به این شکل خواهیم رسید:


اگر دقت کنید، آدرس بالای صفحه، در اولین بار نمایش آن به http://localhost:2222/welcome تنظیم شده و این مقدار دهی بر اساس خاصیت useAsDefault تنظیمات مسیریابی سایت انجام شده‌است (نمایش welcome به عنوان صفحه‌ی اصلی و پیش فرض).
همچنین با کلیک بر روی لینک لیست محصولات، کامپوننت آن فعال شده و نمایش داده می‌شود. محل قرارگیری این کامپوننت‌ها، دقیقا در محل قرارگیری دایرکتیو router-outlet است.


ارسال پارامترها به سیستم مسیریابی

در ابتدا بحث، مقدمات کامپوننت نمایش جزئیات یک محصول انتخابی را تهیه کردیم. برای فعال سازی این کامپوننت و مسیریابی آن، نیاز است بتوان پارامتری را به سیستم مسیریابی ارسال کرد. برای مثال با انتخاب آدرس product/5، جزئیات محصول با ID مساوی 5 نمایش داده شود.
برای این منظور:
الف) اولین قدم، تعریف مسیریابی آن است. به همین جهت به فایل app.component.ts مراجعه و دو تغییر ذیل را به آن اعمال کنید:
//same as before …

import { ProductDetailComponent } from './products/product-detail.component';
 
@Component({
    //same as before …
})
@RouteConfig([
    //same as before …
    { path: '/product/:id', name: 'ProductDetail', component: ProductDetailComponent }
])
//same as before …
ابتدا مسیریابی جدیدی به نام ProductDetail اضافه شده‌است و سپس ماژول دربرگیرنده‌ی کلاس کامپوننت آن نیز import شده‌است.
تفاوت این مسیریابی با نمونه‌های قبلی در تعریف id:/ است. پس از ذکر :/، نام یک متغیر عنوان می‌شود و اگر نیاز به چندین متغیر بود، همین الگو را تکرار خواهیم کرد.

ب) سپس نحوه‌ی فعال سازی این مسیریابی را توسط تعریف لینکی جدید، معرفی می‌کنیم. بنابراین فایل قالب product-list.component.html را گشوده و سپس بجای نمایش عنوان محصول:
 <td>{{ product.productName }}</td>
لینک به جزئیات آن‌را نمایش می‌دهیم:
<td>
    <a [routerLink]="['ProductDetail', {id: product.productId}]">
        {{product.productName}}
    </a>
</td>
نحوه‌ی تعریف این لینک، با لینک‌هایی که در منوی بالای سایت اضافه کردیم، یکی است؛ با این تفاوت که اکنون پارامتر دومی را به قسمت یافتن نام این Route، جهت مشخص سازی روش مقدار دهی متغیر id، تعریف کرده‌ایم. در اینجا id هر لینک از productId بایند شده تامین می‌شود.
اکنون که از دایرکتیو جدید routerLink در این قالب استفاده شده‌است، نیاز است تعریف دایرکتیو آن‌را به متادیتای کلاس کامپوننت لیست محصولات نیز اضافه کنیم تا AngularJS 2.0 بداند آن‌را از کجا باید تامین کند:
import { Component, OnInit } from 'angular2/core';
import { ROUTER_DIRECTIVES } from 'angular2/router';
//same as before …
 
@Component({
    //same as before …
    directives: [StarComponent, ROUTER_DIRECTIVES]
})

در ادامه اگر برنامه را اجرا کنید، عنوان‌های محصولات، به آدرس نمایش جزئیات آن‌ها لینک شده‌اند:


ج) در آخر زمانیکه View نمایش جزئیات محصول فعال می‌شود، نیاز است این id را از url جاری دریافت کند. به همین جهت فایل product-detail.component.ts را گشوده و تغییرات ذیل را به آن اعمال کنید:
import { Component } from 'angular2/core';
import { RouteParams } from 'angular2/router';
 
@Component({
    templateUrl: 'app/products/product-detail.component.html'
})
export class ProductDetailComponent {
    pageTitle: string = 'Product Detail';
 
    constructor(private _routeParams: RouteParams) {
        let id = +this._routeParams.get('id');
        this.pageTitle += `: ${id}`;
    } 
}
با نحوه‌ی تزریق وابستگی‌ها در قسمت هفتم آشنا شدیم. در اینجا سرویس توکار RouteParams به سازنده‌ی کلاس تزریق شده‌‌است. با استفاده از آن می‌توان به id ارسالی از طریق url دسترسی یافت. در اینجا پارامتری که به متد get ارسال می‌شود، باید با نام پارامتری که در تنظیمات آغازین مسیریابی سیستم تعریف گردید، تطابق داشته باشد.
در این حالت، id دریافتی، به متغیر pageTitle اضافه شده و در قالب مربوطه به صورت خودکار نمایش داده می‌شود.

تا اینجا اگر برنامه را اجرا کنید، صفحه‌ی نمایش جزئیات یک محصول، با کلیک بر روی عناوین آن‌ها به صورت زیر نمایش داده می‌شود:



افزودن دکمه‌ی back با کدنویسی

اکنون برای بازگشت مجدد به لیست محصولات، می‌توان از دکمه‌ی back مرورگر استفاده کرد، اما امکان طراحی این دکمه در قالب‌ها نیز پیش بینی شده‌است.
برای این منظور قالب product-detail.component.html را به نحو ذیل بازنویسی می‌کنیم:
<div class='panel panel-primary'>
    <div class='panel-heading'>
        {{pageTitle}}
    </div>
    <div class='panel-footer'>
        <a class='btn btn-default' (click)='onBack()' style='width:80px'>
            <i class='glyphicon glyphicon-chevron-left'></i> Back
        </a>
    </div>    
</div>
در اینجا دکمه‌ی بازگشت به صفحه‌ی قبلی اضافه شده‌است که به متد onBack در کلاس مرتبط با این قالب، متصل است.

سپس کدهای product-detail.component.ts را به صورت ذیل تکمیل خواهیم کرد:
import { Component } from 'angular2/core';
import { RouteParams, Router } from 'angular2/router';
 
@Component({
    templateUrl: 'app/products/product-detail.component.html'
})
export class ProductDetailComponent {
    pageTitle: string = 'Product Detail';
 
    constructor(private _routeParams: RouteParams, private _router: Router) {
        let id = +this._routeParams.get('id');
        this.pageTitle += `: ${id}`;
    }
 
    onBack(): void {
        this._router.navigate(['Products']);
    }
}
در اینجا سرویس جدیدی به نام Router در سازنده‌ی کلاس تزریق شده‌است. این سرویس امکان فراخوانی متدهایی مانند navigate را جهت حرکت به مسیریابی مشخصی، میسر می‌کند. پارامتری که به این متد ارسال می‌شود، دقیقا معادل همان پارامتری است که به دایرکتیو routerLink ارسال می‌گردد و در اینجا صرفا نام یک مسیریابی مشخص شده‌است؛ بدون ذکر پارامتری.


رفع تداخل مسیریابی‌های ASP.NET MVC با مسیریابی‌های AngularJS 2.0

در طی بحث جاری عنوان شد که اگر کاربر مسیر http://localhost:2222/product/2 را جایی ثبت کرده یا bookmark کند، پس از فراخوانی مستقیم آن در نوار آدرس‌های مرورگر، بلافاصله به این آدرس هدایت خواهد شد. این مورد صحیح است اگر از index.html بجای بکارگیری ASP.NET MVC، جهت هاست برنامه استفاده شود. اگر چنین آدرسی را در یک برنامه‌ی ASP.NET MVC فراخوانی کنیم، ابتدا به دنبال کنترلری به نام product می‌گردد (ابتدا وارد موتور ASP.NET MVC می‌شود) و چون این کنترلر در سمت سرور تعریف نشده‌است، پیام 404 و یا یافت نشد را مشاهده خواهید کرد و فرصت به اجرای برنامه‌ی AngularJS نخواهد رسید.
برای حل این مشکل نیاز است یک route جدید را به نام catch all، در انتهای مسیریابی‌های فعلی اضافه کنید؛ تا سایر درخواست‌های رسیده را به صفحه‌ی نمایش برنامه‌ی AngularJS هدایت کند:
public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
        routes.MapRoute(
            name: "Default",
            url: "{controller}/{action}/{id}",
            defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
            constraints: new { controller = "Home" } // for catch all to work, Home|About|SomeName
        );
 
        // Route override to work with Angularjs and HTML5 routing
        routes.MapRoute(
            name: "NotFound",
            url: "{*catchall}",
            defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
        );
    }
}


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: MVC5Angular2.part9.zip


خلاصه‌ی بحث

حین ایجاد کامپوننت‌ها باید به نحوه‌ی نمایش آن‌ها نیز فکر کرد. اگر کامپوننتی قرار است داخل یک کامپوننت دیگر نمایش یابد، باید دارای selector باشد. یک چنین کامپوننتی نیاز به تعریف مسیریابی ندارد. برای کامپوننت‌هایی که به عنوان یک View مستقل طراحی می‌شوند و قرار است در یک صفحه‌ی مجزا نمایش داده شوند، نیازی به تعریف selector نیست؛ اما باید برای آن‌ها مسیریابی‌های ویژه‌ای را تعریف کرد. همچنین نیاز است مدیریت اعمال کاربران را جهت فعال سازی آن‌ها نیز مدنظر داشت. برای استفاده از امکانات مسیریابی توکار AngularJS 2.0 نیاز است اسکریپت آن‌را به صفحه‌ی اصلی اضافه کرد. سپس باید المان base را جهت نحوه‌ی تشکیل آدرس‌های مسیریابی، به صفحه اضافه کرد. در ادامه کار ثبت ROUTER_PROVIDERS در بالاترین سطح سلسه مراتب کامپوننت‌های سایت انجام می‌شود. با استفاده از RouteConfig کار تنظیمات ابتدایی مسیریابی صورت خواهد گرفت. این decorator به کامپوننتی که قرار است کار میزبانی مسیریابی را انجام دهد، متصل می‌شود. پس از تعریف مسیریابی‌ها با ذکر یک نام منحصربفرد، مسیری که باید توسط کاربر وارد شود و نام کامپوننت مدنظر، با استفاده از دایرکتیو routerLink کار تعریف این آدرس‌ها، در رابط کاربری برنامه انجام می‌شود. این دایرکتیو جدید، جزئی از مجموعه‌ی ROUTER_DIRECTIVES است که باید به لیست دایرکتیوهای کامپوننت ریشه‌های سایت و هر کامپوننتی که از routeLink استفاده می‌کند، اضافه شود. برای بایند این دایرکتیو به مسیریابی‌های تعریف شده، سمت راست این اتصال باید به آرایه‌ای از مقادیر مقدار دهی شود که اولین عنصر آن، نام یکی از عناصر مسیریابی تعریف شده‌ی در RouteConfig است. از دومین عنصر آن برای مقدار دهی پارامترهای ارسالی به سیستم مسیریابی استفاده می‌شود. کار دایرکتیو router-outlet، مشخص سازی محل نمایش یک View است که عموما در قالب میزبان مسیریابی قرار می‌گیرد. برای تعیین پارامترهای مسیریابی، از الگوی paramName:/ استفاده می‌شود. برای دسترسی به این مقادیر در یک کامپوننت، می‌توان از سرویس RouteParams استفاده کرد. برای فعال سازی یک مسیریابی با کدنویسی، از سرویس Router و متد navigate آن کمک می‌گیریم.
مطالب
تغییر اندازه تصاویر #2
در ادامه مطلب تغییر اندازه تصاویر #1 ، در این پست می‌خواهیم نحوه تغییر اندازه تصاویر را در زمان درخواست کاربر بررسی کنیم.

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

خوب ابتدا فرض می‌کنیم برای نمایش تصاویر چند حالت داریم مثلا کوچک، متوسط، بزرگ و حالت واقعی (اندازه اصلی).
البته دقت نمایید که این طبقه بندی فرضی بوده و ممکن است برای پروژه‌های مختلف این طبقه بندی متفاوت باشد. (در این پست قصد فقط اشنایی با تغییر اندازه تصاویر است و شاید کد به درستی refactor نشده باشد).
برای تغییر اندازه تصاویر در زمان اجرا یکی از روش ها، می‌تواند استفاده از Handler باشد. خوب برای ایجاد Handler ابتدا در پروژه وب خود بروی پروژه راست کلیک کرده، و گزینه New Item را برگزینید، و در پنجره ظاهر شده مانند تصویر زیر گزینه Generic Handler  را انتخاب نمایید.

پس از ایجاد هندلر، فایل کد آن مانند زیر خواهد بود، ما باید کدهای خود را در متد ProcessRequestبنویسیم.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace PWS.UI.Handler
{
    /// <summary>
    /// Summary description for PhotoHandler
    /// </summary>
    public class PhotoHandler : IHttpHandler
    {

        public void ProcessRequest(HttpContext context)
        {
            context.Response.ContentType = "text/plain";
            context.Response.Write("Hello World");
        }

        public bool IsReusable
        {
            get
            {
                return false;
            }
        }
    }
}

خوب برای نوشتن کد در این مرحله ما باید چند کار انجام دهیم.
1- گرفتن پارامتر‌های ورودی کاربر جهت تغییر سایز از طریق روش‌های انتقال مقادیر بین صفحات (در اینجا استفاده از Query String ).
2-بازیابی تصویر از دیتابیس یا از دیسک به صورت یک آرایه بایتی.
3- تغییر اندازه تصویر مرحله 2 و ارسال تصویر به خروجی.
using System;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Globalization;
using System.IO;
using System.Web;

namespace PWS.UI.Handler
{
    /// <summary>
    /// Summary description for PhotoHandler
    /// </summary>
    public class PhotoHandler : IHttpHandler
    {

        /// <summary>
        /// بازیابی تصویر اصلی از بانک اطلاعاتی
        /// </summary>
        /// <param name="photoId">کد تصویر</param>
        /// <returns></returns>
        private byte[] GetImageFromDatabase(int photoId)
        {
            using (var connection = new SqlConnection("ConnectionString"))
            {
                using (var command = new SqlCommand("Select Photo From tblPhotos Where Id = @PhotoId", connection))
                {
                    command.Parameters.Add(new SqlParameter("@PhotoId", photoId));
                    connection.Open();
                    var result = command.ExecuteScalar();
                    return ((byte[])result);
                }
            }
        }

        /// <summary>
        /// بازیابی فایل از دیسک
        /// </summary>
        /// <param name="photoId">با فرض اینکه نام فایل این است</param>
        /// <returns></returns>
        private byte[] GetImageFromDisk(string photoId /* or somting */)
        {
                using (var sourceStream = new FileStream("Original File Path + id", FileMode.Open, FileAccess.Read))
                {
                    return StreamToByteArray(sourceStream);
                }
        }

        /// <summary>
        /// Streams to byte array.
        /// </summary>
        /// <param name="inputStream">The input stream.</param>
        /// <returns></returns>
        /// <exception cref="System.ArgumentException"></exception>
        static byte[] StreamToByteArray(Stream inputStream)
        {
            if (!inputStream.CanRead)
            {
                throw new ArgumentException();
            }

            // This is optional
            if (inputStream.CanSeek)
            {
                inputStream.Seek(0, SeekOrigin.Begin);
            }

            var output = new byte[inputStream.Length];
            int bytesRead = inputStream.Read(output, 0, output.Length);
            Debug.Assert(bytesRead == output.Length, "Bytes read from stream matches stream length");
            return output;
        }

        /// <summary>
        /// Enables processing of HTTP Web requests by a custom HttpHandler that implements the <see cref="T:System.Web.IHttpHandler" /> interface.
        /// </summary>
        /// <param name="context">An <see cref="T:System.Web.HttpContext" /> object that provides references to the intrinsic server objects (for example, Request, Response, Session, and Server) used to service HTTP requests.</param>
        public void ProcessRequest(HttpContext context)
        {
            // Set up the response settings
            context.Response.ContentType = "image/jpeg";
            context.Response.Cache.SetCacheability(HttpCacheability.Public);
            context.Response.BufferOutput = false;

            // مرحله اول
            int size = 0;
            switch (context.Request.QueryString["Size"])
            {
                case "S":
                    size = 100; //100px
                    break;
                case "M":
                    size = 198; //198px
                    break;
                case "L":
                    size = 500; //500px
                    break;
            }
            byte[] changedImage;
            var id = Convert.ToInt32(context.Request.QueryString["PhotoId"]);
            byte[] sourceImage = GetImageFromDatabase(id);
            // یا
            //byte[] sourceImage = GetImageFromDisk(id.ToString(CultureInfo.InvariantCulture));

            //مرحله 2
            if (size != 0)  //غیر از حالت واقعی تصویر
            {
                changedImage = Helpers.ResizeImageFile(sourceImage, size, ImageFormat.Jpeg);
            }
            else
            {
                changedImage = (byte[])sourceImage.Clone();
            }

            // مرحله 3
            if (changedImage == null) return;
            context.Response.AddHeader("Content-Length", changedImage.Length.ToString(CultureInfo.InvariantCulture));
            context.Response.BinaryWrite(changedImage);
        }

        public bool IsReusable
        {
            get
            {
                return false;
            }
        }
    }
}
در این هندلر ما چند متد اضافه کردیم.
1- متد GetImageFromDatabase: این متد یک کد تصویر را گرفته و آن را از بانک اطلاعاتی بازیابی میکند. (در صورتی که تصویر در بانک ذخیره شده باشد)
2- متد GetImageFromDisk: این متد نام تصویر (با فرض اینکه یک عدد می‌باشد) را به عنوان پارامتر گرفته و آنرا بازیابی می‌کند (در صورتی که تصویر در دیسک ذخیره شده باشد.)
3- متد StreamToByteArray: زمانی که تصویر از فایل خوانده می‌شود به صورت Stream است این متد یک Stream را گرفته و تبدیل به یک آرایه بایتی می‌کند.

در نهایت در متد ProcessRequestتصویر خوانده شده با توجه به پارامترهای ورودی تغییر اندازه داده شده و در نهایت به خروجی نوشته می‌شود.

برای استفاده این هندلر، کافی است در توصیر خود به عنوان مسیر رشته ای شبیه زیر وارد نمایید:
PhotoHandler.ashx?PhotoId=10&Size=S

مانند

<img src='PhotoHandler.ashx?PhotoId=10&Size=S' alt='تصویر ازمایشی' />
پ.ن : هرچند می‌توانستیم کد هارا بهبود داده و خیلی بهینه‌تر بنویسیم اما هدف فقط اشنایی با عمل تغییر اندازه تصویر در زمان اجرا بود، امیدوارم اساتید من ببخشن.

نظرات اقای موسوی تا حدودی اعمال شد و در پست تغییراتی انجام شد.
موفق وموید باشید