اشتراک‌ها
تغییرات لازم جهت بهبود نمایش صفحات وب در موبایل مشابه اپلیکیشن ها
با افزودن مانیفست مشابه زیر در فایل manifest.json:
{
   "name": "Appscope",
   "display": "standalone",
   "icons": [{
      "src": "icons/icon-192.png",
      "sizes": "192x192"},
   {
      "src": "icons/icon-512.png",
      "sizes": "512x512"}
   ]
}
و تگ:
 <meta name="apple-mobile-web-app-capable" content="yes"> 
و بهره مندی صفحه وب از ابزار PWA، دسترسی کاربران به صفحات وب مان از طریق موبایل کاربرپسندتر خواهد شد. 
تغییرات لازم جهت بهبود نمایش صفحات وب در موبایل مشابه اپلیکیشن ها
نظرات مطالب
آشنایی با NHibernate - قسمت دوم
سلام
- ضمن تشکر از لطف شما، بنده استاد نیستم. یک سری مطلب رو از این طرف اون طرف پیدا می‌کنم و با هم تقسیم می‌کنیم. فقط همین و لطفا این لفظ رو دیگر بکار نبرید.
- خیر. می‌شد برای آزمایش یک برنامه کنسول هم نوشت. اما دیگر مرسوم نیست. بجای استفاده از یک برنامه کنسول، آزمایش واحد بنویسید. هم روشی است استاندارد، هم به عنوان مستندات نحوه استفاده از متدهای پروژه می‌تونه مورد استفاده قرار بگیره، هم سبب میشه کد بهتری بنویسید چون مجبور خواهید شد در هم تنیدگی کدهای خودتون رو برای متد تست نوشتن کمتر کنید و هم .... در مقالات مربوطه (تگ unit test سمت راست صفحه) مابقی مزایا، نحوه تولید استفاده و غیره را لطفا مطالعه کنید.
مطالب
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 استفاده از یک پروژه جداگانه برای نگهداری این منابع راه حل مناسبتری است.
در مطلب بعدی به شرح نحوه تولید پرووایدرهای سفارشی می‌پردازم.
مطالب
شروع کار با Apache Cordova در ویژوال استودیو #2
در قسمت اول، با Apache Cordova  آشنا شدیم. در این قست قصد دارم در مورد Phonegap, معایب و مزایای Cordova و روش نصب و راه اندازی آن را بر روی Visual Studio، خدمت شما ارائه دهم. 

توضیح مختصری در مورد  Adobe Phonegap
در حوالی سال 2009 ، phonegap به‌واسطه‌ی استارت آپی بنام Nitobi با هدف ایجاد یک راه حل سورس باز  برای  ساخت اپلیکیشن‌های بومی موبایل با تکنولوژی‌های تحت وب، تولید شد. شرکت Adobe در حوالی سال 2011 ، Notobi را به همرا حق مالکیت phonegap خریداری کرد و هسته‌ی سورس باز آن را با نام Cordova به شرکت Apache اهدا کرد. نسبت بین Cordova و phonegap مانند نسبت بین مرورگر Blink و کروم است. در واقع phonegap ترکیبی‌است از Cordova و یک سری امکانات اضافه‌ی شرکت Adobe. تفاوت اصلی بین Cordova و Phonegap مربوط است به ابزارهای Command-Line و سرویس Build فون گپ است که در مقالات بعدی به آنها خواهیم پرداخت.
بیشتر : اینجا و اینجا

 مزایای استفاده از Cordova:
  • محیط برنامه نویسی قدرتمند
  • هسته اصلی  کد‌های همه اپلیکیشن‌ها  تولید شده شبیه به هم است
  • نیازی به یادگیری زبان‌های مربوط به هر پلتفرم را ندارید
  • کم هزینه و زمان کمتر
  • طراحی رابط گرافیکی سریع و منعطف به کمک HTML5 , CSS3 
  • برنامه نویسی آسان و سریع با javascript , Typescript
  • قابلیت اجرا بر روی چندین پلتفرم مختلف(Android,iOS,Widnows Phone )
  • قابلیت استفاده از فریم‌ورکهای تحت وب مانند Bootstrap , Angular JS, ...
  • قابلیت طراحی پلاگین برای ارتباط با سیستم عامل
  • مناسب برای برای برنامه‌های چت و استفاد از وب سرویس‌ها
  • مناسب برای ساخت بازیهای آنلاین و آفلاین با تکنولوژی‌های تحت وب
  • راحتی کار با آن برای برنامه نویسان تحت وب 

معایب  استفاده از Cordova  :
  • نداشتن ابزار گزارش خطاهای مناسب؛ درنتیجه برطرف کردن خطاها خسته کننده خواهد بود .
  • UI, UX اپلیکیشن‌ها باید به نحوی باشد که کاربر حس کند با نرم‌افزارهای بومی گوشی کار می‌کند.
  • کاهش سرعت اجرایی جزئی نسبت به سایر برنامه‌ها (به دلیل استفاده از WebView)
  • عدم دسترسی مستقیم به سیستم عامل و امکانات آن 

نصب اتوماتیک وابستگی ها
ابزارهایی که ما نیاز داریم:

لازم است تا Visual Studio 2013، با حداقل آپدیت 2 بر روی سیستم شما نصب باشد.

بعد از اتمام دانلود فایل، اقدام به نصب آن نمایید. در این حین، یک سری وابستگی‌های مربوط به خود را دانلود و نصب خواهد کرد. لیست وابستگی ها:
  • Node.js
  • Git CLI
  • Google Chrome
  • Apache Ant
  • Oracle Java JDK 7 (حتما نسخه x86 نصب شود)
  • Android SDK 
  • SQLLite For Windows Runtime
  • Apple iTunes
فایل نصاب، همه‌ی این وابستگی‌ها را به‌غیر از Android SDK، نصب می‌کند. 
در آخر هم سیستم خود را راستارت کنید.

