نظرات مطالب
مسیریابی در AngularJs #بخش دوم
مشکلی رخ نداد.
1 - ماژول مسیریابی (ngRoute) رو باید تزریق کنید به ماژول اصلی :
var myFirstRoute = angular.module('myFirstRoute', ['ngRoute']);
2- route$ رو به کنترلر هم تزریق کنید :
myFirstRoute.controller('ShowPage1Controller', function ($route) {
    this.message = 'Content of page-one.html';
    //access the foodata property using $route.current
    var foo = $route.current.foodata;
    alert(foo);
});
3- این هم یک نمونه از تنظیمات مسیریابی :
myFirstRoute.config(['$routeProvider',
  function($routeProvider) {
    $routeProvider.
      when('/pageOne', {
        templateUrl: 'templates/page_one.html',
        controller: 'ShowPage1Controller',
        controllerAs: 'tCtrlOne',
        foodata: 'valueOne'
      }).
      when('/pageTwo', {
        templateUrl: 'templates/page_two.html',
        controller: 'ShowPage2Controller',
        controllerAs: 'tCtrlTwo',
        foodata: 'valueTwo'
      }).
      otherwise({
        redirectTo: '/pageOne'
      });
}]);
البته اینها فقط برای تصحیح اشتباه است. وگر نه به ControllerAs ارتباطی ندارد. به احتمال زیاد مشکل شما عدم تزریق route$ به کنترلر بوده.
نظرات مطالب
ساخت یک Web API که از عملیات CRUD پشتیبانی می کند
باید نوع داده ارسالی رو مشخص کنید، بعنوان مثال:

function postProduct() {
    var product = { Name: "SampleProduct", Category: "TestCategory", Price: 10.99 };

    $.ajax({
        type: 'POST',
        data: JSON.stringify(product),
        url: "/api/products",
        contentType: "application/json"
    }).done(function (data) {
        var message = data.Name + ' $:' + data.Price;
        alert(message);
    });
}


مطالعه بیشتر
مطالب
ایجاد چارت سازمانی تحت وب #1
برای ایجاد چارت سازمانی همواره از راههای دیگر استفاده میکردیم مثلا از تصویر و فایل PDF ، فلش و یا ...
این مورد همیشه باعث اذیت طراحان وب و برنامه نویسان تحت وب بود. با ظهور برخی امکانات در HTML5 که میتوانید توضیحات آن را در مطلب Canvas Reference - قسمت اول سایت جاری مطالعه نمائید ، هم اکنون با استفاده از این امکانات و کتابخانه مربوط به ساخت نمودار سازمانی میتوانید مانند شکل ذیل نمودار در وب ایجاد نمائید.

نمودار سازمانی

برای اینکار ابتدا یک صفحه index.html ایجاد نموده و در قسمت body آن یک المنت از نوع canvas ایجاد نمائید:

<canvas id="canvas1" width="800" height="800" ></canvas>
سپس فایلهای مورد نیاز را در قسمت head آن ارجاع نمائید:
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" >
<!--[if ie]><script type="text/javascript" src="excanvas.js"></script><![endif]-->
<script type="text/javascript" src="orgchart.js"></script>
حال در انتهای متن فایل html این کد را قرار دهید :
var o = new orgChart();

o.addNode(1, '', '', 'Root node');
o.addNode(2, 1, 'u', 'u-node 1');
o.addNode(3, 1, 'u', 'u-node 2');
o.addNode(4, 1, 'u', 'u-node 3');
o.addNode(5, 1, 'l', 'l-node 1');
o.addNode(6, 1, 'l', 'l-node 2');
o.addNode(7, 1, 'r', 'r-node 1');
o.addNode(8, 1, 'r', 'r-node 2');
o.addNode(9, 1, 'r', 'r-node 3');

o.addNode('', '', '', 'Root 2');
o.addNode('', 'Root 2', 'r', 'using');
o.addNode('', 'Root 2', 'r', 'text as\nid');

o.drawChart('canvas1');
انواع نود در نمودار :
u -> نودهایی که در یک سطح افقی (کنار هم ) و در سطح زیرین یک پدر قرار میگیرند.
l -> نودهایی که در یک سطح عمودی ( زیر هم ) و در سمت چپ نود پدر قرار میگیرند.
r -> نوهایی که در یک سطح عمودی ( زیر هم ) و در سمت راست نود پدر قرار میگیرند.

