اشتراک‌ها
معرفی کتابخانه FastMember برای کار با Reflection

خیلی از مواقع ما نیاز داریم از Reflection استفاده کنیم اما Reflection به خودی خود نسبت به حالی عادی خیلی کندتر هست و نیاز به نوشتن کد‌های بیشتری دارد.

برای سرعت بیشتر و استفاده راحت‌تر از Reflection میتونین از کتابخانه FastMember استفاده کنین. (بین تست‌های که شخصا داشتم تو سناریو‌های مختلف، FastMember بین 6 تا 96 برابر سریعتر از Reflection معمولی سریع‌تر بود.)

مثلا برای دسترسی به property‌های یک object در runtime با کمک FastMember، خواهیم داشت:

var accessor = ObjectAccessor.Create(obj);
var propName = "MyProperty"
var value = accessor[propName];

نکته :

این کتابخانه کد‌ها IL برنامه رو در runtime تغییر میده، در نتیجه در محیط‌های AOT (مخفف Ahead of Time) مانند Xamarin iOS کار نخواهد کرد.


در مقاله زیر که مقاله جالبی هست توضیح میده، رفلکشن چطور کار میکنه و چرا کند هست

سپس به روش‌های افزایش پرفرمنس رفلکشن اشاره میکنه و در آخر بنچمارکی از نتیجه اونها رو نشون میده

Why Reflection Is Slow  

معرفی کتابخانه FastMember برای کار با Reflection
مطالب
آشنایی با Contained Databases در SQL Server 2012
مقدمه:
بعضی وقت‌ها به هر دلیلی لازم است پایگاه داده برنامه هایمان را به سرور دیگری انتقال دهیم . برخلاف Oracle که انتقال پایگاه داده به سرور دیگر کار مشکل و تخصصی می‌باشد در SQL Server براحتی با یک Detach و Attach مجدد این انتقال انجام خواهد شد. اما اطلاعات Logins در پایگاه داده Master  سرور ذخیره شده است و با فایل پایگاه داده انتقال نمی‌یابد و به هیمن خاطر برخی مواقع پس از Attach پایگاه داده با پیغام The database is not accessible یا Cannot open user default database مواجع خواهیم شد که مشکل بخاطر همین نبود کاربران سرور قبلی و Owner متفاوت پایگاه داده می‌باشد که با استفاده از رویه ذخیره شده sp_change_users_login این مشکل قابل حل می‌باشد اما در SQL Server 2012 راحل بهتری فراهم شده است.

Contained Databases در Sql Server 2012
یکی از امکانات اضافه شده در SQL Server 2012  امکان Contained Databases  هست که برای حل همین مشکل و رفع کامل وابستی پایگاه داده با Instance نصب شده و خاصیت قابل حمل کامل database‌ها می‌باشد. در ادامه نحوه استفاده از این امکان را بررسی خواهیم کرد:
  1. فعال کردن خاصیت Contained Databases 

    قبل از استفاده از Contained Databases می بایست این امکان را فعال کرد. برای این کار می‌توانید از SQL Server Management Studio یا T-SQL commands استفاده نمایید. بر روی نام Instance راست کلیت کنید و گزینه Properties را انتخاب نمایید. از گزینه Advanced که در شکل زیر مشاهده می‌نمایید خاصیت Enable Contained Databases را بر روی True قرار دهید.

    یا می‌توایند از sp_configure این کار را انجام دهید.دستورات زیر این موضوع را نشان می‌دهد.
    sp_configure 'show advanced options',1
    GO
    RECONFIGURE WITH OVERRIDE
    GO
    sp_configure 'contained database authentication',1
    GO
    RECONFIGURE WITH OVERRIDE
    GO

  2. ایجاد یا تغییر یک پایگاه داده از نوع Contained Databases

    برای ایجاد یک پایگاه داده با این خاصیت یا تغییر پایگاه داده موجود کافیست مقدار گزینه Containment type را بر روی Partial قرار دهید. برای پایگاه داده موجود از پنجره Properties پایگاه داده صفحه Options را انتخاب کنید.


  3. ایجاد یک کاربر برای پایگاه داده Contained Databases

    برای تعریف یک کاربر در سطح پایگاه داده پوشه Security پایگاه داده خود را باز کنید بر روی پوشه Users راست کیلک و گزینه New User را انتخاب نمایید از گزینه User type که در شکل زیر نشان داده شده است SQL user with password را انتخاب نمایید و نام کاربر و رمز عبور و تکرار آن را وارد نمایید. کاربر ایجاد شده در سطح پایگاه داده می‌باشد و با انتقال به سرور دیگر نیر قابل دسترسی می‌باشد.

  4.  اتصال به پایگاه داده Contained Databases

    برای اتصال به پایگاه داده کافیست در حالت SQL Server Authentication نام کاربری و رمز عبور جدید را وارد و گزینه Options  را انتخاب و از برگه Additional Connection Parameters نام پایگاه داده مورد نظر را مانند شکل زیر وارد نمایید پس از ورود تنها پایگاه داده خود را مشاهده می‌نمایید. یکی از کاربرهای این قابلیت برای مدیران سرور پایگاه داده می‌باشد که بدون استفاده از مجوز sysadmin  به کاربران اجازه دسترسی را می‌دهد.



مطالب
نگاهی به تغییرات فایل Program.cs در نگارش‌های مختلف ASP.NET Core تا نگارش 6
در طی سال‌های قبل، نحوه‌ی آغاز برنامه‌های ASP.NET Core دچار تغییرات زیادی شده‌است که حداقل سه مورد مهم آن‌ها به صورت زیر است:
- استفاده از WebHost.CreateDefaultBuilder: این روش جهت تنظیم شروع به کار یک برنامه‌ی ASP.NET Core 2x مورد استفاده قرار می‌گرفت.
- استفاده از Host.CreateDefaultBuilder: روش پیش‌فرض آغاز برنامه‌های وب NET Core 3x. و NET 5x. که با معرفی generic host، امکان تهیه‌ی Worker services را میسر کردند.
- استفاده از WebApplication.CreateBuilder: روش جدید شروع به کار با برنامه‌های وب مبتنی بر NET 6.


استفاده از WebHost.CreateDefaultBuilder در ASP.NET Core 2.x

در نگارش اول ASP.NET Core، مفهومی به نام هاست پیش‌فرض (default host) وجود نداشت. یکی از مهم‌ترین نکات درنظر گرفته شده در طراحی ASP.NET Core، مفهوم «هزینه کردن به ازای احتیاج» است. یعنی اگر نیاز به قابلیتی نیست، نباید وجود داشته باشد. به این ترتیب یک قالب آغازین نباید به همراه تعداد زیادی بسته‌‌های NuGet و مقدار زیادی کد برای تنظیم باشد؛ زمانیکه واقعا قرار نیست از تمام آن‌ها استفاده شود. به همین جهت از نگارش 2، شروع به ساده‌کردن این قالب اولیه کردند و در این زمان، WebHost.CreateDefaultBuilder ارائه شد. این تنظیم به ظاهر ساده، کدهای پیش‌فرض مورد نیاز قابل توجهی را جهت ساخت ساده‌ی IWebHostBuilder و IWebHost به همراه دارد. در قالب به همراه آن، بین مفاهیم تنظیمات application و host تفاوت قائل شده‌اند؛ یعنی دو فایل Program.cs را برای تنظیمات application و فایل Startup.cs را برای ارائه‌ی تنظیمات هاست، تدارک دیدند.
در این طراحی، بین میدان دید Program و Startup تفاوت وجود دارد. هدف از Program، ارائه‌ی تنظیمات زیرساختی برنامه، مانند تنظیمات HTTP Server، یکپارچگی با IIS و امثال آن شد و عموما در طول عمر برنامه ثابت است. اما برای تغییر رفتار برنامه، بیشتر تغییرات و تنظیمات، به Startup محول شدند؛ مانند تنظیمات تزریق وابستگی‌ها، تنظیمات میان‌افزارها، مسیریابی‌ها و غیره.
public class Program
{
    public static void Main(string[] args)
    {
        BuildWebHost(args).Run();
    }