نصب دستی وابستگی‌ها:
اگر به هر دلیلی در نصب خودکار این وابستگی‌ها  توسط نصاب با مشکل بر خورد کردید، می‌توانید تک تک آنها را دانلود کرده و نصب کنید. لینک‌های مورد نظر را هم به همین دلیل قرار دادم. 
  1. node.js را از لینک مقابل دانلود کنید:  اینجا  (پیشنهاد می‌کنیم نسخه‌ی x86 آن را نصب کنید)
  2. Google Chrome را نصب کنید
  3. Git Command Line Tools را نصب کنید و توجه کنید که در هنگام نصب، گزینه مربوط به افزودن Git را به مسیر  Command Prompt  شما، انتخاب کرده باشید.
  4. Apchage Ant را  دانلود  و در مسیری از سیستم خودتان قرار دهید. 
  5. Java JDK 7 x86 را از لینک مشخص شده دانلود کنید و سپس عملیات نصب را انجام دهید.
  6. Android SDK را از آدرس مشحص شده دانلود کنید. پکیچ‌های مورد نیاز، به این SDK افزوده شده است. بعد از دانلود آن را در مسیری از سیستم خود قرار دهید.
  7. Apple iTunes و  SQLite را دانلود و نصب کنید.
  8. اگر از ویندوز 7 استفاده می‌کنید ، WebSocket4Net را از لینک مقابل دانلود کنید ( اینجا ) و سپس  فایل net45\Release\WebSocket4Net.dll  در مسیر زیر کپی کنید:
%ProgramFiles(x86)%\Microsoft Visual Studio 12.0\Common7\IDE\CommonExtensions\Microsoft\WebClient\Diagnostics\ToolWindows 


ویژوال استودیو  پیکربندی‌های مربوط به نرم افزار‌های  thrid-party (سوم شخص/ثالث: نرم افزارهایی که برای دستکاری بر روی سیستم عامل، توسط شرکت‌هایی غیر از شرکت‌های تولید کننده سیستم عامل تولید می‌شوند) را که شما نصب کرده‌اید، تشخیص می‌دهد و مسیر‌های نصب آنها را درون متغیر‌های محیطی (environment variables)  به شکل زیر نگه می‌دارد:
ADT_HOME :به مسیر نصب اندروید اشاره می‌کند
ANT_HOME: به فولدری که Apache Ant در آن قرار دارد اشاره می‌کند
JAVA_HOME: به مسیر نصب جاوا اشاره می‌کند
GIT_HOME: به مسیر نصب GIT اشاره می‌کند.
دقت کنید باید نام‌های متغیر‌ها، دقیقا به همین نام‌ها باشند.
برای تنظیم این متغیر‌ها، به مسیر Control Panel\System and Security\System وارد شده و گزینه‌ی Advanced System Setting را انتخاب کنید. سپس در پنجره‌ی باز شده گزینه‌ی Environment Variables را انتخاب کنید و در قست system variables، این 4 متغیری که ذکر شد را ایجاد کنید. سپس نیاز است این مسیر‌ها را به system path اضافه کنید. برای این کار از همان قسمت system variables متغییر path را انتخاب کرده و گزینه‌ی ویرایش را بزنید و ابتدا محتویات آن را در یک فایل notepad کپی کنید و مسیر‌های زیر را به اول آن اضافه کنید :
%GIT_HOME%\cmd;C:\Program Files (x86)\nodejs\;%JAVA_HOME%\bin;%ANT_HOME%\bin;
%ANDROID_HOME%\tools;%ANDROID_HOME%\platform-tools; C:\ProgramData\Oracle\Java\javapath;
 دقت کنید که مسیر‌های ذکر شده فقط یکبار در کل فایل وجود داشته باشند و سپس کل محتوای فایل را کپی کرده و در قسمت مربوط به path پیست کنید و با کلیک بر روی دکمه‌های OK کار را به اتمام رسانید.












نکته تکمیلی
نیازمندی Apache Cordova CTP3.1   :
یکی از سیستم عامل‌های مقابل: Windows 7, Windows 8, Windows 8.1, or Windows Server 2012 R2. 
آپدیت 4 مربوط به ویژوال استدیو (دقت کنید قبل از نصب آپدیت 4 ویژوال استدیو باید نسخه قبلی  Cordova CTP  را حذف کنید(uninstall) )
امکان توسعه اپلیکیشن‌های windows phone , windows برای کاربران ویندوز 7 وجود ندارد .


در مقاله‌ی بعدی یک پروژه جدید خواهیم ساخت .
منبع مفید برای نصب و راه اندازی :اینجا
ادامه دارد...
بازخوردهای پروژه‌ها
سوال در مورد هدر
سلام
با آرزوی سلامتی و شادی برای شما
سوال اولم اینه که آیا امکان استفاده از قرار دادن شماره صفحه و تعداد صفحات در هدر گزارش وجود داره
سوال دوم اینکه تو گزارشات قسمت هدر تاریخ بر عکس نمایش داده میشه بجا 1392/01/01
01/01/1392 نمایش داده میشه چطوری میشه این مشکل رو رفع کرد
و سوال آخر اینکه راهی برای قرار دادن متن‌ها بصورت پکسلی یا بصورتی که با اختلاف ار لبه کاغذ یا جدول در هدر وجود داره چون با وجود گرید بندی یا جدول سازی دقیقا جایی که میخوام قرار نمیگیره جملات
میخواستم بدونم میشه مشخص کرد دقیقا بصورت x , y یا حالت دیگه آدرس داد
متشکرم از شما و وقتی که میگذارید
مطالب
HTML5 Web Component - قسمت دوم - Custom Elements
Custom Elements، دارای یک چرخه حیات می‌باشند. در طی این چرخه حیات، می‌توان تعدادی متد خاص را به المان سفارشی خود اضافه کرد که به صورت خودکار توسط مرورگر فراخوانی می‌شوند. به این متدها Life-cycle Callbacks یا Custom Element Reactions نیز می‌گویند. برای درک بهتر چرخه حیات مذکور، به تکه کد زیر توجه نمائید:
customElements.define("x-component", class extends HTMLElement {
    constructor() {
        super();
        console.log('constructed!');
    }

    connectedCallback() {
        console.log('connected!');
    }

    disconnectedCallback() {
        console.log('disconnected!');
    }

    adoptedCallback() {
        console.log('adopted!');
    }

    attributeChangedCallback(name, oldValue, newValue) {
        console.log('attirbuteChanged!', name, oldValue, newValue);
    }

    static get observedAttributes() {
        return ['checked','demo','label'];
    }
});

