نظرات مطالب
Blazor 5x - قسمت یازدهم - مبانی Blazor - بخش 8 - کار با جاوا اسکریپت
تکمیل JavaScript Isolation در Blazor 6x

همانطور که کمی بالاتر نیز عنوان شد، CSS Isolation جزئی از تازه‌های Blazor 5x بود؛ اکنون مشابه این قابلیت جهت فایل‌های js. به Blazor 6x هم اضافه شده‌است و روش کار با آن نیز همانند CSS Isolation است (البته این قابلیت از نگارش 5 هم وجود داشت؛ اما اینبار دیگر نیازی به تعریف فایل ماژول آن در wwwroot نیست). یعنی اگر کامپوننت ما در مسیر Pages/Panel.razor قرار داشته باشد، می‌توان برای این تک فایل، فایل js. متناظری را به نام Pages/Panel.razor.js تعریف کرد؛ با الگوی Component>.razor.js>. سپس اگر محتوی این فایل js. به صورت زیر باشد:
export function error(){
    alert('oops, an error');
}
روش فراخوانی آن در همان کامپوننت به صورت زیر خواهد بود (یعنی در اصل دیگر مهم نیست که این فایل js. کجا قرار می‌گیرد، هنگام publish به خروجی کپی خواهد شد):
var module = await JS.InvokeAsync<IJSObjectReference>("import", "./Panel.razor.js");
await module.InvokeVoidAsync("error");
مزیت اینکار، نزدیک نگه داشتن static assets یک کامپوننت، در کنار آن است و به این ترتیب قابل درک‌تر کردن برنامه:
Pages/Panel.razor
Pages/Panel.razor.js
Pages/Panel.razor.css
به علاوه JS Isolation دو مزیت دیگر را هم به همراه دارد:
- دیگر نیازی به تعریف توابع و متدهای جاوا اسکریپتی در global namespace نیست (این متدها و اشیاء، به شیء سراسری window اضافه نمی‌شوند). Isolation در اینجا در اصل به معنای امکان استفاده‌ی از JavaScript modules است.
- استفاده کنندگان از پروژه‌های کتابخانه‌ای دیگر نیازی به الحاق دستی این فایل‌ها ندارند. منظور از الحاق دستی یا ذکر src تگ script اضافه شده به index.html، در مطلب تولید کتابخانه‌های Razor توضیح داده شده‌است و از الگوی content/{PACKAGE ID}/{SCRIPT PATH AND FILENAME (.js)}_/. جهت مشخص سازی نام نهایی فایل js. به همراه کتابخانه، پیروی می‌کند. البته اگر نیاز است این نوع فایل‌ها در کتابخانه‌ها مستقیما استفاده شوند، باید مسیر فوق در کدها حتما ذکر شود:
_module = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/RazorClassLibrary/componentName.razor.js");

یک نکته: روش صحیح کار با ماژول‌ها همانطور که در نکات فوق نیز بررسی شد، به صورت زیر است و باید در OnAfterRenderAsync شروع شده و سپس در آخر کار Dispose شوند:
export function showPrompt(message) {
  return prompt(message, 'Type anything here');
}

@page "/call-js-example-6"
@implements IAsyncDisposable
@inject IJSRuntime JS

<h1>Call JS Example 6</h1>

<p>
    <button @onclick="TriggerPrompt">Trigger browser window prompt</button>
</p>

<p>
    @result
</p>

@code {
    private IJSObjectReference? module;
    private string? result;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSObjectReference>("import", 
                "./scripts.js");
        }
    }

    private async Task TriggerPrompt()
    {
        result = await Prompt("Provide some text");
    }

    public async ValueTask<string?> Prompt(string message) =>
        module is not null ? 
            await module.InvokeAsync<string>("showPrompt", message) : null;

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            await module.DisposeAsync();
        }
    }
}
نظرات مطالب
OutputCache در ASP.NET MVC
به این صورت؛ البته این روش کش تمام child actionها را با هم پاک می‌کند:
OutputCacheAttribute.ChildActionCache = new MemoryCache("NewRandomStringNameToClearTheCache");
مطالب
ارائه‌ی قالبی عمومی برای استفاده از تقویم‌های جاوااسکریپتی در 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
مطالب
ایجاد تایمرها در برنامه‌های Angular
عموما در برنامه‌های جاوا اسکریپتی با استفاده از متدهای setTimeout و setInterval می‌توان یک تایمر را ایجاد کرد. اما در برنامه‌های Angular با توجه به استفاده‌ی از کتابخانه‌ی RxJS، امکان ایجاد تایمرهای reactive نیز وجود دارد که در این مطلب آن‌ها را مرور خواهیم کرد.


ایجاد تایمرهای متوالی و بی‌وقفه

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


در این مثال می‌خواهیم تایمری را ایجاد کنیم که هر ثانیه یکبار، کدی را اجرا کند:
import { Observable } from "rxjs/Observable";
import "rxjs/add/observable/interval";
import { Subscription } from "rxjs/Subscription";

@Component()
export class UsingTimersComponent {

  private intervalSubscription: Subscription;
  interval = 0;

  startInterval() {
    const interval = Observable.interval(1000);
    this.intervalSubscription = interval.subscribe(i => this.interval += i);
  }