     public static IWebHost BuildWebHost(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>()
            .Build();
}
نمونه‌ا‌ی از فایل Program.cs برنامه‌های ASP.NET Core 2x را در اینجا ملاحظه می‌کنید این تنظیم ساده، کار خواندن فایل appsettings.json و برپایی تنظیمات برنامه، تنظیمات logging و همچنین تنظیمات یکپارچگی با Kestrel/IIS را در پشت صحنه انجام می‌دهد. همچنین ارجاعی را نیز به کلاس Startup برنامه دارد. متد UseStartup به صورت خودکار به دنبال متدهای ویژه‌ی ConfigureServices و Configure در کلاس آغازین برنامه گشته و آن‌ها را فراخوانی می‌کند تا تنظیمات تزریق وابستگی‌ها، مسیریابی‌ها و میان‌افزارها صورت گیرند.


معرفی Generic Host در ASP.NET Core 3.x/5

ASP.NET Core 3.x به همراه تغییرات بزرگی در کدهای آغازین برنامه‌های ASP.NET Core بود. تا پیش از آن، امکانات پایه‌ی ASP.NET Core تنها برای مقاصد وب قابل استفاده بود. اما در نگارش‌های 3x، با ارائه‌ی یک هاست عمومی، امکان پشتیبانی از سایر برنامه‌ها، مانند worker services را نیز میسر کرد؛ برای مثال پیشتیبانی از کارهای طولانی پس زمینه، هاست سرویس‌های gRPC، هاست سرویس‌های ویندوز و غیره. هدف اصلی از این تغییرات، به اشتراک گذاری فریم‌ورک پایه‌ی ASP.NET Core که تنها برای برنامه‌های وب ساخته شده بود و به همراه امکاناتی مانند تنظیمات، ثبت وقایع، تزریق وابستگی‌ها و غیره بود، با سایر انواع برنامه‌های یاد شده است. برای رسیدن به این هدف، Web Host نگارش 2x، به Generic Host نگارش 3x تغییر کرد و سپس ASP.NET Core برفراز آن بنا شد. اینبار بجای IWebHostBuilder، نمونه‌ی جدید IHostBuilder را داریم:
public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            }; 
    }
}
در اینجا نمونه‌ا‌ی از فایل Program.cs برنامه‌های ASP.NET Core 3x را ملاحظه می‌کنید. با توجه به عمومی بودن این روش، قسمت ConfigureWebHostDefaults آن، همان کار WebHost.CreateDefaultBuilder نگارش 2 ASP.NET Core را انجام می‌دهد.
ASP.NET Core 5 به همراه تغییرات مهمی در این زمینه نبود و تنها با تغییر target framework در فایل csproj و به روز رسانی بسته‌های نیوگت مرتبط، کار ارتقاء به نگارش جدید در آن صورت می‌گرفت.


معرفی  WebApplicationBuilder در ASP.NET Core 6x  

در دات نت 6، روش آغاز برنامه‌های وب بطور کامل تغییر کرده‌است. در تمام نگارش‌های پیشین ASP.NET Core، همواره شاهد دو فایل Program.cs و Startup.cs بودیم؛ اما در اینجا فقط یک فایل Program.cs بیشتر وجود ندارد. هر چند همانطور که در مطلب «ارتقاء فایل‌های آغازین برنامه‌های ASP.NET Core 5x به 6x» نیز عنوان شد، می‌توان همان سبک و سیاق پیشین را نیز برگرداند و از این لحاظ محدودیتی وجود ندارد.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
   app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.MapGet("/", () => "Hello World!");
app.MapRazorPages();
app.Run();
در اینجا نمونه‌ای از فایل Program.cs برنامه‌های دات نت 6 را مشاهده می‌کنید که تغییرات قابل ملاحظه‌ای داشته‌است:
- استفاده از top level statements
- استفاده از usingهای سراسری و سایر قابلیت‌های C# 10.0
- حذف کامل کلاس Startup؛ اینبار همه چیز فقط یک فایل است.

دیدگاهی را که در اینجا بکار گرفته‌اند شامل این موارد است و بیشتر تازه‌واردان را مدنظر دارند:
- برای شروع به کار، using statements اضافی هستند؛ پس حذف شده‌اند!
- برای شروع به کار، namespaces اضافی هستند؛ پس حذف شده‌اند!
- برای شروع به کار، Program.Main ... هم اضافی است!
- تنظیمات بین دو فایل Program.cs و Startup.cs تقیسم نشده‌اند و همه‌چیز یکجا است و این هم نیازی به توضیح اضافی به تازه‌واردان، ندارد.
- همچنین همانطور که عنوان شد، « ... متد UseStartup به صورت خودکار به دنبال متدهای ویژه‌ی ConfigureServices و Configure در کلاس آغازین برنامه گشته و آن‌ها را فراخوانی می‌کند تا تنظیمات تزریق وابستگی‌ها، مسیریابی‌ها و میان‌افزارها صورت گیرند ...»
نکته‌ی مهم این توضیح، این است که کلاس Startup، هیچ اینترفیسی را پیاده سازی نمی‌کند. یعنی نام این متدها، دقیقا باید به همین صورت باشند (بدون اینکه قرار دادی توسط یک اینترفیس برای آن‌ها وضع شده باشد) و ... چرا واقعا باید به این صورت باشد؟! به همین جهت این‌ها هم حذف شده‌اند!
- در اینجا WebApplication هم مشاهده می‌شود؛ اما آیا واقعا نیازی به آن است؟
پاسخ: خیر! WebApplication.CreateBuilder ای که در اینجا ملاحظه می‌کنید در حقیقت ساده شده‌ی قطعه کد زیر از کدهای ASP.NET Core 3x/5x است:
var hostBuilder = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services => 
    {
        services.AddRazorPages();
    })
    .ConfigureWebHostDefaults(webBuilder =>
    {
        webBuilder.Configure((ctx, app) => 
        {
            if (ctx.HostingEnvironment.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseStaticFiles();
            app.UseRouting();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGet("/", () => "Hello World!");
                endpoints.MapRazorPages();
            });
        });
    }); 
hostBuilder.Build().Run();
که ... WebApplication.CreateBuilder بدون شک ساده‌تر از قطعه کد فوق است. همچنین پس از یک سطر زیر:
 var builder = WebApplication.CreateBuilder(args);
با استفاده از خاصیت builder.Configuration، می‌توان به تنظیمات برنامه دسترسی یافت و یا با استفاده از builder.Services می‌توان سرویس‌های جدیدی را ثبت کرد:
builder.Services.AddRazorPages();
builder.Services.AddSingleton<MyThingy>();
و یا با استفاده از خاصیت builder.Logging می‌توان تنظیمات ثبت وقایع برنامه را تغییر داد:
builder.Logging.AddFile();
همچنین در اینجا زمانیکه کار تنظیمات به پایان رسید، نمونه‌ای از WebApplication را به صورت زیر تولید کرده:
var app = builder.Build();
و اکنون می‌توان تنظیمات میان‌افزارها و یا مسیریابی‌ها را انجام داد:
app.UseStaticFiles();
app.MapRazorPages();
که در مقایسه با نگارش‌های قبلی، بسیار ساده‌تر شده‌، هرچند مرز بین این‌ها و ترتیب آن‌ها اندکی کم‌رنگ‌تر شده‌است.
مطالب
تبدیل PDF به تصویر با استفاده از API توکار Window 8.1 در برنامه‌های غیر مترو دات نت
ویندوز 8.1 دارای امکانات و API توکاری جهت نمایش و خواندن فایل‌های PDF در برنامه‌های مترو است. در ادامه قصد داریم از این امکانات در یک برنامه‌ی متداول دات نت، برای مثال یک برنامه‌ی کنسول غیر مترو استفاده کنیم.


آماده سازی برنامه‌های دات نت برای دسترسی به API مترو ویندوز 8.1

ابتدا یک برنامه‌ی کنسول دات نت 4.5.1 را آغاز کنید. برای دسترسی به API ویندوز 8.1 حتما نیاز است که حداقل از دات نت 4.5.1 شروع کرد. سپس برنامه را در VS.NET بسته و فایل پروژه آن‌را در یک ادیتور متنی باز کنید.
در ابتدای فایل csproj، نیاز است سطر TargetPlatformVersion ذیل اضافه شود.
  <PropertyGroup>
    <TargetFrameworkVersion>v4.5.1</TargetFrameworkVersion>
    <TargetPlatformVersion>8.1</TargetPlatformVersion>
  </PropertyGroup>
سپس در همین فایل، ارجاعات زیر را نیز اضافه نمائید:
  <ItemGroup>
    <Reference Include="System" />
    <Reference Include="System.ComponentModel.DataAnnotations" />
    <Reference Include="System.Core" />
    <Reference Include="System.ObjectModel" />
    <Reference Include="System.Xml.Linq" />
    <Reference Include="System.Data.DataSetExtensions" />
    <Reference Include="Microsoft.CSharp" />
    <Reference Include="System.Data" />
    <Reference Include="System.Xml" />
    <Reference Include="System.Threading" />
    <Reference Include="System.Threading.Tasks" />
  </ItemGroup>
  <ItemGroup>
    <Reference Include="Windows" />
    <Reference Include="System.Runtime" />
    <Reference Include="System.Runtime.WindowsRuntime" />
  </ItemGroup>
مواردی مانند System.Runtime، System.Runtime.WindowsRuntime امکان دسترسی به API ویندوز 8 را در برنامه‌های دات نت میسر می‌کنند.


یک نکته
اگر می‌خواهید این فرآیند را ساده و خودکار کنید، از قالب‌های پروژه‌ی مخصوص DesktopWinRT.Templates.vsix استفاده نمائید.
DesktopWinRT.Templates.vsix


افزودن ارجاعی به Nito.AsyncEx

چون برنامه‌ی مورد استفاده کنسول است و API ویندوز 8 کاملا async طراحی شده‌است، نیاز است با کمک AsyncContext موجود در کتابخانه‌ی Nito.AsyncEx بتوان از امکانات async و await در متد Main برنامه استفاده کرد. البته اگر از سایر برنامه‌های دسکتاپ استفاده می‌کنید، فقط کافی است امضای متد رخدادن گردان را به async تغییر دهید.
 install-package Nito.AsyncEx


تبدیل استریم‌های دات نت به استریم‌های WinRT

اکثر متدهای WinRT با استریم‌هایی از نوع IRandomAccessStream کار می‌کنند. برای اینکه بتوان استریم استاندارد دات نت را به این نوع تبدیل کرد، می‌توان از کلاس‌های ذیل کمک گرفت:
using System;
using System.IO;
using Windows.Storage.Streams;