Element Upgrades
به صورت پیش‌فرض، المان‌های موجود در DOM که مبتنی‌بر استانداردهای HTML تعریف نشده‌ا‌ند، توسط مرورگر به عنوان HTMLUnknownElement تجزیه و تحلیل خواهند شد. ولی این موضوع برای المان‌های سفارشی که نام معتبری دارند (وجود «-»)، صدق نمی‌کند. برای مثال دو خط کد زیر را در کنسول مربوط به Developer tools مرورگر خود اجرا کنید:
// "tabs" is not a valid custom element name
document.createElement('tabs') instanceof HTMLUnknownElement === true //true

// "x-tabs" is a valid custom element name
document.createElement('x-tabs') instanceof HTMLElement === true //true
در این صورت، امکان استفاده از المان سفارشی، قبل از معرفی و ثبت آن توسط متد customElements.define نیز وجود خواهد داشت. یعنی اگر در DOM شما تعدادی المان سفارشی وجود داشته باشند که به هر دلیل نیاز است پس از گذشت یک بازه زمانی کوتاهی معرفی و ثبت شوند (مثال: lazy load اسکریپت‌های متناظر با المان‌های سفارشی در Angular)، این المان‌ها معتبر هستند. فرآیند فراخوانی متد define و استفاده از کلاس معرفی شده برای ارتقاء المان سفارشی موجود در DOM، اصطلاحا Element Upgrades نامیده می‌شود. همچنین با استفاده از متد customElements.whenDefined که یک Promise را بازگشت می‌دهد، می‌توان از معرفی و ثبت شدن المان خاصی آگاه شد:
customElements.whenDefined('x-component').then(() => {
  console.log('x-component defined');
});
یا حتی امکان استفاده از سلکتور «‎:defined‏» نیز به شکل زیر وجود دارد:
<share-buttons>
  <social-button type="twitter"><a href="...">Twitter</a></social-button>
  <social-button type="fb"><a href="...">Facebook</a></social-button>
  <social-button type="plus"><a href="...">G+</a></social-button>
</share-buttons>

// Fetch all the children of <share-buttons> that are not defined yet.
let undefinedButtons = buttons.querySelectorAll(':not(:defined)');

let promises = [...undefinedButtons].map(socialButton => {
  return customElements.whenDefined(socialButton.localName);
));

// Wait for all the social-buttons to be upgraded.
Promise.all(promises).then(() => {
  // All social-button children are ready.
});
در اینجا ابتدا تمام المان‌های تعریف نشده، کوئری شده‌اند و با استفاده از متد map و اجرای متد whenDefined، به لیستی از Promiseها رسیده‌ایم و در نهایت با استفاده از Promise.all منتظر اتمام مرحله upgrade المان‌های مذکور هستیم.
سازنده مرتبط با کلاس المان سفارشی x-component، در هنگام وهله‌سازی یا فرآیند upgrades فراخوانی می‌شود و می‌تواند برای مقداردهی اولیه خواص وهله جاری، تنظیم رخدادگردان‌ها (Event Listeners) و یا ایجاد و اتصال ShadowDOM با استفاده از متد attachShadow، محل مناسبی باشد. طبق مستندات مرتبط، فراخوانی ()super بدون ارسال هیچ آرگومانی باید در اولین خط این سازنده انجام شود.
برای وهله‌سازی المان سفارشی نیز می‌توان از متد customeElements.get به شکل زیر استفاده کرد:
customeElements.define('x-component',...)
let XComponent = customElements.get('x-component');
document.body.appendChild(new XComponent())
متد get، ارجاعی به سازنده کلاس x-component را بازگشت خواهد داد.

connectedCallback 
 اولین متد بعد از فراخوانی سازنده، connectedCallback نام دارد و زمانی رخ می‌دهد که وهله‌ای از یک المان سفارشی به Light DOM افزوده شده‌است و یا توسط Parser مرورگر، در DOM شناسایی شود. این متد، محل پیشنهاد شده برای اجرای کدهای زمان راه‌اندازی مانند دریافت منابع از سرور و یا رندر کردن محتوایی خاص، می‌باشد. 
نکته: این متد ممکن است بیش از یکبار نیز فراخوانی شود! هنگامیکه یک المان موجود در DOM از طریق کد از DOM جداشده و سپس اضافه شود:
const el = document.createElement('x-component');
document.body.appendChild(el);
// connectedCallback() called

el.remove();
// disconnectedCallback()

document.body.appendChild(el);
// connectedCallback() called again


disconnectedCallback
این متد نیز هر وقت المانی از DOM حذف شود، اجرا خواهد شد و مانند متد connectedCallback ممکن است چندین بار فراخوانی شود. همچنین محل مناسبی برای آزادسازی منابع استفاده شده در پیاده‌سازی المان سفارشی، می‌باشد. مانند: استفاده از متد clearInterval برای پاکسازی یک تایمر که با متد setInterval ایجاد شده‌است.  
نکته: هیچ تعهدی به اجرای متد disconnectedCallback در تمام حالاتی که یک المان از DOM حذف می‌شود، وجود ندارد. به عنوان مثال هنگامیکه یک برگه‌ی مرورگر بسته شود، این متد فراخوانی نخواهد شد.

‌attributeChangedCallback
 این متد هنگامی فراخوانی خواهد شد که خصوصیات مشخص شده از طریق observedAttributes به عنوان یک static getter، اضافه، حذف، ویرایش و یا جایگزین شوند. همچنین در مرحله Upgrades برای مقادیر اولیه خصوصیات که توسط استفاده کننده از المان سفارشی، مشخص شده‌است نیز فراخوانی می‌شود.
adoptedCallback
متدهای قبلی بیشترین استفاده را دارند؛ این متد خاص نیز زمانیکه یک المان سفارشی به یک DOM دیگری منتقل می‌شود، اجرا خواهد شد. برای مثال:
const iframe = document.querySelector('iframe');
const iframeImages = iframe.contentDocument.querySelectorAll('img');
const newParent = document.getElementById('images');

iframeImages.forEach(function(imgEl) {
  newParent.appendChild(document.adoptNode(imgEl));
});
در تکه کد بالا، لیست المان‌های img موجود در داخل iframe کوئری شده و سپس با پیمایش بر روی لیست بدست آمده و در زمان فراخوانی متد adoptNode که کار تغییر ownerDocument مرتبط با یک المان را انجام می‌دهد، متد adoptedCallback ما نیز اجرا خواهد شد.

Methods

اگر المان‌های بومی و استاندارد موجود را بررسی کنید، همه آنها دارای یکسری متد، پراپرتی و صفات مشخصی هستند. در اینجا نیز تعریف رفتاری برای یک المان سفارشی و کپسوله، نکته خاصی ندارد و به شکل زیر قابل تعریف و استفاده می‌باشد:
class XComponent extends HTMLElement {
  constructor() {
    super();
  }
  
