مطالب
نحوه ایجاد یک اسلایدشو به صورت داینامیک

در این آموزش قصد دارم نحوه ایجاد یک Slideshow به صورت داینامیک را با ASP.NET طراحی کنم(منظور از ایجاد Slideshow به صورت داینامیک این است که عکس‌ها را به صورت داینامیک از DB بخواند).

اولین گام این است که Plugin مورد نظر را دریافت کنید که من از پلاگین Orbit استفاده کرده ام

ابتدا یک DataBase با نام DynamicSlideShow ایجاد و یک جدول با ساختار زیر با نام Picturesدرون آن ایجاد می‌کنیم

گام بعدی ایجاد یک پروژه Asp.Net با زبان C# و اضافه کردن فایلهای پلاگین به پروژه و ایجاد یک Handler برای بازیابی داده‌ها از DB است. ساختار   Solutionما باید به صورت زیر باشد

برای اینکه بتوانیم با DB ارتباط برقرار کنیم از EF استفاده می‌کنیم به همین منظور ابتدا یک Model  به نام DynamicSlideShowModel ایجاد می‌کنیم

در گام بعد بر روی GenerateFromDatabase  انتخاب کرده و بر روی دکمه Next کلیک می‌کنیم و در مراحل بعد ابتدا DB مربوط به مثال (DynamicSlideShow) و جدول آن را انتخاب می‌کنیم . حال یک Model به درون پوشه App_Code  اضافه شده است  

در ادامه برای واکشی رکوردهای موجود در جدول  Pictures  کدهای زیر را درون Handler می‌نویسیم

  var ctx = new DynamicSlideShowEntities();
  var list = ctx.Pictures.ToList();
  string str = JsonConvert.SerializeObject(list);
  context.Response.Write(str);

  

در این کدها تنها نکته‌ای که احتیاج به توضیح دارد این است که ابتدا یک لیست از رکوردها را از جدول Pictures واکشی می‌کنیم و برای پاس دادن به کلاینت ما آن‌ها را به فرمت Json تبدیل کرده ایم که برای تبدبل از کنابخانه آماده Newtonsoft.Json.dll استفاده کرده ایم 

حال باید با استفاده از jQuery کدهای درون Handler را اجرا کنیم؛ برای همین منظور یک صفحه با نام default.aspx ایجاد کرده و کدهای زیر را درون آن می‌نویسیم

<head runat="server">
    <title>Dynamic SlideShow</title>
    <link href="CSS/orbit-1.2.3.css" rel="stylesheet" type="text/css" />
    <script src="Script/jquery-1.7.1.min.js" type="text/javascript"></script>
    <script src="Script/jquery.orbit-1.2.3.min.js" type="text/javascript"></script>
    <script type="text/javascript">
        $(function () {
            $.ajax({
                url: "Handler.ashx",
                contentType: "application/json; charset=utf-8",
                success: function (data) {
                    $.each(data, function (i, b) {
                        var str = '<img src="' + b.PicturePath + '" alt="' + b.PictureText + '"/>';
                        $("#featured").append(str);
                    });
                    $('#featured').orbit();
                },
                dataType: "json"

            });
            });
    </script>
</head>
<body>
    <form id="form1" runat="server">
    <div id="featured">
    </div>
    </form>
</body>

در اینجا ابتدا با استفاده از متد ajax کتابخانه jQuery کدهای درون Handler را اجرا کرده و به ازای هر المان موجود در جدول یک تگ img به صفحه اضافه می‌کنیم.

مطالب
چه زمانی بهتر است از Silverlight استفاده شود؟

1- نیاز به توانایی‌های موجود در برنامه‌های Desktop را دارید اما همچنین نیاز است تا آن‌ها را تحت وب نیز ارائه دهید.
یکی از دلایل اقبال به برنامه‌های تحت وب در سازمان‌ها عدم نیاز به نصب آن‌ها و توزیع هر چه ساده‌تر اینگونه برنامه‌ها در شبکه است. تنها کافی است چند فایل را بر روی سرور به روز رسانی کنید و پس از آن تمام کلاینت‌ها از آخرین نگارش برنامه شما بهره‌مند خواهند شد (+). توزیع برنامه‌های سیلورلایت نیز به همین منوال است. علاوه بر آن استفاده از فناورهایی مانند MEF امکان ماژولار ساختن برنامه و دریافت آخرین ماژول‌های تهیه شده (فایل‌های XAP مجزای از برنامه به صورت افزونه) را بر اساس انتخاب و سطح دسترسی کاربر نیز میسر می‌سازد.

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

3- برنامه‌ی شما نیاز است تا از طریق وب توزیع شود اما نیاز به سطح دسترسی بیشتری نسبت به یک برنامه‌ی وب معمولی دارد.
تمام برنامه‌های توزیع شده از طریق مرورگرها محدود به سطوح دسترسی آن‌ها نیز هستند. اما امکان نصب خارج از مرورگر برنامه‌های سیلورلایت نیز وجود دارد. در این حالت می‌توان در صورت نیاز و همچنین تائید صریح کاربر، به سطوح دسترسی بیشتری دست یافت. برای مثال دسترسی به اسکنر در یک برنامه‌ی وب متداول بی‌معنا است. اما سیلورلایت 4 در حالت اجرای در خارج از مرورگر امکان تعامل با اشیاء COM را نیز دارد.

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

5- از مشکلات مدیریت حالت در برنامه‌های متداول وب به تنگ آمده‌اید.
اگر برای مثال برنامه نویس ASP.NET باشید حتما با مباحث State management آشنایی دارید (از سشن و کوکی گرفته تا ViewState (ایی که همه به نحوی قصد کوچک کردن آن‌را دارند!) و غیره). تمام این‌ها هم برای این است که بتوان تجربه‌ی کاری برنامه‌های دسکتاپ را در محیط مرورگرها شبیه سازی کرد. این مشکلات در سیلورلایت حل شده است. یک برنامه‌ی سیلورلایت State full است نه Stateless . همچنین اگر از حافظه‌ای هم استفاده می‌کند این مورد در سمت کاربر است و نه سمت سرور و نه منقضی شدن زود هنگام سشن‌ها و صدها ترفند برای مقیاس پذیری همین مساله‌ی بسیار کوچک با تعداد کاربران بالا در برنامه‌های متداول وب.
به عبارتی تصور کنید که برنامه‌ی دسکتاپ سال‌های قبل شما هم اکنون داخل مرورگر دارد اجرا می‌شود و چیزی به نام وب سرور وجود ندارد که پس از نمایش صفحه‌ی وب شما، کلیه‌ی اشیاء مرتبط با آن‌را در سمت سرور تخریب کند چون باید پاسخگوی کاربران همزمان بی‌شماری باشد و منابع سرور هم محدود است. (سیلورلایت یک فناوری سمت کاربر است. بنابراین وب سرور صرفا نقش توزیع آن‌را به عهده دارد یا حداکثر ارائه‌ی یک وب سرویس جهت تعاملات بعدی مانند کار با بانک اطلاعاتی)

6- نیاز دارید تا برنامه‌ی وب شما تحت تمام مرورگرها به یک شکل به نظر برسد و همچنین رفتار یکسانی هم داشته باشد.
هیچ وقت روزی را فراموش نمی‌کنم که حین پرداخت الکترونیکی بانک XYZ به کمک مرورگر فایرفاکس، دکمه‌ی پرداخت در مرحله‌ی آخر، کار نمی‌کرد! هر چقدر روی آن کلیک می‌کردم اتفاقی نمی‌افتاد! تراکنش برگشت خورد و همین خرید ساده با مرورگر IE به سادگی انجام شد.
با سیلورلایت این مشکلات را نخواهید داشت زیرا کار نمایش برنامه شما توسط افزونه‌ی مربوطه صورت می‌گیرد و این افزونه مستقل است از نوع مرورگر شما.

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

8- برنامه وب شما نیاز است تا با فایل‌های مالتی مدیا تعامل داشته و آن‌ها را پخش کند.
حتی تگ Video در HTML5 نیز به پای توانایی‌های مالتی مدیا در Silverlight مانند smooth streaming, multicasting, editing, video brushes نمی‌رسد. برای مثال با استفاده از video brushes می‌توان یک فایل ویدیویی در حال پخش را بر روی یک وجه یک شیء در حال پویانمایی نقاشی و نمایش داد.

9- نیاز به پشتیبانی از multi-touch در برنامه‌ی وب شما وجود دارد.
برخلاف HTML ، تعاملات multi-touch در Silverlight میسر است.

10- نیاز به ایجاد برنامه‌های بازی تحت وب دارید.
به طور قطع می‌توان بازیی‌هایی در حد Pong را با جاوا اسکریپت هم ایجاد کرد، اما اگر نیاز به تولید بازی‌هایی جدی‌تر وجود داشت برای مثال انتقال بازی Quake به محیط وب، Silverlight در این زمینه هم حرف‌های زیادی برای گفتن دارد (+).

11- نیاز به تولید برنامه‌ی دسکتاپ چند سکویی دارید.
سیلورلایت هم اکنون تحت ویندوز، MAC OS-X ، لینوکس و ... پشتیبانی می‌شود (+). همچنین برنامه‌های سیلورلایت قابلیت اجرای در خارج از مرورگر را هم دارند.
با سیلورلایت دیگر نیازی نخواهد بود تا کاربران لینوکسی ابتدا Wine را نصب کنند تا بتوانند از یک برنامه‌ی ویندوزی که انتقال پذیر نیست در لینوکس هم بتوانند استفاده کنند؛ چون پروژه‌ی مون لایت لینوکسی برای این منظور مهیا است.