namespace ConsoleWin81PdfApiTest
{
    public static class MicrosoftStreamExtensions
    {
        public static IRandomAccessStream AsRandomAccessStream(this Stream stream)
        {
            return new RandomStream(stream);
        }

    }

    class RandomStream : IRandomAccessStream
    {
        readonly Stream _internstream;

        public RandomStream(Stream underlyingstream)
        {
            _internstream = underlyingstream;
        }

        public IInputStream GetInputStreamAt(ulong position)
        {
            _internstream.Position = (long)position;
            return _internstream.AsInputStream();
        }

        public IOutputStream GetOutputStreamAt(ulong position)
        {
            _internstream.Position = (long)position;
            return _internstream.AsOutputStream();
        }

        public ulong Size
        {
            get
            {
                return (ulong)_internstream.Length;
            }
            set
            {
                _internstream.SetLength((long)value);
            }
        }

        public bool CanRead
        {
            get { return _internstream.CanRead; }
        }

        public bool CanWrite
        {
            get { return _internstream.CanWrite; }
        }

        public IRandomAccessStream CloneStream()
        {
            throw new NotSupportedException();
        }

        public ulong Position
        {
            get { return (ulong)_internstream.Position; }
        }

        public void Seek(ulong position)
        {
            _internstream.Seek((long)position, SeekOrigin.Begin);
        }

        public void Dispose()
        {
            _internstream.Dispose();
        }

        public Windows.Foundation.IAsyncOperationWithProgress<IBuffer, uint> ReadAsync(IBuffer buffer, uint count, InputStreamOptions options)
        {
            return GetInputStreamAt(Position).ReadAsync(buffer, count, options);
        }

        public Windows.Foundation.IAsyncOperation<bool> FlushAsync()
        {
            return GetOutputStreamAt(Position).FlushAsync();
        }

        public Windows.Foundation.IAsyncOperationWithProgress<uint, uint> WriteAsync(IBuffer buffer)
        {
            return GetOutputStreamAt(Position).WriteAsync(buffer);
        }
    }
}
تا اینجا به یک متد الحاقی جدیدی به نام AsRandomAccessStream می‌رسیم که امکان تبدیل استریم استاندارد دات نت را به IRandomAccessStream مخصوص WinRT دارد. از آن می‌توان برای باز کردن یک فایل و ارسال استریم آن به توابع WinRT و یا ثبت استریم WinRT در یک فایل استفاده کرد.


خواندن فایل‌های PDF و تبدیل صفحات آن‌ها به تصویر

در ادامه کد کامل استفاده از API جدید ویندوز 8.1 را جهت خواندن فایل‌های PDF ملاحظه می‌کنید. این امکانات جدید در فضای نام Windows.Data.Pdf قرار دارند و صرفا امکان خواندن فایل‌های PDF را تدارک دیده‌اند.
using System;
using System.IO;
using System.Threading.Tasks;
using Windows.Data.Pdf;
using Nito.AsyncEx;

namespace ConsoleWin81PdfApiTest
{
    class Program
    {
        static void Main(string[] args)
        {
            AsyncContext.Run(async () =>
            {
                await test();
            });
        }

        private static async Task test()
        {
            using (var randomAccessStream = File.Open("PieChartPdfReport.pdf", FileMode.Open).AsRandomAccessStream())
            {
                var pdfDocument = await PdfDocument.LoadFromStreamAsync(randomAccessStream);
                for (uint i = 0; i < pdfDocument.PageCount; i++)
                {
                    using (var page = pdfDocument.GetPage(i))
                    {
                        /*var renderOptions = new PdfPageRenderOptions
                        {
                            BackgroundColor = Colors.LightGray,
                            DestinationHeight = (uint) (page.Size.Height*10)
                        };*/

                        using (var stream = File.Open(string.Format("page-{0}.png", i + 1), FileMode.OpenOrCreate).AsRandomAccessStream())
                        {
                            await page.RenderToStreamAsync(stream/*, renderOptions*/);
                            await stream.FlushAsync();
                        }
                    }
                }
            }
        }
    }
}

توضیحات:
- متد AsyncContext.Run جزو امکانات Nito.AsyncEx است و امکان نوشتن کدهای await دار را در متد Main یک برنامه‌ی کنسول فراهم می‌کند.
- متد  File.Openدات نت، خروجی از نوع استریم دارد. برای تبدیل آن به نوع IRandomAccessStream، از متد الحاقی AsRandomAccessStream که پیشتر تهیه کردیم، می‌توان استفاده کرد.
- در ادامه متد PdfDocument.LoadFromStreamAsync این استریم خاص را دریافت کرده و امکان دسترسی به API ویندوز 8.1 را میسر می‌کند.
- توسط متد pdfDocument.GetPage می‌توان به صفحات مختلف فایل PDF باز شده دسترسی یافت. در اینجا متد page.RenderToStreamAsync، سبب رندر شدن صفحه با فرمت PNG می‌شود. این خروجی نهایتا باید در یک استریم از نوع IRandomAccessStream ثبت شود. در اینجا نیز می‌توان از متد File.Open در حالت FileMode.OpenOrCreate استفاده کرد.
- اگر می‌خواهید ابعاد تصویر نهایی و ویژگی‌های آن‌را تغییر دهید، می‌توان از پارامتر دوم متد page.RenderToStreamAsync استفاده کرد که شیءایی از نوع PdfPageRenderOptions را می‌پذیرد.


کدهای کامل این پروژه را از اینجا می‌توانید دریافت کنید
MicrosoftStreamExtensions.zip


برای مطالعه بیشتر
How to use specific WinRT API from Desktop apps
How to call WinRT APIs from .NET desktop apps
مطالب
ساده سازی تعریف فضاهای نام در C# 10.0
در ادامه‌ی طراحی مبتنی بر مینی‌مالیسم C# 10.0، پس از پیش‌فرض شدن «top level programs» و همچنین «کاهش تعداد بار تعاریف usingها»، تغییر سوم صورت گرفته‌ی در قالب‌های پروژه‌های مبتنی بر دات نت 6، ساده سازی تعاریف فضاهای نام است. برای مثال یک کنترلر، به این صورت تعریف شده‌است:
namespace mvc.Controllers;

public class HomeController : Controller
{
}
که به آن «File-Scoped Namespaces» هم گفته می‌شود.


بررسی مفهوم «File-Scoped Namespaces»

یکی از اهداف مهم C# 10.0، کاهش نویز موجود در فایل‌های cs. است. اگر قرار است صدها بار در فایل‌های مختلف برنامه، using System نوشته شود، چرا یکبار آن‌را به صورت عمومی تعریف نکنیم و یا اگر در 99 درصد موارد، توسعه دهنده‌ها به ازای یک فایل، تنها یک فضای نام را تعریف می‌کنند، چرا باید یک فضای اضافی خالی، برای تعریف آن اختصاص داده شود و تمام فایل‌ها به همراه یک «tab فاصله‌ی» اضافی مختص به این فضای نام باشند؟
تعریف فعلی فضاهای نام در #C به صورت زیر است:
namespace MyNamespace
{
    public class MyClass
    {
        public void MyMethod()
        {
            //...Method implementation
        }
    }
}
در این حالت هر شیءای که داخل {} این فضای نام قرار گیرد، متعلق به آن است.
در C# 10.0، می‌توان این تعریف را ساده کرد؛ از آنجائیکه به ندرت چند فضای نام در یک تک فایل تعریف می‌شوند، می‌توان تعریف فضای نام را در یک سطر، در ابتدای فایل ذکر کرد، تا به صورت خودکار به کل فایل و اشیاء موجود در آن اعمال شود:
namespace MyNamespace;
public class MyClass
{
    public void MyMethod()
    {
        //...Method implementation
    }
}
در این حالت، روش استفاده‌ی از یک چنین اشیایی هیچ تغییری نخواهد کرد؛ فقط یک tab space و فاصله از کنار صفحه، صرفه‌جویی می‌شود!


محدویت‌های «File-Scoped Namespaces»

- بدیهی است در این حالت دیگر نمی‌توان چندین فضای نام را همانند قبل در یک فایل cs. تعریف کرد:
namespace Name1
{
    public class Class1
    {
    }
}

namespace Name1.Name2
{
  public class Class2
  {
  }
}
 و البته این موردی است که جزو best practices توسعه‌ی برنامه‌های #C به هیچ عنوان توصیه نمی‌شود.
- همچنین امکان ترکیب روش قبلی تعریف فضاهای نام، با روش جدید، در یک فایل وجود ندارد.
- به علاوه امکان تعریف فضاهای نام تو در تو که با روش قدیمی وجود دارد:
namespace Name1
{
    public class Class1
    {
    }

    namespace Name1.Name2
    {
        public class Class2
        {
        }
    }
}
در این حالت جدید پشتیبانی نمی‌شود.
مطالب
بررسی Source Generators در #C - قسمت دوم - یک مثال
یک مثال: پیاده سازی INotifyPropertyChanged توسط Source Generators