پارامترهای نودها :
  1. شناسه نود است و میتواند عدد یا رشته باشد . در صورتی که خالی بماند متن نود بعنوان شناسه استفاده میشود.
  2. شناسه نود پدر است و در صورتی که خالی ( یا ناشناس ) باشد بعنوان نود اصلی ( پدر ) نمایش داده میشود.
  3. نوع اتصال نودهای r , l , u . برای نود پدر نادیده گرفته میشود.
  4. متن نود . استفاده از "n\"  جلمه را شکسته و به خط جدید منتقل میکند.
  5. نود با نشانه مشخص ( اگر 1 باشد ، حاشیه با ضخامت کشیده میشود. ( اختیاری )
  6. آدرس یک نود - اختصاص آدرس url  جهت اضافه کردن آن به یک نود ( اختیاری )
  7. رنگ خط حاشیه ( اختیاری )
  8. رنگ پس زمینه ( اختیاری )
  9. رنگ متن و فونت ( اختیاری )

پارامترهای تابع ()drowChart :

  1. شناسه المنت canvas
  2. تراز کردن نمودار با استفاده از حرف 'c' یا کلمه 'center' . در صورت حذف ، نمودار در سمت چپ صفحه نمایش داده خواهد شد. تراز چپ ( اختیاری )
  3. تناسب اندازه canvas .  اگر true باشد canvas به اندازه نمودار چارت ، تغییر اندازه میدهد . (اختیاری)
مطمئن باشید که فایل index.html , excanvas.js  ,orgchart.js  در یک فولدر قرار گرفته باشند .
در مطلب بعدی نمایش با رنگهای مختلف ارائه خواهد شد.

مورد نیازهای مطلب جاری :
  1. excanvas.js
  2. orgchart.js
  3. index.html
نظرات مطالب
نمایش پیام هشدار در Blazor با استفاده از کامپوننت Alert بوت استرپ ۵
روش دوم پیاده سازی این مثال: ارسال یک کامپوننت محصور کننده‌ی سراسری، به صورت پارامتر آبشاری، به تمام زیر کامپوننت‌ها

با استفاده از پارامترهای آبشاری می‌توان شیءای را در اختیار تمام کامپوننت‌های قرار گرفته شده‌ی در سلسله مراتب آن‌ها قرار داد. برای مثال اگر در فایل Client\Shared\MainLayout.razor، جائیکه سایر کامپوننت‌ها قرار است رندر شوند را توسط یک کامپوننت سطح بالا محصور کنیم:
<Alert>
  @Body
</Alert>
در این حالت هر کامپوننتی که بجای Body درج شود، می‌تواند به پارامترهای آبشاری Alert دسترسی پیدا کند؛ یعنی تمام کامپوننت‌های نمایشی برنامه. یا حتی می‌توان این Alert را در فایل Client\App.razor نیز در بالاترین سطح قرار داد و کل Router را توسط آن محصور کرد.
بنابراین طراحی ساده‌ی کامپوننت Alert ای (Client\Shared\Alert.razor) که تامین کننده‌ی یک پارامتر آبشاری سراسری است، به صورت زیر می‌تواند باشد:
<CascadingValue Value=this>
    @if(IsVisible)
    {
        <div class="alert @Css" role="alert">
            @Message
            <button type="button" class="close" data-dismiss="alert" aria-label="Close" @onclick="HideAlert">
                <span aria-hidden="true">&times;</span>
            </button>
        </div>
    }
    @ChildContent
</CascadingValue>

@code {
    [Parameter]
    public RenderFragment ChildContent { get; set; }

    private bool IsVisible;
    private string Message;
    private string Css = "alert-primary";

    public void ShowAlert(string message, AlertType alertType)
    {
        IsVisible = true;
        Message = message;

        Css = alertType switch
        {
            AlertType.Success => "alert-success",
            AlertType.Info => "alert-primary",
            AlertType.Danger => "alert-danger",
            AlertType.Warning => "alert-warning",
            _ => "alert-primary"
        };

        StateHasChanged();
    }

    public void HideAlert()
    {
        IsVisible = false;
    }
}
که البته در همان پوشه به همراه فایل Client\Shared\AlertType.cs نیز هست:
namespace BlazorWasmAlert.Client.Shared
{
    public enum AlertType
    {
        Success,
        Info,
        Danger,
        Warning
    }
}
در کامپوننت ویژه‌ی Alert، دو قابلیت استاندارد Blazor بکار گرفته شده‌اند:
الف) وجود یک CascadingValue که اینبار Value آن به خود کامپوننت اشاره می‌کند (Value=this). یعنی پارامتر آبشاری که در اختیار سایر کامپوننت‌های محصور شده‌ی توسط آن ارسال می‌شود، دقیقا وهله‌ای از کامپوننت Alert است که توسط آن می‌توان برای مثال، متد عمومی ShowAlert آن‌را فراخوانی کرد:
<CascadingValue Value=this>
ب) چون کامپوننت Alert قرار است کامپوننت‌هایی را که بجای body@ درج می‌شوند را نمایش دهد، اینکار را توسط یک RenderFragment انجام داده‌است.


پس از درج این کامپوننت در فایل layout، روش استفاده‌ی از آن برای مثال در کامپوننت Index به صورت زیر است:
@page "/"

<h1>Hello, world!</h1>

<button class="btn btn-primary" @onclick="ShowAlert">Show Alert!</button>

@code
{
    [CascadingParameter]
    public Alert Alert { get; set; }

    private void ShowAlert()
    {
        Alert.ShowAlert("This is a test!", AlertType.Info);
    }
}
در اینجا پارامتر آبشاری که دریافت می‌شود، دقیقا به کلاس و شیء Alert (وهله‌ای از کامپوننت Alert) اشاره می‌کند. به همین جهت می‌توان متد عمومی ShowAlert آن‌را در اینجا فراخوانی کرد.


پ.ن.
در طراحی Blazor، از طراحی React الهام گرفته شده‌است و CascadingValue آن دقیقا معادل Context API جدید React است.
مطالب
نوشتن آزمون‌های واحد به کمک کتابخانه‌ی Moq - قسمت پنجم - نکات و مباحث تکمیلی
پس از بررسی مباحث و نکات پایه‌ای کار با کتابخانه‌ی Moq، در این قسمت تعدادی از نکات تکمیلی آن‌را بررسی خواهیم کرد.


حالت‌های عملکرد کتابخانه‌ی Moq

کتابخانه‌ی Moq، دو حالت عملکرد را دارد: Strict Mode و Loose mode. زمانیکه یک Mock object را نمونه سازی می‌کنیم، به صورت پیش‌فرض کتابخانه‌ی Moq، یک Loose mock را ایجاد می‌کند. در این حالت این شیء، مقادیر پیش‌فرض خواص و اشیاء را بازگشت می‌دهد و استثنائی را صادر نمی‌کند. اگر این موارد مدنظر نیستند، می‌توان به حالت Strict آن رجوع کرد که روش تنظیم آن به صورت زیر است:
var mockIdentityVerifier = new Mock<IIdentityVerifier>(MockBehavior.Strict);
در این حالت اگر متد آزمون واحد را اجرا کنیم، با پیام زیر، با شکست مواجه خواهد شد:
Test method Loans.Tests.LoanApplicationProcessorShould.Accept threw exception:
Moq.MockException: IIdentityVerifier.Initialize() invocation failed with mock behavior Strict.
All invocations on the mock must have a corresponding setup.
در حالت Strict، تمام فراخوانی‌های شیء Mock شده باید دارای Setup باشند (نیازی به Setup تمام موارد نیست؛ فقط مواردی که در فراخوانی‌های آزمون واحد، مورد استفاده قرار می‌گیرند، حتما باید تنظیم شوند). برای نمونه در اینجا عنوان کرده‌است که در این آزمایش، تنظیمات متد Initialize انجام نشده‌است که با تعریف سطر زیر، این مشکل برطرف می‌شود:
mockIdentityVerifier.Setup(x => x.Initialize());