  stopInterval() {
    this.intervalSubscription.unsubscribe();
  }
}
با این قالب:
<div class="panel panel-default">
  <div class="panel-heading">
    <h2 class="panel-title">Observable.interval(1000)</h2>
  </div>
  <div class="panel-body">
    <div>
      <label>interval: </label> {{interval}}
    </div>
    <div>
      <button (click)="startInterval()" class="btn btn-success">Start</button>
      <button (click)="stopInterval()" class="btn btn-danger">Stop</button>
    </div>
  </div>
</div>
عملگر interval باید از مسیر rxjs/add/observable/interval دریافت شود که در ابتدای تعاریف کامپوننت مشخص شده‌است.
 پس از آن فراخوانی Observable.interval(1000) یک Observable را ایجاد می‌کند که توانایی صدور رخ‌دادهایی را در بازه‌های زمانی متوالی 1000 میلی ثانیه‌ای، دارا است.
اکنون مشترکین به آن، اعداد متوالی شروع شده‌ی از صفر را در هر ثانیه یکبار، دریافت می‌کنند:
this.intervalSubscription = interval.subscribe(i => this.interval += i);
این تایمر، به نحوی که تعریف شده‌است، تا ابد ادامه پیدا خواهد کرد. برای توقف آن نیاز است همانند روال معمول کار با Observableها، اشتراک به آن را لغو کرد:
this.intervalSubscription.unsubscribe();


مطلع شدن از پایان کار یک تایمر

با استفاده از اپراتور finally که از مسیر rxjs/add/operator/finally قابل import است، می‌توان رخ‌داد لغو اشتراک به این Observable و یا همان خاتمه‌ی تایمر را در اینجا دریافت کرد:
this.intervalSubscription = interval
      .finally(() => console.log("All done!"))
      .subscribe(i => this.interval += i);


ایجاد تایمرهای خود متوقف شونده

با استفاده از عملگر Observable.timer که در مسیر rxjs/add/observable/timer قرار دارد، می‌توان تایمری را ایجاد کرد که پس از یک تاخیر مشخص شده‌، اجرا شود و بلافاصله خاتمه یابد:
const timer = Observable.timer(1000);
timer.subscribe(data => console.log('ding!'));
در اینجا تایمری ایجاد شده‌است که پس از یک ثانیه اجرا شده و کد نمایش ding را در کنسول مرورگر اجرا می‌کند. سپس به صورت خودکار خاتمه خواهد یافت. در اینجا data نیز مساوی صفر است (اولین بار اجرای تایمر).
این تایمر امکان اجرای در بازه‌های زمانی مشخصی را نیز دارا است:
const moreThanOne$ = Observable.timer(2000, 500);
moreThanOne$.subscribe(data => console.log('timer with args', data));
اولین پارامتر آن مشخص می‌کند که این تایمر باید پس از 2 ثانیه تاخیر، شروع به کار کند و دومین آرگومان آن مشخص می‌کند که این تایمر تا ابد، با فواصل زمانی هر 500 میلی‌ثانیه یکبار، اجرا خواهد شد.


محدود کردن تعداد بار اجرای تایمر

اگر Observable.timer با پارامتر دوم آن بکار رود، بی‌نهایت بار اجرا خواهد شد. اما می‌توان این تعداد بار اجرا را توسط اپراتور take که از مسیر rxjs/add/operator/take قابل import است، محدود کرد:
let moreThanOne$ = Observable.timer(2000, 500).take(3);
moreThanOne$.subscribe(data => console.log('timer with args', data));
در اینجا تایمر تعریف شده، پس از یک وقفه‌ی آغازین 2 ثانیه‌ای شروع به کار می‌کند. سپس تنها دو بار دیگر در بازه‌های متوالی زمانی 500 میلی ثانیه یکبار، اجرا خواهد شد. یعنی جمعا سه بار با توجه به take(3) اجرا خواهد شد.


اجرای با تاخیر بازه‌های زمانی

با استفاده از اپراتور delay که از مسیر rxjs/add/operator/delay قابل import است، می‌توان هر بار اجرای callback تایمر را با یک تاخیر دریافت کرد:
const start = new Date();
const stream$ = Observable.interval(500).take(3);
stream$.delay(300).subscribe(x => {
    console.log('val',x);
    console.log( new Date() - start );
})
در اینجا تایمر از نوع interval تعریف شده، با توجه به استفاده‌ی از عملگر take، تنها سه بار اجرا می‌شود. اما این اجراها با تاخیری 300 میلی‌ثانیه‌ای به مشترکین آن‌ها اطلاع رسانی می‌گردند. به این ترتیب خروجی لاگ شده‌ی این عملیات به صورت ذیل خواهد بود:
val:0
800ms
val:1
1300ms
val:2
1800ms


ایجاد یک تایمر شمارش معکوس

فرض کنید می‌خواهید تایمری را ایجاد کنید که در طی یک شمارش معکوس، از عدد 10000 شروع شود و هر ثانیه یکبار 1000 واحد از آن کاهش یابد و زمانیکه به صفر رسید، متوقف شود.
این تایمر پس از import وابستگی‌های آن:
import { Observable } from "rxjs/Observable";
import "rxjs/add/observable/timer";
import "rxjs/add/operator/finally";
import "rxjs/add/operator/takeUntil";
import "rxjs/add/operator/map";
یک چنین تعریفی را پیدا می‌کند:
const interval = 1000;
const duration = 10 * 1000;
const stream$ = Observable.timer(0, interval)
      .finally(() => console.log("All done!"))
      .takeUntil(Observable.timer(duration + interval))
      .map(value => duration - value * interval);
