مطالب
کنترل DatePicker شمسی مخصوص Silverlight 4

Silverlight 4 تاریخ شمسی را از دات نت فریم ورک به ارث نبرده است (+). اما اضافه کردن آن کار خاصی نیست. مجموعه‌ی سورس باز Silverlight toolkit هم دارای DatePicker تاریخ میلادی است اما به دلایلی که عرض شد، تاریخ شمسی را پشتیبانی نمی‌کند.

کارهایی که توسط سایر برنامه نویس‌های ایرانی تابحال در این مورد انجام شده است:
- اضافه کردن DatePicker فارسی به مجموعه‌ی Silverlight toolkit : (+)
به دو دلیل من از این راه حل استفاده نخواهم کرد:
الف) patch ارائه شده هنوز با Silverlight toolkit یکپارچه نشده است و هربار باید این تغییرات را اعمال کرد و غیره ...
ب) شاید من اصلا نخواهم که از Silverlight toolkit استفاده کنم. آن وقت چه باید کرد؟
این تنها کاری است که جهت Silverlight انجام شده است.

دو نمونه‌ی خوب دیگر هم برای WPF موجود است که تبدیل آن‌ها به Silverlight کار ساده‌ای نیست (چون Silverlight تمام کلاس‌های WPF را نیز به ارث نبرده است):
- Farsi Library - Working with Dates, Calendars, and DatePickers
- PersianDate and some WPF controls for it

به همین جهت یک کنترل DatePicker و تقویم شمسی مستقل را برای Silverlight 4 آماده کرده‌ام که از آدرس ذیل قابل دریافت است:





نحوه استفاده:
الف) ارجاعی را به اسمبلی SilverlightPersianDatePicker.dll به پروژه خود اضافه کنید. اگر مباحث library caching هم برای شما مهم است، فایل SilverlightPersianDatePicker.extmap.xml پیوست شده را نیز فراموش نکنید.
ب) xmlns آن باید به XAML جاری اضافه شود؛ برای مثال:
xmlns:dp="clr-namespace:SilverlightPersianDatePicker.Views;assembly=SilverlightPersianDatePicker"
ج) سپس استفاده از آن به سادگی یک سطر زیر خواهد بود:
<dp:PDatePicker x:Name="txtDate" TextBoxWidth="100"  Margin="5"  />

خاصیت SelectedDate آن تاریخ میلادی و خاصیت SelectedPersianDate آن تاریخ شمسی را بر می‌گرداند.


کتابخانه‌های کمکی که در حین توسعه‌ی آن استفاده شدند:
کلاس تقویم شمسی امید خندان راد (که برای روزهای دات نت 1 تهیه شده بود).
پنل UniformGrid که در Silverlight موجود نیست.(+)
رفتار StaysOpen مرتبط با Popup که در Silverlight از WPF به ارث نرسیده است.(+)
استفاده از کلاس DelegateCommand جان پاپا (برای سهولت Commanding در الگوی MVVM). (+)


مطالب
ASP.NET MVC #4

بررسی نحوه ارتباطات بین اجزای مختلف الگوی MVC در ASP.NET MVC

اینبار برخلاف قسمت قبل، قالب پروژه خالی ASP.NET MVC را در VS.NET انتخاب کرده (ASP.NET MVC 3 Web Application و بعد انتخاب قالب Empty نمایش داده شده) و سپس پروژه جدیدی را شروع می‌کنیم. View Engine را هم Razor در نظر خواهیم گرفت.
پس از ایجاد ساختار اولیه پروژه، بدون اعمال هیچ تغییری، برنامه را اجرا کنید. بلافاصله با پیغام The resource cannot be found یا 404 یافت نشد، مواجه خواهیم شد.
همانطور که در پایان قسمت دوم نیز ذکر شد، پردازش‌ها در ASP.NET MVC از کنترلرها شروع می‌شوند و نه از صفحات وب. بنابراین برای رفع این مشکل نیاز است تا یک کلاس کنترلر جدید را اضافه کنیم. به همین جهت بر روی پوشه استاندارد Controllers کلیک راست کرده، از منوی ظاهر شده قسمت Add، گزینه‌ی Controller را انتخاب کنید:


در صفحه بعدی که ظاهر می‌شود، نام HomeController را وارد کنید (با توجه به اینکه مطابق قراردادهای ASP.NET MVC، نام کنترلر باید به کلمه Controller ختم شود). البته لازم به ذکر است که این مراحل را به همان شکل متداول مراجعه به منوی Project و انتخاب Add Class و سپس ارث بری از کلاس Controller نیز می‌توان انجام داد و طی این مراحل الزامی نیست. کلاسی که به صورت خودکار از طریق منوی Add Controller یاد شده ایجاد می‌شود، به شکل زیر است:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace MvcApplication1.Controllers
{
public class HomeController : Controller
{
//
// GET: /Home/
public ActionResult Index()
{
return View();
}
}
}

سؤال:
در قسمت دوم عنوان شد که کنترلر باید کلاسی باشد که اینترفیس IController را پیاده سازی کرده است، اما در اینجا ارث بری از کلاس Controller را شاهد هستیم. جریان چیست؟!
سلسله مراتبی که بکارگرفته شده به صورت زیر است:
public abstract class ControllerBase : IController
public abstract class Controller : ControllerBase

ControllerBase، اینترفیس IController را پیاده سازی کرده و سپس کلاس Controller از کلاس ControllerBase مشتق شده است. شاید بپرسید که این همه پیچ و تاب برای چیست؟!
مشکلی که با اینترفیس خالص وجود دارد، عدم نگارش پذیری آن است. به این معنا که اگر متدی یا خاصیتی در نگارش بعدی به این اینترفیس اضافه شد، هیچکدام از پروژه‌های قدیمی دیگر کامپایل نخواهند شد و باید ابتدا این متد یا خاصیت جدید را نیز لحاظ کنند. اینجا است که کار کلاس‌های abstract شروع می‌شود. در یک کلاس abstract می‌توان پیاده سازی پیش فرضی را نیز ارائه داد. به این ترتیب مصرف کننده نهایی کلاس Controller متوجه این تغییرات نخواهد شد.
اگر برنامه نویس «من» باشم، شما رو وادار خواهم کرد که متدهای جدید اینترفیس تعریفی‌ام را پیاده سازی کنید! همینه که هست! اما اگر طراح مایکروسافت باشد، بلافاصله انبوهی از جماعت ایرادگیر که بالای 100 تا از کنترلر‌های اون‌ها الان فقط در یک پروژه از کار افتاده،‌ ممکن است جلوی دفتر مایکروسافت دست به خود سوزی بزنند! اینجا است که مایکروسافت مجبور است تا این پیچ و تاب‌ها را اعمال کند که اگر روزی متدی در اینترفیس IController بنابر نیازهای جدید درنظر گرفته شد، بتوان سریع یک پیاده سازی پیش فرض از آن‌را در کلاس‌های abstract یاد شده قرار داد (یکی از تفاوت‌های مهم کلاس‌های abstract با اینترفیس‌ها) تا جماعت ایرادگیر و نق‌زن متوجه تغییری نشوند و باز هم پروژه‌های قدیمی بدون مشکل کامپایل شوند. تابحال به فلسفه وجودی کلاس‌های abstract از این دیدگاه فکر کرده بودید؟!

بعد از این توضیحات و کارها، اگر اینبار برنامه را اجرا کنیم، خطای زیر نمایش داده می‌شود:

The view 'Index' or its master was not found or no view engine supports the searched locations. The following locations were searched:
~/Views/Home/Index.aspx
~/Views/Home/Index.ascx
~/Views/Shared/Index.aspx
~/Views/Shared/Index.ascx
~/Views/Home/Index.cshtml
~/Views/Home/Index.vbhtml
~/Views/Shared/Index.cshtml
~/Views/Shared/Index.vbhtml