  doSomething(){
    console.log('doSomething');
  }
}

let component = document.querySelector('x-component');
component.doSomething();
مشخص است که امکان تعریف انواع و اقسام متدها با پارامترها و خروجی‌های مختلفی و حتی نسخه‌های همزمان یا ناهمزمان آن‌ها وجود دارد.

Attributes & Properties

در HTML خیلی رایج است که مقادیر پراپرتی‌های یک المان در قالب یکسری صفات، نمودی در DOM هم داشته باشند. برای مثال:
div.id = 'id-value';
div.hidden = true;
انتظار چنین خروجی داریم:
<div id="id-value" hidden>
این موضوع، تحت عنوان «Reflecting Properties to Attributes» مطرح می‌باشد. در این صورت علاوه بر اینکه با یک نگاه به DOM، از مقادیر خصوصیات یک المان آگاه خواهیم بود، امکان استفاده از این صفات به عنوان سلکتورهایی در زمان استایل‌دهی نیز وجود دارد. حال از این مکانیزم برای اعمال یکسری صفات دسترسی‌پذیری مانند صفات ARIA به المان سفارشی خود نیز می‌توان استفاده کرد. برای مثال:
class XComponent extends HTMLElement {
    constructor() {
        super();
    }

    connectedCallback() {
        this._render();
    }

    get disabled() {
        return this.hasAttribute('disabled');
    }

    set disabled(val) {
        // Reflect the value of `disabled` as an attribute.
        if (val) {
            this.setAttribute('disabled', '');
        } else {
            this.removeAttribute('disabled');
        }

        this._render();
    }