stream$.subscribe(value => console.log(value));
در اینجا تایمر تعریف شده با توجه به آرگومان صفر تاخیر آن، بلافاصله شروع به کار می‌کند. همچنین با توجه به عدد interval آن، هر یک ثانیه یکبار اعداد صفر، یک و ... را به مشترکین خود ارسال خواهد کرد. اکنون می‌خواهیم این تایمر دقیقا پس از 11 ثانیه متوقف شود. یکی از روش‌های پیاده سازی آن استفاده از takeUntil است که در اینجا یک تایمر خود متوقف شوند را دریافت کرده‌است. این تایمر دقیقا پس از 11 ثانیه از شروع عملیات، یکبار اجرا شده و بلافاصله خاتمه پیدا می‌کند. همین صدور رخ‌داد، کار takeUntil را به پایان می‌رساند که این مورد نیز سبب خاتمه‌ی تایمر اصلی می‌شود.
در اینجا چون اعداد صادر شده‌ی از طرف تایمر، افزایشی هستند، نیاز است به روشی آن‌ها را تغییر داد. در یک چنین حالتی از اپراتور map استفاده می‌شود. در اینجا value، هربار مقدار افزایشی شروع شده‌ی از صفر را ارائه می‌دهد. توسط عملگر map، این خروجی افزایشی را به یک خروجی کاهشی تبدیل کرده‌ایم تا بتوان به یک تایمر شمارش معکوس رسید.


دریافت مدت زمان بین اجرای بازه‌های زمانی

Observable.timer با هر بار اجرا، اعداد شروع شده‌ی از صفر را به مشترکین ارسال می‌کند. اگر در این بین از اپراتور timeInterval قرار گرفته‌ی در مسیر rxjs/add/operator/timeInterval استفاده شود، این مقدار ارسالی از نوع مخصوص <TimeInterval<number خواهد بود که دارای خواص value و interval است:
const source = Observable.timer(0, 1000)
      .timeInterval()
      .map(x => x.value + ":" + x.interval)
      .take(5);

const subscription = source.subscribe(
      x => console.log("Next timeInterval: " + x),
      err => console.log("Error: " + err),
      () => console.log("Completed")
    );
در اینجا value همان صفر، یک و ... است و interval بیانگر زمان سپری شده‌ی بین دو صدور رخ‌داد می‌باشد.
در این مثال با استفاده از متد map، یک خروجی سفارشی تهیه شده‌است. اگر صرفا علاقمند به دریافت مقدار خاصیت interval باشید، می‌توان به صورت ذیل نیز عمل کرد:
const source = Observable.timer(0, 1000)
      .timeInterval()
      .pluck("interval")
      .take(5);
عملگر pluck که در مسیر rxjs/add/operator/pluck قرار دارد، خاصیت و یا خاصیت‌هایی از منبع را جهت بازگشت، انتخاب می‌کند. برای مثال در اینجا خاصیت interval یک شیء TimeInterval انتخاب شده‌است.


تعلیق و از سرگیری مجدد تایمرها

با قطع اشتراک از یک منبع تایمر، سبب توقف کامل آن خواهیم شد. اما اگر برای مدتی بخواهیم آن‌را در حالت تعلیق قرار دهیم، می‌توان به صورت ذیل عمل کرد:
import { Observable } from "rxjs/Observable";
import "rxjs/add/observable/never";
import "rxjs/add/observable/timer";
import { Subject } from "rxjs/Subject";

  tick: number;
  pauser = new Subject();
  tickerSource = new Subject();
  startTicker() {
    Observable.timer(0, 1000)
      .subscribe(this.tickerSource);

    this.pauser
      .switchMap(paused => paused ? Observable.never() : this.tickerSource).
      subscribe(t => this.tickerFunc(t));

    this.pauser.next(false); // resume
  }

  tickerFunc(tick) {
    this.tick = tick;
  }

  pauseTicker() {
    this.pauser.next(true);
  }

  resumeTicker() {
    this.pauser.next(false);
  }
نکته‌ی اصلی این طراحی در switchMap و Observable.never آن نهفته‌است. در اینجا وجود Subject سبب صدور رخدادی به مشترکین آن می‌شود. اگر توسط متد next آن false ارسال شود، سبب از سرگیری مجدد منبع اصلی یا همان تایمر برنامه می‌شود و اگر true ارسال شود، عملیات فراخوانی tickerFunc را با فراخوانی Observable.never به حالت تعلیق می‌برد.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
اشتراک‌ها
TypeScript 3.2 منتشر شد
let foo: bigint = BigInt(100); // the BigInt function
let bar: bigint = 100n;        // a BigInt literal
TypeScript 3.2 منتشر شد
مطالب
نمایش Breadcrumbs در برنامه‌های Angular
داشتن Breadcrumbs یکی از گزینه‌های مفید بهبود کاربری هر سایتی است و در برنامه‌های Angular با کوئری گرفتن از سیستم مسیریابی آن می‌توان به سادگی آن‌ها را تولید کرد.



ایجاد ساختاری برای نگهداری آرایه‌ی breadcrumbs