همانطور که ملاحظه می‌کنید چون نام کنترلر ما Home است، فریم ورک در پوشه استاندارد Views در زیر پوشه‌ای به همان نام Home، به دنبال یک سری فایل می‌گردد. فایل‌های aspx مربوط به View Engine ایی به همین نام بوده و فایل‌های cshtml و vbhtml مربوط به View Engine دیگری به نام Razor هستند.
بنابراین نیاز است تا یکی از این فایل‌ها را در مکان‌های یاد شده ایجاد کنیم. برای این منظور حداقل دو راه وجود دارد. یا دستی اینکار را انجام دهیم یا اینکه از ابزار توکار خود VS.NET برای ایجاد یک View جدید استفاده کنیم.
برای ایجاد View ایی مرتبط با متد Index (در ASP.NET MVC نام دیگر متدهای قرار گرفته در یک کنترلر، Action نیز می‌باشد)، روی خود متد کلیک راست کرده و گزینه Add View را انتخاب کنید:


در صفحه بعدی ظاهر شده، پیش فرض‌ها را پذیرفته و بر روی دکمه Add کلیک نمائید. اتفاقی که رخ خواهد داد شامل ایجاد فایل Index.cshtml، با محتوای زیر است (با توجه به اینکه زبان پروژه سی شارپ است و View Engine انتخابی Razor می‌باشد، cshtml تولید گردید و گرنه vbhtml ایجاد می‌شد):

@{
ViewBag.Title = "Index";
}
<h2>Index</h2>

مجددا برنامه را اجرا کنید. اینبار بدون خطایی کلمه Index را در صفحه تولیدی می‌توان مشاهده کرد. نکته جالب این فایل‌های View جدید، عدم مشاهده ویژگی‌های runat=server و سایر موارد مشابه است.

چند سؤال مهم:
در حین ایجاد اولین کنترلر جهت نمایش صفحه پیش فرض برنامه، نام HomeController انتخاب شد. چرا مثلا نام TestController وارد نشد؟ برنامه از کجا متوجه شد که باید حتما این کنترلر را پردازش کند. نقش متد Index چیست؟ آیا حتما باید Index باشد و در اینجا نام دیگری را نمی‌توان وارد کرد؟ «قرارداد» پردازشی این‌ها کجا تنظیم می‌شود؟ فریم ورک، این سیم کشی‌ها را چگونه انجام می‌دهد؟
پاسخ به تمام این سؤال‌ها، در ویژگی «مسیریابی یا Routing» نهفته است. فایل Global.asax.cs برنامه را باز کنید. تعاریف مرتبط با مسیریابی پیش فرض را در متد RegisterRoutes آن می‌توان مشاهده کرد:

public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
}



پروسه هدایت یک درخواست HTTP به یک کنترلر، در اینجا مسیریابی یا Routing نامیده می‌شود. این قابلیت در فضای نام System.Web.Routing تعریف شده است و باید دقت داشت که جزو ASP.NET MVC نیست. این امکانات جزو ASP.NET Runtime است و به همراه دات نت 3.5 سرویس پک یک برای اولین بار ارائه شد. بنابراین جهت استفاده در ASP.NET Web forms نیز مهیا است. در ASP.NET MVC از این امکانات برای ارسال درخواست‌ها به کنترلرها استفاده می‌شود.
در متد routes.MapRoute فراخوانی شده‌ای که در کدهای بالا ملاحظه می‌کنید، کار نگاشت یک URL به Actionهای یک کنترلر صورت می‌گیرد (یا همان متدهای تعریف شده در کنترلرها). همچنین از این URLها پارامترهای این متدها یا اکشن‌ها نیز قابل استخراج است.
در متد MapRoute، اولین پارامتر تعریف شده، یک نام پیش فرض است و در ادامه اگر آدرسی را به فرم «یک چیزی اسلش یک چیزی اسلش یک چیزی» یافت، اولین قسمت آن‌را به عنوان نام کنترلر تفسیر خواهد کرد، دومین قسمت آن، نام متد عمومی موجود در کنترلر فرض شده و سومین قسمت به عنوان پارامتر ارسالی به این متد پردازش می‌شود.
برای مثال از آدرس زیر اینطور می‌توان دریافت که:
http://hostname/home/about

Home نام کنترلی است که فریم ورک به دنبال آن خواهد گشت تا این درخواست رسیده را پردازش کند و about نام متدی عمومی در این کلاس است که به صورت خودکار فراخوانی می‌گردد. در اینجا پارامتر id ایی هم وجود ندارد. در یک برنامه امکان تعریف چندین و چند مسیریابی وجود دارد.
پارامتر سوم متد routes.MapRoute، یک سری پیش فرض را تعریف می‌کند و این مورد همانجایی است که از اطلاعات آن جهت تعریف کنترلر پیش فرض استفاده کردیم. برای مثال به چهار آدرس زیر دقت نمائید:

http://localhost/
http://localhost/home
http://localhost/home/about
http://localhost/process/list

در حالت http://localhost/،‌ هر سه مقدار پیش فرض مورد استفاده قرار خواهند گرفت چون سه جزئی ({controller}/{action}/{id}) که موتور مسیریابی به دنبال آن‌ها می‌گردد، در این آدرس وجود خارجی ندارد. بنابراین نام کنترلر پیش فرض در این حالت همان Home مشخص شده در پارامتر سوم متد routes.MapRoute خواهد بود و متد پیش فرض نیز Index و پارامتری هم به آن ارسال نخواهد شد.
در حالت http://localhost/home نام کنترلر مشخص است اما دو جزء دیگر ذکر نشده‌اند، بنابراین مقادیر آن‌ها از پیش فرض‌های صریح ذکر شده در متد routes.MapRoute گرفته می‌شود. یعنی نام متد اکشن مورد پردازش، Index خواهد بود به همراه هیچ آرگومان خاصی.
به علاوه باید خاطرنشان کرد که این مقادیر case sensitive یا حساس به بزرگی و کوچکی حروف نیستند. بنابراین مهم نیست که http://localhost/HoMe باشد یا http://localhost/HOMe یا هر ترکیب دیگری.
یا اگر آدرس رسیده http://localhost/process/list باشد، این مسیریابی پیش فرض تعریف شده قادر به پردازش آن می‌باشد. به این معنا که کنترلری به نام Process وهله سازی و سپس متدی به نام List در آن فراخوانی خواهند شد.

یک نکته:
ترتیب routes.MapRouteهای تعریف شده در اینجا مهم است و اگر اولین URL رسیده با الگوی تعریف شده مطابقت داشت، کار را تمام خواهد کرد و به سایر تعاریف نخواهد رسید. مثلا اگر در اینجا یک مسیریابی دیگر را به نام Process تعریف کنیم:

public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

routes.MapRoute(
"Process", // Route name
"Process/{action}/{id}", // URL with parameters
new { controller = "Process", action = "List", id = UrlParameter.Optional } // Parameter defaults
);

routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
}

آدرسی به فرم http://localhost/Process، به صورت خودکار به کنترلر Process و متد عمومی List آن نگاشت خواهد شد و کار به مسیریابی بعدی نخواهد رسید.


بازخوردهای دوره
استفاده از StructureMap به عنوان یک IoC Container
من از structure در پروژه م به صورتی که توضیح دادین استفاده کردم.
در یه مورد خاص null هست. وقتی نیاز به پارشال اکشنی دارم که در کنترل دیگری قرار داره، درست کار میکنه سیم کشی‌ها و هیچ چیزی نال نیست.، اما وقتی نیاز دارم که پارشالی از اکشن کنترل جاری که در حال رندر هست ، استفاده کنم، نال هست همه‌ی اینترفیس ها. سازنده کنترلر هم فراخونی نمیشه.
ساختار کنترلر به این صورت هست:
 public partial class ContactController : Controller
    {
        private IGroupsBusiness _groupsBusiness;
        private IContactsBusiness _contactsBusiness;

        public ContactController(IContactsBusiness contactsBusiness, IGroupsBusiness groupsBusiness)
        {
            _groupsBusiness = groupsBusiness;
            _contactsBusiness = contactsBusiness;
        }

     

        public virtual ActionResult View(int id)
        {
            var model = _contactsBusiness.Select(id);
            return View(model);
        }

        public virtual ActionResult ViewGroups(int contactId)
        {
            var model = _groupsBusiness.SelectByContactId(contactId);
            return PartialView(model);
        }
}
ابتدا view اجرا میشه و سیم کشی برقرار هست. داخل ویو ارجاعی به اکشن viewgroups داره.  اما این بار نال هست و به مشکل برمیخورم. 
من توی ویو نوشتم 
 @{ Html.RenderAction(MVC.Contact.ViewGroups(Model.Id)); }