    _render() {
        //... 
    }
}
ایده کار خیلی ساده است؛ پراپرتی‌های یک المان‌سفارشی را از طریق متدهای getter و setter تعریف کرده و در بدنه پیاده‌سازی آنها، صفات HTML ای المان جاری را تغییر داده و یا از مقادیر آنها استفاده کنیم.
اینبار با مقداردهی پراپرتی disabled برروی وهله‌ای از المان سفارشی ما، این مقادیر نمودی در DOM هم خواهند داشت. با استفاده از متدهای setAttribute یا removeAttribute کار همگام‌سازی پراپرتی‌ها با صفات را انجام داده‌ایم.
همچین با استفاده از متد attributeChangedCallback نیز می‌توان برای اعمال صفات ARIA که اشاره شد، به شکل زیر استفاده کرد:
attributeChangedCallback(name, oldValue, newValue) {
  switch (name) {
    case 'checked':
      // Note the attributeChangedCallback is only handling the *side effects*
      // of setting the attribute.
      this.setAttribute('aria-disabled', !!newValue);
      break;
    ...
  }
یا حتی به شکل زیر:
attributeChangedCallback(name, oldValue, newValue) {
    // When the component is disabled, update keyboard/screen reader behavior.
    if (this.disabled) {
      this.setAttribute('tabindex', '-1');
      this.setAttribute('aria-disabled', 'true');
    } else {
      this.setAttribute('tabindex', '0');
      this.setAttribute('aria-disabled', 'false');
    }
    // TODO: also react to the other attribute changing.
  }
در اینجا از همان getter که طبق پیاده‌سازی ما، در پشت صحنه از همان مقدار صفت disabled استفاده می‌کند، برای تنظیم یکسری صفات دیگر استفاده کرده‌ایم. به عنوان مثال اگر المان ما غیرفعال شده بود، صفت tabindex آن را با «‎-1‏» مقداردهی می‌کنیم تا از توالی پیمایش مبتنی‌بر Tab خارج شود.
نکته: پیشنهاد می‌شود از مکانیزم همگام‌سازی پراپرتی‌ها و صفات، برای انواع داده‌ای اولیه (رشته، عدد و ...) استفاده شود و برای دریافت مقادیری مانند objects و یا arrays، از متدها یا پراپرتی‌ها استفاده کنید. در غیر این صورت نیاز خواهد بود که این مقادیر را سریالایز و در داخل المان سفارشی، عملیات معکوس آن را انجام دهید که می‌تواند هزینه‌ی زیادی داشته باشد. عملیات سریالایز نیز خود باعث از دست دادن ارجاعات به آن مقادیر خواهد شد. به صورت کلی هیچکدام از المان‌های بومی موجود، چنین اطلاعاتی را دریافت نمی‌کند. برای مثال:
constructor() {
    super();

    this._data = [];
}

get data() {
    return _data;
}

set data(value) {
    if (this_data === value) return;
    this._data = value;
    this._render();
}

Lazy Properties

همانطور که اشاره شد حتی قبل از مرحله upgrades مربوط به المان‌های سفارشی استفاده شده در سند HTML برنامه شما، به عنوان المان‌های معتبری هستند که امکان کوئری کردن و مقداردهی اولیه خصوصیات آنها از طریق کد نیز ممکن است. این موضوع زمانیکه از فریم‌ورکی مثل Angular استفاده می‌شود، المان موردنظر به صورت خودکار توسط فریم‌ورک لود و به صفحه اضافه شده و در انتهای عملیات، binding پراپرتی‌های آن به خصوصیات موجود در کامپوننت Angular ای انجام خواهد شد. به مثال زیر توجه کنید:
<x-component [disabled]="model.disabled"></x-component>
در این صورت اگر لود اسکریپت، معرفی و ثبت این المان سفارشی به صورت Lazy انجام شود، امکان آن وجود دارد که فریم‌ورک، عملیات binding را قبل از مرحله upgrades انجام دهد. خوب... این موضوع چه مشکلی را ایجاد می‌کند؟ در این صورت چون مرحله upgrades تمام نشده است، پیاده‌سازی بدنه متد setter متناظر با خصوصیات المان سفارشی، توسط پراپرتی جدیدی که توسط فریم‌ورک برروی وهله موجود تعریف می‌شود، بی‌استفاده خواهد ماند. برای مثال، اگر سعی کنیم قبل از مرحله upgrades خصوصیت disabled المان x-component را مقداردهی کنیم، عملیات مکانیزم همگام‌سازی مدنظر ما اجرا نخواهد شد:
let el = document.querySelector('x-component');
el.disabled = true;

customElements.define("x-component", class extends HTMLElement {
    constructor() {
        super();
    }

    get disabled() {
        return this.hasAttribute('disabled');
    }

    set disabled(val) {
        // Reflect the value of `disabled` as an attribute.
        if (val) {
            this.setAttribute('disabled', '');
        } else {
            this.removeAttribute('disabled');
        }

        this._render();
    }
});

با این خروجی مواجه خواهیم شد که هیچ اثری از صفت disabled دیده نمی‌شود:


یکی از روش‌های پیشنهاد شده برای حل این مشکل، مقداردهی مجدد پراپرتی‌ها بعد از مرحله upgrades و پس از اینکه متد setter تعریف شده‌است، می‌باشد:
let el = document.querySelector('x-component');
el.disabled = true;

customElements.define("x-component", class extends HTMLElement {
    constructor() {
        super();
    }

    connectedCallback() {
        this._upgradeProp('disabled');
    }

    get disabled() {
        return this.hasAttribute('disabled');
    }

    set disabled(val) {
        // Reflect the value of `disabled` as an attribute.
        if (val) {
            this.setAttribute('disabled', '');
        } else {
            this.removeAttribute('disabled');
        }

        this._render();
    }

    _upgradeProp(prop) {
        if (this.hasOwnProperty(prop)) {
            let value = this[prop];
            delete this[prop]; //delete instance property
            this[prop] = value; // set prototype property
        }
    }
});
ایده کار به این صورت است که مقدار پراپرتی مورد نظر را که قبل از مرحله upgrades برروی وهله جاری (instance property) تنظیم شده‌است، در متغییری نگهداری کرده و آن پراپرتی را حذف و سپس پراپرتی تعریف شده در کلاس (prototype property) را برای وهله جاری مقداردهی کنیم.
نکته: به یاد داشته باشید که قبل از اینکه یکسری صفات خاص را مقدار دهی کنید، بررسی شود که استفاده کننده از المان سفارشی، مقداری را تنظیم نکرده باشد. برای مثال اگر قصد دارید المان سفارشی شما قابلیت focus را داشته باشد، نیاز است شما حداقل tabindex=-1 را تنظیم کنید؛ حتی اگر استفاده کننده، آن را مقداردهی نکرده باشد:
connectedCallback() {
  if (!this.hasAttribute('role'))
    this.setAttribute('role', 'checkbox');
  if (!this.hasAttribute('tabindex'))
    this.setAttribute('tabindex', -1); //element is not reachable via sequential keyboard navigation, but could be focused
}

مطالب
مهاجرت از SQL Membership به ASP.NET Identity
در این مقاله مهاجرت یک اپلیکیشن وب که توسط SQL Membership ساخته شده است را به سیستم جدید ASP.NET Identity بررسی می‌کنیم. برای این مقاله از یک قالب اپلیکیشن وب (Web Forms) که توسط Visual Studio 2010 ساخته شده است برای ساختن کاربران و نقش‌ها استفاده می‌کنیم. سپس با استفاده از یک SQL Script دیتابیس موجود را به دیتابیسی که ASP.NET Identity نیاز دارد تبدیل می‌کنیم. در قدم بعدی پکیج‌های مورد نیاز را به پروژه اضافه می‌کنیم و صفحات جدیدی برای مدیریت حساب‌های کاربری خواهیم ساخت. بعنوان یک تست، کاربران قدیمی که توسط SQL Membership ساخته شده بودند باید قادر باشند به سایت وارد شوند. همچنین کاربران جدید باید بتوانند بدون هیچ مشکلی در سیستم ثبت نام کنند. سورس کد کامل این مقاله را می‌توانید از این لینک دریافت کنید.


یک اپلیکیشن با SQL Membership بسازید

برای شروع به اپلیکیشنی نیاز داریم که از SQL Membership استفاده می‌کند و دارای داده هایی از کاربران و نقش‌ها است. برای این مقاله، بگذارید پروژه جدیدی توسط VS 2010 بسازیم.

حال با استفاده از ابزار ASP.NET Configuration دو کاربر جدید بسازید: oldAdminUser و oldUser.

نقش جدیدی با نام Admin بسازید و کاربر oldAdminUser را به آن اضافه کنید.

بخش جدیدی با نام Admin در سایت خود بسازید و فرمی بنام Default.aspx به آن اضافه کنید. همچنین فایل web.config این قسمت را طوری پیکربندی کنید تا تنها کاربرانی که در نقش Admin هستند به آن دسترسی داشته باشند. برای اطلاعات بیشتر به این لینک مراجعه کنید.

پنجره Server Explorer را باز کنید و جداول ساخته شده توسط SQL Membership را بررسی کنید. اطلاعات اصلی کاربران که برای ورود به سایت استفاده می‌شوند، در جداول aspnet_Users و aspnet_Membership ذخیره می‌شوند. داده‌های مربوط به نقش‌ها نیز در جدول aspnet_Roles ذخیره خواهند شد. رابطه بین کاربران و نقش‌ها نیز در جدول aspnet_UsersInRoles ذخیره می‌شود، یعنی اینکه هر کاربری به چه نقش هایی تعلق دارد.

برای مدیریت اساسی سیستم عضویت، مهاجرت جداول ذکر شده به سیستم جدید ASP.NET Identity کفایت می‌کند.

مهاجرت به Visual Studio 2013

  • برای شروع ابتدا Visual Studio Express 2013 for Web یا Visual Studio 2013 را نصب کنید.
  • حال پروژه ایجاد شده را در نسخه جدید ویژوال استودیو باز کنید. اگر نسخه ای از SQL Server Express را روی سیستم خود نصب نکرده باشید، هنگام باز کردن پروژه پیغامی به شما نشان داده می‌شود. دلیل آن وجود رشته اتصالی است که از SQL Server Express استفاده می‌کند. برای رفع این مساله می‌توانید SQL Express را نصب کنید، و یا رشته اتصال را طوری تغییر دهید که از LocalDB استفاده کند.
  • فایل web.config را باز کرده و رشته اتصال را مانند تصویر زیر ویرایش کنید.

  • پنجره Server Explorer را باز کنید و مطمئن شوید که الگوی جداول و داده‌ها قابل رویت هستند.
  • سیستم ASP.NET Identity با نسخه 4.5 دات نت فریم ورک و بالا‌تر سازگار است. پس نسخه فریم ورک پروژه را به آخرین نسخه (4.5.1) تغییر دهید.

پروژه را Build کنید تا مطمئن شوید هیچ خطایی وجود ندارد.

نصب پکیج‌های NuGet

در پنجره Solution Explorer روی نام پروژه خود کلیک راست کرده، و گزینه Manage NuGet Packages را انتخاب کنید. در قسمت جستجوی دیالوگ باز شده، عبارت "Microsoft.AspNet.Identity.EntityFramework" را وارد کنید. این پکیج را در لیست نتایج انتخاب کرده و آن را نصب کنید. نصب این بسته، نیازمندهای موجود را بصورت خودکار دانلود و نصب می‌کند: EntityFramework و ASP.NET Identity Core. حال پکیج‌های زیر را هم نصب کنید (اگر نمی‌خواهید OAuth را فعال کنید، 4 پکیج آخر را نادیده بگیرید).
  • Microsoft.AspNet.Identity.Owin
  • Microsoft.Owin.Host.SystemWeb
  • Microsoft.Owin.Security.Facebook
  • Microsoft.Owin.Security.Google
  • Microsoft.Owin.Security.MicrosoftAccount
  • Microsoft.Owin.Security.Twitter

مهاجرت دیتابیس فعلی به سیستم ASP.NET Identity

قدم بعدی مهاجرت دیتابیس فعلی به الگویی است، که سیستم ASP.NET Identity به آن نیاز دارد. بدین منظور ما یک اسکریپت SQL را اجرا می‌کنیم تا جداول جدیدی بسازد و اطلاعات کاربران را به آنها انتقال دهد. فایل این اسکریپت را می‌توانید از لینک https://github.com/suhasj/SQLMembership-Identity-OWIN دریافت کنید.
این اسکریپت مختص این مقاله است. اگر الگوی استفاده شده برای جداول سیستم عضویت شما ویرایش/سفارشی-سازی شده باید این اسکریپت را هم بر اساس این تغییرات بروز رسانی کنید.
پنجره Server Explorer را باز کنید. گره اتصال ApplicationServices را باز کنید تا جداول را مشاهده کنید. روی گره Tables کلیک راست کرده و گزینه New Query را انتخاب کنید.

در پنجره کوئری باز شده، تمام محتویات فایل Migrations.sql را کپی کنید. سپس اسکریپت را با کلیک کردن دکمه Execute اجرا کنید.

ممکن است با اخطاری مواجه شوید مبنی بر آنکه امکان حذف (drop) بعضی از جداول وجود نداشت. دلیلش آن است که چهار عبارت اولیه در این اسکریپت، تمام جداول مربوط به Identity را در صورت وجود حذف می‌کنند. از آنجا که با اجرای اولیه این اسکریپت چنین جداولی وجود ندارند، می‌توانیم این خطاها را نادیده بگیریم. حال پنجره Server Explorer را تازه (refresh) کنید و خواهید دید که پنج جدول جدید ساخته شده اند.

لیست زیر نحوه Map کردن اطلاعات از جداول SQL Membership به سیستم Identity را نشان می‌دهد.

  • aspnet_Roles --> AspNetRoles
  • aspnet_Users, aspnet_Membership --> AspNetUsers
  • aspnet_UsersInRoles --> AspNetUserRoles

جداول AspNetUserClaims و AspNetUserLogins خالی هستند. فیلد تفکیک کننده (Discriminator) در جدول AspNetUsers باید مطابق نام کلاس مدل باشد، که در مرحله بعدی تعریف خواهد شد. همچنین ستون PasswordHash به فرم 'encrypted password|password salt|password format' می‌باشد. این شما را قادر می‌سازد تا از رمزنگاری برای ذخیره و بازیابی کلمه‌های عبور استفاده کنید. این مورد نیز در ادامه مقاله بررسی شده است.



ساختن مدل‌ها و صفحات عضویت

بصورت پیش فرض سیستم ASP.NET Identity برای دریافت و ذخیره اطلاعات در دیتابیس عضویت از Entity Framework استفاده می‌کند. برای آنکه بتوانیم با جداول موجود کار کنیم، می‌بایست ابتدا مدل هایی که الگوی دیتابیس را نمایندگی می‌کنند ایجاد کنیم. برای این کار مدل‌های ما یا باید اینترفیس‌های موجود در Identity.Core را پیاده سازی کنند، یا می‌توانند پیاده سازی‌های پیش فرض را توسعه دهند. پیاده سازی‌های پیش فرض در Microsoft.AspNet.Identity.EntityFramework وجود دارند.
در نمونه ما، جداول AspNetRoles, AspNetUserClaims, AspNetLogins و AspNetUserRole ستون هایی دارند که شباهت زیادی به پیاده سازی‌های پیش فرض سیستم Identity دارند. در نتیجه می‌توانیم از کلاس‌های موجود، برای Map کردن الگوی جدید استفاده کنیم. جدول AspNetUsers ستون‌های جدیدی نیز دارد. می‌توانیم کلاس جدیدی بسازیم که از IdentityUser ارث بری کند و آن را گسترش دهیم تا این فیلدهای جدید را پوشش دهد.
پوشه ای با نام Models بسازید (در صورتی که وجود ندارد) و کلاسی با نام User به آن اضافه کنید.

کلاس User باید کلاس IdentityUser را که در اسمبلی Microsoft.AspNet.Identity.EntityFramework وجود دارد گسترش دهد. خاصیت هایی را تعریف کنید که نماینده الگوی جدول AspNetUser هستند. خواص ID, Username, PasswordHash و SecurityStamp در کلاس IdentityUser تعریف شده اند، بنابراین این خواص را در لیست زیر نمی‌بینید.

  public class User : IdentityUser
    {
        public User()
        {
            CreateDate = DateTime.Now;
            IsApproved = false;
            LastLoginDate = DateTime.Now;
            LastActivityDate = DateTime.Now;
            LastPasswordChangedDate = DateTime.Now;
            LastLockoutDate = DateTime.Parse("1/1/1754");
            FailedPasswordAnswerAttemptWindowStart = DateTime.Parse("1/1/1754");
            FailedPasswordAttemptWindowStart = DateTime.Parse("1/1/1754");
        }

        public System.Guid ApplicationId { get; set; }
        public string MobileAlias { get; set; }
        public bool IsAnonymous { get; set; }
        public System.DateTime LastActivityDate { get; set; }
        public string MobilePIN { get; set; }
        public string Email { get; set; }
        public string LoweredEmail { get; set; }
        public string LoweredUserName { get; set; }
        public string PasswordQuestion { get; set; }
        public string PasswordAnswer { get; set; }
        public bool IsApproved { get; set; }
        public bool IsLockedOut { get; set; }
        public System.DateTime CreateDate { get; set; }
        public System.DateTime LastLoginDate { get; set; }
        public System.DateTime LastPasswordChangedDate { get; set; }
        public System.DateTime LastLockoutDate { get; set; }
        public int FailedPasswordAttemptCount { get; set; }
        public System.DateTime FailedPasswordAttemptWindowStart { get; set; }
        public int FailedPasswordAnswerAttemptCount { get; set; }
        public System.DateTime FailedPasswordAnswerAttemptWindowStart { get; set; }
        public string Comment { get; set; }
    }

حال برای دسترسی به دیتابیس مورد نظر، نیاز به یک DbContext داریم. اسمبلی Microsoft.AspNet.Identity.EntityFramework کلاسی با نام IdentityDbContext دارد که پیاده سازی پیش فرض برای دسترسی به دیتابیس ASP.NET Identity است. نکته قابل توجه این است که IdentityDbContext آبجکتی از نوع TUser را می‌پذیرد. TUser می‌تواند هر کلاسی باشد که از IdentityUser ارث بری کرده و آن را گسترش می‌دهد.

در پوشه Models کلاس جدیدی با نام ApplicationDbContext بسازید که از IdentityDbContext ارث بری کرده و از کلاس User استفاده می‌کند.

public class ApplicationDbContext : IdentityDbContext<User>
{
        
}

مدیریت کاربران در ASP.NET Identity توسط کلاسی با نام UserManager انجام می‌شود که در اسمبلی Microsoft.AspNet.Identity.EntityFramework قرار دارد. چیزی که ما در این مرحله نیاز داریم، کلاسی است که از UserManager ارث بری می‌کند و آن را طوری توسعه می‌دهد که از کلاس User استفاده کند.

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

public class UserManager : UserManager<User>
{
        
}

کلمه عبور کاربران بصورت رمز نگاری شده در دیتابیس ذخیره می‌شوند. الگوریتم رمز نگاری SQL Membership با سیستم ASP.NET Identity تفاوت دارد. هنگامی که کاربران قدیمی به سایت وارد می‌شوند، کلمه عبورشان را توسط الگوریتم‌های قدیمی SQL Membership رمزگشایی می‌کنیم، اما کاربران جدید از الگوریتم‌های ASP.NET Identity استفاده خواهند کرد.

کلاس UserManager خاصیتی با نام PasswordHasher دارد. این خاصیت نمونه ای از یک کلاس را ذخیره می‌کند، که اینترفیس IPasswordHasher را پیاده سازی کرده است. این کلاس هنگام تراکنش‌های احراز هویت کاربران استفاده می‌شود تا کلمه‌های عبور را رمزنگاری/رمزگشایی شوند. در کلاس UserManager کلاس جدیدی بنام SQLPasswordHasher بسازید. کد کامل را در لیست زیر مشاهده می‌کنید.

public class SQLPasswordHasher : PasswordHasher
{
        public override string HashPassword(string password)
        {
            return base.HashPassword(password);
        }

        public override PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword)
        {
            string[] passwordProperties = hashedPassword.Split('|');
            if (passwordProperties.Length != 3)
            {
                return base.VerifyHashedPassword(hashedPassword, providedPassword);
            }
            else
            {
                string passwordHash = passwordProperties[0];
                int passwordformat = 1;
                string salt = passwordProperties[2];
                if (String.Equals(EncryptPassword(providedPassword, passwordformat, salt), passwordHash, StringComparison.CurrentCultureIgnoreCase))
                {
                    return PasswordVerificationResult.SuccessRehashNeeded;
                }
                else
                {
                    return PasswordVerificationResult.Failed;
                }
            }
        }

        //This is copied from the existing SQL providers and is provided only for back-compat.
        private string EncryptPassword(string pass, int passwordFormat, string salt)
        {
            if (passwordFormat == 0) // MembershipPasswordFormat.Clear
                return pass;

            byte[] bIn = Encoding.Unicode.GetBytes(pass);
            byte[] bSalt = Convert.FromBase64String(salt);
            byte[] bRet = null;

            if (passwordFormat == 1)
            { // MembershipPasswordFormat.Hashed 
                HashAlgorithm hm = HashAlgorithm.Create("SHA1");
                if (hm is KeyedHashAlgorithm)
                {
                    KeyedHashAlgorithm kha = (KeyedHashAlgorithm)hm;
                    if (kha.Key.Length == bSalt.Length)
                    {
                        kha.Key = bSalt;
                    }
                    else if (kha.Key.Length < bSalt.Length)
                    {
                        byte[] bKey = new byte[kha.Key.Length];
                        Buffer.BlockCopy(bSalt, 0, bKey, 0, bKey.Length);
                        kha.Key = bKey;
                    }
                    else
                    {
                        byte[] bKey = new byte[kha.Key.Length];
                        for (int iter = 0; iter < bKey.Length; )
                        {
                            int len = Math.Min(bSalt.Length, bKey.Length - iter);
                            Buffer.BlockCopy(bSalt, 0, bKey, iter, len);
                            iter += len;
                        }
                        kha.Key = bKey;
                    }
                    bRet = kha.ComputeHash(bIn);
                }
                else
                {
                    byte[] bAll = new byte[bSalt.Length + bIn.Length];
                    Buffer.BlockCopy(bSalt, 0, bAll, 0, bSalt.Length);
                    Buffer.BlockCopy(bIn, 0, bAll, bSalt.Length, bIn.Length);
                    bRet = hm.ComputeHash(bAll);
                }
            }

            return Convert.ToBase64String(bRet);
    }
}