کامپوننت نمایش breadcrumbs را در مسیر src\app\core\bread-crumb ایجاد می‌کنیم. یعنی قصد داریم آن‌را به CoreModule برنامه اضافه کنیم؛ از این جهت که کامپوننت آن، تکمیل کننده‌ی app.component است و هر کامپوننتی که تنها در این کامپوننت ویژه بکار رود، محل تعریف آن در CoreModule خواهد بود.
به همین جهت کامپوننت bread-crumb را به صورت ذیل ایجاد می‌کنیم:
 ng g c core/bread-crumb
که تعاریف آن در فایل src\app\core\core.module.ts در قسمت exports و declarations درج خواهد شد:
import { BreadCrumbComponent } from "./bread-crumb/bread-crumb.component";

@NgModule({
  imports: [CommonModule, RouterModule],
  exports: [
    // components that are used in app.component.ts will be listed here.
    BreadCrumbComponent
  ],
  declarations: [
    // components that are used in app.component.ts will be listed here.
    BreadCrumbComponent
  ]
})
export class CoreModule {}
و سپس اینترفیس bread-crumb.ts را در مسیر src\app\core\bread-crumb\bread-crumb.ts به نحو زیر تعریف می‌کنیم:
export interface BreadCrumb {
  label: string;
  url: string;
};
برچسب و url، دو حداقل پیش‌فرض نمایش قطعات منوی breadcrumbs هستند.


تکمیل کامپوننت Breadcrumb

کدهای کامل کامپوننت Breadcrumb را در ذیل مشاهده می‌کنید:
import { Component, OnInit, ViewEncapsulation } from "@angular/core";
import { ActivatedRoute, NavigationEnd, Router } from "@angular/router";
import { BreadCrumb } from "./bread-crumb";
import { Observable } from "rxjs/Observable";

@Component({
  selector: "app-bread-crumb",
  templateUrl: "./bread-crumb.component.html",
  styleUrls: ["./bread-crumb.component.css"],
  encapsulation: ViewEncapsulation.None
})
export class BreadCrumbComponent implements OnInit {

  breadcrumbs$: Observable<BreadCrumb[]>;

  constructor(private activatedRoute: ActivatedRoute, private router: Router) { }

  ngOnInit() {
    this.breadcrumbs$ = this.router.events
      .filter(event => event instanceof NavigationEnd)
      .distinctUntilChanged()
      .map(event => event ? this.buildBreadCrumbs(this.activatedRoute.root) : []);
  }

  buildBreadCrumbs(route: ActivatedRoute, url: string = "", breadcrumbs: Array<BreadCrumb> = []): Array<BreadCrumb> {
    const routeDataBreadCrumbKey = "breadcrumb";
    const routeConfig = route.routeConfig;
    const path = routeConfig && routeConfig.path !== undefined ? routeConfig.path : "";

    let label = path;
    if (url === "") {
      label = "Home";
    } else if (routeConfig && routeConfig.data !== undefined) {
      label = routeConfig.data[routeDataBreadCrumbKey];
    }

    const nextUrl = `${url}${path}/`;
    const breadcrumb: BreadCrumb = {
      label: label,
      url: nextUrl
    };
    console.log("breadcrumb", { path: path, label: label, url: nextUrl, route: route });
    const newBreadcrumbs = [...breadcrumbs, breadcrumb];
    if (route.firstChild) {
      return this.buildBreadCrumbs(route.firstChild, nextUrl, newBreadcrumbs);
    }
    return newBreadcrumbs;
  }

}
توضیحات:
در اینجا در ابتدای کار، مشترک رخ‌داد NavigationEnd سیستم مسیریابی خواهیم شد:
  ngOnInit() {
    this.breadcrumbs$ = this.router.events
      .filter(event => event instanceof NavigationEnd)
      .distinctUntilChanged()
      .map(event => event ? this.buildBreadCrumbs(this.activatedRoute.root) : []);
  }
هر زمانیکه رخ‌داد مرور صفحه‌ی جاری به پایان رسید، بر اساس مسیر ریشه‌ی آن، متد buildBreadCrumbs فراخوانی می‌شود. این متد، یک متد بازگشتی است. از این جهت که مسیر جاری می‌تواند حاصل مرور یک مسیر والد و سپس چندین مسیر تو در توی فرزند و والد آن باشد. برای نمونه ممکن است مانند تصویر ابتدای بحث، مسیریابی شما به صورت ذیل تعریف شده باشد:
const routes: Routes = [
  {
    path: "breadCrumbTest",
    data: { breadcrumb: "Parent1" },
    children: [
      {
        path: "", component: Parent1Component
      },
      {
        path: "Parent1Child1",
        data: { breadcrumb: "Parent1-Child1" },
        children: [
          {
            path: "", component: Parent1Child1Component
          },
          {
            path: "Parent1Child1Child1", component: Parent1Child1Child1Component,
            data: { breadcrumb: "Parent1-Child1 Child1" }
          }
        ]
      }
    ]
  }
];
در اینجا چندین مسیر والد و فرزند تو در تو را مشاهده می‌کنید.
بر اساس قراردادی که در کامپوننت نمایش bread-crumbs درنظر گرفته‌ایم، عنوان هر مسیر در خاصیت data آن با کلید breadcrumb درج می‌شود:
 data: { breadcrumb: "Parent1-Child1 Child1" }