هدف از اینترفیس INotifyPropertyChanged که به همراه یک رخ‌داد است:
public interface INotifyPropertyChanged  
{ 
   event PropertyChangedEventHandler PropertyChanged;  
}
مطلع سازی استفاده کننده‌ی از یک شیء، از تغییرات رخ‌داده‌ی در مقادیر خواص آن است که نمونه‌ی آن، در برنامه‌های WPF، جهت به روز رسانی UI، زیاد مورد استفاده قرار می‌گیرد. البته این رخ‌داد به خودی خود کار خاصی را انجام نمی‌دهد و برای استفاده‌ی از آن، باید مقدار زیادی کد نوشت و این مقدار کد نیز باید به ازای تک تک خواص یک کلاس مدل، تکرار شوند:
  partial class CarModel : INotifyPropertyChanged
  {

    private double _speedKmPerHour;
    
    public double SpeedKmPerHour
    {
      get => _speedKmPerHour;
      set
      {
        _speedKmPerHour = value;
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SpeedKmPerHour)));
      }
    }

    public event PropertyChangedEventHandler? PropertyChanged;
  }
همچنین باید درنظر داشت که با تغییر نام خاصیتی، میزان قابل ملاحظه‌ای از این کدهای تکراری نیز باید به روز رسانی شوند که این عملیات می‌تواند ایده‌ی خوبی برای استفاده‌ی از Source Generators باشد.
اگر بخواهیم تولید این کدهای تکراری را به Source Generators محول کنیم، می‌توان برای مثال فیلد خصوصی مرتبط را نگه داشت و تولید مابقی کدها را خودکار کرد:
  partial class CarModel : INotifyPropertyChanged
  {
    private double _speedKmPerHour;    
  }
در این حالت کلاس مدل، به صورت partial تعریف می‌شود و فقط فیلد خصوصی، در کدهای ما حضور خواهد داشت. مابقی کدهای این کلاس partial به صورت خودکار توسط یک Source Generator سفارشی تولید خواهد شد. همانطور که ملاحظه می‌کنید، کاهش حجم قابل ملاحظه‌ای حاصل شده و همچنین اگر فیلد خصوصی دیگری نیز در اینجا اضافه شود، واکنش Source Generator ما آنی خواهد بود و بلافاصله کدهای مرتبط را تولید می‌کند و برنامه، بدون مشکلی کامپایل خواهد شد؛ هرچند به ظاهر INotifyPropertyChanged ذکر شده، در این کلاس اصلا پیاده سازی نشده‌است.


ایجاد پروژه‌ی Source Generator

در ابتدا برای ایجاد تولید کننده‌ی خودکار کدهای INotifyPropertyChanged، یک class library را به solution جاری اضافه می‌کنیم. سپس نیاز است ارجاعاتی را به دو بسته‌ی نیوگت زیر نیز افزود:
<Project Sdk="Microsoft.NET.Sdk">

  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.2.0" PrivateAssets="all" />
  </ItemGroup>