دقت کنید تا فضاهای نام System.Text و System.Security.Cryptography را وارد کرده باشید.

متد EncodePassword کلمه عبور را بر اساس پیاده سازی پیش فرض SQL Membership رمزنگاری می‌کند. این الگوریتم از System.Web گرفته می‌شود. اگر اپلیکیشن قدیمی شما از الگوریتم خاصی استفاده می‌کرده است، همینجا باید آن را منعکس کنید. دو متد دیگر نیز بنام‌های HashPassword و VerifyHashedPassword نیاز داریم. این متدها از EncodePassword برای رمزنگاری کلمه‌های عبور و تایید آنها در دیتابیس استفاده می‌کنند.

سیستم SQL Membership برای رمزنگاری (Hash) کلمه‌های عبور هنگام ثبت نام و تغییر آنها توسط کاربران، از PasswordHash, PasswordSalt و PasswordFormat استفاده می‌کرد. در روند مهاجرت، این سه فیلد در ستون PasswordHash جدول AspNetUsers ذخیره شده و با کاراکتر '|' جدا شده اند. هنگام ورود کاربری به سایت، اگر کله عبور شامل این فیلدها باشد از الگوریتم SQL Membership برای بررسی آن استفاده می‌کنیم. در غیر اینصورت از پیاده سازی پیش فرض ASP.NET Identity استفاده خواهد شد. با این روش، کاربران قدیمی لازم نیست کلمه‌های عبور خود را صرفا بدلیل مهاجرت اپلیکیشن ما تغییر دهند.