اگر این اکشن رو بذارم داخل کنترلر دیگه و صداش بزنم کار میکنه. 
آیا نباید کد بالا درست کار بکنه؟
نظرات مطالب
پیاده سازی Option یا Maybe در #C
با تشکر از شما
لزوما با پیاده سازی ارائه شده در مطلب جاری، از شر بررسی Null بودن یا نبودن خلاص نشده ایم (از دید استفاده کننده) چرا که خروجی متد همچنان می‌تواند Nullable باشد (کلاس Option یک نوع ارجاعی می‌باشد). چرا که استفاده کننده از آن لازم است برروی خروجی خود متد که یک وهله از Option می‌باشد بررسی Null بودن یا عدم آن را انجام دهد. برای رهایی از این موضوع استفاده از struct راه حل معقولی می‌باشد؛ یک پیاده سازی از آن به صورت زیر می‌باشد:
    public struct Maybe<T> : IEquatable<Maybe<T>>
        where T : class
    {
        private readonly T _value;

        private Maybe(T value)
        {
            _value = value;
        }

        public bool HasValue => _value != null;
        public T Value => _value ?? throw new InvalidOperationException();
        public static Maybe<T> None => new Maybe<T>();


        public static implicit operator Maybe<T>(T value)
        {
            return new Maybe<T>(value);
        }

        public static bool operator ==(Maybe<T> maybe, T value)
        {
            return maybe.HasValue && maybe.Value.Equals(value);
        }

        public static bool operator !=(Maybe<T> maybe, T value)
        {
            return !(maybe == value);
        }

        public static bool operator ==(Maybe<T> left, Maybe<T> right)
        {
            return left.Equals(right);
        }

        public static bool operator !=(Maybe<T> left, Maybe<T> right)
        {
            return !(left == right);
        }

        /// <inheritdoc />
        /// <summary>
        ///     Avoid boxing and Give type safety
        /// </summary>
        /// <param name="other"></param>
        /// <returns></returns>
        public bool Equals(Maybe<T> other)
        {
            if (!HasValue && !other.HasValue)
                return true;

            if (!HasValue || !other.HasValue)
                return false;

            return _value.Equals(other.Value);
        }

        /// <summary>
        ///     Avoid reflection
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public override bool Equals(object obj)
        {
            if (obj is T typed)
            {
                obj = new Maybe<T>(typed);
            }

            if (!(obj is Maybe<T> other)) return false;

            return Equals(other);
        }

        /// <summary>
        ///     Good practice when overriding Equals method.
        ///     If x.Equals(y) then we must have x.GetHashCode()==y.GetHashCode()
        /// </summary>
        /// <returns></returns>
        public override int GetHashCode()
        {
            return HasValue ? _value.GetHashCode() : 0;
        }

        public override string ToString()
        {
            return HasValue ? _value.ToString() : "NO VALUE";
        }
    }

 این بار می‌توان به امضای متد مذکور اعتماد کرد که قطعا خروجی null ارائه نخواهد داد؛ مگر اینکه به صورت صریح مشخص شود.
نکته: پیاده سازی صحیحی از واسط IEquatable برای Value Typeها در پیاده سازی struct بالا در نظر گرفته شده است.
استفاده از آن
public virtual async Task<Maybe<TModel>> GetByIdAsync(long id)
{
    Guard.ArgumentInRange(id, 1, long.MaxValue, nameof(id));

    var entity = await UnTrackedEntitySet.Where(a => a.Id == id)
        .ProjectTo<TModel>(_mapper.ConfigurationProvider).SingleOrDefaultAsync();

    return entity;
}
ساختار داده Maybe تعریف شده در بالا شبیه است با ساختار داده Nullable با این تفاوت که برای انواع ارجاعی مورد استفاده می‌باشد.
Maybe<T> = Nullable<T>

مطالب
بررسی تغییرات Blazor 8x - قسمت پنجم - امکان تعریف جزیره‌های تعاملی Blazor Server
در Blazor 8x می‌توان صفحات SSR ای را به همراه Blazor server islands و یا Blazor WASM islands داشت؛ یعنی یک کامپوننت Blazor Server که داخل یک صفحه‌ی معمولی SSR قرار گرفته و با سرور، ارتباط SiganlR برقرار می‌کند و یا یک کامپوننت Blazor WASM که در قسمتی از صفحه‌ی SSR درج شده و درون مرورگر کاربر اجرا می‌شود. به هر کدام از این‌ها یک «جزیره‌ی تعاملی» گفته می‌شود (interactive island). در این قسمت، نکات مرتبط با جزایر تعاملی Blazor Server را بررسی می‌کنیم.


بررسی یک مثال: تهیه یک برنامه‌ی Blazor 8x برای نمایش لیست محصولات، به همراه جزئیات آن‌ها

به لطف وجود SSR در Blazor 8x، می‌توان HTML نهایی کامپوننت‌ها و صفحات Blazor را همانند صفحات MVC و یا Razor pages، در سمت سرور تهیه و بازگشت داد. این خروجی در نهایت یک static HTML بیشتر نیست و گاهی از اوقات ما به بیش از یک خروجی ساده HTML ای نیاز داریم.
در این مثال که بر اساس قالب dotnet new blazor --interactivity Server تهیه می‌شود، قصد داریم موارد زیر را پیاده سازی کنیم:
- صفحه‌ای که یک لیست محصولات فرضی را نمایش می‌دهد : بر اساس SSR
- صفحه‌ای که جزئیات یک محصول را نمایش می‌دهد: بر اساس SSR
- دکمه‌ای در ذیل قسمت نمایش جزئیات یک محصول، برای دریافت و نمایش لیست محصولات مشابه و مرتبط: بر اساس Blazor server islands

یعنی تا جائیکه ممکن است قصد نداریم تمام صفحات و تمام قسمت‌های برنامه را با فعالسازی سراسری حالت تعاملی Blazor server که در قسمت‌های قبل در مورد آن توضیح داده شد، پیاده سازی کنیم. می‌خواهیم فقط قسمت کوچکی از این سناریو را که واقعا نیاز به یک چنین قابلیتی را دارد، توسط یک جزیره‌ی تعاملی Blazor server واقع شده‌ی در قسمتی از یک صفحه‌ی استاتیک SSR، مدیریت کنیم.


مدل برنامه: رکوردی برای ذخیره سازی اطلاعات یک محصول

namespace BlazorDemoApp.Models;

public record Product
{
    public int Id { get; set; }
    public required string Title { get; set; }
    public required string Description { get; set; }
    public decimal Price { get; set; }

    public List<int> Related { get; set; } = new();
}
در اینجا، هدف تعریف لیستی از محصولات فرضی است؛ به همراه خاصیتی که Id محصولات مشابه را نگهداری می‌کند (خاصیت Related).


سرویس برنامه: سرویسی برای بازگشت لیست محصولات

چون Blazor Server و SSR هر دو بر روی سرور اجرا می‌شوند، از لحاظ دسترسی به اطلاعات و کار با سرویس‌ها، هماهنگی کاملی وجود داشته و می‌توان کدهای یکسان و یکدستی را در اینجا بکار گرفت.
در ادامه کدهای کامل سرویس Services\ProductStore.cs را مشاهده می‌کنید:
using BlazorDemoApp.Models;

namespace BlazorDemoApp.Services;

public interface IProductStore
{
    IList<Product> GetAllProducts();
    Product GetProduct(int id);
    IList<Product> GetRelatedProducts(int productId);
}