12- نیاز به تولید برنامه‌های تحت وب سریع و با کارآیی بالا دارید.
فایل‌های نهایی Silverlight با توجه به ماهیت کامپایل شده‌ی آن‌ها به طور قطع از کدهای جاوا اسکریپتی سمت کلاینت که باید توسط مرورگر تفسیر و پردازش شوند (و هر کدام هم از موتور خاص خودشان استفاده می‌کنند)، سریعتر اجرا می‌شوند (+).

13- از پیچیدگی‌های پیاده سازی برنامه‌های متداول وب خسته شده‌اید.
هنوز هم با تمام پیشرفت‌های حاصل، تولید برنامه‌های وب پیشرفته مشکل است. از یک طرف ناسازگاری یک سری از مرورگرها با یک سری از قابلیت‌ها را باید در نظر داشت، تا فراگیری فریم ورک‌های Ajax و غیره تا مشکل بودن طراحی کنترل‌های جدید فراتر از آن چیزی که HTML استاندارد ارائه می‌دهد. بله، به طور قطع دانش فنی بالایی در این زمینه در طی سالیان تولید شده است، اما باز هم فراگیری و تسلط به آن‌ها زمان قابل توجهی را طلب می‌کند.
در سیلورلایت کلیه تعاملات با شبکه به صورت پیش فرض غیرهمزمان است (همان ایده‌ی اصلی Ajax) همچنین با توجه به state full بودن اینگونه برنامه‌ها، عملا برنامه نویس‌ها بدون درگیر شدن با مفاهیم اجکسی و مدیریت حالت، برنامه‌ی پیشرفته‌ی وبی را در مدت زمان کوتاهی تولید کرده‌اند و این برنامه در تمام مرورگرهایی که قابلیت بارگذاری افزونه‌ی سیلورلایت را دارند به یک شکل و کیفیت اجرا می‌شود.

14- در زمینه میزان مصرف پهنای باند ملاحظاتی ویژه‌ای وجود دارد.
یک برنامه‌ی سیلورلایت تنها یکبار باید دریافت شود. پس از آن در سمت کاربر کش خواهد شد (تا زمان به روز رسانی بعدی برنامه در سرور). همین مساله در دفعات بعدی مراجعه کاربر به سایت نقش قابل توجهی را در کاهش میزان مصرف پهنای باند (یا به قولی میزان کمتر data transfer) کلی دارد.

15- فرصت کافی برای فراگیری انبوهی از فناوری‌های مختلف را ندارید!
بله! برای ایجاد یک برنامه‌ی تحت وب که کاربر آن پس از مشاهده بگوید WOW نیاز است به HTML ، JS ، CSS ، AJAX ، یکی از فناوری‌های سمت سرور و ... مسلط بود (علاوه بر اینکه باید بدانید فلان کد JS در IE کار می‌کند اما در فایرفاکس خیر. فایرفاکس فلان قسمت CSS را پشتیبانی می‌کند اما IE خیر! و ...).
اما برای استفاده از سیلورلایت فقط کافی است به XAML و یکی از زبان‌های دات نت مانند سی شارپ یا VB.NET مسلط باشید (البته هیچ وقت از دست ASP.NET خلاص نخواهید شد! حداقل در حد راه اندازی یک وب سرویس یا مفاهیم امنیتی آن).
این مورد خصوصا برای افرادی که برنامه نویس دسکتاپ هستند اما علاقمندند تا برنامه‌ی وب نیز تولید کنند بسیار مهم است. با حداقل آموزش می‌توانند توانایی‌های خود را به وب نیز گسترش دهند. علاوه بر آن عمده‌ی دانش Silverlight شما جهت تولید برنامه‌های WPF (با توجه به اینکه Silverlight فرزند WPF محسوب می‌شود) یا Windows phone 7‌ و غیره نیز می‌تواند بکار گرفته شود.

16- نیاز به اجرای کدهای چند ریسمانی در سمت کاربر دارید.
تا این لحظه پشتیبانی رسمی از مباحث چند ریسمانی در JavaScript و استانداردهای مرتبط با آن وجود ندارد. Silverlight به اکثر امکانات Threading موجود در دات نت فریم ورک دسترسی داشته و دانش فعلی شما قابل انتقال است.


و دست آخر باید به نکته اشاره کرد که هدف از Silverlight ساخت وب سایت معمولی نیست. این نوع کارها را با همان ابزارهای متداول انجام دهید. هدف اصلی آن ساخت برنامه است (Application در مقابل Web site). مشتری‌های اصلی این نوع برنامه‌ها هم بیشتر سازمان‌ها و اینترانت‌های پر سرعت و بسته‌ی آن‌ها هستند که نه نگران حجم افزونه‌ی سیلورلایت هستند و نه مشکلی با حجم برنامه‌ی سیلورلایت شما در یک شبکه‌ی داخلی پر سرعت دارند.

اشتراک‌ها
ادغام Firebug و Firefox DevTools

نگارش‌های آتی فایرفاکس قرار است چند پروسه‌ای باشند و بازنویسی Firebug جهت این امر بسیار هزینه‌بر بوده‌است. به همین جهت اعلام کرده‌اند که دیگر آن‌را ادامه نخواهند داد. اکنون تیم فایرفاکس مشغول ادغام قابلیت‌های این افزونه با DevTools توکار این مرورگر هستند.

ادغام Firebug و Firefox DevTools
مطالب
آشنایی با WPF قسمت پنجم : DataContext بخش دوم
در ادامه قسمت قبلی قصد داریم دو کنترل دیگر را نیز بایند کنیم؛ ولی از آنجا که مقادیر آن‌ها رشته‌ای یا عددی نیست و مقداری متفاوت هست، از مبحثی به نام ValueConverter استفاده خواهیم کرد.
Value Converter چیست؟


موقعی که شما قصد بایند کردن دو نوع داده متفاوت را به هم دارید، نیاز به یک کد واسط پیدا می‌کنید تا این کد واسط مقادیر شما را از مبدا دریافت کرده و تبدیل به نوعی کند که مقصد بتواند از آن استفاده کند یا بلعکس. ValueConverter نام کلاسی است که از یک اینترفیس به نام IValueConverter ارث بری کرده است و شامل دو متد تبدیل نوع از مبدا به مقصد Convert و دیگری از مقصد به مبدا  ConvertBack می‌شود که خیلی کمتر پیاده سازی می‌شود.
پیاده سازی یک کلاس مبدل سه مرحله دارد:
  • مرحله اول :ساخت کلاس ValueConverter
  • مرحله دوم : تعریف آن به عنوان یک منبع یا ریسورس
  • مرحله سوم : استفاده از آن در عملیات بایند کردن Binding

در مرحله اول، نحوه پیاده سازی کلاس ValueConverter به شکل زیر است:
public class BoolToVisibilityConverter : IValueConverter
{
    public object Convert(object value, Type targetType, 
        object parameter, CultureInfo culture)
    {
        // Do the conversion from bool to visibility
    }
 
    public object ConvertBack(object value, Type targetType, 
        object parameter, CultureInfo culture)
    {
        // Do the conversion from visibility to bool
    }
}

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

در مرحله‌ی دوم نحوه تعریف مبدل ما در پنجره XAML به صورت زیر می‌باشد:
<Window x:Class="test.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:نامی دلخواه="clr-namespace:فضای نامی که کلاس مبدل در آن قرار دارد"
        >

    <Window.Resources>
        <نام دلخواهی که در بالا تعریف کرده اید: ClassNameنام کلاس  x:Key="کلید این آیتم در ریسورس"/>
    </Window.Resources>

کلمه‌های کلیدی xmlns که مخفف XML NameSpace هستند جهت تعریف دسترسی به فضاهای نام طراحی شده‌اند و طبق تعریف مایکروسافت همان مفهوم تگ‌های تعریف فضای نام در XML را دارند که توسط مایکروسافت توسعه یافته‌اند. این تعریف‌ها در تگ ریشه (در اینجا window) تعریف می‌شوند. دو فضای نام اولی که به طور پیش فرض در همه جای پروژه قرار دارند، اشاره به فریم ورک WPF دارند. کلمه‌ی کلیدی x در خط شماره سه، نام دلخواهی است که دسترسی ما را به خصوصیات یا تعاریف XAML موجود در sdk باز می‌کند؛ مثلا استفاده از خصوصیاتی چون x:key یا x:class را به همراه دارد.
پس الان باید خط چهارم برای ما روشن باشد؛ فضای نام جدیدی را در برنامه خودمان ایجاد کرده‌ایم که این تگ به آن اشاره می‌کند و نام دلخواهی هم برای اشاره به این فضای نام برایش در نظر گرفته‌ایم. هر موقع در برنامه این نام دلخواه تعیین شده قرار گیرد، یعنی اشاره به این فضای نام که در قسمت Window.resource خط هشتم تعریف شده است.
در خط هشتم، یک ریسورس (منبع) را به برنامه معرفی کرده‌ایم:
ریسورس‌ها برای ذخیره سازی داده‌ها در سطح یک کنترل، سطح محلی در یک پنجره، یا سطح عمومی در کل پروژه به کار می‌روند. محدودیتی در ذخیره داده‌ها وجود ندارد و هر چقدر که دوست دارید می‌توانید داده به آن پاس کنید. این داده‌ها می‌توانند یک سری اطلاعات ذخیره شده در یک ساختار ساده تا یک ساختار سلسه مراتبی از کنترل‌ها باشند. ریسورس‌ها به شما این اجازه را می‌دهند تا داده‌ها را در یک مکان ذخیره کرده و آن‌ها را در یک یا چندجا مورد استفاده قرار دهید.