</Project>
سپس کلاس جدید NotifyPropertyChangedGenerator را به نحو زیر به آن اضافه می‌کنیم:
  [Generator]
  public class NotifyPropertyChangedGenerator : ISourceGenerator
  {
    public void Initialize(GeneratorInitializationContext context)
    {
    }

    public void Execute(GeneratorExecutionContext context)
    {
- این کلاس باید اینترفیس ISourceGenerator را پیاده سازی کرده و همچنین مزین به ویژگی Generator باشد.
- اینترفیس ISourceGenerator به همراه دو متد Initialize و Execute است که در صورت نیاز باید پیاده سازی شوند.

در متد Execute، به خاصیت context.Compilation دسترسی داریم. این خاصیت تمام اطلاعاتی را که کامپایلر از Solution جاری در اختیار دارد، به توسعه دهنده ارائه می‌دهد. برای نمونه پیاده سازی متد Execute تولید کننده‌ی کد مثال جاری، چنین شکلی را دارد:
    public void Execute(GeneratorExecutionContext context)
    {
      // uncomment to debug the actual build of the target project
      // Debugger.Launch();
      var compilation = context.Compilation;
      var notifyInterface = compilation.GetTypeByMetadataName("System.ComponentModel.INotifyPropertyChanged");

      foreach (var syntaxTree in compilation.SyntaxTrees)
      {
        var semanticModel = compilation.GetSemanticModel(syntaxTree);
        var immutableHashSet = syntaxTree.GetRoot()
          .DescendantNodesAndSelf()
          .OfType<ClassDeclarationSyntax>()
          .Select(x => semanticModel.GetDeclaredSymbol(x))
          .OfType<ITypeSymbol>()
          .Where(x => x.Interfaces.Contains(notifyInterface))
          .ToImmutableHashSet();

        foreach (var typeSymbol in immutableHashSet)
        {
          var source = GeneratePropertyChanged(typeSymbol);
          context.AddSource($"{typeSymbol.Name}.Notify.cs", source);
        }
      }
    }
در اینجا با استفاده از context.Compilation به اطلاعات کامپایلر دسترسی پیدا کرده و سپس SyntaxTrees آن‌را یکی یکی، جهت یافتن کلاس‌ها و یا همان ClassDeclarationSyntax ها، پیمایش و بررسی می‌کنیم. سپس از بین این کلاس‌ها، کلاس‌هایی که INotifyPropertyChanged را پیاده سازی کرده باشند، انتخاب می‌کنیم که اطلاعات آن در پایان کار، به متد GeneratePropertyChanged جهت تولید مابقی کدهای partial class ارسال شده و کد تولیدی، به context اضافه می‌شود تا به نحو متداولی همانند سایر کدهای برنامه، به مجموعه کدهای مورد بررسی کامپایلر اضافه شود.

نکته‌ی مهم و جالب در اینجا این است که نیازی نیست تا قطعه کد جدید را به صورت SyntaxTrees در آورد و به کامپایلر اضافه کرد. می‌توان این قطعه کد را به نحو متداولی، به صورت یک قطعه رشته‌ی استاندارد #C، تولید و به کامپایلر با متد context.AddSource ارائه کرد که نمونه‌ای از آن‌را در ذیل مشاهده می‌کنید:
    private string GeneratePropertyChanged(ITypeSymbol typeSymbol)
    {
      return $@"
using System.ComponentModel;

namespace {typeSymbol.ContainingNamespace}
{{
  partial class {typeSymbol.Name}
  {{
    {GenerateProperties(typeSymbol)}
    public event PropertyChangedEventHandler? PropertyChanged;
  }}
}}";
    }

    private static string GenerateProperties(ITypeSymbol typeSymbol)
    {
      var sb = new StringBuilder();
      var suffix = "BackingField";

      foreach (var fieldSymbol in typeSymbol.GetMembers().OfType<IFieldSymbol>()
        .Where(x=>x.Name.EndsWith(suffix)))
      {
        var propertyName = fieldSymbol.Name[..^suffix.Length];
        sb.AppendLine($@"
    public {fieldSymbol.Type} {propertyName}
    {{
      get => {fieldSymbol.Name};
      set
      {{
        {fieldSymbol.Name} = value;
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof({propertyName})));
      }}
    }}");
      }

      return sb.ToString();
    }
در اینجا در ابتدا بدنه‌ی کلاس partial تکمیل می‌شود. سپس خواص عمومی آن بر اساس فیلدهای خصوصی تعریف شده، تکمیل می‌شوند. در این مثال اگر یک فیلد خصوصی به عبارت BackingField ختم شود، به عنوان فیلدی که قرار است معادل کدهای INotifyPropertyChanged را داشته باشد، شناسایی می‌شود و به همراه کدهای تولید شده‌ی خودکار خواهد بود.

کدهای source generator ما همین مقدار بیش‌تر نیست. اکنون می‌خواهیم از آن در یک برنامه‌ی کنسول جدید (برای مثال به نام NotifyPropertyChangedGenerator.Demo) استفاده کنیم. برای اینکار نیاز است ارجاعی را به آن اضافه کنیم؛ اما این ارجاع، یک ارجاع متداول نیست و نیاز به ذکر چنین ویژگی خاصی وجود دارد:
<Project Sdk="Microsoft.NET.Sdk">

  <ItemGroup>
    <ProjectReference Include="..\NotifyPropertyChangedGenerator\NotifyPropertyChangedGenerator.csproj"
                      OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
  </ItemGroup>
</Project>
در اینجا میسر دهی پروژه‌ی تولید کننده‌ی کد، همانند سایر پروژه‌ها است؛ اما نوع آن باید آنالایزر معرفی شود. به همین جهت از خاصیت OutputItemType با مقدار Analyzer استفاده شده‌است. همچنین تنظیم ReferenceOutputAssembly به false به این معنا است که این اسمبلی ویژه، یک وابستگی و dependency واقعی پروژه‌ی جاری نیست و ما قرار نیست به صورت مستقیمی از کدهای آن استفاده کنیم.

برای آزمایش این تولید کننده‌ی کد، کلاس CarModel را به صورت زیر به پروژه‌ی کنسول آزمایشی اضافه می‌کنیم:
using System.ComponentModel;

namespace NotifyPropertyChangedGenerator.Demo
{
  public partial class CarModel : INotifyPropertyChanged
  {
    private double SpeedKmPerHourBackingField;
    private int NumberOfDoorsBackingField;
    private string ModelBackingField = "";

    public void SpeedUp() => SpeedKmPerHour *= 1.1;
  }
}
این کلاس پیاده سازی کننده‌ی INotifyPropertyChanged است؛ اما به همراه هیچ خاصیت عمومی نیست. فقط به همراه یکسری فیلد خصوصی ختم شده‌ی به «BackingField» است که توسط تولید کننده‌ی کد شناسایی شده و اطلاعات آن‌ها تکمیل می‌شود. فقط باید دقت داشت که این کلاس حتما باید به صورت partial تعریف شود تا امکان تکمیل خودکار کدهای آن وجود داشته باشد.

یک نکته:   در این حالت هرچند برنامه بدون مشکل کامپایل و اجرا می‌شود، ممکن است خطوط قرمزی را در IDE خود مشاهده کنید که عنوان می‌کند این قطعه از کد قابل کامپایل نیست. اگر با چنین صحنه‌ای مواجه شدید، یکبار solution را بسته و مجددا باز کنید تا تولید کننده‌ی کد، به خوبی شناسایی شود. البته نگارش‌های جدیدتر Visual Studio و Rider به همراه قابلیت auto reload پروژه برای کار با تولید کننده‌‌های کد هستند و دیگر شاهد چنین صحنه‌هایی نیستیم و حتی اگر برای مثال فیلد جدیدی را به CarModel اضافه کنیم، نه فقط بلافاصله کدهای متناظر آن تولید می‌شوند، بلکه خواص عمومی تولید شده در Intellisense نیز قابل دسترسی هستند.


نحوه‌ی مشاهده‌ی کدهای خودکار تولید شده

اگر علاقمند باشید تا کدهای خودکار تولید شده را مشاهده کنید، در Visual Studio، در قسمت و درخت نمایشی dependencies پروژه، گره‌ای به نام Analyzers وجود دارد که در آن برای مثال نام NotifyPropertyChangedGenerator و ذیل آن، کلاس‌های تولید شده‌ی توسط آن، قابل مشاهده و دسترسی هستند و حتی قابل دیباگ نیز می‌باشند؛ یعنی می‌توان بر روی سطور مختلف آن، break-point قرار داد.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: SourceGeneratorTests.zip

معرفی تعدادی منبع تکمیلی
- برنامه Source generator playground
در اینجا تعدادی مثال را که توسط مایکروسافت توسعه یافته‌است، مشاهده می‌کنید که اتفاقا یکی از آن‌ها پیاده سازی تولید کننده‌ی کد اینترفیس INotifyPropertyChanged است. در این برنامه، خروجی کدهای تولیدی نیز به سادگی قابل مشاهده‌است.

- برنامه SharpLab
برای توسعه‌ی تولید کننده‌های کد، عموما نیاز است تا با Roslyn API آشنا بود. در این برنامه اگر از منوی بالای صفحه قسمت results، گزینه‌ی «syntax tree» را انتخاب کنید و سپس قسمتی از کد خود را انتخاب کنید، بلافاصله معادل Roslyn API آن، در سمت راست صفحه نمایش داده می‌شود.

- معرفی مجموعه‌ای از Source Generators
در اینجا می‌توان مجموعه‌ای از پروژه‌های سورس باز Source Generators را مشاهده و کدهای آن‌ها را مطالعه کنید و یا از آن‌ها در پروژه‌های خود استفاده نمائید.

- معرفی یک cookbook در مورد Source Generators
این cookbook توسط خود مایکروسافت تهیه شده‌است و جهت شروع به کار با این فناوری، بسیار مفید است.

- مجموعه مثال‌های Source generators از مایکروسافت
در اینجا می‌توانید مجموعه مثال‌هایی از Source generators را که توسط مایکروسافت تهیه شده‌است، مشاهده کنید. شرح و توضیحات تعدادی از آن‌ها را هم در اینجا مطالعه کنید.
اشتراک‌ها
تبدیل کدهای Java به #C

When C# started back in 2001, Java and C# were similar languages. But in the last 20 years, C# has quickly evolved in its unique way.  

تبدیل کدهای Java به #C
مطالب
افزونه نویسی برای مرورگرها : قسمت دوم : فایرفاکس
در مقاله پیشین، افزونه نویسی برای فایرفاکس را آغاز و مسائل مربوط به رابط‌های کاربری را بررسی کردیم. در این قسمت که قسمت پایانی افزونه نویسی برای فایرفاکس است، به مباحث پردازشی و دیگر خصوصیت‌ها می‌پردازیم.
اولین موردی که باید برای برنامه‌ی ما در نظر گرفت، ذخیره و بازیابی مقادیر است که باید روی پنجره‌ی popup.html اعمال گردد و همچنین مقداردهی مقادیر پیش فرض برنامه بعد از نصب افزونه اعمال شود. برای ذخیره‌ی مقادیر، طبق نوشته موجود در راهنمای موزیلا، از روش زیر بهره برده و می‌توان مقادیر زیر را به راحتی در آن‌ها ذخیره کرد:
var ss = require("sdk/simple-storage");
ss.storage.myArray = [1, 1, 2, 3, 5, 8, 13];
ss.storage.myBoolean = true;
ss.storage.myNull = null;
ss.storage.myNumber = 3.1337;
ss.storage.myObject = { a: "foo", b: { c: true }, d: null };
ss.storage.myString = "O frabjous day!";
برای خواندن موارد ذخیره شده هم که مشخصا نوشتن اسم property کفایت می‌کند و برای حذف مقادیر نیز به راحتی از عبارت delete در جلوی پراپرتی استفاده کنید:
delete ss.storage.value;
برای ذخیره مقادیر پیش فرض اولین، کاری که می‌کنیم اسم متغیرها را چک می‌کنیم. اگر مخالف null بود، یعنی قبلا ست شده‌اند؛ ولی اگر null شد، عمل ذخیره سازی اولیه را انجام می‌دهیم:
if (!ss.storage.Variables)
 {
 ss.storage.Variables=[];
 ss.storage.Variables.push(true);
 ss.storage.Variables.push(false);
 ss.storage.Variables.push(false);
 ss.storage.Variables.push(false);
 }

if (!ss.storage.interval)
ss.storage.interval=1;

 if (!ss.storage.DateVariables)
 {
 var now=String(new Date());
 ss.storage.DateVariables=[];
 ss.storage.DateVariables.push(now);
 ss.storage.DateVariables.push(now);
 ss.storage.DateVariables.push(now);
 ss.storage.DateVariables.push(now);
 }

برای ذخیره مقادیر popup.html، به طور مستقیم نمی‌توانیم از کدهای بالا در جاوااسکریپت استفاده کنیم. مجبور هستیم که یک پل ارتباطی بین فایل main.js و فایل جاوااسکریپت داشته باشیم. در مقاله پیشین در مورد postmessage که ارتباطی از/به محتوا یا فایل جاوااسکریپت به/از main.js برقرار می‌کرد، صحبت کردیم و در این قسمت راه حل بهتری را مورد استفاده قرار می‌دهیم. برای ایجاد چنین ارتباطی، آن هم به صورت دو طرفه از port استفاده می‌کنیم. دستور پورت در اکثر اشیایی که ایجاد می‌کنید وجود دارد ولی باز هم همیشه قبل از استفاده، از مستندات موزیلا حتما استفاده کنید تا مطمئن شوید دسترسی به شیء پورت در همه‌ی اشیا وجود دارد. ولی به صورت کلی تا آنجایی که من دیدم، در همه‌ی اشیا قرار دارد. از کد port.emit برای ارسال مقادیر به سمت فایل اسکریپت یا حتی بالعکس مورد استفاده قرار می‌گیرد و port.on هم یک شنونده برای آن است. شکل زیر به خوبی این مبحث را نشان می‌دهد.
addon process در شکل بالا همان فایل main.js هست که کد اصلی addon داخل آن است و content process نیز محتوای اسکریپت است و حالا میتواند با استفاده از خاصیت contentscrip که به صورت رشته ای اعمال شده باشد یا اینکه با استفاده از خاصیت contentscriptfile، در یک یا چند فایل js استفاده نماید:
contentScriptFile: self.data.url("jquery.min.js")
contentScriptFile: [self.data.url("jquery.min.js"),self.data.url("const.js"),self.data.url("popup.js")]

از شیء port به صورت عملی استفاده می‌کنیم. کد  main.js را به صورت زیر تغییر دادیم:
function handleChange(state) {
  if (state.checked) {
    panel.show({
      position: button
    });

 var v1=[],v2;
 if (ss.storage.Variables)
v1=ss.storage.Variables;

if (ss.storage.interval)
v2=ss.storage.interval;

panel.port.emit("vars",v1,v2);
  }
}
panel.port.on("vars", function (vars,interval) {
  ss.storage.Variables=vars;
  ss.storage.interval=interval;
});
در شماره پیشین گفتیم که رویداد handlechange وظیفه نمایش پنل را دارد، ولی الان به غیر آن چند سطر، کد دیگری را هم اضافه کردیم تا موقعی که پنل باز می‌شود، تنظیمات قبلی را که ذخیره کرده‌ایم روی صفحه نمایش داده شوند. با استفاده از شیء port.emit محتواهای دریافت شده را به سمت فایل اسکریپت ارسال می‌کنیم تا تنظیمات ذخیره شده را برای نمایش اعمال کند. پارامتر اول رشته vars نام پیام رسان شما خواهد بود و در فایل مقصد هم تنها به پیامی با این نام گوش داده خواهد شد. این خصوصیت زمانی سودمندی خود را نشان می‌دهد که بخواهید در زمینه‌های مختلف، از چندین پیام رسان به سمت یک مقصد استفاده کنید. شیء panel.port.on هم برای گوش دادن به متغیرهایی است که از آن سمت برای ما ارسال می‌شود و از آن برای ذخیره‌ی مواردی استفاده میگردد که کاربر از آن سمت برای ما ارسال می‌کند. پس ما در این مرحله، یک ارتباطه کاملا دو طرفه داریم.
کد  فایل popup.js که به صورت تگ script در popup.html معرفی شده است:
$(document).ready(function () {
addon.port.on("vars",  function(vars,interval) {
 if (vars)
{
 $("#chkarticles").attr("checked", vars[0]);
$("#chkarticlescomments").attr("checked", vars[1]);
$("#chkshares").attr("checked", vars[2]);
$("#chksharescomments").attr("checked", vars[3]);
}

$("#interval").val(interval);
});   

    $("#btnsave").click(function() {

         var Vposts = $("#chkarticles").is(':checked');
         var VpostsComments = $("#chkarticlescomments").is(':checked');
         var  Vshares = $("#chkshares").is(':checked');
         var VsharesComments = $("#chksharescomments").is(':checked');
 var Vinterval = $("#interval").val() ;
 var Variables=[];
 Variables[0]=Vposts;
 Variables[1]=VpostsComments;
 Variables[2]=Vshares;
 Variables[3]=VsharesComments;
 interval=Vinterval;
 
 addon.port.emit("vars", Variables,Vinterval);
$("#messageboard").text( Messages.SettingsSaved);

    });
});
در همان ابتدای امر با استفاده از addon.port.on منتظر یک پیام رسان، به اسم vars می‌شود تا اطلاعات آن را دریافت کند که در اینجا بلافاصله بعد از نمایش پنل، اطلاعات برای آن ارسال شده و در صفحه، جایگذاری می‌کند. در قسمت رویداد کلیک دکمه ذخیره هم با استفاده از addon.port.emit اطلاعاتی را که کاربر به روز کرده است، به یک پیام رسان میدهیم تا برای آن سمت نیز ارسال کند تا در آن سمت، تنظیمات جدید ذخیره و جایگزین شوند.
نکته بسیار مهم: در کد بالا ما فایل جاوااسکریت را از طریق فایل popup.html معرفی کردیم، نه از طریق خصوصیت contentscriptfile. این نکته را همیشه به خاطر داشته باشید. فایل‌های js خود را تنها در دو حالت استفاده کنید:
  1. از طریق دادن رشته به خصوصیت contentScript و استفاده از self به جای addon
  2. معرفی فایل js داخل خود فایل html با تگ script که به درد اسکریپت‌های با کد زیاد می‌خورد.
اگر فایل شما شامل استفاده از کلمه‌ی کلیدی addon نمی‌شود، می‌توانید فایل js خود را از طریق contentScriptFile هم اعمال کنید.
فایل popup.html
<script src="jquery.min.js"></script> <!-- Including jQuery -->
<script type="text/javascript" src="const.js"></script> 
<script type="text/javascript" src="popup.js"></script>


خواندن فید RSS سایت
خواندن فید سایت توسط فایل Rssreader.js انجام می‌شود که تمام اسکریپت‌های مورد نیاز برای اجرای آن، توسط background.htm صدا زده شده است:
<script type="text/javascript" src="const.js"></script>
<script type="text/javascript" src="jquery.min.js"></script>
    <script type="text/javascript" src="https://www.google.com/jsapi"></script>
<script type="text/javascript" src="rssreader.js"></script>
تنها کاری که باید انجام دهیم اجرای این فایل به عنوان یک فرآیند پس زمینه است. در کروم ما عادت داشتیم برای این کار در فایل manifest.json از خصوصیت background استفاده کنیم، ولی از آنجا که خود فایل main.js یک فایل اسکریپتی است که در پس زمینه اجرا می‌شود، طبق منابع موجود در نت چنین چیزی وجود ندارد و این فرآیند را به خود فایل main.js مربوط می‌دانستند. ولی من با استفاده از page worker چنین خصوصیتی را پیاده سازی کردم. page worker وظیفه دارد تا یک آدرس یا فایلی را در یک تب پنهان و در پشت صحنه اجرا کرده و به شما اجازه‌ی استفاده از DOM آن بدهد. نحوه‌ی دسترسی به فایل background.htm توسط page worker به صورت زیر تعریف می‌شود:
pageWorker = require("sdk/page-worker");
  page= pageWorker.Page({
  contentScriptWhen: "ready",
  contentURL: self.data.url("./background.htm")
});
page.port.emit("vars",ss.storage.Variables,ss.storage.DateVariables,ss.storage.interval);
در فایل بالا شیء pageworker ساخته شد و درخواست یک پیج نهان را برای فایل background.htm در دایرکتوری data می‌کند. استفاده از گزینه‌ی contentScriptWhen  برای دسترسی به شیء addon در فایل‌های جاوااسکریپتی که استفاده می‌کنید ضروری است. در صورتی که حذف شود و نوشته نشود با خطای addon is not defined روبرو خواهید شد، چرا که هنوز این شیء شناسایی نشده است. در خط نهایی هم برای آن سمت یک پیام ارسال شده که حاوی مقادیر ذخیره شده می‌باشد.

فایل RSSReader.js
در اینجا هم مانند مطلبی که برای کروم گذاشتیم، خواندن فید، در یک دوره‌ی زمانی اتفاق می‌افتد. در کروم ما از chrome.alarm استفاده می‌کردیم، ولی در فایرفاکس از همان تایمرهای جاوااسکریپتی بهره میبریم. کد زیر را به فایلی به اسم rssreader.js اضافه می‌کنیم:
var variables=[];
var datevariables=[];
var period_time=60000;
var timer;
google.load("feeds", "1");

$(document).ready(function () {
 addon.port.on("vars",  function(vars,datevars,interval) {
 if (vars)
{
Variables=vars;
}
if (datevars)
{
datevariables=datevars;
}
if(interval)
period_time=interval*60000;
alarmManager();
});
});

function alarmManager()
{
timer = setInterval(Run,period_time);
}

function Run() {
if(Variables[0]){RssReader(Links.postUrl,0, Messages.PostsUpdated);}
if(Variables[1]){RssReader(Links.posts_commentsUrl,1,Messages.CommentsUpdated); }
if(Variables[2]){RssReader(Links.sharesUrl,2,Messages.SharesUpdated);}
if(Variables[3]){RssReader(Links.shares_CommentsUrl,3,Messages.SharesCommentsUpdated);}
}

function RssReader(URL,index,Message) {


            var feed = new google.feeds.Feed(URL);
            feed.setResultFormat(google.feeds.Feed.XML_FORMAT);
                    feed.load(function (result) {
if(result!=null)
{
var strRssUpdate = result.xmlDocument.firstChild.firstChild.childNodes[5].textContent;
var RssUpdate=new Date(strRssUpdate);
var lastupdate=new Date(datevariables[index]);
if(RssUpdate>lastupdate)
{
datevariables[index]=strRssUpdate;
addon.port.emit("notification",datevariables,Message);
}

}
                      });
        }
در خطوط بالا متغیرها تعریف و توابع گوگل بارگزاری می‌شوند. سپس توسط addon.port یک شنونده ایجاد شده، تا بتواند مقادیر ذخیره شده را بازیابی کند. این مقادیر شامل موارد زیر است:
  • چه بخش‌هایی از سایت باید بررسی شوند.
  • آخرین تاریخ تغییر هر کدام که در زمان نصب افزونه، تاریخ نصب افزونه می‌شود و با اولین به روز رسانی، تاریخ جدیدی جای آن را می‌گیرد.
  • دوره‌ی سیکل زمانی یا همان interval بر اساس دقیقه
پس از اینکه شنونده مقادیر را دریافت کرد، تابع alarmManager اجرا شده و یک تایمر ایجاد می‌کند. بر خلاف کروم که برای این کار api تدارک دیده بود، اینجا شما باید از تایمرهای خود جاوااسکریپت مانند SetTimeout یا SetInterval استفاده کنید. موقع دریافت interval یا period_time ما آن را در 60000 ضرب کردیم تا دقیقه تبدیل به میلی ثانیه شود؛ چرا که تایمر، زمان را بر حسب میلی ثانیه دریافت می‌کند. وظیفه تایمر این هست که در هر دوره‌ی زمانی تابع Run را اجرا کند.

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

RSSReader
این تابع را قبلا در این مقاله  توضیح دادیم. تنها تغییری که کرده است، بدنه‌ی شرط بررسی تاریخ است که در صورت موفقیت، تاریخ جدید، جایگزین تاریخ قبلی شده و یک پیام به فایل main.js ارسال می‌کند تا از آن درخواست ذخیره‌ی تاریخی جدید و هچنین ایجاد یک notification برای آگاه سازی کاربر کند. پس باز به فایل main.js رفته و شنونده آن را تعریف می‌کنیم:
page.port.on("notification",function(lastupdate,Message)
{
ss.storage.DateVariables=lastupdate;
Make_a_Notification(Message);
})
function Make_a_Notification(Message)
{
var notifications = require("sdk/notifications");
notifications.notify({
  title: "سایت به روز شد",
  text: Message,
  iconURL:self.data.url("./icon-64.png"),
  data:"https://www.dntips.ir",
  onClick: function (data) {
tabs.open(data);
  }
});
}
شنونده مورد نظر دو پارامتر تاریخ آخرین به روزرسانی را دریافت کرده و آن را جایگزین قبلی می‌کند و پیام را به تایع Make_a_Notification پاس میکند. پارامترهای ساخت نوتیفیکیشن به ترتیب شامل عنوان، متن پیام، آیکن و نهایتا data است. دیتا شامل آدرس سایت است. زمانیکه کاربر روی نوتیفیکیشن کلیک می‌کند، استفاده شده و یک تب جدید را با آدرس سایت باز می‌کنیم. به این ترتیب افزونه‌ی ما تکمیل می‌شود. برای اجرا و تست افزونه بر روی مرورگر فایرفاکس از دستور cfx run استفاده کنید.


البته این نکته قابل ذکر است که اگر کاربر طلاعات پنل را به روزرسانی کند، تا وقتی که مرورگر بسته نشده و دوباره باز نشود تغییری نمی‌کند؛ چرا که ما تنها در ابتدای امر مقادیر ذخیره شده را به RSSReader فرستاده و اگر کاربر آن‌ها را به روز کند، ارسال پیام دیگری توسط page worker صورت نمی‌گیرد. پس کد موجود در main.js را به صورت زیر ویرایش می‌کنیم:
  pageWorker = require("sdk/page-worker");
  page= pageWorker.Page({
  contentScriptWhen: "ready",
  contentURL: self.data.url("./background.htm")
});

function SendData()
{
page.port.emit("vars",ss.storage.Variables,ss.storage.DateVariables,ss.storage.interval);
}
SendData();
panel.port.on("vars", function (vars,interval) {
  ss.storage.Variables=vars;
  ss.storage.interval=interval;
  SendData();
});
در کد بالا ما خطی که به سمت rssreader.js پیام ارسال می‌کند را داخل یک تابع به اسم SendDate قرار دادیم و بعد از تشکیل page worker آن را صدا زدیم و کد آن دقیقا مانند قبل است؛ با این تفاوت که اینبار این تابع را در جای دیگری هم صدا میزنیم و آن زمانی است که برای پنل، پیام مقادیر جدید ارسال می‌شود که در آن پس از ذخیره موارد جدید تابع SendData را صدا می‌زنیم. پس موقع به روزرسانی هم مقادیر ارسال خواهند شد. مقادیر جدید به سمت rssreader.js رفته و تشکیل یک تایمر جدید را می‌دهند و البته چون قبلا تایمر ایجاد شده است، پس باید چند خطی را هم به فایل rssreader.js اضافه کنیم تا تایمر قبلی را نابود کرده و تایمر جدیدی را ایجاد کند:
var timer;

function alarmManager()
{
timer = setInterval(Run,period_time);
}

 addon.port.on("vars",  function(vars,datevars,interval) {
 if (vars)
{
Variables=vars;

}
if (datevars)
{
datevariables=datevars;
}
if(interval)
period_time=interval*60000;

if(timer!=null)
{
clearInterval(timer);
}

alarmManager();
});
در خط بالا متغیری به اسم timer ایجاد شده است که کد timer را در خود ذخیره می‌کند. پس موقع دریافت مقادیر بررسی میکنیم که اگر مقدار timer مخالف نال بود تایمر قبلی را با clearInterval از بین برده و تایمر جدیدی ایجاد کند. پس مشکل تایمری که از قبل موجود است نیز حل می‌گردد.
افزونه‌ی ما تکمیل شد. اجازه بدهید قبل از بستن بحث چندتا از موارد مهم موجود در sdk را نام ببریم:

Page Mod
page mod موقعی که کاربر آدرسی را مطابق با الگویی (pattern) که ما دادیم، باز کند یک اسکریپت را اجرا خواهد کرد:
var pageMod = require("sdk/page-mod");

pageMod.PageMod({
  include: "*.mozilla.org",
  contentScript: 'window.alert("Page matches ruleset");'
});
var data = require("sdk/self").data;
var pageMod = require("sdk/page-mod");

pageMod.PageMod({
  include: "*.mozilla.org",
  contentScriptFile: [data.url("jquery-1.7.min.js"),
                      data.url("my-script.js")]
});

پنل تنظیمات
موقعی که شما افزونه‌ای را در فایرفاکس اضافه می‌کنید، در پنلی که مدیریت افزونه‌ها قرار دارد می‌توانید در تنظیمات هر افزونه، تغییری ایجاد کنید. برای ساخت چنین صفحه‌ای از خصوصیت preferences در فایل package.json کمک می‌گیریم که مقادیر به صورت آرایه ای داخل آن قرار می‌گیرند. مثال زیر پنج کنترل را به بخش تنظیمات افزونه اضافه می‌کند که چهار کنترل اول چک باکس Checkbox هستند؛ چرا که خصوصیت type آنها به bool ست شده است و شامل یک نام و عنوان یا برچسب label و یک توضیح کوتاه است و مقدار پیش فرض آن با خصوصیت value مشخص شده است. آخرین کنترل هم یک کادر عددی است؛ چرا که خاصیت type آن با integer مقداردهی شده و مقدار پیش فرض آن 10 می‌باشد.
  "preferences": [{
    "description": "مطالب سایت",
    "type": "bool",
    "name": "post",
    "value": true,
    "title": "مطالب سایت"
},
{
    "description": "نظرات مطالب سایت",
    "type": "bool",
    "name": "postcomments",
    "value": false,
    "title": "نظرات مطالب سایت"
},
{
    "description": "اشتراک ها",
    "type": "bool",
    "name": "shares",
    "value": false,
    "title": "اشتراک ها"
},
{
    "description": "نظرات اشتراک ها",
    "type": "bool",
    "name": "sharescomments",
    "value": false,
    "title": "نظرات اشتراک ها"
},
    {
        "description": "دوره زمان برای بررسی سایت",
        "name": "interval",
        "type": "integer",
        "value": 10,
        "title": "دوره زمانی"
    }]

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

function Perf_Default_Value()
{
var preferences = require("sdk/simple-prefs").prefs;

preferences.post = ss.storage.Variables[0];
preferences.postcomments = ss.storage.Variables[1];
preferences.shares = ss.storage.Variables[2];
preferences.sharescomments = ss.storage.Variables[3];
preferences["myinterval"] =parseInt(ss.storage.interval);

}
Perf_Default_Value();

panel.port.on("vars", function (vars,interval) {
  ss.storage.Variables=vars;
  ss.storage.interval=interval;
  SendData();
  Perf_Default_Value();
});
البته کاربر فقط برای دیدن اطلاعات بالا به این صفحه‌ی تنظیمات نمی‌آید؛ بلکه بیشتر برای تغییر آن‌ها می‌آید. پس باید به تغییر مقدار کنترل‌ها گوش فرا دهیم. برای گوش دادن به تغییر تنظیمات، برای موقعی که کاربر قسمتی از تنظیمات را ذخیره کرد، از کدهای زیر بهره می‌بریم: 
perf=require("sdk/simple-prefs");
var preferences = perf.prefs;
function onPrefChange(prefName) {

  switch(prefName)
  {
    case "post":
ss.storage.Variables[0]=preferences[prefName];
break;
case "postcomments":
ss.storage.Variables[1]=preferences[prefName];
break;
case "shares":
ss.storage.Variables[2]=preferences[prefName];
break;
case "sharescomments":
ss.storage.Variables[3]=preferences[prefName];
break;
case "myinterval":
ss.storage.interval=preferences[prefName];
break;
  }
}
//perf.on("post", onPrefChange);
//perf.on("postcomments", onPrefChange);

perf.on("", onPrefChange);
متد on دو پارامتر دارد: اولی، نام کنترل مورد نظر که با خصوصیت name تعریف کردیم و دومی هم تابع callback آن می‌باشد و در صورتی که پارامتر اول با "" مقداردهی شود، هر تغییری که در هر کنترلی رخ بدهد، تابع callback صدا زده می‌شود. از آنجا که نام کنترل‌ها به صورت string برگشت داده می‌شوند، برای دسترسی به مقادیر موجود در تنظیمات از همان روش داخل [""] بهره می‌گیریم. مقادیر را گرفته و داخل storage ذخیره می‌کنیم.

  اشکال زدایی Debug

یکی از روش‌های اشکال زدایی، استفاده از console.log هست که میتونید برای بازبینی مقادیر و وضعیت‌ها، از آن استفاده کنید که نتیجه‌ی آن داخل کنسول نمایش داده می‌شود و اگر هم دربرنامه خطایی رخ دهد، داخل کنسول به شما نمایش خواهد داد.
مطالب
آماده سازی Jasmine برای پروژه های Asp.Net MVC
با گسترش روز افزون برنامه‌های تحت وب، نیاز به یک سری ابزار برای تست و اطمینان از نحوه عملکرد صحیح کدهای نوشته شده احساس می‌شود. Jasmine یکی از این ابزار‌های قدرتمند برای تست کد‌های JavaScript است.
چندی پیش در سایت جاری چند مقاله خوب توسط یکی از دوستان درباره Qunit منتشر شد. Qunit یک ابزار قدرتمند و مناسب برای تست کد‌های جاوااسکریپت است و در اثبات صحت این گفته همین کافیست که بدانیم برای تست کد‌های نوشته شده در پروژه‌های متن بازی هم چون Backbone.Js و JQuery از این فریم ورک استفاده شده است. اما به احتمال قوی در ذهن شما این سوال مطرح شده است که خب! در صورت آشنایی با Qunit چه نیاز به یادگیری Jasmine یا خدای نکرده Mocha و FuncUnit است؟ هدف صرفا معرفی یک ابزار غیر برای تست کد است نه مقایسه و نتیجه گیری برای تعیین میزان برتری این ابزارها. اصولا مهم‌ترین دلیل برای انتخاب، علاوه بر امکانات و انعطاف پذیری، فاکتور راحتی و آسان بودن در هنگام استفاده است که به صورت مستقیم به شما و تیم توسعه نرم افزار بستگی دارد.

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

»Jasmine یک فریم ورک تست کدهای جاوا اسکریپ بر مبنای Behavior-Driven Development است در حالی که Qunit بر مبنای Test-Driven Development است و همین مسئله مهم‌ترین تفاوت بین این دو فریم ورک می‌باشد.
»اگر قصد دارید که از Qunit نیز به روش BDD استفاده نمایید باید از ترکیب Pavlov به همراه Qunit استفاده کنید.
»Jasmine از مباحث مربوط به Spies و Mocking به خوبی پشتیبانی می‌کند ولی این امکان به صورت توکار در Qunit فراهم نیست. برای اینکه بتوانیم این مفاهیم را در Qunit پیاده سازی کنیم باید از فریم ورک‌های دیگر نظیر SinonJS به همراه Qunit استفاده کنیم.
»هر دو فریم ورک بالا به سادگی و راحتی کار معروف هستند 
»تمام موارد مربوط به الگوهای Matching در هر دو فریم ورک به خوبی تعبیه شده است
» هر دو فریم ورک بالا از مباحث مربوط به Asynchronous Testing  برای تست کد‌های Ajax ای به خوبی پشتیبانی می‌کنند.

بررسی چند مفهوم
قبل از شروع، بهتر است که با چند مفهوم کلی و در عین حال مهم این فریم ورک آشنا شویم
describe('JavaScript addition operator', function () {
 it('adds two numbers together', function () {
  expect(1 + 2).toEqual(3);
 });
});


در کد بالا یک نمونه از تست نوشته شده با استفاده از Jasmine را مشاهده می‌کنید. دستور describe  برای تعریف یک تابع تست مورد استفاده قرار می‌گیرد که دارای دو پارامتر ورودی است. ابتدا یک نام را به این تست اختصاص دهید(بهتر است که این عنوان به صورت یک جمله قابل فهم باشد). سپس یک تابع به عنوان بدنه تست نوشته می‌شود. به این تابع Spec گفته می‌شود.
در تابع it کد بالا شما می‌توانید کد‌های مربوط بدنه توابع تست خود را بنویسید. برای پیاده سازی Assert در توابع تست مفهوم expectation‌ها وجود دارد. در واقع expect برای بررسی مقادیر حقیقی با مقادیر مورد انتظار مورد استفاده قرار می‌گیرد و شامل مقادیر true یا false خواهد بود.
برای Setup و Teardown توابع تست خود باید از توابع beforeEach و afterEach که بدین منظور تعبیه شده اند استفاده کنید.
describe("A spec (with setup and tear-down)", function() {
  var foo;

  beforeEach(function() {
    foo = 0;
    foo += 1;
  });

  afterEach(function() {
    foo = 0;
  });

  it("is just a function, so it can contain any code", function() {
    expect(foo).toEqual(1);
  });

  it("can have more than one expectation", function() {
    expect(foo).toEqual(1);
    expect(true).toEqual(true);
  });
});
کاملا واضح است که در تابع beforeEach مجموعه دستورالعمل‌های مربوط به setup تست وجود دارد. سپس دو تابع it برای پیاده سازی عملیات Assertion نوشته شده است. در پایان هم دستورات تابع afterEach ایجاد می‌شوند.

اگر در کد تست خود قصد دارید که یک تابع describe یا it را غیر فعال کنید کافیست یک x به ابتدای آن‌ها اضافه کنید و دیگر نیاز به هیچ کار اضافه دیگری برای comment کردن کد نیست.
xdescribe("A spec", function() {
  var foo;

  beforeEach(function() {
    foo = 0;
    foo += 1;
  });

  xit("is just a function, so it can contain any code", function() {
    expect(foo).toEqual(1);
  });
});
توابع describe و it بالا در هنگام تست نادیده گرفته می‌شوند و خروجی آن‌ها مشاهده نخواهد شد.

درادامه قصد پیاده سازی یک مثال  را با استفاده از Jasmine و RequireJs در پروژه Asp.Net MVC دارم.
برای شروع آخرین نسخه Jasmine را از اینجا دریافت نمایید. یک پروژه Asp.Net MVC به همراه پروژه تست به صورت Empty ایجاد کنید(در هنگام ایجاد پروژه، گزینه create unit test را انتخاب نمایید). فایل دانلود شده را unzip نمایید و دو پوشه lib و spec ،به همراه فایل specRunner.html را در پروژه تست خود کپی نمایید. 
  • فولدر lib شامل فایل‌ها کد‌های Jasmine برای setup و tear down و spice و تست کد‌های شما می‌باشد.
  • فایل specRunner.html به واقع یک فایل برای نمایش فایل‌های تست و همچنین نمایش نتیجه تست است.
  • فولدر spec نیز شامل کد‌های Jasmine برای کمک به نوشتن تست می‌باشد.

در این مثال قصد داریم فایل‌های player.js و song.js که به عنوان نمونه به همراه این فریم ورک قرار دارد را در قالب یک پروژه MVC به همراه RequireJs، تست نماییم. در نتیجه این فایل‌ها را از فولدر src انتخاب نمایید و آن‌ها را در قسمت Scripts پروژه اصلی خود کپی کنید(ابتدا بک پوشه به نام App بسازید و فایل‌ها را در آن قرار دهید)

برای استفاده از requireJs باید دستور define را در ابتدا این فایل‌ها اضافه نماییم. در نتیجه فایل‌های Player.js و Song.js را باز کنید و تغییرات زیر را در ابتدای این فایل‌ها اعمال نمایید.

Song.js

define(function () {
    function Song() {
    }

    Song.prototype.persistFavoriteStatus = function (value) {
        // something complicated
        throw new Error("not yet implemented");
    };
});
Player.js
define(function () {
    function Player() {
    }
    Player.prototype.play = function (song) {
        this.currentlyPlayingSong = song;
        this.isPlaying = true;
    };

    Player.prototype.pause = function () {
        this.isPlaying = false;
    };

    Player.prototype.resume = function () {
        if (this.isPlaying) {
            throw new Error("song is already playing");
        }

        this.isPlaying = true;
    };

    Player.prototype.makeFavorite = function () {
        this.currentlyPlayingSong.persistFavoriteStatus(true);
    };
});
حال فایل SpecRunner.html را بازکنید و کد‌های مربوط به تگ script که به مسیر اصلی فایل‌های تست  اشاره می‌کند را Comment نمایید و به جای آن تگ Script مربوط به RequireJs را اضافه نمایید. برای پیکر بندی RequireJs باید از baseUrl و paths استفاده کرد.

baseUrl در پیکر بندی requireJs به مسیر فایل‌های پروژه که در پروژه اصلی MVC قرار دارد اشاره می‌کند. paths برای تعیین مسیر فایل‌های تست که در پوشه spec در پروژه تست قرار دارد اشاره می‌کند. اگر دقت کرده باشید به دلیل اینگه تگ‌های script مربوط به لود فایل‌های SpecHelper.js و PlayerSpec.js به صورت comment در آمده اند در نتیجه این فایل‌ها لود نخواهند شد و خروجی مورد نظر مشاهده نمی‌شود. در این جا باید از مکانیزم AMD موجود در RequireJs استفاده نماییم و فایل‌های مربوطه را لود کنیم. برای این کار نیاز به اضافه کردن دستور require در ابتدای تگ script به صورت زیر در این فایل است. در نتیجه فایل‌های PlayerSpec و SpecHelper نیز توسط RequireJs لود خواهند شد.



نیاز به یک تغییر کوچک دیگر نیز وجود دارد. فایل PlayerSpec را باز نمایید و وابستگی فایل‌های آن را تعیین نمایید. از آن جا که این فایل برای تست فایل‌های Player , Song ایجاد شده است در نتیجه باید از define برای تعیین این وابستگی‌ها استفاده نماییم.

یادآوری:

»دستور describe در فایل بالا برای تعریف تابع تست است. همان طور که می‌بینید بک نام به آن داده می‌شود به همراه بدنه تابع تست. 

»دستور beforeEach برای آماده سازی مواردی است که قصد داریم در تست مورد استفاده قرار گیرند. همانند متد‌های Setup در UnitTest.

» دستور expect نیز معادل Assert در UnitTest است و برای بررسی صحت عملکرد تست نوشته می‌شود.

اگر فایل SpecRunner.html را دوباره در مرورگر خود باز نمایید تصویر زیر را مشاهده خواهید کرد که به عنوان موفقیت آمیز بودن پیکر بندی پروژه و تست‌های آن می‌باشد.