public class ProductStore : IProductStore
{
    private static readonly List<Product> ProductsDataSource =
        new()
        {
            new Product
            {
                Id = 1, Title = "Smart speaker", Price = 22m,
                Description =
                    "This smart speaker delivers excellent sound quality and comes with built-in voice control, offering an impressive music listening experience.",
                Related = new List<int> { 2, 3 },
            },
            new Product
            {
                Id = 2, Title = "Regular speaker", Price = 89m,
                Description =
                    "Enjoy room-filling sound with this regular speaker. With its slick design, it perfectly fits into any room in your house.",
                Related = new List<int> { 1, 3 },
            },
            new Product
            {
                Id = 3, Title = "Speaker cable", Price = 12m,
                Description =
                    "This high-quality speaker cable ensures a reliable and clear audio connection for your sound system.",
            },
        };

    public IList<Product> GetAllProducts() => ProductsDataSource;

    public Product GetProduct(int id) => ProductsDataSource.Single(p => p.Id == id);

    public IList<Product> GetRelatedProducts(int productId)
    {
        var product = ProductsDataSource.Single(x => x.Id == productId);
        return ProductsDataSource.Where(p => product.Related.Contains(p.Id)).ToList();
    }
}
هدف از این سرویس، ارائه‌ی لیست تمام محصولات، دریافت اطلاعات یک محصول و همچنین یافتن لیست محصولات مشابه یک محصول خاص است.
این سرویس را باید در فایل Program.cs برنامه به صورت زیر معرفی کرد تا در فایل‌های razor برنامه‌ی جاری قابل دسترسی شود:
builder.Services.AddScoped<IProductStore, ProductStore>();


تکمیل صفحه‌ی نمایش لیست محصولات

قصد داریم زمانیکه کاربر برای مثال به آدرس فرضی http://localhost:5136/products مراجعه کرد، با تصویر لیستی از محصولات مواجه شود:


کدهای این صفحه را که در فایل Components\Pages\Store\ProductsList.razor قرار می‌گیرند، در ادامه مشاهده می‌کنید:

@page "/Products"
@using BlazorDemoApp.Models
@using BlazorDemoApp.Services

@inject IProductStore Store

@attribute [StreamRendering]

<h3>Products</h3>

@if (_products == null)
{
    <p>Loading...</p>
}
else
{
    @foreach (var item in _products)
    {
        <a href="/ProductDetails/@item.Id">
            <div>
                <div>
                    <h5>@item.Title</h5>
                </div>
                <div>
                    <h5>@item.Price.ToString("c")</h5>
                </div>
            </div>
        </a>
    }
}

@code {
    private IList<Product>? _products;

    protected override Task OnInitializedAsync() => GetProductsAsync();

    private async Task GetProductsAsync()
    {
        await Task.Delay(1000); // Simulates asynchronous loading to demonstrate streaming rendering
        _products = Store.GetAllProducts();
    }

}
توضیحات:
- جهت دسترسی به سرویس لیست محصولات، ابتدا سرویس IProductStore به این صفحه تزریق شده‌است.
- سپس در روال رویدادگردان آغازین OnInitializedAsync، کار دریافت اطلاعات و انتساب آن به لیستی، صورت گرفته‌است.
- در این متد جهت شبیه سازی یک عملیات async از یک Task.Delay استفاده شده‌است.
- چون این صفحه، یک صفحه‌ی SSR عادی است، بدون تعریف ویژگی StreamRendering در آن، پس از اجرای برنامه، هیچگاه قسمت loading که در حالت products == null_ قرار است ظاهر شود، نمایش داده نمی‌شود؛ چون در این حالت (حذف نوع رندر)، صفحه‌ی نهایی که به کاربر ارائه خواهد شد، یک صفحه‌ی استاتیک کاملا رندر شده‌ی در سمت سرور است و کاربر باید تا زمان پایان این رندر در سمت سرور، منتظر بماند و سپس صفحه‌ی نهایی را دریافت و مشاهده کند. در حالت Streaming rendering، ابتدا می‌توان یک قالب HTML ای را بازگشت داد و سپس مابقی محتوای آن‌را به محض آماده شدن در طی چند مرحله بازگشت داد.
- لینک‌های نمایش داده شده‌ی در اینجا، به صفحه‌ی ProductDetails اشاره می‌کنند که در آن، جزئیات محصول انتخابی نمایش داده می‌شوند.


تکمیل صفحه‌ی نمایش جزئیات یک محصول


در صفحه‌ی کامپوننت Components\Pages\Store\ProductDetails.razor، کار نمایش جزئیات محصول انتخابی صورت می‌گیرد:

@page "/ProductDetails/{ProductId}"
@using BlazorDemoApp.Models
@using BlazorDemoApp.Services

@inject IProductStore Store

@attribute [StreamRendering]

@if (_product == null)
{
    <p>Loading...</p>
}
else
{
    <div>
        <div>
            <h5>
                @_product.Title (@_product.Price.ToString("C"))
            </h5>
            <p>
                @_product.Description
            </p>
        </div>
        @if (_product.Related.Count > 0)
        {
            <div>
                <RelatedProducts ProductId="Convert.ToInt32(ProductId)" />
            </div>
        }
    </div>
    <NavLink href="/Products">Back</NavLink>
}

@code {
    private Product? _product;

    [Parameter]
    public string? ProductId { get; set; }

    protected override Task OnInitializedAsync() => GetProductAsync();

    private async Task GetProductAsync()
    {
        await Task.Delay(1000); // Simulates asynchronous loading to demonstrate streaming rendering
        _product = Store.GetProduct(Convert.ToInt32(ProductId));
    }

}
توضیحات:
- باتوجه به نحوه‌ی تعریف مسیریابی این صفحه، پارامتر ProductId از طریق آدرسی مانند http://localhost:5136/ProductDetails/1 دریافت می‌شود.
- سپس این ProductId را در روال رخ‌دادگردان OnInitializedAsync، برای یافتن جزئیات محصول انتخابی از سرویس تزریقی IProductStore، بکار می‌گیریم.
- در اینجا نیز از Task.Delay برای شبیه سازی یک عملیات طولانی async مانند دریافت اطلاعات از یک بانک اطلاعاتی، کمک گرفته شده‌است.
- همچنین برای نمایش قسمت loading صفحه در حالت SSR، بازهم از StreamRendering استفاده کرده‌ایم.
- اگر دقت کرده باشید، ذیل تصویر اطلاعات محصول، دکمه‌ای نیز جهت بارگذاری اطلاعات محصولات مشابه، قرار دارد که ProductId محصول انتخابی را دریافت می‌کند:
<RelatedProducts ProductId="Convert.ToInt32(ProductId)" />
بنابراین در ادامه کامپوننت RelatedProducts فوق را تکمیل می‌کنیم.


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

در فایل Components\Pages\Store\RelatedProducts.razor، کار نمایش یک دکمه و سپس نمایش لیستی از محصولات مشابه، صورت می‌گیرد:
@using BlazorDemoApp.Models
@using BlazorDemoApp.Services
@inject IProductStore Store

<button @onclick="LoadRelatedProducts">Related products</button>

@if (_loadRelatedProducts)
{
    @if (_relatedProducts == null)
    {
        <p>Loading...</p>
    }
    else
    {
        <div>
            @foreach (var item in _relatedProducts)
            {
                <a href="/ProductDetails/@item.Id">
                    <div>
                        <h5>@item.Title (@item.Price.ToString("C"))</h5>
                    </div>
                </a>
            }
        </div>
    }
}

@code{

    private IList<Product>? _relatedProducts;
    private bool _loadRelatedProducts;

    [Parameter]
    public int ProductId { get; set; }

    private async Task LoadRelatedProducts()
    {
        _loadRelatedProducts = true;
        await Task.Delay(1000); // Simulates asynchronous loading to demonstrate InteractiveServer mode
        _relatedProducts = Store.GetRelatedProducts(ProductId);
    }

}

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