بنابراین هرچند کارکردن با حالت پیش‌فرض کتابخانه‌ی Moq ساده‌است، اما تنظیم حالت Strict سبب می‌شود تا تنظیمی را فراموش نکنیم و در نتیجه کیفیت آزمون واحد تهیه شده افزایش می‌یابد.


صدور استثناءها از طریق Mock objects

اگر در سیستم در حال آزمایش، قسمتی به بررسی خطاها اختصاص دارد، می‌توان توسط Mock objects استثناءهایی را تولید و به این ترتیب منطق بررسی خطاها را آزمایش کرد.
برای نمونه در متد Process کلاس LoanApplicationProcessor، یک try/catch را به قسمت CalculateScore اضافه می‌کنیم:
try
{
    _creditScorer.CalculateScore(application.Applicant.Name, application.Applicant.Address);
}
catch
{
    return application.IsAccepted;
}
زمانیکه کار فراخوانی متد CalculateScore صورت می‌گیرد، برای تنظیم آزمون واحد آن می‌توان از متد Throws، برای صدور یک استثناء استفاده کرد:
mockCreditScorer.Setup(x =>
                    x.CalculateScore(It.IsAny<string>(), It.IsAny<string>()))
                .Throws(new InvalidOperationException("Test Exception"));
صدور این استثناء سبب خواهد شد تا درخواست شخص، رد شود. بنابراین در آزمایش آن می‌توان این مساله را بررسی کرد و از رسیدن به این قسمت (رد شدن درخواست) اطمینان حاصل نمود:
Assert.IsFalse(application.IsAccepted);


صدور رخدادها از طریق Mock objects

فرض کنید یک EventArgs سفارشی را به صورت زیر تعریف:
using System;