از آن جا که مباحث ریسورس‌ها را در یک مقاله‌ی جداگانه بررسی می‌کنیم، فقط به ذکر نکات بالا جهت کد فعلی بسنده خواهیم کرد و ادامه‌ی آن را در یک مقاله دیگر مورد بررسی قرار می‌دهیم.
هر ریسورس دارای یک نام یا یک کلید است که با خصوصیت x:key تعریف می‌شود.
ریسورس بالا یک کلاس را که در فضای نام دلخواهی قرار دارد، تعریف می‌کند و یک کلید هم به آن انتساب می‌دهد.
مرحله‌ی سوم معرفی ریسورس به عملیات Binding است:
{Binding نام پراپرتی کلاس, Converter={StaticResource کلید آیتم مربوطه در ریسورس}, ConverterParameter=پارامتری که به کلاس مبدل پاس می‌شود}

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

حال که با اصول نوشتار آشنا شدیم کار را آغاز می‌کنیم.
قصد داریم یک مبدل برای فیلد جنیست درست کنیم. از آنجا که این فیلد Boolean است و خصوصیت IsChecked یک RadioButton هم Boolean است، می‌توان یک ارتباط مستقیم را ایجاد کرد. ولی مشکل در اینجا هست که True برای مذکر است و false برای مؤنث. در نتیجه تنها Radiobutton مربوطه به جنس مذکر به این حالت پاسخ می‌دهد و از آنجا که  برای جنس مونث  false در نظر گرفته شده است، انتخاب آن هم false خواهد بود. پس باید در مبدل، مقداری که کنترل می‌خواهد را شناسایی کرده و اگر مقدار با آن برابر بود، True را بازگردانیم. مقداری که هر کنترل درخواست می‌کند را از طریق پارامتر به تابع مبدل ارسال می‌کنیم. radiobutton مذکر، مقدار True را به عنوان پارامتر ارسال می‌کند و radiobutton مونث هم مقدار false را به عنوان پارامتر ارسال می‌کند. اگر تابع مبدل را ببینید، این مقدار‌ها با پارامترها همخوانی دارند، True در غیر این صورت false بر میگرداند.

مرحله اول تعریف کلاس ValueConveter:
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;

namespace test.ValueConverters
{
    public class GenderConverter: IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var ParameterString = parameter as string;
            if (ParameterString == null)
                return DependencyProperty.UnsetValue;

            bool bparam;

            bool test = bool.TryParse(parameter.ToString(), out bparam);
            if (test)
            {
                return ((bool)value).Equals(bparam);
            }
            return DependencyProperty.UnsetValue;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}
در کد بالا پارامتر ارسالی را دریافت می‌کنیم و اگر برابر با Null باشد، مقداری برگشتی را عدم ذکر شدن خصوصیت وابسته اعلام می‌کنیم. در نتیجه انگار این خصوصیت مقداردهی نشده است. اگر مخالف Null باشد، کار ادامه پیدا می‌کند. در نهایت مقایسه‌ای بین پارامتر و مقدار پراپرتی (value) صورت گرفته و نتیجه‌ی مقایسه را برگشت می‌دهیم.

برای تعریف این مبدل در محیط XAML به صورت زیر اقدام می‌کنیم:
<Window x:Class="test.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:valueConverters="clr-namespace:test.ValueConverters"
        Title="MainWindow" Height="511.851" Width="525">

    <Window.Resources>
        <valueConverters:GenderConverter x:Key="GenderConverter"/>
    </Window.Resources>
</window>
 کلمه valueConveter به فضای نام test.valueConverters اشاره می‌کند که در قسمت ریسورس، از کلاس GenderConverter آن استفاده می‌کنیم و کلیدی که برای این ریسورس در نظر گرفتیم را GenderConverter تعریف کردیم (هم نامی کلید ریسورس و نام کلاس مبدل اتفاقی است و ارتباطی با یکدیگر ندارند).

نکته: در صورتی که بعد از تعریف ریسورس با خطای زیر روبرو شدید و محیط طراحی Design را از دست دادید یکبار پروژه را بیلد کنید تا مشکل حل شود.
The name "GenderConverter" does not exist in the namespace "clr-namespace:test.ValueConverters".


اکنون در عملیات بایندینگ دو کنترل اینگونه می‌نویسیم:
     <RadioButton GroupName="Gender" IsChecked="{Binding Gender, Converter={StaticResource GenderConverter}, ConverterParameter=True}" Name="RdoMale"  >Male</RadioButton>
                <RadioButton GroupName="Gender" IsChecked="{Binding Gender, Converter={StaticResource GenderConverter}, ConverterParameter=False}" Name="RdoFemale" Margin="0 5 0 0"  >Female</RadioButton>
حال برنامه را اجرا کرده و نتیجه را ببینید. برای تست بهتر می‌توانید جنسیت فرد را در منبع داده تغییر دهید. همچنین از آنجا که این مقدار برای جنس مذکر نیازی به بررسی و تبدیل ندارد می‌توانید عملیات بایند را عادی بنویسید:
<RadioButton GroupName="Gender" IsChecked="{Binding Gender}" Name="RdoMale"  >Male</RadioButton>
این کد می‌تواند برای تمامی وضعیت‌های دو مقداری چون جنسیت و وضعیت تاهل و ... هم به کار برود. ولی حالا سناریویی را تصور کنید که که مقادیر از دو تا بیشتر شود؛ مثل وضعیت تحصیلی ، دسترسی‌ها و غیره که این‌ها نیاز به داده‌های شمارشی چون Enum‌ها دارند. روند کار دقیقا مانند بالاست:
هر کنترل مقداری از یک enum را میپذیرد که میتواند آن مقدار را با استفاده از پارامتر، به تابع مبدل ارسال کند و سپس تابع چک می‌کند که آیا چنین مقداری در enum مدنظر یافت می‌شود یا خیر؛ این کد الان کار می‌کندو واقعا هم کد درستی است و هیچ مشکلی هم ندارد. ولی برای یک لحظه تصور کنید پنجره شما شامل چهار خصوصیت enum دار است. یعنی الان باید 4 تابع مبدل بنویسید. پس باید کد را بازنویسی کنیم تا به هر enum که مدنظر است پاسخ دهد؛ به این ترتیب تنها یک تابع مبدل می‌نویسیم.