مشکل! اگر در این حالت برنامه را اجرا کرده و بر روی دکمه‌ی related products کلیک کنیم، هیچ اتفاقی رخ نمی‌دهد! یعنی روال رویدادگران LoadRelatedProducts اصلا اجرا نمی‌شود. علت اینجا است که صفحات SSR، در نهایت یک static HTML بیشتر نیستند و فاقد قابلیت‌های تعاملی، مانند واکنش نشان دادن به کلیک بر روی یک دکمه هستند.
محدودیتی که به همراه صفحات SSR وجود دارد این است: این نوع کامپوننت‌ها و صفحات فقط یکبار رندر می‌شوند و نه بیشتر. بله می‌توان بر روی آن‌ها ده‌ها دکمه، نوارهای لغزان، دراپ‌داون و غیره را قرار داد، اما ... نمی‌توان هیچگونه تعاملی را با آن‌ها داشت. کامپوننت نهایی رندر شده و نمایش داده شده، دیگر در هیچ‌جائی اجرا نمی‌شود. در این حالت است که می‌توان تصمیم گرفت که نیاز است قسمتی از این صفحه، تعاملی شود.
به همین جهت باید نحوه‌ی رندر کامپوننت RelatedProducts را به صورت یک جزیره‌ی تعاملی Blazor server درآورد تا رویداد منتسب به دکمه‌ی related products موجود در آن، پردازش شود. بنابراین به صفحه‌ی ProductDetails.razor مراجعه کرده و rendermode@ این کامپوننت را به صورت زیر به حالت InteractiveServer تغییر می‌دهیم:
<RelatedProducts ProductId="Convert.ToInt32(ProductId)" @rendermode="@InteractiveServer"/>
اکنون اگر برنامه را مجددا اجرا کرده و بر روی دکمه‌ی نمایش محصولات مشابه قرار گرفته در ذیل جزئیات یک محصول کلیک کنیم، بدون مشکل کار می‌کند:


نحوه‌ی پردازش پشت صحنه‌ی این نوع صفحات هم جالب است. برای اینکار به برگه‌ی network مخصوص developer tools مرورگر مراجعه کرده و مراحل رسیدن به صفحه‌ی نمایش جزئیات محصول را طی می‌کنیم:


- اگر دقت کنید، جابجایی بین صفحات، با استفاده از fetch انجام شده؛ یعنی با اینکه این صفحات در اصل static HTML خالص هستند، اما ... کار full reload صفحه مانند ASP.NET Web forms قدیمی انجام نمی‌شود (و یا حتی برنامه‌های MVC و Razor pages) و نمایش صفحات، Ajax ای است و با fetch استاندارد آن صورت می‌گیرد تا هنوز هم حس و حال SPA بودن برنامه حفظ شود. همچنین اطلاعات DOM کل صفحه را هم به‌روز رسانی نمی‌کند؛ فقط موارد تغییر یافته در اینجا به روز رسانی خواهند شد.
این موارد توسط فایل blazor.web.js درج شده‌ی در کامپوننت آغازین App.razor، به صورت خودکار مدیریت می‌شوند:
<script src="_framework/blazor.web.js"></script>

به علاوه در این حالت ای‌جکسی fetch، کار دریافت مجدد فایل‌های استاتیک مرتبط یک صفحه، مانند فایل‌های js.، css.، تصاویر و غیره، مجددا انجام نمی‌شود که این مورد خود مزیتی است نسبت به حالت متداول برنامه‌های ASP.NET Core MVC و یا Razor pages. در حالت Blazor 8x SSR، فقط یک partial update از نوع Ajax ای انجام می‌شود.
به این قابلیت، enhanced navigation هم گفته می‌شود. برای مثال زمانیکه یک فرم SSR را در Blazor 8x به سمت سرور ارسال می‌کنیم، موقعیت scroll به صورت خودکار ذخیره و بازیابی می‌شود تا کاربر با یک full post back مواجه نشده و موقعیت جاری خود را در صفحه از دست ندهد (چنین ایده‌ای، یک زمانی در برنامه‌های ASP.NET Web forms هم برقرار بود و هست! به نظر مایکروسافت هنوز دلتنگ طراحی قدیمی ASP.NET Web forms است!).

- همچنین به محض نمایش صفحه‌ی جزئیات محصول، پس از پایان کار نمایش آن، یک اتصال وب‌سوکت هم برقرار شده که مرتبط با جزیره‌ی تعاملی Blazor server تعریف شده، یا همان کامپوننت RelatedProducts است.

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


در حین disconnect، شماره ID اتصال SignalR ای که دیگر به آن نیازی نیست، به برنامه ارسال می‌شود تا به صورت خودکار در سمت سرور بسته شود. تمام این موارد توسط blazor.web.js فریم‌ورک، مدیریت می‌شوند.
در این تصویر ابتدا به آدرس http://localhost:5136/ProductDetails/1 مراجعه کرده‌ایم که سبب برقراری اتصال یک وب‌سوکت شده‌است. سپس با کلیک بر روی دکمه‌ی back، به صفحه‌ی SSR مشاهده‌ی لیست محصولات برگشته‌ایم. در این حالت، دستور قطع اتصال SignalR قبلی صادر شده‌است.


نحوه‌ی مدیریت Pre-rendering در جزایر تعاملی Blazor 8x

به صورت پیش‌فرض زمانیکه از حالت رندر InteractiveServer استفاده می‌کنیم، قابلیت pre-rendering آن نیز فعال است. یعنی ابتدا حداقل قالب و قسمت‌های ثابت کامپوننت، در سمت سرور پردازش و رندر شده و سپس به سمت کلاینت ارسال می‌شوند. در این حالت کاربر، تجربه‌ی کاربری روان‌تری را شاهد خواهد بود؛ چون برای مدتی نباید منتظر آماده شدن کل UI مرتبط باشد و حداقل، قسمت‌هایی از صفحه که تعاملی نیستند، قابل دسترسی و مشاهده هستند.
اگر به هر دلیلی نیاز به غیرفعال کردن این قابلیت را دارید، باید به صورت زیر عمل کرد:
<RelatedProducts ProductId="Convert.ToInt32(ProductId)" @rendermode="@(new InteractiveServerRenderMode(false))"/>
در این حالت اگر برنامه را اجرا کنید، در حین نمایش صفحه‌ی اصلی در برگیرنده‌ی از نوع SSR، فقط جای این کامپوننت در صفحه مشخص می‌شود و پس از برقراری اتصال با سرور از طریق اتصال SignalR، شاهد UI کامپوننت RelatedProducts خواهیم بود، که نسبت به قبل، وقفه‌ای را سبب خواهد شد.

نحوه‌ی تعریف خواص استاتیک InteractiveServer بکار گرفته شده و یا کلاس InteractiveServerRenderMode را در ادامه مشاهده می‌کنید. جهت سهولت تعریف این موارد، سطر زیر که یک using static است، به فایل Imports.razor_ اضافه شده‌است:
@using static Microsoft.AspNetCore.Components.Web.RenderMode

public static class RenderMode
  {
    public static InteractiveServerRenderMode InteractiveServer { get; } = new InteractiveServerRenderMode();

    public static InteractiveWebAssemblyRenderMode InteractiveWebAssembly { get; } = new InteractiveWebAssemblyRenderMode();

    public static InteractiveAutoRenderMode InteractiveAuto { get; } = new InteractiveAutoRenderMode();
  }


public class InteractiveServerRenderMode : IComponentRenderMode
  {
    public InteractiveServerRenderMode()
      : this(true)
    {
    }

    public InteractiveServerRenderMode(bool prerender) => this.Prerender = prerender;

    public bool Prerender { get; }
  }


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید: Blazor8x-Server-Normal.zip  
بازخوردهای دوره
آشنایی با AOP Interceptors
باید از قابلیت scan در StructureMap استفاده کنید:
            ObjectFactory.Initialize(x =>
            {
                var dynamicProxy = new ProxyGenerator();
                x.Scan(scanner =>
                    {
                        scanner.AssemblyContainingType<IMyType>(); // نحوه یافتن اسمبلی لایه سرویس

                        // Connect `IName` interface to 'Name' class automatically
                        scanner.WithDefaultConventions()
                               .OnAddedPluginTypes(plugin => plugin.EnrichWith(target =>
                                    dynamicProxy.CreateInterfaceProxyWithTargetInterface(target.GetType().GetInterfaces().First(),
                                                                 target.GetType().GetInterfaces(), target,
                                                                 new LoggingInterceptor())
                               ));
                    });
            });