مقدار این خاصیت را به صورت ذیل در متد buildBreadCrumbs می‌خوانیم:
  buildBreadCrumbs(route: ActivatedRoute, url: string = "", breadcrumbs: Array<BreadCrumb> = []): Array<BreadCrumb> {
    const routeDataBreadCrumbKey = "breadcrumb";
    const routeConfig = route.routeConfig;
    const path = routeConfig && routeConfig.path !== undefined ? routeConfig.path : "";

    let label = path;
    if (url === "") {
      label = "Home";
    } else if (routeConfig && routeConfig.data !== undefined) {
      label = routeConfig.data[routeDataBreadCrumbKey];
    }
برای بار اول فراخوانی متد بازگشتی buildBreadCrumbs، مقدار url خالی است. به همین جهت برچسب home را به آن نسبت خواهیم داد. در بار بعدی، اطلاعات path و data مسیر فعال شده را از شیء route.routeConfig استخراج کرده و توسط آن یک bread-crumb جدید را ایجاد می‌کنیم:
    const nextUrl = `${url}${path}/`;
    const breadcrumb: BreadCrumb = {
      label: label,
      url: nextUrl
    };
سپس اگر این مسیر فرزندی را داشته باشد، مقدار خاصیت route.firstChild آن نال نبوده و می‌توان متد بازگشتی را در همینجا با فراخوانی متد جاری تشکیل داد:
    const newBreadcrumbs = [...breadcrumbs, breadcrumb];
    if (route.firstChild) {
      return this.buildBreadCrumbs(route.firstChild, nextUrl, newBreadcrumbs);
    }
    return newBreadcrumbs;
علت استفاده‌ی از روش [breadcrumb, breadcrumbs...] بجای استفاده از متد push آرایه، مربوط است به
 encapsulation: ViewEncapsulation.None
در مورد ViewEncapsulation در مطلب «بررسی استراتژی‌های تشخیص تغییرات در برنامه‌های Angular» بیشتر بحث کرده‌ایم و تنظیم آن به None، کارآیی برنامه را با کاهش کار سیستم ردیابی تغییرات Angular، بهبود می‌بخشد.


تکمیل قالب نمایش Breadcrumbs

پس از تکمیل BreadCrumbComponent، اکنون قالب آن به صورت ذیل تعریف می‌شود:
<ol class="breadcrumb">
  <li *ngFor="let breadcrumb of breadcrumbs$ | async" class="breadcrumb-item">
    <a [routerLink]="[breadcrumb.url]">{{ breadcrumb.label }}</a>
  </li>
</ol>
در اینجا بر روی آرایه‌ی تشکیل شده‌ی توسط متد buildBreadCrumbs، یک حلقه ایجاد شده و عناصر آن نمایش داده می‌شوند. چون خاصیت عمومی breadcrumbs به صورت یک Observable تعریف شده‌است، در اینجا استفاده از async pipe را نیز در این حلقه مشاهده می‌کنید:
   breadcrumbs$: Observable<BreadCrumb[]>;


نمایش نهایی BreadCrumbs

در پایان کافی است به فایل app.component.html مراجعه کرده و selector کامپوننت نمایش bread crumbs را در آن درج کنیم:
<div class="container">
  <app-bread-crumb></app-bread-crumb>
  <router-outlet></router-outlet>
</div>


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید.
مطالب
Blazor 5x - قسمت دهم - مبانی Blazor - بخش 7 - مسیریابی
تا اینجا به صورت بسیار مختصری با نحوه‌ی مسیریابی برنامه‌های مبتنی بر Blazor توسط دایرکتیو page@ آشنا شدیم. برای مثال با داشتن تعریف زیر در ابتدای یک کامپوننت:
@page "/LearnRouting"

<h3>Learn Routing</h3>
این کامپوننت جدید، صرفنظر از محل قرارگیری فایل آن که برای مثال در پوشه‌ی Pages\LearnBlazor\LearnRouting.razor است، در مسیر https://localhost:5001/LearnRouting قابل دسترسی خواهد شد و برای تعریف مدخل منوی جدید آن، به کامپوننت Shared\NavMenu.razor مراجعه کرده و NavLink جدیدی را برای آن تعریف می‌کنیم:
<li class="nav-item px-3">
    <NavLink class="nav-link" href="LearnRouting">
        <span class="oi oi-list-rich" aria-hidden="true"></span> Learn Routing
    </NavLink>
</li>
در اینجا برچسب مدخل جدید تعریف شده، Learn Routing است و href لینک به آن، دقیقا به مسیریابی تعریف شده اشاره می‌کند.

یک نکته: مسیریابی‌های تعریف شده‌ی در Blazor، حساس به حروف کوچک و بزرگ نیستند.


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

در کامپوننت‌های Blazor، محدودیتی از لحاظ تعداد بار تعریف دایرکتیو page@ وجود ندارد:
@page "/LearnRouting"
@page "/NewRouting"

<h3>Learn Routing</h3>
در این حالت می‌توان در حین تعریف یک مسیریابی جدید، مسیریابی قبلی را نیز حفظ کرد. در اینجا کامپوننت فوق، از طریق هر دو آدرس https://localhost:5001/LearnRouting و https://localhost:5001/NewRouting تعریف شده، قابل دسترسی‌است.


روش تعریف پارامترهای مسیریابی

تا اینجا اگر مسیر جدید https://localhost:5001/NewRouting/1/2 را درخواست کنیم چه اتفاقی رخ می‌دهد؟


در مورد نحوه‌ی تعریف قالب «یافت نشد» فوق، در قسمت دوم بیشتر بحث شد.
برای تعریف پارامترهای مسیریابی، می‌توان مسیریابی سومی را با پارامترهای مدنظر تعریف کرد که در مثال زیر، ذکر پارامتر دوم اختیاری است؛ چون سومین مسیریابی تعریف شده، امکان پردازش مسیرهایی با یک پارامتر را هم ممکن می‌کند:
@page "/LearnRouting"
@page "/NewRouting"
@page "/LearnRouting/{parameter1}"
@page "/LearnRouting/{parameter1}/{parameter2}"

<h3>Learn Routing</h3>

<p>Parameter1: @Parameter1</p>
<p>Parameter2: @Parameter2</p>

@code
{
    [Parameter]
    public string Parameter1 { set; get; }

    [Parameter]
    public string Parameter2 { set; get; }
}
سپس جهت دست‌یابی به مقادیر این پارامترها می‌توان در قسمت کدهای کامپوننت، از خواص عمومی مزین شده‌ی با ویژگی Parameter استفاده کرد. در اینجا هر خاصیت تعریف شده، باید هم نام پارامتر تعریف شده باشد (و این مورد نیز غیر حساس به حروف بزرگ و کوچک است).
پس از این تعاریف، مسیریابی مانند https://localhost:5001/LearnRouting/1 با یک پارامتر و یا https://localhost:5001/LearnRouting/1/2 که به همراه دو پارامتر است، قابل فراخوانی می‌شود.





روش تعریف لینک به سایر کامپوننت‌های Blazor

در ادامه کامپوننت جدید Pages\LearnBlazor\LearnAdvancedRouting.razor را اضافه می‌کنیم؛ با این محتوای آغازین:
@page "/LearnAdvancedRouting"

<h3>Learn Advanced Routing</h3>
در اینجا قصد نداریم لینک به این کامپوننت را به منوی اصلی برنامه اضافه کنیم؛ بلکه می‌خواهیم از طریق همان کامپوننت LearnRouting.razor ابتدای بحث، این مسیریابی را برقرار کنیم. برای اینکار یا می‌توان از یک anchor tag استاندارد استفاده کرد و یا همانند کامپوننت Shared\NavMenu.razor، از کامپوننت NavLink استفاده نمود. NavLink‌ها نیز همانند anchor tag‌های استاندارد HTML هستند، با این تفاوت که این کامپوننت، افزودن کلاس active مخصوص بوت استرپ را هم بر اساس فعال بودن مسیریابی مرتبط به آن، انجام می‌دهد ("class="nav-link active). به همین علت است که اگر گزینه‌ی منوی خاصی را انتخاب کنیم، این گزینه با رنگ متمایزی نشان داده می‌شود:


بنابراین یک روش تعریف لینک به کامپوننتی دیگر، استفاده از کامپوننت NavLink است که href آن به مسیریابی مقصد اشاره می‌کند:
<NavLink class="btn btn-secondary" href="LearnAdvancedRouting">
    <span class="oi oi-list-rich" aria-hidden="true"></span> Learn Advanced Routing
</NavLink>


ارسال کوئری استرینگ‌ها به کامپوننت‌های مختلف

پس از تعریف لینکی به کامپوننتی دیگر از درون یک کامپوننت، اکنون می‌خواهیم دو کوئری استرینگ param1 و param2 را نیز به آن ارسال کنیم:
<NavLink class="btn btn-secondary" href="LearnAdvancedRouting?param1=value1&param2=value2">
    <span class="oi oi-list-rich" aria-hidden="true"></span> Learn Advanced Routing
</NavLink>
در کامپوننت LearnAdvancedRouting برای دریافت این پارامترها، نیاز است آن‌ها را از URL جاری استخراج کرد:
@page "/LearnAdvancedRouting"
@inject NavigationManager NavigationManager

<h3>Learn Advanced Routing</h3>

<h4>Parameter 1 : @Param1</h4>
<h4>Parameter 2 : @Param2</h4>

@code
{
    string Param1;
    string Param2;

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

        var absoluteUri = new Uri(NavigationManager.Uri);
        var queryParam = System.Web.HttpUtility.ParseQueryString(absoluteUri.Query);
        Param1 = queryParam["Param1"];
        Param2 = queryParam["Param2"];
    }
}
ابتدا سرویس توکار NavigationManager توسط دایرکتیو inject@ به کامپوننت جاری تزریق شده‌است که برای کار و دسترسی به آن، نیاز به تنظیمات ابتدایی خاصی نیست و پیشتر به مجموعه‌ی سرویس‌های برنامه افزوده شده‌است. برای نمونه توسط آن می‌توان به Uri در حال پردازش، دسترسی یافت. اکنون که این Uri را داریم، با استفاده از متد HttpUtility.ParseQueryString می‌توان به مجموعه‌ی کوئری استرینگ‌های ارسالی، به صورت key/value دسترسی یافت و برای مثال آن‌ها را در روال رویدادگردان OnInitialized، دریافت و با انتساب آن‌ها به دو فیلد تعریف شده، سبب نمایش مقادیر دریافتی شد:



هدایت به یک کامپوننت دیگر با کد نویسی

فرض کنید می‌خواهیم دکمه‌ای را اضافه کنیم که با کلیک بر روی آن، ما را به کامپوننت LearnRouting هدایت می‌کند:
@page "/LearnAdvancedRouting"
@inject NavigationManager NavigationManager

@*<NavLink href="/learnrouting" class="btn btn-secondary">Back to Routing</NavLink>*@
@*<a href="/learnrouting" class="btn btn-secondary">Back to Routing</a>*@
<button class="btn btn-secondary" @onclick="BackToRouting">Back to Routing</button>

@code
{
    private void BackToRouting()
    {
        NavigationManager.NavigateTo("learnrouting");
    }
}
در اینجا روش‌های مختلف تعریف لینک به کامپوننتی دیگر را مشاهده می‌کنید. یا می‌توان از کامپوننت NavLink استفاده کرد و یا از یک anchor tag استاندارد، که href هر دوی آن‌ها به مسیریابی مقصد اشاره می‌کنند و یا می‌توان با استفاده از سرویس NavigationManager و متد NavigateTo آن مانند کدهای فوق، سبب هدایت کاربر به صفحه‌ای دیگر شد.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-10.zip
نظرات مطالب
SQL Antipattern #2
نیازی به استفاده از Id نیست. مسیر زیر را در نظر بگیرید:
/// Example: "00001.00042.00005".
مسیر بالا متناظر با نودی در درخت می‌باشد که در عمق 2 بوده و فرزند 5 ام مربوط به نود 00001.00042 می‌باشد. اگر نیاز باشد فرزند جدیدی به نود 00001.00042 اضافه شود، باید ابتدا مسیر آخرین فرزند آن یعنی الگوی بالایی واکشی شده و سپس مسیر جدیدی برای نود جدید به شکل زیر تشکیل شود:
/// Example: "00001.00042.00006".
دقیقا مشابه به کاری می‌باشد که نوع داده hierarchyid موجود در Sql Server انجام می‌دهد. با این روش دقیقا مشخص می‌باشد که نود x در چه مکانی قرار داد.