جهت یادآوری نگاهی به کلاس برنامه می‌اندازیم:
public enum FieldOfWork
    {
        Actor=0,
        Director=1,
        Producer=2
    }
    public class Person : INotifyPropertyChanged
    { 
        public bool Gender { get; set; }

        public string ImageName { get; set; }

        public string Country { get; set; }

        public DateTime Date { get; set; }

        public FieldOfWork FieldOfWork { get; set; }

        private string _name;
        public string Name
        {
            get { return _name; }
            set
            {
                _name = value;
                OnPropertyChanged();
            }
        }
فعلا IList را از پراپرتی FieldOfWork بر میداریم تا این سناریو باشد که تنها یک Enum قابل انتخاب است:
public FieldOfWork FieldOfWork { get; set; }
بعدا حالت قبلی را بررسی میکنیم.

کد کلاس مبدل را به صورت زیر می‌نویسیم:
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;

namespace test.ValueConverters
{
    public class EnumConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var ParameterString = parameter as string;
            if (ParameterString == null)
                return DependencyProperty.UnsetValue;

            if (Enum.IsDefined(value.GetType(), value) == false)
                return DependencyProperty.UnsetValue;

            object paramvalue = Enum.Parse(value.GetType(), ParameterString);
            return paramvalue.Equals(value);
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}
در مرحله اول مثل کد قبلی بررسی می‌شود که آیا پارامتری ارسال شده است یا خیر. در مرحله دوم بررسی می‌شود که نوع داده مقدار پراپرتی چیست یعنی چه Enum ایی مورد استفاده قرار گرفته است. اگر در Enum مقداری که در پارامتر به آن ذکر شده است وجود نداشته باشد، بهتر هست که کار در همین جا به پایان برسد؛ زیرا که یک پارامتر اشتباهی ارسال شده است و چنین مقداری در Enum وجود ندارد. در غیر اینصورت کار ادامه می‌یابد و پارامتر را به enum تبدیل کرده و با مقدار مقایسه می‌کنیم. اگر برابر باشند نتیجه true را باز میگردانیم.

کد قسمت ریسورس را با کلاس جدید به روز می‌کنیم:
    <Window.Resources>
        <valueConverters:GenderConverter x:Key="GenderConverter"/>
        <valueConverters:EnumConverter x:Key="EnumConverter"></valueConverters:EnumConverter>
    </Window.Resources>
کد چک باکس‌ها هم به شکل زیر تغییر می‌یابد:
    <CheckBox Name="ChkActor" IsChecked="{Binding FieldOfWork, Converter={StaticResource EnumConverter}, ConverterParameter=Actor}" >Actor/Actress</CheckBox>
                <CheckBox Name="ChkDirector" IsChecked="{Binding FieldOfWork, Converter={StaticResource EnumConverter}, ConverterParameter=Director}" >Director</CheckBox>
                <CheckBox Name="ChkProducer" IsChecked="{Binding FieldOfWork, Converter={StaticResource EnumConverter}, ConverterParameter=Producer}" >Producer</CheckBox>
کلاس GetPerson که منبع داده ما را فراهم می‌کند هم به شکل زیر است:
  public static Person GetPerson()
        {
            return new Person()
            {
                Name = "Leo",
                Gender =true,
                ImageName ="man.jpg",
                Country = "Italy",
                FieldOfWork = test.FieldOfWork.Actor,
                Date = DateTime.Now.AddDays(-3)
            };
        }

برنامه را اجرا کنید تا نتیجه کار را ببینید. باید چک باکس Actor تیک خورده باشد. میتوانید منبع داده را تغییر داده تا نتیجه کار را ببینید.

بگذارید فیلد FieldOfWork را به حالت قبلی یعنی IList برگردانیم. در بسیاری از اوقات ما چند گزینه از یک Enum را انتخاب می‌کنیم، مثل داشتن چند سطح دسترسی یا چند سمت کاری و ...

کلاس را به همراه کد GetPerson به شکل زیر تغییر می‌دهیم:
    public enum FieldOfWork
    {
        Actor=0,
        Director=1,
        Producer=2
    }
    public class Person : INotifyPropertyChanged
    { 
        public bool Gender { get; set; }

        public string ImageName { get; set; }

        public string Country { get; set; }

        public DateTime Date { get; set; }

        public IList<FieldOfWork> FieldOfWork { get; set; }

        private string _name;
        public string Name
        {
            get { return _name; }
            set
            {
                _name = value;
                OnPropertyChanged();
            }
        }

        public static Person GetPerson()
        {
            return new Person
            {
                Name = "Leo",
                Gender = true,
                ImageName = "man.jpg",
                Country = "Italy",
                FieldOfWork = new FieldOfWork[] { test.FieldOfWork.Actor, test.FieldOfWork.Producer },
                Date = DateTime.Now.AddDays(-3)
            };
        }
}

دو Enum بازیگر و تهیه کننده را انتخاب کرده‌ایم، پس در زمان اجرا باید این دو گزینه انتخاب شوند.
کد مبدل را به صورت زیر می‌نویسیم:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;

namespace test.ValueConverters
{
    public class EnumList : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var ParameterString = parameter as string;
            if (ParameterString == null)
                return DependencyProperty.UnsetValue;

         var enumlist= (IList) value;         

            if(enumlist==null && enumlist.Count<1)
                return DependencyProperty.UnsetValue;

            if (Enum.IsDefined(enumlist[0].GetType(), ParameterString) == false)
                return DependencyProperty.UnsetValue;


            /*
              foreach (var item  in enumlist)
            {
                object paramvalue = Enum.Parse(item.GetType(), ParameterString);
                bool result = item.Equals(paramvalue);
                if (result)
                    return true;
            }
            return false;
             */
            return (from object item in enumlist let paramvalue = Enum.Parse(item.GetType(), ParameterString) select item.Equals(paramvalue)).Any(result => result);
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}
مقدار پراپرتی را به نوع IList تبدیل می‌کنید و اگر لیست معتبر بود، برنامه را ادامه می‌دهیم؛ در غیر اینصورت تابع خاتمه می‌یابد. بعد از اینکه صحت وجود لیست اعلام شد، بررسی میکنیم آیا Enum چنین مقداری که پارامتر ذکر کرده است را دارد یا یک پارامتر اشتباهی است.
در حلقه‌ای که به شکل توضیح درآمده، همه آیتم‌های مربوطه در لیست را بررسی کرده و اگر آیتمی برابر پارامتر باشد، True بر میگرداند و در صورتی که حلقه به اتمام برسد و آیتم پیدا نشود، مقدار False را برمی‌گرداند. این حلقه از آن جهت به شکل توضیح درآمده است که کد Linq آن در زیر نوشته شده است.

تعریف کلاس بالا در ریسورس:
   <Window.Resources>
        <valueConverters:GenderConverter x:Key="GenderConverter"/>
        <valueConverters:EnumConverter x:Key="EnumConverter"></valueConverters:EnumConverter>
        <valueConverters:EnumList x:Key="EnumList"></valueConverters:EnumList>
    </Window.Resources>
و تغییر کلید ریسورس در خطوط چک باکس ها، باقی موارد همانند قبل ثابت هستند:
     <CheckBox Name="ChkActor" IsChecked="{Binding FieldOfWork, Converter={StaticResource EnumList}, ConverterParameter=Actor}" >Actor/Actress</CheckBox>
                <CheckBox Name="ChkDirector" IsChecked="{Binding FieldOfWork, Converter={StaticResource EnumList}, ConverterParameter=Director}" >Director</CheckBox>
                <CheckBox Name="ChkProducer" IsChecked="{Binding FieldOfWork, Converter={StaticResource EnumList}, ConverterParameter=Producer}" >Producer</CheckBox>

نتیجه‌ی کار باید به شکل زیر باشد:
فیلد جنسیت و زمینه کاری از تابع مبدل به دست آمده است.

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

دانلود فایل‌های این قسمت
مطالب
پیاده سازی Remote Validation در Blazor
فرم‌های Blazor به همراه پشتیبانی از ویژگی Remote که به همراه ASP.NET Core ارائه می‌شود، نیستند. هرچند می‌توان در حین ارسال فرم به سرور، نتیجه‌ی اعتبارسنجی از راه دور و سمت سرور را به کاربر نمایش داد، اما تجربه‌ی کاربری آن در حد Remote validation نیست. یعنی می‌خواهیم در حین ورود اطلاعات و یا انتقال focus به کنترل دیگری، اعتبارسنجی سمت سرور صورت گیرد و نه فقط در زمان ارسال کل اطلاعات به سرور، در پایان کار. در این مطلب روشی را جهت پیاده سازی این عملیات بررسی خواهیم کرد.


ایجاد یک پروژه‌ی ابتدایی Blazor WASM

پروژه‌ای را که در این مطلب تکمیل خواهیم کرد، از نوع Blazor WASM هاست شده‌است. بنابراین در پوشه‌ی فرضی BlazorAsyncValidation، دستور «dotnet new blazorwasm --hosted» را صادر می‌کنیم تا ساختار ابتدایی پروژه که به همراه یک کلاینت Blazor WASM و یک سرور ASP.NET Core Web API است، تشکیل شود. از قسمت Web API، برای پیاده سازی اعتبارسنجی سمت سرور استفاده خواهیم کرد.


مدل ثبت نام برنامه

مدل ثبت نام برنامه تنها از یک خاصیت نام تشکیل شده و در پروژه‌ی Shared قرار می‌گیرد تا هم در کلاینت و هم در سرور قابل استفاده باشد:
using System.ComponentModel.DataAnnotations;

namespace BlazorAsyncValidation.Shared;

public class UserDto
{
    [Required] public string Name { set; get; } = default!;
}
این نام است که می‌خواهیم عدم تکراری بودن آن‌را حین ثبت نام در سمت سرور، بررسی کنیم. به همین جهت کنترلر API زیر را برای آن تدارک خواهیم دید.


کنترلر API ثبت نام برنامه

کنترلر زیر که در پوشه‌ی BlazorAsyncValidation\Server\Controllers قرار می‌گیرد، منطق بررسی تکراری نبودن نام دریافتی از برنامه‌ی کلاینت را شبیه به منطق remote validation استاندارد MVC، پیاده سازی می‌کند که در نهایت یک true و یا false را باز می‌گرداند.
در اینجا خروجی بازگشت داده کاملا در اختیار شما است و نیازی نیست تا حتما ارتباطی با MVC داشته باشد؛ چون مدیریت سمت کلاینت بررسی آن‌را خودمان انجام خواهیم داد و نه یک کتابخانه‌ی از پیش نوشته شده و مشخص.
using BlazorAsyncValidation.Shared;
using Microsoft.AspNetCore.Mvc;

namespace BlazorAsyncValidation.Server.Controllers;

[ApiController, Route("api/[controller]/[action]")]
public class RegisterController : ControllerBase
{
    [HttpPost]
    public IActionResult IsUserNameUnique([FromBody] UserDto userModel)
    {
        if (string.Equals(userModel?.Name, "Vahid", StringComparison.OrdinalIgnoreCase))
        {
            return Ok(false);
        }

        return Ok(true);
    }
}

غنی سازی فرم استاندارد Blazor جهت انجام Remote validation



اگر بخواهیم از EditForm استاندارد Blazor در حالت متداول آن و بدون هیچ تغییری استفاده کنیم، مانند مثال زیر که InputText متصل به خاصیت Name مربوط به Dto برنامه را نمایش می‌دهد:
@page "/"

<PageTitle>Index</PageTitle>

<h2>Register</h2>

<EditForm EditContext="@EditContext" OnValidSubmit="DoSubmitAsync">
    <DataAnnotationsValidator/>
    <div class="row mb-2">
        <label class="col-form-label col-lg-2">Name:</label>
        <div class="col-lg-10">
            <InputText @bind-Value="Model.Name" class="form-control"/>
            <ValidationMessage For="@(() => Model.Name)"/>
        </div>
    </div>

    <button class="btn btn-secondary" type="submit">Submit</button>
</EditForm>
 تنها رخ‌دادی که در اختیار ما قرار می‌گیرد، رخ‌داد submit (در حالت موفقیت اعتبارسنجی سمت کلاینت و یا تنها submit معمولی) است. بنابراین برای دسترسی به رخ‌دادهای بیشتر EditForm، نیاز است با EditContext آن کار کنیم:
public partial class Index
{
    private const string UserValidationUrl = "/api/Register/IsUserNameUnique";

    private ValidationMessageStore? _messageStore;
    [Inject] private HttpClient HttpClient { set; get; } = default!;
    private EditContext? EditContext { set; get; }

    private UserDto Model { get; } = new();
به همین جهت EditContext را در سطح کامپوننت جاری تعریف کرده و همچنین سرویس HttpClient را جهت ارسال اطلاعات Name و دریافت پاسخ true/false از سرور و یک ValidationMessageStore را برای نگهداری تعاریف خطاهای سفارشی که قرار است در فرم نمایش داده شوند، معرفی می‌کنیم.
ValidationMessageStore به همراه متد Add است و اگر به آن نام فیلد مدنظر را به همراه پیامی، اضافه کنیم، این اطلاعات را به صورت خطای اعتبارسنجی توسط کامپوننت ValidationMessage نمایش می‌دهد.

محل مقدار دهی اولیه‌ی این اشیاء نیز در روال رویدادگردان OnInitialized به صورت زیر است:
    protected override void OnInitialized()
    {
        EditContext = new EditContext(Model);
        _messageStore = new ValidationMessageStore(EditContext);
        EditContext.OnFieldChanged += (sender, eventArgs) =>
        {
            var fieldIdentifier = eventArgs.FieldIdentifier;
            _messageStore?.Clear(fieldIdentifier);
            _ = InvokeAsync(async () =>
            {
                var errors = await OnValidateFieldAsync(fieldIdentifier.FieldName);
                if (errors?.Any() != true)
                {
                    return;
                }

                foreach (var error in errors)
                {
                    _messageStore?.Add(fieldIdentifier, error);
                }

                EditContext.NotifyValidationStateChanged();
            });
            StateHasChanged();
        };
        EditContext.OnValidationStateChanged += (sender, eventArgs) => StateHasChanged();
        EditContext.OnValidationRequested += (sender, eventArgs) => _messageStore?.Clear();
    }
در اینجا ابتدا یک نمونه‌ی جدید از EditContext، بر اساس Model فرم، تشکیل می‌شود. سپس ValidationMessageStore سفارشی که قرار است خطاهای اعتبارسنجی remote ما را نمایش دهد نیز نمونه سازی می‌شود. در ادامه امکان دسترسی به رخ‌دادهای OnFieldChanged ، OnValidationStateChanged و OnValidationRequested را خواهیم داشت که در حالت معمولی کار با EditForm میسر نیستند.
برای مثال اگر فیلدی تغییر کند، رویداد OnFieldChanged صادر می‌شود. در همینجا است که کار فراخوانی متد OnValidateFieldAsync که در ادامه معرفی می‌شود را انجام می‌دهیم تا کار اعتبارسنجی Async سمت سرور را انجام دهد. اگر نتیجه‌ای به همراه آن بود، توسط messageStore به صورت یک خطای اعتبارسنجی نمایش داده خواهد شد و همچنین EditContext نیز با فراخوانی متد NotifyValidationStateChanged، وادار به به‌روز رسانی وضعیت اعتبارسنجی خود می‌گردد.

متد سفارشی OnValidateFieldAsync که کار اعتبارسنجی سمت سرور را انجام می‌دهد، به صورت زیر تعریف شده‌است:
    private async Task<IList<string>?> OnValidateFieldAsync(string fieldName)
    {
        switch (fieldName)
        {
            case nameof(UserDto.Name):
                var response = await HttpClient.PostAsJsonAsync(
                    UserValidationUrl,
                    new UserDto { Name = Model.Name });
                var responseContent = await response.Content.ReadAsStringAsync();
                if (string.Equals(responseContent, "false", StringComparison.OrdinalIgnoreCase))
                {
                    return new List<string>
                    {
                        $"`{Model.Name}` is in use. Please choose another name."
                    };
                }

                // TIP: It's better to use the `DntDebounceInputText` component for this case to reduce the network round-trips.

                break;
        }

        return null;
    }
در اینجا درخواستی به سمت آدرس api/Register/IsUserNameUnique ارسال شده و سپس بر اساس مقدار true و یا false آن، پیامی را بازگشت می‌دهد. اگر نال را بازگشت دهد یعنی مشکلی نبوده.

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


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: BlazorAsyncValidation.zip
مطالب
آشنایی با M.A.F - قسمت دوم

قسمت قبل بیشتر آشنایی با یک سری از اصطلاحات مرتبط با فریم ورک MAF بود و همچنین نحوه‌ی کلی استفاده از آن. در این قسمت یک مثال ساده را با آن پیاده سازی خواهیم کرد و فرض قسمت دوم بر این است که افزونه‌ی Visual Studio Pipeline Builder را نیز نصب کرده‌اید.

یک نکته پیش از شروع:
- اگر افزونه‌ی Visual Studio Pipeline Builder پس از نصب به منوی Tools اضافه نشده است، یک پوشه‌ی جدید را به نام Addins در مسیر Documents\Visual Studio 2008 ایجاد کرده و سپس فایل‌های آن‌را در این مسیر کپی کنید.

ساختار اولیه یک پروژه MAF

- پروژ‌ه‌هایی که از MAF استفاده می‌کنند، نیاز به ارجاعاتی به دو اسمبلی استاندارد System.AddIn.dll و System.AddIn.Contract.dll دارند (مطابق شکل زیر):



- ساختار آغازین یک پروژه MAF از سه پروژه تشکیل می‌شود که توسط افزونه‌ی Visual Studio Pipeline Builder به 7 پروژه بسط خواهد یافت.
این سه پروژه استاندارد آغازین شامل موارد زیر هستند:



- هاست: همان برنامه‌ی اصلی که قرار است از افزونه استفاده کند.
- قرار داد: نحو‌ه‌ی تعامل هاست و افزونه در این پروژه تعریف می‌شود. (یک پروژه از نوع class library)
- افزونه: کار پیاده سازی قرار داد را عهده دار خواهد شد. (یک پروژه از نوع class library)

- همچنین مرسوم است جهت مدیریت بهتر خروجی‌های حاصل شده یک پوشه Output را نیز به این solution اضافه کنند:



اکنون با توجه به این محل خروجی، به خواص Build سه پروژه موجود مراجعه کرده و مسیر Build را اندکی اصلاح خواهیم کرد (هر سه مورد بهتر است اصلاح شوند)، برای مثال:



نکته‌ی مهم هم اینجا است که خروجی host باید به ریشه این پوشه تنظیم شود و سایر پروژه‌ها هر کدام خروجی خاص خود را در پوشه‌ای داخل این ریشه باید ایجاد کنند.



تا اینجا قالب اصلی کار آماده شده است. قرارداد ما هم به شکل زیر است (ویژگی AddInContract آن نیز نباید فراموش شود):

using System.AddIn.Pipeline;
using System.AddIn.Contract;

namespace CalculatorConract
{
[AddInContract]
public interface ICalculatorContract : IContract
{
double Operate(string operation, double a, double b);
}
}

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

ایجاد pipeline

اگر قسمت قبل را مطالعه کرده باشید، یک راه حل مبتنی بر MAF از 7 پروژه تشکیل می‌شود که عمده‌ترین خاصیت آن‌ها مقاوم کردن سیستم در مقابل تغییرات نگارش قرارداد است. در این حالت اگر قرار داد تغییر کند، نه هاست و نه افزونه‌ی قدیمی، نیازی به تغییر در کدهای خود نخواهند داشت و این پروژه‌های میانی هستند که کار وفق دادن (adapters) نهایی را برعهده می‌گیرند.


برای ایجاد خودکار View ها و همچنین Adapters ، از افزونه‌ی Visual Studio Pipeline Builder که پیشتر معرفی شد استفاده خواهیم کرد.



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


پس از ایجاد این پروژه‌ها، نیاز به اصلاحات مختصری در مورد نام اسمبلی و فضای نام هر کدام می‌باشد؛ زیرا به صورت پیش فرض هر کدام به نام template نامگذاری شده‌اند:



پیاده سازی افزونه

قالب کاری استفاده از این فریم ورک آماده است. اکنون نوبت به پیاده سازی یک افزونه می‌باشد. به پروژه AddIn مراجعه کرده و ارجاعی را به اسمبلی AddInView خواهیم افزود. به این صورت افزونه‌ی ما به صورت مستقیم با قرارداد سروکار نداشته و ارتباطات، در راستای همان pipeline تعریف شده، جهت مقاوم شدن در برابر تغییرات صورت می‌گیرد:
using System;
using CalculatorConract.AddInViews;
using System.AddIn;

namespace CalculatorAddIn
{
[AddIn]
public class MyCalculatorAddIn : ICalculator
{
public double Operate(string operation, double a, double b)
{
throw new NotImplementedException();
}
}
}

در اینجا افزونه‌ی ما باید اینترفیس ICalculator مربوط به AddInView را پیاده سازی نماید که برای مثال خواهیم داشت:

using System;
using CalculatorConract.AddInViews;
using System.AddIn;

namespace CalculatorAddIn
{
[AddIn("افزونه یک", Description = "توضیحات", Publisher = "نویسنده", Version = "نگارش یک")]
public class MyCalculatorAddIn : ICalculator
{
public double Operate(string operation, double a, double b)
{
switch (operation)
{
case "+":
return a + b;
case "-":
return a - b;
case "*":
return a * b;
default:
throw new NotSupportedException("عملیات مورد نظر توسط این افزونه پشتیبانی نمی‌شود");
}
}
}
}

همانطور که در قسمت قبل نیز ذکر شد، این کلاس باید با ویژگی AddIn مزین شود که توسط آن می‌توان توضیحاتی در مورد نام ، نویسنده و نگارش افزونه ارائه داد.


استفاده از افزونه‌ی تولید شده

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

نکته‌ی مهم!
در هر دو ارجاع به HostView و یا AddInView باید خاصیت Copy to local به false تنظیم شود، در غیر اینصورت افزونه‌ی شما بارگذاری نخواهد شد.



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

using System;
using System.AddIn.Hosting;
using CalculatorConract.HostViews;

namespace Calculator
{
class Program
{
private static ICalculator _calculator;

static void doOperation()
{
Console.WriteLine("1+2: {0}", _calculator.Operate("+", 1, 2));
}

static void Main(string[] args)
{
//مسیر پوشه ریشه مربوطه به خط لوله افزونه‌ها
string path = Environment.CurrentDirectory;

//مشخص سازی مسیر خواندن و کش کردن افزونه‌ها
AddInStore.Update(path);

//یافتن افزونه‌هایی سازگار با شرایط قرارداد پروژه
//در اینجا هیچ افزونه‌ای بارگذاری نمی‌شود
var addIns = AddInStore.FindAddIns(typeof(ICalculator), path);

//اگر افزونه‌ای یافت شد
if (addIns.Count > 0)
{
var addIn = addIns[0]; //استفاده از اولین افزونه
Console.WriteLine("1st addIn: {0}", addIn.Name);

//فعال سازی افزونه و همچنین مشخص سازی سطح دسترسی آن
_calculator = addIn.Activate<ICalculator>(AddInSecurityLevel.Intranet);

//یک نمونه از استفاده آن
doOperation();
}

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}

چند نکته جالب توجه در مورد قابلیت‌های ارائه شده:
- مدیریت load و unload پویا
- امکان تعریف سطح دسترسی و ویژگی‌های امنیتی اجرای یک افزونه
- امکان ایزوله سازی پروسه اجرای افزونه از هاست (در ادامه توضیح داده خواهد شد)
- مقاوم بودن پروژه به نگارش‌های مختلف قرارداد


اجرای افزونه در یک پروسه مجزا

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

//فعال سازی افزونه و همچنین مشخص سازی سطح دسترسی آن
//همچنین جدا سازی پروسه اجرایی افزونه از هاست
_calculator = addIn.Activate<ICalculator>(
new AddInProcess(),
AddInSecurityLevel.Intranet);

در این حالت اگر پس از فعال شدن افزونه، یک break point قرار دهیم و به task manager ویندوز مراجعه نمائیم، پروسه‌ی مجزای افزونه قابل مشاهده است.



برای مطالعه بیشتر + ، + ، + و +

مطالب
Angular Material 6x - قسمت اول - افزودن آن به برنامه
کتابخانه‌ی Angular Material تعدادی کامپوننت زیبای با قابلیت استفاده‌ی مجدد، به خوبی آزمایش شده و با قابلیت دسترسی بالا را بر اساس الگوهای Material Design ارائه می‌دهد. برای توسعه دهندگان Angular، کتابخانه‌ی Angular Material پیاده سازی مرجع رهنمودهای طراحی متریال گوگل است که توسط تیم اصلی Angular پیاده سازی و توسعه داده می‌شود. در این سری، مفاهیم طراحی نگارش 6x این کتابخانه را به همراه نحوه‌ی برپایی و تنظیم آن و همچنین کار با کامپوننت‌های پیشرفته‌ی آن، بررسی خواهیم کرد.


منابع و مآخذ مرتبط با کتابخانه‌ی Angular Material

در اینجا مآخذ اصلی کار با این کتابخانه را ملاحظه می‌کنید که شامل اصول طراحی متریال و مخازن اصلی توسعه‌ی آن می‌باشند:
Material Design Specification
- https://material.io/design
Angular Material
- https://material.angular.io
- https://github.com/angular/material2


مفاهیم پایه‌ی طراحی متریال

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

اما ... طراحی برنامه‌های زیبا مشکل است. به همین جهت استفاده از کتابخانه‌های غنی مانند طراحی متریال که این امر را سهولت می‌بخشند، ضروری است. طراحی متریال یک زبان کامل طراحی برنامه‌های زیبا است. توسط گوگل طراحی شده‌است و دو هدف اصلی را دنبال می‌کند:
- وفاداری به اصول کلاسیک طراحی رابط کاربری
- ارائه‌ی تجربه‌ی کاربری یک‌دست و هماهنگ، در بین وسایل و اندازه‌های صفحات نمایشی مختلف

اصول پایه‌ی طراحی متریال نیز شامل موارد زیر است:
- «متریال» یک متافور است و بر اساس مطالعه‌ی نحوه‌ی کار با کاغذ، مرکب و ارتباط بین اشیاء در دنیای واقعی پدید آمد‌ه‌است.
- اشیاء در دنیای واقعی دارای ارتباط‌های ابعادی و حجمی هستند. برای مثال دو برگه‌ی کاغذ یک فضا را اشغال نمی‌کنند. طراحی متریال برای نمایش این ارتباط سه بعدی بین اشیاء، از نور و سایه استفاده می‌کند.
- در دنیای واقعی، اشیاء از درون یکدیگر رد نمی‌شوند. این مورد در طراحی متریال نیز صادق است.
- طراحی متریال به همراه جعبه‌ی رنگ مخصوص و بکارگیری فضاهای خالی و عناوین درشت بسیار مشخص، واضح و عمدی است.
- طراحی متریال به همراه حرکت و پویانمایی، جهت ارائه‌ی مفاهیم مختلف به کاربر، جهت درک بهتر او از برنامه است.


برپایی پیشنیازهای ابتدایی کار با Angular Material

پیش از ادامه‌ی بحث فرض بر این است که آخرین نگارش Angular CLI را نصب کرده‌اید و اگر پیشتر آن‌را نصب کرده‌اید، یکبار دستور ذیل را اجرا کنید تا تمام وابستگی‌های سراسری نصب شده‌ی در سیستم به صورت خودکار به روز رسانی شوند:
 npm update -g
سپس برنامه‌ی کلاینت Angular این سری را به همراه تنظیمات ابتدایی مسیریابی آن از طریق صدور فرمان ذیل آغاز می‌کنیم:
 ng new MaterialAngularClient --routing
پس از ایجاد ساختار اولیه‌ی برنامه و نصب خودکار وابستگی‌های آن، جهت آزمایش برنامه، به پوشه‌ی آن وارد شده و آن‌را اجرا می‌کنیم:
cd MaterialAngularClient
ng serve -o
که به این ترتیب برنامه در آدرس http://localhost:4200 و مرورگر پیش‌فرض سیستم نمایش داده خواهد شد.


افزودن کتابخانه‌ی Angular Material به برنامه

در طول این سری از سایت https://material.angular.io زیاد استفاده خواهیم کرد. همواره به روزترین روش افزودن کتابخانه‌ی Angular Material به یک برنامه‌ی موجود را در آدرس https://material.angular.io/guide/getting-started می‌توانید مشاهده کنید که خلاصه‌ی آن به صورت زیر است:
البته در Angular 6 روش تفصیلی نصب فوق که شامل 6 مرحله‌است، به صورت زیر هم خلاصه شده‌است:
 ng add @angular/material
متاسفانه در زمان نگارش این مطلب، نگارش 6.3.1 آن توسط دستور فوق نصب نشد و خطای «Error: Collection "@angular/material" cannot be resolved.» ظاهر گردید. البته روش رفع آن در اینجا بحث شده‌است که مهم نیست و در نگارش‌های رسمی بعدی حتما لحاظ خواهد شد. به همین جهت روش تفصیلی آن‌را که همیشه کار می‌کند، در ادامه پیگیری می‌کنیم. ابتدا بسته‌های ذیل را نصب کنید:
npm install --save @angular/material @angular/cdk
npm install --save @angular/animations
npm install --save hammerjs
- دستور اول  angular/cdk و angular/material را نصب می‌کند. cdk در اینجا به معنای کیت توسعه‌ی کامپوننت‌های Angular است که امکان استفاده‌ی از ویژگی‌های Angular Material را بدون الزامی به پیروی از زبان طراحی متریال، میسر می‌کند.
- همانطور که عنوان شد، طراحی متریال مبتنی بر حرکت و پویانمایی است. به همین جهت تعدادی از کامپوننت‌های آن نیاز به بسته‌ی angular/animations را دارند که توسط دستور دوم نصب می‌شود.
- دستور سوم نیز کامپوننت‌های slide و slider را پشتیبانی می‌کند (Gesture Support). البته پس نصب این وابستگی، نیاز است به فایل src/main.ts مراجعه کرده و یک سطر زیر را نیز افزود:
 import "hammerjs";
در ادامه پس از نصب بسته‌ی پویانمایی، به فایل app.module.ts مراجعه کرده و BrowserAnimationsModule را به لیست imports اضافه می‌کنیم:
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";

@NgModule({
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    AppRoutingModule
  ]
})
export class AppModule { }

مدیریت بهتر import کامپوننت‌های Angular Material

در ادامه به ازای هر کامپوننت Angular Material باید ماژول آن‌را به لیست imports افزود که پس از مدتی به یک فایل app.module.ts بسیار شلوغ خواهیم رسید. برای مدیریت بهتر این فایل، از روش مطرح شده‌ی در مطلب «سازماندهی برنامه‌های Angular» استفاده خواهیم کرد.
به همین جهت دو پوشه‌ی core و shared را درون پوشه‌ی src/app ایجاد می‌کنیم:


محتویات فایل src\app\core\core.module.ts به صورت زیر است:
import { CommonModule } from "@angular/common";
import { NgModule, Optional, SkipSelf } from "@angular/core";
import { RouterModule } from "@angular/router";


@NgModule({
  imports: [CommonModule, RouterModule],
  exports: [
    // components that are used in app.component.ts will be listed here.
  ],
  declarations: [
    // components that are used in app.component.ts will be listed here.
  ],
  providers: [
    /* ``No`` global singleton services of the whole app should be listed here anymore!
       Since they'll be already provided in AppModule using the `tree-shakable providers` of Angular 6.x+ (providedIn: 'root').
       This new feature allows cleaning up the providers section from the CoreModule.
       But if you want to provide something with an InjectionToken other that its class, you still have to use this section.
    */
  ]
})
export class CoreModule {
  constructor(@Optional() @SkipSelf() core: CoreModule) {
    if (core) {
      throw new Error("CoreModule should be imported ONLY in AppModule.");
    }
  }
}
در مورد جزئیات آن در مطلب «سازماندهی برنامه‌های Angular توسط ماژول‌ها» کاملا بحث شده‌است.
محتویات فایل src\app\shared\shared.module.ts نیز به صورت زیر است:
import { CommonModule } from "@angular/common";
import { HttpClientModule } from "@angular/common/http";
import { ModuleWithProviders, NgModule } from "@angular/core";
import { FormsModule } from "@angular/forms";

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    HttpClientModule
  ],
  entryComponents: [
    // All components about to be loaded "dynamically" need to be declared in the entryComponents section.
  ],
  declarations: [
    // common and shared components/directives/pipes between more than one module and components will be listed here.
  ],
  exports: [
    // common and shared components/directives/pipes between more than one module and components will be listed here.
    CommonModule,
    FormsModule,
    HttpClientModule,
  ]
  /* No providers here! Since they’ll be already provided in AppModule. */
})
export class SharedModule {
  static forRoot(): ModuleWithProviders {
    // Forcing the whole app to use the returned providers from the AppModule only.
    return {
      ngModule: SharedModule,
      providers: [ /* All of your services here. It will hold the services needed by `itself`. */]
    };
  }
}
سپس تعاریف import این دو فایل را به فایل app.module.ts اضافه می‌کنیم:
import { CoreModule } from "./core/core.module";
import { SharedModule } from "./shared/shared.module";