کلاس UserManager را مانند قطعه کد زیر بروز رسانی کنید.

public UserManager()
            : base(new UserStore<User>(new ApplicationDbContext()))
        {
            this.PasswordHasher = new SQLPasswordHasher();
 }

ایجاد صفحات جدید مدیریت کاربران

قدم بعدی ایجاد صفحاتی است که به کاربران اجازه ثبت نام و ورود را می‌دهند. صفحات قدیمی SQL Membership از کنترل هایی استفاده می‌کنند که با ASP.NET Identity سازگار نیستند. برای ساختن این صفحات جدید به این مقاله مراجعه کنید. از آنجا که در این مقاله پروژه جدید را ساخته ایم و پکیج‌های لازم را هم نصب کرده ایم، می‌توانید مستقیما به قسمت Adding Web Forms for registering users to your application بروید.
چند تغییر که باید اعمال شوند:
  • فایل‌های Register.aspx.cs و Login.aspx.cs از کلاس UserManager استفاده می‌کنند. این ارجاعات را با کلاس UserManager جدیدی که در پوشه Models ساختید جایگزین کنید.
  • همچنین ارجاعات استفاده از کلاس IdentityUser را به کلاس User که در پوشه Models ساختید تغییر دهید.
  • لازم است توسعه دهنده مقدار ApplicationId را برای کاربران جدید طوری تنظیم کند که با شناسه اپلیکیشن جاری تطابق داشته باشد. برای این کار می‌توانید پیش از ساختن حساب‌های کاربری جدید در فایل Register.aspx.cs ابتدا شناسه اپلیکیشن را بدست آورید و اطلاعات کاربر را بدرستی تنظیم کنید.