مدیریت واحدهای سازمانی
یکسری متد کمکی هم برای مدیریت فیلد Path در نظر گرفته شده است.
    public class OrganizationalUnit : TrackableEntity<User>, IHasRowVersion, IPassivable
    {
        #region Constants

        /// <summary>
        /// Maximum depth of an UO hierarchy.
        /// </summary>
        public const int MaxDepth = 16;

        /// <summary>
        /// Length of a code unit between dots.
        /// </summary>
        public const int PathUnitLength = 5;

        /// <summary>
        /// Maximum length of the <see cref="Path"/> property.
        /// </summary>
        public const int MaxPathLength = MaxDepth * (PathUnitLength + 1) - 1;

        public const char HierarchicalDisplayNameSeperator = '»';

        #endregion

        #region Properties

        public string Name { get; set; }
        public string NormalizedName { get; set; }
        public string HierarchicalDisplayName { get; set; }
        /// <summary>
        /// Hierarchical Path of this organization unit.
        /// Example: "00001.00042.00005".
        /// It's changeable if OU hierarch is changed.
        /// </summary>
        public string Path { get; set; }
        public bool IsActive { get; set; } = true;
        public byte[] RowVersion { get; set; }

        #endregion

        #region Navigation Properties

        public OrganizationalUnit Parent { get; set; }
        public long? ParentId { get; set; }
        public ICollection<OrganizationalUnit> Children { get; set; } = new HashSet<OrganizationalUnit>();
        public ICollection<UserOrganizationalUnit> UserOrganizationalUnits { get; set; } =
            new HashSet<UserOrganizationalUnit>();

        #endregion

        #region Public Methods

        /// <summary>
        /// Creates path for given numbers.
        /// Example: if numbers are 4,2 then returns "00004.00002";
        /// </summary>
        /// <param name="numbers">Numbers</param>
        public static string CreatePath(params int[] numbers)
        {
            if (numbers.IsNullOrEmpty())
            {
                return null;
            }

            return numbers.Select(number => number.ToString(new string('0', PathUnitLength))).JoinAsString(".");
        }

        /// <summary>
        /// Appends a child path to a parent path. 
        /// Example: if parentPath = "00001", childPath = "00042" then returns "00001.00042".
        /// </summary>
        /// <param name="parentPath">Parent path. Can be null or empty if parent is a root.</param>
        /// <param name="childPath">Child path.</param>
        public static string AppendPath(string parentPath, string childPath)
        {
            if (childPath.IsNullOrEmpty())
            {
                throw new ArgumentNullException(nameof(childPath), "childPath can not be null or empty.");
            }

            if (parentPath.IsNullOrEmpty())
            {
                return childPath;
            }

            return parentPath + "." + childPath;
        }

        /// <summary>
        /// Gets relative path to the parent.
        /// Example: if path = "00019.00055.00001" and parentPath = "00019" then returns "00055.00001".
        /// </summary>
        /// <param name="path">The path.</param>
        /// <param name="parentPath">The parent path.</param>
        public static string GetRelativePath(string path, string parentPath)
        {
            if (path.IsNullOrEmpty())
            {
                throw new ArgumentNullException(nameof(path), "Path can not be null or empty.");
            }

            if (parentPath.IsNullOrEmpty())
            {
                return path;
            }

            if (path.Length == parentPath.Length)
            {
                return null;
            }

            return path.Substring(parentPath.Length + 1);
        }

        /// <summary>
        /// Calculates next path for given path.
        /// Example: if code = "00019.00055.00001" returns "00019.00055.00002".
        /// </summary>
        /// <param name="path">The path.</param>
        public static string CalculateNextPath(string path)
        {
            if (path.IsNullOrEmpty())
            {
                throw new ArgumentNullException(nameof(path), "Path can not be null or empty.");
            }

            var parentPath = GetParentPath(path);
            var lastUnitPath = GetLastUnitPath(path);

            return AppendPath(parentPath, CreatePath(Convert.ToInt32(lastUnitPath) + 1));
        }

        /// <summary>
        /// Gets the last unit path.
        /// Example: if path = "00019.00055.00001" returns "00001".
        /// </summary>
        /// <param name="path">The path.</param>
        public static string GetLastUnitPath(string path)
        {
            if (path.IsNullOrEmpty())
            {
                throw new ArgumentNullException(nameof(path), "Path can not be null or empty.");
            }

            var splittedPath = path.Split('.');
            return splittedPath[splittedPath.Length - 1];
        }

        /// <summary>
        /// Gets parent path.
        /// Example: if path = "00019.00055.00001" returns "00019.00055".
        /// </summary>
        /// <param name="path">The path.</param>
        public static string GetParentPath(string path)
        {
            if (path.IsNullOrEmpty())
            {
                throw new ArgumentNullException(nameof(path), "Path can not be null or empty.");
            }

            var splittedPath = path.Split('.');
            if (splittedPath.Length == 1)
            {
                return null;
            }

            return splittedPath.Take(splittedPath.Length - 1).JoinAsString(".");
        }

        #endregion
    }