@NgModule({
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    CoreModule,
    SharedModule.forRoot(),
    AppRoutingModule
  ]
})
export class AppModule { }
پس از این مقدمات، فایل جدید src\app\shared\material.module.ts را در پوشه‌ی shared ایجاد می‌کنیم تا بتوانیم مداخل کامپوننت‌های Angular Material را صرفا به آن اضافه کنیم؛ با این محتوا:
import { CdkTableModule } from "@angular/cdk/table";
import { NgModule } from "@angular/core";
import {
  MatAutocompleteModule,
  MatButtonModule,
  MatButtonToggleModule,
  MatCardModule,
  MatCheckboxModule,
  MatChipsModule,
  MatDatepickerModule,
  MatDialogModule,
  MatExpansionModule,
  MatFormFieldModule,
  MatGridListModule,
  MatIconModule,
  MatInputModule,
  MatListModule,
  MatMenuModule,
  MatNativeDateModule,
  MatPaginatorModule,
  MatProgressBarModule,
  MatProgressSpinnerModule,
  MatRadioModule,
  MatRippleModule,
  MatSelectModule,
  MatSidenavModule,
  MatSliderModule,
  MatSlideToggleModule,
  MatSnackBarModule,
  MatSortModule,
  MatStepperModule,
  MatTableModule,
  MatTabsModule,
  MatToolbarModule,
  MatTooltipModule,
} from "@angular/material";