مثال: در فایل Register.aspx.cs متد جدیدی تعریف کنید که جدول aspnet_Applications را بررسی میکند و شناسه اپلیکیشن را بر اساس نام اپلیکیشن بدست می‌آورد.
private Guid GetApplicationID()
{
    using (SqlConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings["ApplicationServices"].ConnectionString))
            {
                string queryString = "SELECT ApplicationId from aspnet_Applications WHERE ApplicationName = '/'"; //Set application name as in database

                SqlCommand command = new SqlCommand(queryString, connection);
                command.Connection.Open();

                var reader = command.ExecuteReader();
                while (reader.Read())
                {
                    return reader.GetGuid(0);
                }

            return Guid.NewGuid();
        }
}


  حال می‌توانید این مقدار را برای آبجکت کاربر تنظیم کنید.
var currentApplicationId = GetApplicationID();

User user = new User() { UserName = Username.Text,
ApplicationId=currentApplicationId, …};
در این مرحله می‌توانید با استفاده از اطلاعات پیشین وارد سایت شوید، یا کاربران جدیدی ثبت کنید. همچنین اطمینان حاصل کنید که کاربران پیشین در نقش‌های مورد نظر وجود دارند.
مهاجرت به ASP.NET Identity مزایا و قابلیت‌های جدیدی را به شما ارائه می‌کند. مثلا کاربران می‌توانند با استفاده از تامین کنندگان ثالثی مثل Facebook, Google, Microsoft, Twitter و غیره به سایت وارد شوند. اگر به سورس کد این مقاله مراجعه کنید خواهید دید که امکانات OAuth نیز فعال شده اند.
در این مقاله انتقال داده‌های پروفایل کاربران بررسی نشد. اما با استفاده از نکات ذکر شده می‌توانید پروفایل کاربران را هم بسادگی منتقل کنید. کافی است مدل‌های لازم را در پروژه خود تعریف کرده و با استفاده از اسکریپت‌های SQL داده‌ها را انتقال دهید.
بازخوردهای دوره
صفحات مودال در بوت استرپ 3
- روی صفحه نهایی کلیک راست کنید و سپس سورس خروجی HTML تولید شده را به دقت بررسی کنید.
- برگه‌ی network کنسول developer مرورگر را جهت بررسی خروجی درخواست ارسالی به سرور، به دقت بررسی کنید. اینجا علت اصلی را پیدا خواهید کرد.
- خطاهای سمت سرور را توسط ELMAH لاگ و بررسی کنید.
اشتراک‌ها
چرا لینک های درون صفحه ای (Anchor Tags) در Blazor عمل نمی کند و چگونه این مشکل را برطرف کنیم

اگر در درون یک کامپوننت بخواهیم لینکی تعریف کنیم که کاربر با کلیک بر روی آن به بخش دیگری از همان صفحه برود (کاری که به راحتی در Html با تعریف یک آنکور <a> میسر است) در Blazor میسر نیست.

در این مقاله روش برطرف کردن این مشکل به همراه سورس توضیح داده شده است.

چرا لینک های درون صفحه ای (Anchor Tags) در Blazor عمل نمی کند و چگونه این مشکل را برطرف کنیم
نظرات مطالب
شروع به کار با AngularJS 2.0 و TypeScript - قسمت ششم - کامپوننت‌های تو در تو
اگر بخواهیم داخل یک کامپوننت والد به متدها و پروپرتی‌های یک کامپوننت دیگر(الزاما کامپوننت دیگر جز فرزندان آن کامپوننت نیست) دسترسی داشته باشیم .مثلا کامپوننت A  روی صفحه نمایش داده شده و با ایونت کلیک کامپوننت B متد CreateDesign متد A صدا زده شده و باعث تغییر UI کامپوننت A شود. روش کار چگونه است؟