namespace Loans.Models
{
    public class CreditScoreResultArgs : EventArgs
    {
        public int Score { get; set; }
    }
}
و سپس رخدادی را به نحو زیر به ICreditScorer اضافه کرده‌ایم:
public interface ICreditScorer
{
   event EventHandler<CreditScoreResultArgs> ResultAvailable;
برای اینکه یک Mock object سبب بروز رخداد ResultAvailable شود (به صورت دستی و دقیقا در سطری که مشخص می‌کنیم)، می‌توان به صورت زیر عمل کرد:
mockCreditScorer.Raise(x => x.ResultAvailable += null, new CreditScoreResultArgs());
ابتدا توسط متد Raise، رخ‌داد مدنظر را ذکر می‌کنیم و سپس یک نمونه‌ی EventArgs را به آن ارسال خواهیم کرد.
روش دیگر انجام اینکار به صورت زیر است:
mockCreditScorer.Setup(x =>
                x.CalculateScore(It.IsAny<string>(), It.IsAny<string>()))
                .Raises(x => x.ResultAvailable += null, new CreditScoreResultArgs());
در این حالت با فراخوانی متد CalculateScore، رخداد ResultAvailable به صورت خودکار صادر می‌شود.


معرفی Partial Mocks

در اغلب آزمون‌های واحدی که تا اینجا بررسی شدند، ابتدا یک Mock object را ایجاد و سپس وهله‌ای از سرویس مدنظر را توسط آن تهیه می‌کنیم. در ادامه تعدادی از متدهای این سرویس را مانند متد Process کلاس LoanApplicationProcessor، فراخوانی می‌کنیم. اینکار سبب اجرای فعالیتی در این سیستم شده و به همراه آن تعاملی با اشیاء Mock شده نیز صورت می‌گیرد. در نهایت حالت و یا نتیجه‌ای را دریافت می‌کنیم و آن‌را با حالت یا نتیجه‌ای که انتظار داریم، مقایسه خواهیم کرد. در این روش پس از پایان اجرای سیستم در حال اجرا، حالت و نتیجه‌ی نهایی حاصل از عملکرد آن، مورد بررسی قرار می‌گیرد. این بررسی‌ها را نیز بر روی اینترفیس‌ها انجام دادیم. اگر بجای اینترفیس‌ها از یک class استفاده شود، به آن partial mock گفته می‌شود. عموما مواردی را که آزمایش آن‌ها سخت است، با Partial mocks پیاده سازی می‌کنند؛ مانند کار با فایل سیستم، کار با قطعه کدهای نامعین مانند DateTime.Now، اعداد اتفاقی و یا Guidها.

در مثال زیر، شبیه به متد آزمون واحد Accept که تاکنون آن‌را بررسی کردیم، از اشیاء Mock شده استفاده شده‌است؛ با یک تفاوت: بجای اینترفیس IIdentityVerifier، از کلاس پیاده سازی کننده‌ی آن که در اینجا IdentityVerifierServiceGateway است، استفاده شده:
namespace Loans.Tests
{
    [TestClass]
    public class LoanApplicationProcessorShould
    {        
        [TestMethod]
        public void AcceptUsingPartialMock()
        {
            var product = new LoanProduct {Id = 99, ProductName = "Loan", InterestRate = 5.25m};
            var amount = new LoanAmount {CurrencyCode = "Rial", Principal = 2_000_000_0};
            var applicant =
                new Applicant {Id = 1, Name = "User 1", Age = 25, Address = "This place", Salary = 1_500_000_0};
            var application = new LoanApplication {Id = 42, Product = product, Amount = amount, Applicant = applicant};

            var mockIdentityVerifier = new Mock<IdentityVerifierServiceGateway>();

            mockIdentityVerifier.Setup(x => x.CallService(applicant.Name, applicant.Age, applicant.Address))
                .Returns(true);

            var mockCreditScorer = new Mock<ICreditScorer>();
            mockCreditScorer.Setup(x => x.ScoreResult.ScoreValue.Score).Returns(110_000);

            var sut = new LoanApplicationProcessor(mockIdentityVerifier.Object, mockCreditScorer.Object);
            sut.Process(application);

            Assert.IsTrue(application.IsAccepted);
        }
    }
}
در اینجا برای اینکه بتوانیم متد CallService را که private بوده، بررسی و تنظیم کنیم، آن‌را به public virtual تبدیل کرده‌ایم تا توسط Moq قابل دسترسی و همچنین قابل بازنویسی شود:
public virtual bool CallService(string applicantName, int applicantAge, string applicantAddress)


تبدیل DateTime.Now به یک مقدار ثابت قابل آزمایش توسط Partial Mocks

در کلاس IdentityVerifierServiceGateway، یک چنین کدی را داریم که از DateTime.Now نامشخص استفاده می‌کند و آزمون واحد نوشتن برای آن مشکل است؛ چون DateTime.Now در هربار که آزمایش اجرا می‌شود، تغییر می‌کند:
public bool Validate(string applicantName, int applicantAge, string applicantAddress)
{
    Connect();
    var isValidIdentity = CallService(applicantName, applicantAge, applicantAddress);
    LastCheckTime = DateTime.Now;
    Disconnect();

    return isValidIdentity;
}
برای بالابردن قابلیت آزمون نویسی این کلاس، آن‌را به صورت زیر Refactor می‌کنیم تا DateTime.Now را به صورت یک متد public virtual دریافت کند:
public bool Validate(string applicantName, int applicantAge, string applicantAddress)
{
    Connect();
    var isValidIdentity = CallService(applicantName, applicantAge, applicantAddress);
    LastCheckTime = GetCurrentTime();
    Disconnect();

    return isValidIdentity;
}

public virtual DateTime GetCurrentTime()
{
    return DateTime.Now;
}
اکنون آزمون واحد نویسی برای این کلاس توسط Mock objects بسیار ساده‌است:
var expectedTime = new DateTime(2000, 1, 1);
mockIdentityVerifier.Setup(x => x.GetCurrentTime())
    .Returns(expectedTime);
// ...
Assert.AreEqual(expectedTime, mockIdentityVerifier.Object.LastCheckTime);
در اینجا خروجی متد GetCurrentTime بر روی Mock object تهیه شده، به یک مقدار ثابت تنظیم شده‌است که با هر بار اجرای آزمایش در زمان‌های مختلف، تغییری نمی‌کند و وابسته‌ی به DateTime.Now نامشخص، نیست.


استفاده از متدهای protected بجای استفاده از متدهای public virtual در Partial Mocks

همانطور که مشاهده کردید، برای کار با Partial Mocks نیاز است متدهای معرفی شده، از نوع public virtual باشند. برای نمونه حتی مجبور شدیم یک متد private را نیز public کنیم. اگر علاقمند به این نوع تغییرات نیستید، می‌توان بجای public کردن متدهای private، آن‌ها را protected تعریف کرد. به همین جهت دو متدی را که تاکنون public virtual تعریف کردیم، تبدیل به protected virtual می‌کنیم.
پس از آن در کلاسی که آزمون‌های واحد را تهیه کردیم، ابتدا using Moq.Protected را ذکر می‌کنیم تا بتوانیم به قابلیت‌های ویژه‌ی کار با متدهای Protected دسترسی پیدا کنیم.
سپس روش تنظیم این نوع متدهای protected، چون دسترسی مستقیمی به آن‌ها وجود ندارد، به صورت زیر، با ذکر نام رشته‌ای آن‌ها تغییر می‌کند:
mockIdentityVerifier.Protected().Setup<bool>(
        "CallService",applicant.Name, applicant.Age, applicant.Address)
    .Returns(true);

var expectedTime = new DateTime(2000, 1, 1);
mockIdentityVerifier.Protected().Setup<DateTime>("GetCurrentTime")
    .Returns(expectedTime);
ابتدا متد Protected شیء Mock شده ذکر می‌شود و پس از آن متد Setup باید دقیقا نوع بازگشتی متد در حال تنظیم را ذکر کند؛ چون دیگر دسترسی strongly typed ای به آن نداریم. پس ا‌ز آن، لیست پارامترهای متد، ذکر می‌شوند.

روش دیگری نیز برای تعریف متدهای protected وجود دارد که اینبار strongly typed است. بالای متد آزمون واحد، اینترفیس private زیر را تعریف می‌کنیم:
interface IIdentityVerifierServiceGatewayProtectedMembers
{
   DateTime GetCurrentTime();
   bool CallService(string applicantName, int applicantAge, string applicantAddress);
}
که در آن متدهای تعریف شده، با متدهای protected در حال بررسی، امضای یکسانی دارند (و همواره با هر تغییری در برنامه نیز باید این وضعیت حفظ شود). در ادامه تعاریف تنظیمات این متدها به صورت strongly typed زیر قابل انجام است:
mockIdentityVerifier.Protected()
    .As<IIdentityVerifierServiceGatewayProtectedMembers>()
    .Setup(x => x.CallService(It.IsAny<string>(),
        It.IsAny<int>(),
        It.IsAny<string>()))
    .Returns(true);

var expectedTime = new DateTime(2000, 1, 1);
mockIdentityVerifier.Protected()
    .As<IIdentityVerifierServiceGatewayProtectedMembers>()
    .Setup(x => x.GetCurrentTime())
    .Returns(expectedTime);


معرفی روش دیگری بجای استفاده از متدهای protected

اگر در کدهای خود نیاز به استفاده‌ی بیش از حد از متدهای protected را مشاهده کردید، این مورد می‌توان نشانه‌ی امکان Refactoring این قسمت از کدها به سرویس‌هایی مجزا باشند. برای مثال می‌توان یک اینترفیس INowProvider را به صورت زیر تعریف کرد:
using System;

namespace Loans.Services.Contracts
{
    public interface INowProvider
    {
        DateTime GetNow();
    }
}
و سپس آن‌را به سازنده‌ی کلاس IdentityVerifierServiceGateway تزریق کرد:
    public class IdentityVerifierServiceGateway : IIdentityVerifier
    {
        private readonly INowProvider _nowProvider;
        
        public DateTime LastCheckTime { get; private set; }