- در این حالت AssemblyContainingType مشخص می‌کند که کدام اسمبلی باید اسکن شود.
- WithDefaultConventions یعنی هرجایی IName داشتیم را به صورت خودکار به Name متصل کن. (روال پیش فرض سیم کشی اینترفیس‌ها و کلاس‌ها برای وهله سازی)
- OnAddedPluginTypes یک Callback هست که زمان انجام اولیه تنظیمات به ازای هر type یافت شده فراخوانی می‌شود. در اینجا می‌شود با استفاده از EnrichWith و ProxyGenerator کار اتصال کلاس Interceptor را انجام داد.
مطالب
ساختار داده‌های خطی 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 نیز حفظ می‌کند.

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

مطالب
امکان تعریف اعضای static abstract در اینترفیس‌های C# 11
امکان داشتن اعضای static abstract در اینترفیس‌ها شاید عجیب به‌نظر برسد یا حتی غیرضروری؛ اما در C# 11، پایه‌ی قابلیت جدیدی به نام «ریاضیات جنریک» شده‌است. به همین جهت در ابتدا نیاز است با اعضای static abstract آشنا شد و در قسمتی دیگر به «ریاضیات جنریک» پرداخت.


مثالی جهت توضیح علت نیاز به اعضای static abstract در اینترفیس‌ها

فرض کنید قصد داریم حاصل جمع اعضای یک آرایه‌ی int را محاسبه کنیم:
namespace CS11Tests;

public class StaticAbstractMembers
{
    public static void Test()
    {
        var sum = AddAll(new[] { 1, 2, 3, 4 });
        Console.WriteLine(sum);
    }

    private static int AddAll(int[] values)
    {
        int result = 0;
        foreach (var value in values)
        {
            result += value;
        }
        return result;
    }
}
روش متداول اینکار را در اینجا ملاحظه می‌کنید که حلقه‌ای بر روی عناصر آرایه، جهت یافتن حاصل جمع آن‌ها تشکیل شده‌است. اکنون فرض کنید بجای آرایه‌ای که در متد Test استفاده شده، از آرایه‌ی زیر استفاده شود:
var sum = AddAll(new[] { 1, 2, 3, 4, 0.68 });
اینبار با خطای زیر متوقف می‌شویم:
Argument 1: cannot convert from 'double[]' to 'int[]' [CS11Tests]csharp(CS1503)
عنوان می‌کند که آرایه‌ی مدنظر از نوع []double تشخیص داده شده‌است و متد AddAll، تنها آرایه‌های از نوع int را قبول می‌کند. در جهت رفع این مشکل شاید بهتر باشد نمونه‌ی جنریک متد AddAll را ایجاد کنیم، تا بتوان انواع و اقسام نوع‌های ممکن را به آن ارسال کرد:
private static T AddAll<T>(T[] values)
    {
        T result = 0;
        foreach (var value in values)
        {
            result += value;
        }
        return result;
    }
اما اینکار میسر نیست. چون زمانیکه از T استفاده می‌شود، مفهوم و امکان وجود «عدد صفر» در آن نوع، مشخص نیست. یک روش حل این مشکل، مقید و محدود کردن نوع T است. برای مثال عنوان کنیم که T، عددی است و از نوع INumber (فرضی/خیالی) است و این INumber فرضی، به همراه مفهوم عدد صفر هم هست. یعنی اولین سطر بدنه‌ی متد AddAll را باید بتوان به صورت زیر بازنویسی کرد:
T result = T.Zero;
یعنی باید بتوان از طریق یک «نوع» عمومی مانند T (نه وهله‌ای/نمونه‌ای/instance ای از آن نوع؛ دقیقا خود آن نوع) به خاصیت Zero آن نوع، دسترسی یافت و آن خاصیت هم باید از نوع استاتیک باشد و چون تا C# 10 و دات نت 6، چنین امکانی مهیا نشده بود (البته در حالت preview قرار داشت)، تنها راه ممکن، تهیه‌ی یک نمونه‌ی جدید double متد AddAll است/بود.
در C# 11 و دات نت 7، با معرفی اینترفیس جدید INumber، می‌توان قید <where T : INumber<T را به T اعمال کرد (مانند نمونه‌ی زیر) و همچنین با استفاده از اعضای static abstract این اینترفیس، به مقدار T.Zero هم دسترسی یافت و اینبار قطعه کد زیر، بدون مشکل در C# 11 کامپایل می‌شود:
using System.Numerics;

namespace CS11Tests;

public class StaticAbstractMembers
{
    public static void Test()
    {
        //var sum = AddAll(new[] { 1, 2, 3, 4 });
        var sum = AddAll(new[] { 1, 2, 3, 4, 0.68 });
        Console.WriteLine(sum);
    }

    private static T AddAll<T>(T[] values) where T : INumber<T>
    {
        T result = T.Zero;
        foreach (var value in values)
        {
            result += value;
        }
        return result;
    }
}
اگر به تعاریف INumber جدید مراجعه کنیم، نه فقط به خواص abstract static جدیدی می‌رسیم (که امکان دسترسی به T.Zero را میسر کرده‌اند)،
abstract static TSelf One { get; }
abstract static TSelf Zero { get; }
بلکه امکان تعریف اپراتورهای abstract static هم میسر شده‌اند (به همین جهت است که در کدهای فوق سطر result += value، هنوز هم کار می‌کند):
abstract static TResult operator +(TSelf left, TOther right);


مثال دیگری از کاربرد اعضای abstract static در اینترفیس‌ها

فرض کنید اینترفیس ISport را به همراه دو پیاده سازی از آن، به صورت زیر تعریف کرده‌ایم:
public interface ISport
{
    bool IsTeamSport();
}

public class Swimming : ISport
{
    public bool IsTeamSport() => false;
}

public class Football : ISport
{
    public bool IsTeamSport() => true;
}
اکنون جهت کار با متد IsTeamSport و تعریف جنریک این متد، می‌توان به صورت متداول زیر عمل کرد که در آن T، مقید به ISport شده‌است:
public class StaticAbstractMembers
{
    public static void Display<T>(T sport) where T : ISport
    {
        Console.WriteLine("Is Team Sport:" + sport.IsTeamSport());
    }
}
برای کار با آن هم باید حتما نمونه‌ای از ()new Football و یا ()new Swimming را به آن ارسال کرد:
Display(new Football());
سؤال: آیا با توجه به مشخص بودن و محدود بودن نوع T، می‌توان با حذف پارامتر T sport، به متد IsTeamSport اینترفیس ISport دسترسی یافت؟ یعنی تعریف متد Display را طوری تغییر داد تا دیگر نیاز به نمونه سازی ()new Football نداشته باشد. همینقدر که نوع Football مشخص بود، بتوان متد IsTeamSport آن‌را فراخوانی کرد.
پاسخ: تا پیش‌از C# 11 یکی از روش‌‌های انجام اینکار، استفاده از reflection بود. اما در C# 11 با کمک static abstractها می‌توان تعاریف این اینترفیس و پیاده سازی‌های آن‌را به صورت زیر تغییر داد:
public interface ISport
{
    static abstract bool IsTeamSport();
}

public class Swimming : ISport
{
    public static bool IsTeamSport() => false;
}

public class Football : ISport
{
    public static bool IsTeamSport() => true;
}
تا اینبار جهت دسترسی به متد IsTeamSport،‌مستقیما بتوان به خود «نوع»، «بدون نیاز به نمونه سازی آن» مراجعه کرد و قطعه کد زیر در C# 11 معتبر است:
public class StaticAbstractMembers
{
    public static void Display<T>() where T : ISport
    {
        Console.WriteLine("Is Team Sport:" + T.IsTeamSport());
    }
}
مطالب
ارائه‌ی قالبی عمومی برای استفاده از تقویم‌های جاوااسکریپتی در Blazor
در این مطلب قصد داریم کتابخانه‌ای با قابلیت استفاده‌ی مجدد را جهت بکارگیری «PersianDatePicker یک DatePicker شمسی به زبان JavaScript که از تاریخ سرور استفاده می‌کند» ارائه دهیم. نکات ارائه شده‌ی در آن‌را می‌توان جهت تبدیل و استفاده‌ی از تمام DatePickerهای مشابه نیز بکاربرد.