@NgModule({
  imports: [
    MatAutocompleteModule,
    MatButtonModule,
    MatButtonToggleModule,
    MatCardModule,
    MatCheckboxModule,
    MatChipsModule,
    MatDatepickerModule,
    MatDialogModule,
    MatExpansionModule,
    MatFormFieldModule,
    MatGridListModule,
    MatIconModule,
    MatInputModule,
    MatListModule,
    MatMenuModule,
    MatNativeDateModule,
    MatPaginatorModule,
    MatProgressBarModule,
    MatProgressSpinnerModule,
    MatRadioModule,
    MatRippleModule,
    MatSelectModule,
    MatSidenavModule,
    MatSliderModule,
    MatSlideToggleModule,
    MatSnackBarModule,
    MatStepperModule,
    MatSortModule,
    MatTableModule,
    MatTabsModule,
    MatToolbarModule,
    MatTooltipModule,
    CdkTableModule
  ],
  exports: [
    MatAutocompleteModule,
    MatButtonModule,
    MatButtonToggleModule,
    MatCardModule,
    MatCheckboxModule,
    MatChipsModule,
    MatDatepickerModule,
    MatDialogModule,
    MatExpansionModule,
    MatGridListModule,
    MatIconModule,
    MatInputModule,
    MatListModule,
    MatMenuModule,
    MatNativeDateModule,
    MatPaginatorModule,
    MatProgressBarModule,
    MatProgressSpinnerModule,
    MatRadioModule,
    MatRippleModule,
    MatSelectModule,
    MatSidenavModule,
    MatSliderModule,
    MatSlideToggleModule,
    MatSnackBarModule,
    MatStepperModule,
    MatSortModule,
    MatTableModule,
    MatTabsModule,
    MatToolbarModule,
    MatTooltipModule,
    CdkTableModule
  ]
})
export class MaterialModule {
}
در اینجا هر کامپوننت مورد نیاز، به قسمت‌های import و exports اضافه شده‌اند.
سپس MaterialModule را نیز به قسمت‌های imports و exports فایل src\app\shared\shared.module.ts اضافه خواهیم کرد:
import { MaterialModule } from "./material.module";

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    HttpClientModule,
    MaterialModule
  ],
  exports: [
    // common and shared components/directives/pipes between more than one module and components will be listed here.
    CommonModule,
    FormsModule,
    HttpClientModule,
    MaterialModule
  ]
})
export class SharedModule {
}
به این ترتیب در هر ماژول جدیدی که به برنامه اضافه شود و نیاز به کار با Angular Material را داشته باشد، تنها کافی است SharedModule را import کرد؛ مانند app.module.ts برنامه (البته بدون ذکر متد forRoot آن که این forRoot فقط محض ماژول اصلی برنامه است).