        public IdentityVerifierServiceGateway(INowProvider nowProvider)
        {
            _nowProvider = nowProvider;
        }
 و متد GetCurrentTime را حذف و آن‌را با متد GetNow این سرویس جایگزین نمود:
        public bool Validate(string applicantName, int applicantAge, string applicantAddress)
        {
            Connect();
            var isValidIdentity = CallService(applicantName, applicantAge, applicantAddress);
            LastCheckTime = _nowProvider.GetNow();
            // ...
 به این ترتیب نیاز به تنظیم متد protected بازگشت زمان، حذف شده و می‌توان از این سرویس جدید استفاده کرد:
var mockNowProvider = new Mock<INowProvider>();
mockNowProvider.Setup(x => x.GetNow()).Returns(expectedTime);

var mockIdentityVerifier =  new Mock<IdentityVerifierServiceGateway>(mockNowProvider.Object);


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

نکته: اگر فایل‌های Resource درون اسمبلی‌های جداگانه‌ای باشند (مثلا در یک پروژه جداگانه، همانطور که در قسمت اول این سری مطالب پیشنهاد شده است)، موتور پیش فرض منابع در ASP.NET بدرد نخواهد خورد! بنابراین یا باید از نمونه‌های اختصاصی کلاس ResourceManager استفاده کرد (کاری که کلاس‌های خودکار تولیدشده توسط ابزارهای ویژوال استودیو انجام می‌دهند)، یا باید از پرووایدرهای سفارشی استفاده کرد که در مطالب بعدی نحوه تولید آن‌ها شرح داده خواهد شد.
 
همانطور که در ابتدای این مطلب اشاره شد، این مقدمه در اینجا صرفا برای آشنایی بیشتر با این دونوع Resource آورده شده تا ادامه مطلب روشن‌تر باشد، زیرا با توجه به مطالب ارائه شده در قسمت اول این سری، در پروژه‌های MVC استفاده از یک پروژه جداگانه برای نگهداری این منابع راه حل مناسبتری است.
در مطلب بعدی به شرح نحوه تولید پرووایدرهای سفارشی می‌پردازم.
مطالب
ساختار داده‌های خطی Linear Data Structure قسمت اول
بعضی از داده‌ها ساختارهای ساده‌ای دارند و به صورت یک صف یا یک نوار ضبط به ترتیب پشت سر هم قرار می‌گیرند؛ مثل ساختاری که صفحات یک کتاب را نگهداری می‌کند. یکی از نمونه‌های این ساختارها، List، صف، پشته و مشتقات آن‌ها می‌باشند.

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

نوع داده انتزاعی Abstraction Data Type -ADT
به زبان خیلی ساده لایه انتزاعی به ما تنها یک تعریف از ساختار مشخص شده‌ای را می‌دهد و هیچگونه پیاده سازی در آن وجود ندارد. برای مثال در لایه انتزاعی، تنها خصوصیت و عملگر‌ها و ... مشخص می‌شوند. ولی کد آن‌ها را پیاده سازی نمی‌کنیم و این باعث می‌شود که از روی این لایه بتوانیم پیاده سازی‌های متفاوت و کارآیی‌های مختلفی را ایجاد کنیم.
ساختار داده‌های مختلف در برنامه نویسی:
  • خطی یا Linear: شامل ساختارهایی چون لیست و صف و پشته است: List ,Queue,Stack
  • درختی یا Tree-Like: درخت باینری ، درخت متوازن و B-Trees
  • Dictionary : شامل یک جفت کلید و مقدار است در جدول هش
  • بقیه: گراف‌ها، صف الویت، bags, Multi bags, multi sets
در این مقاله تنها ساختارهای خطی را دنبال می‌کنیم و در آینده ساختارهای پیچیده‌تری را نیز بررسی خواهیم کرد و نیاز است بررسی کنیم کی و چگونه باید از آن‌ها استفاده کنیم.
ساختارهای لیستی از محبوبترین و پراستفاده‌ترین ساختارها هستند که با اشیاء زیادی در دنیای واقعی سازگاری دارند. مثال زیر را در نظر بگیرید:
قرار است که ما از فروشگاهی خرید کنیم و هر کدام از اجناس (المان‌ها) فروشگاه را که در سبد قرار دهیم، نام آن‌ها در یک لیست ثبت خواهد شد و اگر دیگر المان یا جنسی را از سبد بیرون بگذاریم، از لیست خط خواهد خورد.
همان که گفتیم یک ADT میتواند ساختارهای متفاوتی را پیاده سازی کند. یکی از این ساختارها اینترفیس system.collection.IList است که پیاده سازی آن منجر به ایجاد یک کلاس جدید در سیستم دات نت خواهد شد. پیاده سازی اینترفیس‌ها در سی شارپ، قوانین و قرادادهای خاص خودش را دارد و این قوانین شامل مجموعه‌ای از متد‌ها و خصوصیت‌هاست. برای پیاده سازی هر کلاسی از این اینترفیس‌ها باید این متدها و خصوصیت‌ها را هم در آن پیاده کرد.
با ارث بری از اینترفیس system.collection.IList باید رابط‌های زیر در آن پیاده سازی گردد:
(void Add(object    افزودن المان به آخر لیست 
(void Remove(object   حذف یک المان خاص از لیست  
 ()void Clear    حذف کلیه المان‌ها
( bool Contains(object   شامل این داده میشود یا خیر؟
( void RemoveAt(int  حذف یک المان بر اساس  جایگاه یا اندیسش 
(void Insert(int, object
 افزودن یک المان در جایگاهی (اندیس) خاص بر اساس مقدار position 
(int IndexOf(object اندیس یا جایگاه یک عنصر را بر می‌گرداند
 [this[int ایندکسر ، برای دستریس به عنصر در اندیس مورد نظر

لیست‌های ایستا static Lists
آرایه‌ها می‌توانند بسیاری از خصوصیات ADT را پیاده کنند ولی تفاوت بسیار مهم و بزرگی با آن‌ها دارند و آن این است که لیست به شما اجازه می‌دهد به هر تعدادی که خواستید، المان‌های جدیدی را به آن اضافه کنید؛ ولی یک آرایه دارای اندازه‌ی ثابت Fix است. البته این نکته قابل تامل است که پیاده سازی لیست با آرایه‌ها نیز ممکن است و باید به طور خودکار طول آرایه را افزایش دهید. دقیقا همان اتفاقی که برای stringbuilder در این مقاله توضیح دادیم رخ می‌دهد. به این نوع لیست‌ها، لیست‌های ایستایی که به صورت آرایه ای توسعه پذیر پیاده سازی میشوند می‌گویند. کد زیر پیاده سازی چنین لیستی است:
public class CustomArrayList<T>
{
    private T[] arr;
    private int count;
 
    public int Count
    {
        get
        {
            return this.count;
        }
    }
 
    private const int INITIAL_CAPACITY = 4;
 
    public CustomArrayList(int capacity = INITIAL_CAPACITY)
    {
        this.arr = new T[capacity];
        this.count = 0;
    }
در کد بالا یک آرایه با طول متغیر INITIAL_CAPACITY که پیش فرض آن را 4 گذاشته ایم می‌سازیم و از متغیر count برای حفظ تعداد عناصر آرایه استفاده می‌کنیم و اگر حین افزودن المان جدید باشیم و count بزرگتر از INITIAL_CAPACITY رسیده باشد، باید طول آرایه افزایش پیدا کند که کد زیر نحوه‌ی افزودن المان جدید را نشان می‌دهد. استفاده از حرف T بزرگ مربوط به مباحث Generic هست. به این معنی که المان ورودی می‌تواند هر نوع داده‌ای باشد و در آرایه ذخیره شود.
public void Add(T item)
{
    GrowIfArrIsFull();
    this.arr[this.count] = item;
    this.count++;
} 

public void Insert(int index, T item)
{
    if (index > this.count || index < 0)
    {
        throw new IndexOutOfRangeException(
            "Invalid index: " + index);
    }
    GrowIfArrIsFull();
    Array.Copy(this.arr, index,
        this.arr, index + 1, this.count - index);
    this.arr[index] = item;
    this.count++;
} 

private void GrowIfArrIsFull()
{
    if (this.count + 1 > this.arr.Length)
    {
        T[] extendedArr = new T[this.arr.Length * 2];
        Array.Copy(this.arr, extendedArr, this.count);
        this.arr = extendedArr;
    }
}
 
public void Clear()
{
    this.arr = new T[INITIAL_CAPACITY];
    this.count = 0;
}
در متد Add خط اول با تابع GrowIfArrIsFull بررسی می‌کند آیا خانه‌های آرایه کم آمده است یا خیر؟ اگر جواب مثبت باشد، طول آرایه را دو برابر طول فعلی‌اش افزایش می‌دهد و خط دوم المان جدیدی را در اولین خانه‌ی جدید اضافه شده قرار می‌دهد. همانطور که می‌دانید مقدار count همیشه یکی بیشتر از آخرین اندیس است. پس به این ترتیب مقدار count همیشه به  خانه‌ی بعدی اشاره می‌کند و سپس مقدار count به روز میشود. متد دیگری که در کد بالا وجود دارد insert است که المان جدیدی را در اندیس داده شده قرار می‌دهد. جهت این کار از سومین سازنده‌ی array.copy استفاده می‌کنیم. برای این کار آرایه مبدا و مقصد را یکی در نظر می‌گیریم و از اندیس داده شده به بعد در آرایه فعلی، یک کپی تهیه کرده و در خانه‌ی بعد اندیس داده شده به بعد قرار می‌دهیم. با این کار آرایه ما یک واحد از اندیس داده شده یک خانه، به سمت جلو حرکت می‌کند و الان خانه index و index+1 دارای یک مقدار هستند که در خط بعدی مقدار جدید را داخل آن قرار می‌دهیم و متغیر count را به روز می‌کنیم. باقی موارد را چون پردازش‌های جست و جو، پیدا کردن اندیس یک المان و گزینه‌های حذف، به خودتان واگذار می‌کنم.

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

برای پیاده سازی آن به دو کلاس نیاز داریم. کلاس ListNode برای نگهداری هر المان و اطلاعات المان بعدی به کار می‌رود که از این به بعد به آن Node یا گره می‌گوییم و دیگری کلاس <DynamicList<T برای نگهداری دنباله ای از گره‌ها و متدهای پردازشی آن.

public class DynamicList<T>
{
    private class ListNode
    {
        public T Element { get; set; }
        public ListNode NextNode { get; set; }
 
        public ListNode(T element)
        {
            this.Element = element;
            NextNode = null;
        }
 
        public ListNode(T element, ListNode prevNode)
        {
            this.Element = element;
            prevNode.NextNode = this;
        }
    }
 
    private ListNode head;
    private ListNode tail;
    private int count;
 
    // …
}

از آن جا که نیازی نیست کاربر با کلاس ListNode آشنایی داشته باشد و با آن سر و کله بزند، آن را داخل همان کلاس اصلی به صورت خصوصی استفاده می‌کنیم. این کلاس دو خاصیت دارد؛ یکی برای المان اصلی و دیگر گره بعدی. این کلاس دارای دو سازنده است که اولی تنها برای عنصر اول به کار می‌رود. چون اولین بار است که یک گره ایجاد می‌شود، پس باید خاصیت NextNode یعنی گره بعدی در آن Null باشد و سازنده‌ی دوم برای گره‌های شماره 2 به بعد به کار می‌رود که همراه المان داده شده، گره قبلی را هم ارسال می‌کنیم تا خاصیت NextNode آن را به گره جدیدی که می‌سازیم مرتبط سازد. سه خاصیت کلاس اصلی به نام‌های Count,Tail,Head به ترتیب برای اشاره به اولین گره، آخرین گره و تعداد گره‌ها، به کار می‌روند که در ادامه کد آن‌را در زیر می‌بینیم:

public DynamicList()
{
    this.head = null;
    this.tail = null;
    this.count = 0;
}

public void Add(T item)
{
    if (this.head == null)
    {
        this.head = new ListNode(item);
        this.tail = this.head;
    }
    else
    {
        ListNode newNode = new ListNode(item, this.tail);
        this.tail = newNode;
    }
    this.count++;
}

سازنده مقدار دهی پیش فرض را انجام می‌دهد. در متد Add المان جدیدی باید افزوده شود؛ پس چک می‌کند این المان ارسالی قرار است اولین گره باشد یا خیر؟ اگر head که به اولین گره اشاره دارد Null باشد، به این معنی است که این اولین گره است. پس اولین سازنده‌ی کلاس ListNode را صدا می‌زنیم و آن را در متغیر Head قرار می‌دهیم و چون فقط همین گره را داریم، پس آخرین گره هم شناخته می‌شود که در tail نیز قرار می‌گیرد. حال اگر فرض کنیم المان بعدی را به آن بدهیم، اینبار دیگر Head برابر Null نخواهد بود. پس دومین سازنده‌ی ListNode صدا زده می‌شود که به غیر از المان جدید، باید آخرین گره قبلی هم با آن ارسال شود و گره جدیدی که ایجاد می‌شود در خاصیت NextNode آن نیز قرار بگیرد و در نهایت گره ایجاد شده به عنوان آخرین گره لیست در متغیر Tail نیز قرار می‌گیرد. در خط پایانی هم به هر مدلی که المان جدید به لیست اضافه شده باشد متغیر Count به روز می‌شود.

public T RemoveAt(int index)
{
    if (index >= count || index < 0)
    {
        throw new ArgumentOutOfRangeException(
            "Invalid index: " + index);
    }
 
    int currentIndex = 0;
    ListNode currentNode = this.head;
    ListNode prevNode = null;
    while (currentIndex < index)
    {
        prevNode = currentNode;
        currentNode = currentNode.NextNode;
        currentIndex++;
    }
 

    RemoveListNode(currentNode, prevNode);
 
    return currentNode.Element;
}

private void RemoveListNode(ListNode node, ListNode prevNode)
{
    count--;
    if (count == 0)
    {
        this.head = null;
        this.tail = null;
    }
    else if (prevNode == null)
    {
        this.head = node.NextNode;
    }
    else
    {
        prevNode.NextNode = node.NextNode;
    }

    if (object.ReferenceEquals(this.tail, node))
    {
        this.tail = prevNode;
    }
}

برای حذف یک گره شماره اندیس آن گره را دریافت می‌کنیم و از Head، گره را بیرون کشیده و با خاصیت nextNode آنقدر به سمت جلو حرکت می‌کنیم تا متغیر currentIndex یا اندیس داده شده برابر شود و سپس گره دریافتی و گره قبلی آن را به سمت تابع RemoveListNode ارسال می‌کنیم. کاری که این تابع انجام می‌دهد این است که مقدار NextNode گره فعلی که قصد حذفش را داریم به خاصیت Next Node گره قبلی انتساب می‌دهد. پس به این ترتیب پیوند این گره از لیست از دست می‌رود و گره قبلی به جای اشاره به این گره، به گره بعد از آن اشاره می‌کند. مابقی کد از قبیل جست و برگردان اندیس یک عنصر و ... را به خودتان وگذار می‌کنم.

در روش‌های بالا ما خودمان 2 عدد ADT را پیاده سازی کردیم و متوجه شدیم برای دخیره داده‌ها در حافظه روش‌های متفاوتی وجود دارند که بیشتر تفاوت آن در مورد استفاده از حافظه و کارآیی این روش هاست.


لیست‌های پیوندی دو طرفه Doubly Linked_List

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

این مبحث را در اینجا می‌بندیم و در قسمت بعدی آن را ادامه می‌دهیم.

مطالب
MongoDb در سی شارپ (بخش دوم)
در قسمت پیشین عملیات درج و واکشی را در مونگو مورد بررسی قرار دادیم. در این مقاله به عملیات CRUD، پایان داده و عملیات ایندکس گذاری را نیز مورد بررسی قرار می‌دهیم.
در مقاله قبلی از بیلدر برای فیلترگذاری و مرتب سازی نتایج استفاده کردیم و در این مقاله هم برای به روزرسانی و ایندکس گذاری استفاده میکنیم.

به روزرسانی
کد زیر، تاریخ آخرین ورود کتب به انبار را به روزرسانی میکند؛ بدین صورت که اگر تاریخ آخرین به روزرسانی انبار کمتر یا مساوی ده روز است، آن‌را برابر 5 روز قبل قرار دهد.
var update = Builders<Book>.Update.Set("LastStock", new DateTime().AddDays((-5)));
var filter = Builders<Book>.Filter.Lte("LastStock", new DateTime().AddDays(-10));
collection.UpdateManyAsync(filter, update);
در خط اول ابتدا یک فرمان به روزرسانی را می‌سازیم و مقدار جدید را انتساب می‌دهیم. در خط بعدی، از گزینه فیلتر برای تعیین شرط استفاده میکنیم که در قسمت قبلی به آن پرداخته‌ایم. حال هم شرط را داریم و عمل به روزرسانی و از کالکشن میخواهیم که با استفاده از این دو شیء عملیات به روزرسانی را روی اسناد آغاز کند. توجه داشته باشید که دستور UpdateOne که به دو حالت همزمان و غیرهمزمان وجود دارد، وقتی به اولین مورد که با شرط همخوانی داشته باشد برسد و عملیات به روزرسانی را روی آن سند انجام دهد، دیگر باقی عملیات را ادامه نمی‌دهد. پس اگر میخواهید که کلیه اسناد در این فرمان شرکت کنند حتما از متد UpdateMany که به دو شکل همزمان و غیرهمزمان وجود دارد، استفاده کنید.

در بسیاری از موارد نیاز است که به روزرسانی یک فیلد، بسته به مقدار فعلی‌اش صورت بگیرد. برای مثال در کد زیر ما قصد داریم کتبی را که قیمت آن‌ها بالای دویست هزار ریال است، سی هزار ریال از قیمت آن‌ها کم کنیم:
var update = Builders<Book>.Update.Inc("Price", -30000);
var filter = Builders<Book>.Filter.Gt("Price", 200000);
collection.UpdateMany(filter, update);
اگر به متدهای خصوصیت Update نگاهی بیندازید، میتوانید خصوصیات مختلف و ترکیبات مختلفی از آن را نیز ببینید.

حذف
جهت حذف یک سند از سیستم با توجه به مواردی که تا به الان داشتیم باید این حدس را زده باشید که برای حذف، باید از متد Delete استفاده کنیم و یک فیلتر را باید به عنوان شرط گذاری به عنوان پارامتر آن، در نظر بگیریم. کد زیر کتاب‌هایی را که به زبان انگلیسی هستند، از کالکشن حذف میکند:
var filter = Builders<Book>.Filter.Eq("Language.Name", "English");
collection.DeleteMany(filter);
دستور Delete هم همانند آپدیت میتواند اولین سند برابر با شرط و همچنین تمامی اسناد برابر شرط را حذف کند. نکته‌ای که در این کد به نسبت کدهای دیگر وجود دارد این است که اگر سند توکار (Embed) در سند شما وجود دارد، باید ابتدا نام خصوصیت اول و بعد از علامت «.» نام خصوصیت درونی نوشته شود و اگر باز سند توکار دیگری نیز بود، این رویه ادامه پیدا کند. در اینجا برای دسترسی به خاصیت نام زبان ابتدا نام خصوصیت Language آورده شده است و سپس نام خصوصیت Name آن ذکر شده است:
Language.Name
در صورتی که قصد شما حذف یک کالکشن به طور کلی باشد، از خصوصیت DropCollection در شیء دیتابیس استفاده میکنیم:
var client = new MongoClient();
var db = client.GetDatabase("publisher");
db.DropCollection("books");
برای حذف دیتابیس هم از دستور DropDatebase در شیء کلاینت استفاده میکنیم:
var client = new MongoClient();
client.DropDatabase("publisher");

ایندکس گذاری Indexing
ایندکسها باعث میشوند که دسترسی سریعتری را به داده‌ها داشته باشیم و فیلدهایی را که در پرس و جوها مورد استفاده قرار میگیرند، سریعتر پیدا کنیم. به عنوان مثال فیلد Year فیلدی است که به احتمال زیاد در پرس و جوهایمان مرتبا استفاده خواهیم کرد تا به لیست کتب آن سال دسترسی سریعتری داشته باشیم. برای ایندکس گذاری هم از کلاس Builders استفاده میکنیم:
var index = Builders<Book>.IndexKeys.Ascending("Year");
collection.Indexes.CreateOneAsync(index);

ایندکس‌ها ترکیبی Compound Index

حتی میتوانید این ایندکس گذاری را بر روی دو فیلد، یعنی به حالت ترکیبی هم انجام دهید:
var index = Builders<Book>.IndexKeys.Ascending("Year").Descending("LastStock");
collection.Indexes.CreateOneAsync(index);
در اولین نگاه ممکن از است بپرسید که چرا این مرتب سازی بر روی اندیس‌ها مهم است؟ منظور از این صعودی و نزولی چیست؟ در اینجا باید ساختاری را که مونگو از آن برای یافتن آیتم‌ها استفاده میکند، بررسی کنیم.

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

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

ایندکس جغرافیایی GeoSpatial Index
برای داده‌های جرافیای می‌توان این نوع ایندکس را پیاده سازی کرد. موقعیکه شما با مختصات جغرافیایی کار میکنید، این ایندکس‌ها میتوانند به شما در بازیابی سریعتر مکان‌ها و مختصات کمک کنند. از آنجا که مطالب این نوع ایندکس‌ها تخصصی‌تر است، آن را به زمان دیگری موکول می‌کنیم.
var index2 = Builders<Book>.IndexKeys.Geo2D("Location");
var index3 = Builders<Book>.IndexKeys.Geo2DSphere("Location");
var index4 = Builders<Book>.IndexKeys.GeoHaystack("Location");

ایندکس متنی Text Index

این نوع ایندکس‌ها بر روی فیلدهای نوع رشته String قرار داده میشوند تا جست و جوی عبارت‌ها بین آن‌ها ساده‌تر گردد. زمانیکه ما در مورد جست و جو در بین مطالب یک سایت صحبت میکنیم، سریعا عبارات Full Text Search یا Elastic و لوسین را به یاد می‌آوریم. در مونگو هم برای این نوع جست و جو‌ها ایندکس Text را داریم که از این پس بتوانیم جست و جوی سریعتری بر روی آن داشته باشیم.
var index5 = Builders<Book>.IndexKeys.Text("Content");


قابلیت‌های ایندکس گذاری این نوع دیتابیس حتی به مواردی چون Sharding ، SparseIndex ,TTL Indexes و ... هم میرسد که از حوصله مقاله فعلی خارج است و باید به زمان دیگری و در یک مقاله تخصصی‌تر آنها را بررسی کرد.
 
در قسمت بعدی، ذخیره و بازیابی فایل‌ها در سطح دیتابیس را مورد بررسی قرار می‌دهیم.
نظرات مطالب
کار با اسکنر در برنامه های تحت وب (قسمت دوم و آخر)
سلام؛ ممنون از مطلب مفیدتون. من راهی رو گفتیم رفتم wcfservice هم run کردم و درست بود اما با اجرای WinForm  و  زدن آدرس locallhost:8010 چیزی باز نمیشه. کد زیر هم اصلا کار نمیکنه:
 <script>
  $( "#get-scan" ).click(function () {
       var url = 'http://localhost:8010/' ;
       $. get (url, function (data) {
       $( "#img-scanned" ).attr( "src" , "data:image/Jpeg;base64,  " + data.GetScanResult);
    });
  });
پورت رو هم 6019 میذارم بازم همینطور. ممنون میشم راهنمایی کنید.
مطالب
بررسی 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 را که توسط مایکروسافت تهیه شده‌است، مشاهده کنید. شرح و توضیحات تعدادی از آن‌ها را هم در اینجا مطالعه کنید.