البته یک ویو نمایشی برای حالت درختی هم بهتر است داشته باشید.


یکسری متد DomainService

       public virtual async Task<string> GetNextChildPathAsync(long? parentId)
        {
            var lastChild = await GetLastChildOrNullAsync(parentId).ConfigureAwait(false);
            if (lastChild == null)
            {
                var parentPath = parentId != null ? await GetPathAsync(parentId.Value).ConfigureAwait(false) : null;
                return OrganizationalUnit.AppendPath(parentPath, OrganizationalUnit.CreatePath(1));
            }

            return OrganizationalUnit.CalculateNextPath(lastChild.Path);
        }

        public async Task<string> GetNextChildHierarchicalDisplayNameAsync(string name, long? parentId)
        {
            var parent = parentId != null
                ? await _organizationalUnits.SingleOrDefaultAsync(a => a.Id == parentId.Value).ConfigureAwait(false)
                : null;

            return parent == null
                ? name
                : $"{parent.HierarchicalDisplayName} {OrganizationalUnit.HierarchicalDisplayNameSeperator} {name}";
        }

        public virtual async Task<OrganizationalUnit> GetLastChildOrNullAsync(long? parentId)
        {
            return await _organizationalUnits.OrderByDescending(c => c.Path)
                .FirstOrDefaultAsync(ou => ou.ParentId == parentId).ConfigureAwait(false);
        }

        public virtual async Task<string> GetPathAsync(long id)
        {
            Guard.ArgumentNotZero(id, nameof(id));
            var organizationalUnit = await _organizationalUnits.SingleOrDefaultAsync(ou => ou.Id == id).ConfigureAwait(false);
            if (organizationalUnit == null)
            {
                throw new KeyNotFoundException();
            }
            return organizationalUnit.Path;
        }

        public async Task<List<OrganizationalUnit>> FindChildrenAsync(long? parentId, bool recursive = false)
        {
            if (!recursive)
            {
                return await _organizationalUnits.Where(ou => ou.ParentId == parentId).ToListAsync().ConfigureAwait(false);
            }

            if (!parentId.HasValue)
            {
                return await _organizationalUnits.ToListAsync().ConfigureAwait(false);
            }

            var path = await GetPathAsync(parentId.Value).ConfigureAwait(false);

            return await _organizationalUnits.Where(
                ou => ou.Path.StartsWith(path) && ou.Id != parentId.Value).ToListAsync().ConfigureAwait(false);
        }

        public virtual async Task MoveAsync(long id, long? parentId)
        {
            Guard.ArgumentNotZero(id, nameof(id));
            var organizationalUnit = await _organizationalUnits.SingleOrDefaultAsync(ou => ou.Id == id).ConfigureAwait(false);
            if (organizationalUnit == null || organizationalUnit.ParentId == parentId)
            {
                return;
            }

            //Should find children before Path change
            var children = await FindChildrenAsync(id, true).ConfigureAwait(false);

            //Store old Path of OU
            var oldPath = organizationalUnit.Path;

            //Move OU
            organizationalUnit.Path = await GetNextChildPathAsync(parentId).ConfigureAwait(false);
            organizationalUnit.ParentId = parentId;

            //Update Children Paths
            foreach (var child in children)
            {
                child.Path = OrganizationalUnit.AppendPath(organizationalUnit.Path, OrganizationalUnit.GetRelativePath(child.Path, oldPath));
            }
        }