تا اینجا جهت اطمینان از اجرای برنامه، دستور ng serve -o را از ابتدا اجرا کنید.


افزودن چند کامپوننت مقدماتی متریال به برنامه

بهترین روش کار با این مجموعه، بررسی مستندات آن در سایت https://material.angular.io/components است. برای مثال برای افزودن دکمه، به مستندات آن مراجعه کرده و بر روی دکمه‌ی view source کلیک می‌کنیم:


سپس کدهای قسمت HTML آن‌را به برنامه و فایل app.component.html اضافه خواهیم کرد:
 <button mat-button>Click me!</button>
به همین ترتیب مستندات check box را یافته و آن‌را نیز اضافه می‌کنیم:
 <mat-checkbox>Check me!</mat-checkbox>
تا اینجا اگر برنامه را توسط دستور ng serve -o اجرا کنیم، یک چنین خروجی حاصل می‌شود:


البته شکل ظاهری آن‌ها تا اینجا آنچنان مطلوب نیست. برای رفع این مشکل، نیاز است یک قالب را به این کنترل‌ها و کامپوننت‌ها اعمال کرد. به همین جهت فایل styles.css واقع در ریشه‌ی برنامه را گشوده و قالب پیش‌فرض متریال را به آن اضافه می‌کنیم:
 @import "~@angular/material/prebuilt-themes/indigo-pink.css";
قالب‌های از پیش آماده‌ی متریال را در پوشه‌ی node_modules\@angular\material\prebuilt-themes می‌توانید مشاهده کنید.



پس از اعمال قالب، اکنون است که شکل ظاهری کنترل‌های آن بسیار بهتر شده‌اند و همچنین کار با آن‌ها به همراه پویانمایی نیز شده‌است:



افزودن آیکن‌های متریال به برنامه

مرحله‌ی آخر این تنظیمات، افزودن آیکن‌های متریال به برنامه‌است. برای این منظور فایل src\index.html را گشوده و یک سطر ذیل را به head اضافه کنید:
 <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