نیازهای یک ورودی تاریخ سازگار با EditForm

- باید قابلیت استفاده‌ی مجدد را داشته باشد. یعنی باید به صورت یک کامپوننت مجزا و یا به صورت یک کتابخانه‌ی مجزا ارائه شود.
- باید با سیستم اعتبارسنجی EditForm یکپارچه باشد.
- باید جنریک باشد. یعنی باید بتوان در صورت نیاز DateTime ، DateTimeOffset و DateOnly و نمونه‌های nullable آن‌هارا توسط این کامپوننت دریافت کرد و ورودی و خروجی آن رشته‌ای نباشد.


نیاز به ارث‌بری از <InputBase<T جهت ارائه‌ی کامپوننت‌هایی سازگار با EditForm

تقریبا تمام کامپوننت‌های استاندارد EditForm ارائه شده‌ی توسط Blazor، از کامپوننت پایه‌ای به نام <InputBase<T مشتق می‌شوند. این کلاس، یک کلاس abstract است که قابلیت‌های بیشتری را نسبت به یک input ساده‌ی HTML ای مانند اعتبارسنجی سازگار با EditForm ارائه می‌دهد. به همین جهت توصیه می‌شود تا اگر خواستید یک کامپوننت ورودی را برای استفاده‌ی در Blazor و EditForm آن طراحی کنید، با ارث‌بری از این کلاس شروع کنید و صرفا کار را با یک input ساده، شروع نکنید.
برای استفاده‌ی از آن، ابتدای کامپوننت Blazor ما به این صورت شروع خواهد شد:
@typeparam T
@inherits InputBase<T>
که دو متد را برای بازنویسی در اختیار ما قرار می‌دهد:
    protected override bool TryParseValueFromString(
        string? value,
        [MaybeNullWhen(false)] out T result,
        [NotNullWhen(false)] out string? validationErrorMessage)
    {
      // ...
    }

    protected override string FormatValueAsString(T? value)
    {
      // ...
    }
علت وجود این دو متد، این است که مرورگرها، رشته‌ها را در اختیار ما قرار می‌دهند و ما باید راهی را برای تبدیل T به یک رشته و عکس آن را ارائه دهیم. با بازنویسی متد TryParseValueFromString، می‌توان رشته‌ی دریافتی از کاربر را تبدیل به T کرد و اگر این تبدیل میسر نبود، با مقدار دهی validationErrorMessage، مشکل را به کاربر، با یک پیام شکست اعتبارسنجی، اعلام کرد. کار متد FormatValueAsString، تبدیل T به یک رشته‌است تا در input واقع در صفحه، نمایش داده شود. در اینجا می‌توان فرمت خاصی را به شیء دریافتی اعمال و نمایش داد.


ایجاد یک کتابخانه‌ی جدید برای محصور سازی DatePicker جاوااسکریپتی

چون قصد استفاده‌ی مجدد از این کامپوننت جدید را در پروژه‌های مختلف داریم، بهتر است آن‌را تبدیل به یک «کتابخانه‌ی Blazor» کنیم. به همین جهت کتابخانه‌ی فرضی BlazorPersianJavaScriptDatePicker.Lib را در اینجا ایجاد کرده‌ایم.
در ابتدا دو فایل PersianDatePicker.js و PersianDatePicker.css موجود و مدنظر را در پوشه‌های js و css پوشه‌ی wwwroot این کتابخانه کپی می‌کنیم. بنابراین استفاده کننده‌ی از آن، مانند پروژه‌ی blazor wasm جدیدی به نام BlazorPersianJavaScriptDatePicker، باید ارجاعاتی را به آن‌ها به صورت زیر اضافه کند:
<link href="_content/BlazorPersianJavaScriptDatePicker.Lib/css/PersianDatePicker.css" rel="stylesheet"/>
<script src="_content/BlazorPersianJavaScriptDatePicker.Lib/js/PersianDatePicker.js?v=1"></script>
همچنین در فایل Imports.razor_ آن نیز بهتر است فضای نام این کتابخانه، ذکر شود تا به سادگی بتوان از کامپوننت PersianDatePicker در آن استفاده کرد:
@using BlazorPersianJavaScriptDatePicker.Lib


شروع به پیاده سازی کامپوننت PersianDatePicker

در ادامه کامپوننت جدید PersianDatePicker.razor را به پروژه‌ی کتابخانه اضافه می‌کنیم. قسمت razor آن به صورت زیر است:
@typeparam T
@inherits InputBase<T>

<div>
    <span style="cursor:pointer"
          onclick="PersianDatePicker.Show(document.getElementById('@ElementId'), '@Today')">
        📅
    </span>
    <input
        @attributes="@AdditionalAttributes"
        type="text" dir="ltr"
        @ref="ElementReference"
        name="@ElementId" id="@ElementId"
        autocapitalize="off" autocorrect="off" autocomplete="off"
       
        value="@EnteredValue"
        @oninput="OnInput"/>

    @if (ValueExpression is not null)
    {
        <ValidationMessage For="@ValueExpression"/>
    }
</div>
همانطور که مشاهده می‌کنید، کار با جنریک تعریف کردن و ارث‌بری از InputBase شروع می‌شود.
در اینجا با کلیک بر روی دکمه‌ی 📅، کار فراخوانی متد PersianDatePicker.Show مربوط به datePicker جاوا اسکریپتی صورت می‌گیرد. همچنین هر طراحی را که در اینجا ارائه دهیم، قالب UI پیش‌فرض InputBase را بازنویسی می‌کند.


نیاز به دریافت تاریخ تنظیم شده‌ی توسط کدهای جاوااسکریپتی در کامپوننت Blazor

کتابخانه‌های جاوااسکریپتی با مقداردهی مستقیم textbox.value سبب تغییر مقدار آن می‌شوند. نکته‌ی مهم اینجا است که نه فقط Blazor این تغییرات را ردیابی نمی‌کند، بلکه اگر با استفاده از متد استاندارد جاوااسکریپتی addEventListener به تغییرات این input گوش فرا دهیم، هیچ رخدادی را مشاهده نخواهیم کرد. به همین جهت نیاز است اندکی کدهای PersianDatePicker.js را تغییر دهیم (و این مورد جهت تمام کتابخانه‌های مشابه یکسان است):
    function setValue(date) {
        _textBox.value = date;

        // NOTE: To notify the addEventListener('change', fn)
        _textBox.dispatchEvent(new Event('change'));

        _textBox.focus();
        hide();
        try {
            _textBox.onchange();
        }catch(ex) {}
    }
در اینجا پس از تغییر خاصیت value، باید به صورت دستی سبب بروز رخداد change شد تا addEventListenerها بتوانند این رخداد را ردیابی کنند. به همین جهت فایل مجزایی را به نام wwwroot\js\activateDatePicker.js به کتابخانه‌ی blazor اضافه می‌کنیم:
window.activateDatePicker = {
  enableDatePicker: function (element, objectReference) {
       element.addEventListener('change', function (evt) {    
            objectReference.invokeMethodAsync("OnInputFieldChanged", this.value);
       });
  }
};
هدف از این کدها این است که جهت element یا همان datePicker جاری، بتوان رخ‌داد change را ثبت کرد و به تغییرات آن گوش فرا داد تا هر زمانیکه کدهای جاوا اسکریپتی datePicker سبب تغییری در خاصیت value شدند، بتوان آن‌را به کامپوننت Blazor ارسال کرد. وهله‌ای از این کامپوننت توسط objectReference در اینجا دریافت شده و سپس متد OnInputFieldChanged کامپوننت را با مقدار جدید وارد شده، فراخوانی می‌کند.
بنابراین این فایل جدید نیز باید به index.html مصرف کننده اضافه شود:
<script src="_content/BlazorPersianJavaScriptDatePicker.Lib/js/activateDatePicker.js?v=1"></script>


فعالسازی DatePicker در اولین بار نمایش کامپوننت Blazor

تا اینجا زیرساخت دریافت مقدار تنظیمی توسط کاربر را در کامپوننت Blazor فراهم کردیم. اکنون نوبت به استفاده‌ی از آن است:
public partial class PersianDatePicker<T> : IDisposable
{
    private bool _isDisposed;
    private DotNetObjectReference<PersianDatePicker<T>>? _objectReference;
    private string ElementId { get; } = Guid.NewGuid().ToString("N");
    private ElementReference? ElementReference { set; get; }
    private string Today { get; } = DateTime.Now.ToShortPersianDateString();

    [Inject] private IJSRuntime JsRuntime { set; get; } = default!;

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            _objectReference = DotNetObjectReference.Create(this);
            await JsRuntime.InvokeVoidAsync("activateDatePicker.enableDatePicker", ElementReference, _objectReference);
            EnteredValue = CurrentValueAsString;
            StateHasChanged();
        }
    }

    protected override void Dispose(bool disposing)
    {
        base.Dispose(disposing);
        if (!_isDisposed)
        {
            try
            {
                _objectReference?.Dispose();
            }
            finally
            {
                _isDisposed = true;
            }
        }
    }
}
- اگر دقت کرده باشید در تعاریف razor کامپوننت، "ref="ElementReference@ وجود دارد که یک ElementReference است و توسط آن می‌توان در متد OnAfterRenderAsync، ارجاعی را به المان جاری، به کدهای جاوااسکریپتی متد enableDatePicker ارسال کرد.
- همچنین چون نمی‌خواهیم متد OnInputFieldChanged را به صورت static تعریف کنیم، نیاز است تا یک DotNetObjectReference را ایجاد و به متد enableDatePicker ارسال کرد تا توسط آن بتوان به یک instance method کلاس جاری دسترسی یافت و به سادگی مقادیر کامپوننت را تغییر داد:
[JSInvokable]
public void OnInputFieldChanged(string? value)
- در پایان کار کامپوننت، باید این DotNetObjectReference را Dispose کرد.


نیاز به تبدیل T به تاریخ رشته‌ای و برعکس

زیر ساخت تبدیلات جنریک تاریخ میلادی به شمسی در کتابخانه‌ی « DNTPersianUtils.Core » پیش‌بینی شده‌است و فقط کافی است از آن استفاده کنیم. با وجود این زیرساخت، تهیه‌ی کامپوننت‌های جنریک تاریخ شمسی بسیار ساده می‌شود:
public partial class PersianDatePicker<T> : IDisposable
{
    private string? _enteredValue;

    private string? EnteredValue
    {
        set => _enteredValue = value;
        get => UsePersianNumbers ? _enteredValue.ToPersianNumbers() : _enteredValue;
    }

    [Parameter] public bool UsePersianNumbers { set; get; }

    [Parameter] public string ParsingErrorMessage { get; set; } = "لطفا در ورودی {0} تاریخ شمسی معتبری را وارد نمائید.";

    [Parameter] public int BeginningOfCentury { set; get; } = 1400;

    private void OnInput(ChangeEventArgs e)
    {
        SetCurrentValue(e.Value as string);
    }

    private void SetCurrentValue(string? value)
    {
        EnteredValue = value;
        CurrentValueAsString = value;
    }

    [JSInvokable]
    public void OnInputFieldChanged(string? value)
    {
        SetCurrentValue(value);
    }

    protected override void OnInitialized()
    {
        base.OnInitialized();
        SanityCheck();
    }

    protected override bool TryParseValueFromString(
        string? value,
        [MaybeNullWhen(false)] out T result,
        [NotNullWhen(false)] out string? validationErrorMessage)
    {
        validationErrorMessage = string.Format(CultureInfo.InvariantCulture, ParsingErrorMessage, DisplayName);
        if (!value.TryParsePersianDateToDateTimeOrDateTimeOffset(out result, BeginningOfCentury))
        {
            return false;
        }

        if (result is null)
        {
            throw new InvalidOperationException(validationErrorMessage);
        }

        validationErrorMessage = null;
        return true;
    }

    protected override string FormatValueAsString(T? value)
    {
        return !string.IsNullOrWhiteSpace(EnteredValue) ? EnteredValue : value.FormatDateToShortPersianDate();
    }

    private void SanityCheck()
    {
        if (!Value.IsDateTimeOrDateTimeOffsetType())
        {
            throw new InvalidOperationException(
                "The `Value` type is not a supported `date` type. DateTime, DateTime?, DateTimeOffset and DateTimeOffset? are supported.");
        }
    }

    // ...
}
در اینجا قسمت نهایی و تکمیلی کامپوننت محصور کننده‌ی DatePicker را مشاهده می‌کنید که بسیار ساده‌است:
- InputBase به همراه یک خاصیت عمومی دوطرفه‌ی Value است که امکان تعریفی مانند bind-Value@ را میسر می‌کند.
- این Value به همراه یک خاصیت متناظر رشته‌ای به نام CurrentValueAsString نیز هست که در اینجا از آن استفاده می‌کنیم و کار با آن، بایندینگ دوطرفه و همچنین اعتبارسنجی خودکار و فعالسازی متدهای بازنویسی شده‌ی InputBase را میسر می‌کند.
- پیاده سازی متدهای بازنویسی شده‌ی جنریک TryParseValueFromString و FormatValueAsString، با استفاده از دو متد TryParsePersianDateToDateTimeOrDateTimeOffset و FormatDateToShortPersianDate کتابخانه‌ی « DNTPersianUtils.Core » انجام شده‌اند و اصل کار تهیه‌ی یک کامپوننت جنریک تاریخ شمسی را انجام می‌دهند.


استفاده‌ی از کامپوننت Blazor تهیه شده‌

یک کامپوننت تاریخ شمسی باید بتواند تمام حالات و نوع‌های زیر را پوشش دهد که به لطف جنریک بودن کامپوننت تهیه شده، این امر میسر است:
using System.ComponentModel.DataAnnotations;

namespace BlazorPersianJavaScriptDatePicker.ViewModels;

public class InputPersianDateViewModel
{
    [Required] public string Name { set; get; } = default!;

    [Required] public DateTime BirthDayGregorian { set; get; } = DateTime.Now.AddYears(-40);

    public DateTime? LoginAt { set; get; } = DateTime.Now.AddMinutes(-2);

    [Required] public DateTimeOffset LogoutAt { set; get; }

    public DateTimeOffset? RegisterAt { set; get; } = DateTimeOffset.Now.AddMinutes(-10);
}
سپس از این کامپوننت، در صفحه‌ی Index مثال پیوست به صورت زیر استفاده شده‌است:
<EditForm Model="Model" OnValidSubmit="DoSave">
    <DataAnnotationsValidator/>

    <div>
        <label>تاریخ تولد</label>
        <div>
            <PersianDatePicker
                @bind-Value="Model.BirthDayGregorian"
                UsePersianNumbers="false"
               />
        </div>
    </div>

    <button type="submit">ارسال</button>
</EditForm>


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید:  BlazorPersianJavaScriptDatePicker.zip
بازخوردهای دوره
تزریق خودکار وابستگی‌ها در برنامه‌های ASP.NET Web forms
وقت بخیر مهندس نصیری.
من شخصا بیشتر کلاس‌های اصلی مربوط به ASP.NET  رو سفارشی کردم (Page، UserControl، HttpHandler و ...) و در سازنده عملیات‌های مربوط به سیم کشی! رو انجام دادم. (قبلا در مباحث مربوط به Entity Framework این روش رو توضیح داده بودید) ولی به نظرم این روش HttpModule خیلی منظمتره.
مسئله ای که به نظر می‌تونه کمک کنه اینه که کاش فقط Pageها رو سیم کشی نمی‌کردید (همه چیز حتی HttpHandlerها هم میشه همینجا کلکشون کنده بشه و همچنین WebControlها)
در هر حال دستتون درد نکنه.