برای آزمایش آن، به فایل app.component.html مراجعه کرده و تعریف دکمه‌ای را که اضافه کردیم، به صورت ذیل با افزودن mat-icon تغییر می‌دهیم:
<button mat-button>
  <mat-icon>face</mat-icon>
  Click me!
</button>
<mat-checkbox>Check me!</mat-checkbox>
که این خروجی را تولید می‌کند:


لیست کامل این آیکن‌ها را به همراه توضیحات تکمیلی آن‌ها، در آدرس ذیل می‌توانید ملاحظه کنید:
http://google.github.io/material-design-icons

البته چون ما نمی‌خواهیم این آیکن‌ها را از وب بارگذاری کنیم، برای نصب محلی آن‌ها ابتدا دستور زیر را در ریشه‌ی پروژه صادر کنید:
 npm install material-design-icons --save
این آیکن فونت‌ها پس از نصب، در مسیر node_modules\material-design-icons\iconfont قابل مشاهده هستند:


همانطور که مشاهده می‌کنید، برای استفاده‌ی از این فایل‌های آیکن فونت محلی، تنها کافی است فایل material-icons.css را به برنامه معرفی کنیم. برای این منظور فایل angular.json را گشوده و قسمت styles آن‌را به صورت زیر تکمیل می‌کنیم:
"styles": [
   "node_modules/material-design-icons/iconfont/material-icons.css",
   "src/styles.css"
],
اکنون دیگر نیازی به ذکر link href اضافه شده‌ی به فایل src\index.html نداریم و باید از آن حذف شود.



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: MaterialAngularClient-01.zip
برای اجرای آن نیز ابتدا فایل restore.bat و سپس فایل ng-serve.bat را اجرا کنید.
نظرات اشتراک‌ها
Chrome و قابلیت DirectWrite
کلا فونتش را عوض کردم به این فونت خود گوگل.
فونت کودک، با سایر مرورگرها مشکلی نداشت؛ هرچند فایرفاکس این خطا را در کنسول خطاها نمایش می‌داد:
Error: downloadable font: table 'GSUB': OpenType layout data discarded
اطلاعات بیشتر در مورد GSUB
مطالب
ویژگی های کمتر استفاده شده در NET. - بخش ششم

#Execute VB code via C

می توان از طریق #C، ماکروهای Visual Basic مورد استفاده‌ی در Office را تولید کرد.
static void AddChartButton( Workbook workBook,
                            Worksheet xlWorkSheetNew,
                            Range currentRange,  int macroId,
                            int startRow, int endRow,
                            int startCol, int endCol,
                            string buttonImagePath )
{
    var cell = currentRange.Next;
    var width = cell.Width;
    var height = 24;
    var left = cell.Left;
    var top = System.Math.Max( cell.Top + cell.Height - height, 0 );
    var button = xlWorkSheetNew.Shapes.AddPicture( buttonImagePath,
                                                    MsoTriState.msoFalse,
                                                    MsoTriState.msoCTrue,
                                                    left, top, width, height );
    var module = workBook.VBProject.VBComponents.Add( vbext_ComponentType.vbext_ct_StdModule );
    module.CodeModule.AddFromString( GetMacro( macroId,
                                                startRow, endRow,
                                                startCol, endCol ) );
    button.OnAction = "Macro" + macroId;
}

static string GetMacro( int macroId,
                        int startRow,  int endRow,
                        int startCol, int endCol )
{
    var sb = new StringBuilder();
    var range = "ActiveSheet.Range(Cells(" + startRow + "," + startCol + "), Cells(" + endRow + "," + endCol + ")).Select";
    sb.AppendLine( "Sub Macro" + macroId + "()" );
    sb.AppendLine( "On Error Resume Next" );
    sb.AppendLine( range );
    sb.AppendLine( "ActiveSheet.Shapes.AddChart.Select" );
    sb.AppendLine( "ActiveChart.ChartType = xlColumn" );
    sb.AppendLine( "ActiveChart.SetSourceData Source:=" + range );
    sb.AppendLine( "On Error GoTo 0" );
    sb.AppendLine( "End Sub" );
    return sb.ToString();
}

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

var excelApp = new Microsoft.Office.Interop.Excel.Application();
var fileName = @"C:\Users\Vahid\Desktop\VBA.xlsm";
var workBook = excelApp.Workbooks.Open( fileName );
var sheet = workBook.Sheets[1];

AddChartButton( workBook,
                sheet,
                sheet.Range["B1"],
                1231,
                1,
                10,
                1,
                2,
                @"C:\Users\Vahid\Desktop\BarChart.png");

excelApp.DisplayAlerts = false;
workBook.Close( true,
                fileName );

خروجی مثال بالا


نکته: در صورتیکه بعد از اجرای برنامه، خطای " programmatic access to visual basic project is not trusted"  رخ داد از این طریق می‌توانید مشکل را حل کنید.

File -> Options -> Trust Center -> Trust Center Settings -> Macro Settings -> Trust Access to the VBA Project object model


volatile

کلمه کلیدی volatile نشان می‌دهد که یک فیلد ممکن است توسط چندین thread به صورت همزمان تغییر کند. فیلدهایی که به عنوان volatile تعریف می‌شوند، شامل بهینه سازی کامپایلر برای دسترسی از طریق تنها یک thread قرار نمی‌گیرند و بروزرسانی مقدار فعلی این فیلد را در تمامی زمان‌ها، تضمین می‌کند.
class Program
{
    volatile bool _shouldPartyContinue = true;

    static void Main()
    {
        var firstDimension = new Program();
        var secondDimension = new Thread( firstDimension.StartPartyInAnotherDimension );
        secondDimension.Start( firstDimension );
        Thread.Sleep( 5000 );
        firstDimension._shouldPartyContinue = false;
        Console.WriteLine( "Party Finish" );
    }

    void StartPartyInAnotherDimension( object input )
    {
        var currentDimensionInput = (Program)input;
        Console.WriteLine( "let the party begin" );
        while ( currentDimensionInput._shouldPartyContinue ) {}
        Console.WriteLine( "Party ends: (" );
    }
}
نکته: اگر متغیر shouldPartyContinue به وسیله volatile علامت گذاری نشده بود، برنامه در حالت Release (که گزینه Optimize code تیک داشته باشد) هیچگاه به پایان نمی‌رسید.

::global

وقتی که یک عضو توسط موجودیت دیگری با همان نام مخفی شده باشد، با استفاده از کلمه کلیدی ::global (فضای نام سراسری) قابلیت دسترسی به آن امکان پذیر می‌شود.
class Program
{
    public static void Main(string[] args)
    {
        Console.WriteLine( Number );  // Error
        global::System.Console.WriteLine( "Console: " + Console ); //OK
    }

    public class System { }

    // Define a constant called ‘Console’ to cause more problems.
    const int Console = 7;
    const int Number = 67;
}
همانطور که در مثال بالا مشاهده می‌کنید، به دلیل وجود ثابت Console و کلاس System امکان دسترسی به متد WriteLine وجود ندارد، برای در دسترس قرار گرفتن آن باید از ::global استفاده کرد.

DebuggerDisplayAttribute

با استفاده از  DebuggerDisplayAttribute  می‌توانید نحوه نمایش یک فیلد یا یک کلاس را در پنجره متغیر دیباگر مشخص کنید.
[DebuggerDisplay( "{DebuggerDisplay}" )]
public class DebuggerDisplayTest
{
    public string FirstName { get; set; }

    public string LastName { get; set; }

    public int Age { get; set; }

    [DebuggerBrowsable( DebuggerBrowsableState.Never )]
    string DebuggerDisplay => $"{FirstName} {LastName} {Age} years old";
}
و بعد از استفاده‌ی از آن، خروجی زیر بدست می‌آید:

همچنین شما می‌توانید عبارات مختلفی را به صورت مستقیم در این attribute استفاده کنید.
[DebuggerDisplay( "Age {Age > 0 ? Age : 25}" )]
public class DebuggerDisplayTest
{
 //...
}
اگر مقدار پروپرتی Age بیشتر از 0 باشد، مقدار Age و در غیراینصورت 25 نشان داده می‌شود.
نظرات مطالب
فعال سازی عملیات CRUD در Kendo UI Grid
- «Handling Server-Side Validation Errors In Your Kendo UI Grid»
خلاصه‌اش به این صورت است:
- ابتدا رخداد error مربوط به data source را باید مدیریت کرد:
 var dataSource = new kendo.data.DataSource({
            // ... 
            error: function (e) {
                window.SalesHub.OrderDetails_Error(e);
            },
            // ... 
        });
با این متد
window.SalesHub.OrderDetails_Error = function(args) {
    if (args.errors) {
        var grid = $("#orderDetailsGrid").data("kendoGrid");
        var validationTemplate = kendo.template($("#orderDetailsValidationMessageTemplate").html());
        grid.one("dataBinding", function(e) {
            e.preventDefault();

            $.each(args.errors, function(propertyName) {
                var renderedTemplate = validationTemplate({ field: propertyName, messages: this.errors });
                grid.editable.element.find(".errors").append(renderedTemplate);
            });
        });
    }
};
که از این قالب برای نمایش خطاها استفاده می‌کند:
<script type="text/x-kendo-template" id="orderDetailsValidationMessageTemplate">
    # if (messages.length) { #
        <li>#=field#
            <ul>
                # for (var i = 0; i < messages.length; ++i) { #
                    <li>#= messages[i] #</li>
                # } #
            </ul>
        </li>
    # } #
</script>