مطالب
Blazor 5x - قسمت 31 - احراز هویت و اعتبارسنجی کاربران Blazor WASM - بخش 1 - انجام تنظیمات اولیه
در قسمت قبل، امکان سفارش یک اتاق را به همراه پرداخت آنلاین آن، به برنامه‌ی Blazor WASM این سری اضافه کردیم؛ اما ... هویت کاربری که مشغول انجام اینکار است، هنوز مشخص نیست. بنابراین در این قسمت می‌خواهیم مباحثی مانند ثبت نام و ورود به سیستم را تکمیل کنیم. البته مقدمات سمت سرور این بحث را در مطلب «Blazor 5x - قسمت 25 - تهیه API مخصوص Blazor WASM - بخش 2 - تامین پایه‌ی اعتبارسنجی و احراز هویت»، بررسی کردیم.


ارائه‌ی AuthenticationState به تمام کامپوننت‌های یک برنامه‌ی Blazor WASM

در قسمت 22، با مفاهیم CascadingAuthenticationState و AuthorizeRouteView در برنامه‌های Blazor Server آشنا شدیم؛ این مفاهیم در اینجا نیز یکی هستند:
- کامپوننت CascadingAuthenticationState سبب می‌شود AuthenticationState (لیستی از Claims کاربر)، به تمام کامپوننت‌های یک برنامه‌یBlazor  ارسال شود. در مورد پارامترهای آبشاری، در قسمت نهم این سری بیشتر بحث شد و هدف از آن، ارائه‌ی یکسری اطلاعات، به تمام زیر کامپوننت‌های یک کامپوننت والد است؛ بدون اینکه نیاز باشد مدام این پارامترها را در هر زیر کامپوننتی، تعریف و تنظیم کنیم. همینقدر که آن‌ها را در بالاترین سطح سلسله مراتب کامپوننت‌های تعریف شده تعریف کردیم، در تمام زیر کامپوننت‌های آن نیز در دسترس خواهند بود.
- کامپوننت AuthorizeRouteView امکان محدود کردن دسترسی به صفحات مختلف برنامه‌ی Blazor را بر اساس وضعیت اعتبارسنجی و نقش‌های کاربر جاری، میسر می‌کند.

روش اعمال این دو کامپوننت نیز یکی است و نیاز به ویرایش فایل BlazorWasm.Client\App.razor در اینجا وجود دارد:
<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
                <Authorizing>
                    <p>Please wait, we are authorizing the user.</p>
                </Authorizing>
                <NotAuthorized>
                    <p>Not Authorized</p>
                </NotAuthorized>
            </AuthorizeRouteView>
        </Found>
        <NotFound>
                <LayoutView Layout="@typeof(MainLayout)">
                    <p>Sorry, there's nothing at this address.</p>
                </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>
کامپوننت CascadingAuthenticationState، اطلاعات AuthenticationState را در اختیار تمام کامپوننت‌های برنامه قرار می‌دهد و کامپوننت AuthorizeRouteView، امکان نمایش یا عدم نمایش قسمتی از صفحه را بر اساس وضعیت لاگین شخص و یا محدود کردن دسترسی بر اساس نقش‌ها، میسر می‌کند.


مشکل! برخلاف برنامه‌های Blazor Server، برنامه‌های Blazor WASM به صورت پیش‌فرض به همراه تامین کننده‌ی توکار AuthenticationState نیستند.

اگر سری Blazor جاری را از ابتدا دنبال کرده باشید، کاربرد AuthenticationState را در برنامه‌های Blazor Server، در قسمت‌های 21 تا 23، پیشتر مشاهده کرده‌اید. همان مفاهیم، در برنامه‌های Blazor WASM هم قابل استفاده هستند؛ البته در اینجا به علت جدا بودن برنامه‌ی سمت کلاینت WASM Blazor، از برنامه‌ی Web API سمت سرور، نیاز است یک تامین کننده‌ی سمت کلاینت AuthenticationState را بر اساس JSON Web Token دریافتی از سرور، تشکیل دهیم و برخلاف برنامه‌های Blazor Server، این مورد به صورت خودکار مدیریت نمی‌شود و با ASP.NET Core Identity سمت سروری که JWT تولید می‌کند، یکپارچه نیست.
بنابراین در اینجا نیاز است یک AuthenticationStateProvider سفارشی سمت کلاینت را تهیه کنیم که بر اساس JWT دریافتی از Web API کار می‌کند. به همین جهت در ابتدا یک JWT Parser را طراحی می‌کنیم که رشته‌ی JWT دریافتی از سرور را تبدیل به <IEnumerable<Claim می‌کند. سپس این لیست را در اختیار یک AuthenticationStateProvider سفارشی قرار می‌دهیم تا اطلاعات مورد نیاز کامپوننت‌های CascadingAuthenticationState و AuthorizeRouteView تامین شده و قابل استفاده شوند.


نیاز به یک JWT Parser

در قسمت 25، پس از لاگین موفق، یک JWT تولید می‌شود که به همراه قسمتی از مشخصات کاربر است. می‌توان محتوای این توکن را در سایت jwt.io مورد بررسی قرار داد که برای نمونه به این خروجی می‌رسیم و حاوی claims تعریف شده‌است:
{
  "iss": "https://localhost:5001/",
  "iat": 1616396383,
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "vahid@dntips.ir",
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress": "vahid@dntips.ir",
  "Id": "582855fb-e95b-45ab-b349-5e9f7de40c0c",
  "DisplayName": "vahid@dntips.ir",
  "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Admin",
  "nbf": 1616396383,
  "exp": 1616397583,
  "aud": "Any"
}
بنابراین برای استخراج این claims در سمت کلاینت، نیاز به یک JWT Parser داریم که نمونه‌ای از آن می‌تواند به صورت زیر باشد:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text.Json;

namespace BlazorWasm.Client.Utils
{
    /// <summary>
    /// From the Steve Sanderson’s Mission Control project:
    /// https://github.com/SteveSandersonMS/presentation-2019-06-NDCOslo/blob/master/demos/MissionControl/MissionControl.Client/Util/ServiceExtensions.cs
    /// </summary>
    public static class JwtParser
    {
        public static IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
        {
            var claims = new List<Claim>();
            var payload = jwt.Split('.')[1];

            var jsonBytes = ParseBase64WithoutPadding(payload);

            var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);
            claims.AddRange(keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString())));
            return claims;
        }

        private static byte[] ParseBase64WithoutPadding(string base64)
        {
            switch (base64.Length % 4)
            {
                case 2: base64 += "=="; break;
                case 3: base64 += "="; break;
            }
            return Convert.FromBase64String(base64);
        }
    }
}
که آن‌را در فایل BlazorWasm.Client\Utils\JwtParser.cs برنامه‌ی کلاینت ذخیره خواهیم کرد. متد ParseClaimsFromJwt فوق، رشته‌ی JWT تولیدی حاصل از لاگین موفق در سمت Web API را دریافت کرده و تبدیل به لیستی از Claimها می‌کند.


تامین AuthenticationState مبتنی بر JWT مخصوص برنامه‌‌های Blazor WASM

پس از داشتن لیست Claims دریافتی از یک رشته‌ی JWT، اکنون می‌توان آن‌را تبدیل به یک AuthenticationStateProvider کرد. برای اینکار در ابتدا نیاز است بسته‌ی نیوگت Microsoft.AspNetCore.Components.Authorization را به برنامه‌ی کلاینت اضافه کرد:
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="5.0.4" />
  </ItemGroup>
</Project>
سپس سرویس سفارشی AuthStateProvider خود را به پوشه‌ی Services برنامه اضافه می‌کنیم و متد GetAuthenticationStateAsync کلاس پایه‌ی AuthenticationStateProvider استاندارد را به نحو زیر بازنویسی و سفارشی سازی می‌کنیم:
namespace BlazorWasm.Client.Services
{
    public class AuthStateProvider : AuthenticationStateProvider
    {
        private readonly HttpClient _httpClient;
        private readonly ILocalStorageService _localStorage;

        public AuthStateProvider(HttpClient httpClient, ILocalStorageService localStorage)
        {
            _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
            _localStorage = localStorage ?? throw new ArgumentNullException(nameof(localStorage));
        }

        public override async Task<AuthenticationState> GetAuthenticationStateAsync()
        {
            var token = await _localStorage.GetItemAsync<string>(ConstantKeys.LocalToken);
            if (token == null)
            {
                return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
            }

            _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", token);
            return new AuthenticationState(
                        new ClaimsPrincipal(
                            new ClaimsIdentity(JwtParser.ParseClaimsFromJwt(token), "jwtAuthType")
                        )
                    );
        }
    }
}
- اگر با برنامه‌های سمت کلاینت React و یا Angular پیشتر کار کرده باشید، منطق این کلاس بسیار آشنا به نظر می‌رسد. در این برنامه‌ها، مفهومی به نام Interceptor وجود دارد که توسط آن به صورت خودکار، هدر JWT را به تمام درخواست‌های ارسالی به سمت سرور، اضافه می‌کنند تا از تکرار این قطعه کد خاص، جلوگیری شود. علت اینجا است که برای دسترسی به منابع محافظت شده‌ی سمت سرور، نیاز است هدر ویژه‌ای را به نام "Authorization" که با مقدار "bearer jwt" تشکیل می‌شود، به ازای هر درخواست ارسالی به سمت سرور نیز ارسال کرد؛ تا تنظیمات ویژه‌ی AddJwtBearer که در قسمت 25 در کلاس آغازین برنامه‌ی Web API انجام دادیم، این هدر مورد انتظار را دریافت کرده و پردازش کند و در نتیجه‌ی آن، شیء this.User، در اکشن متدهای کنترلرها تشکیل شده و قابل استفاده شود.
در اینجا نیز مقدار دهی خودکار httpClient.DefaultRequestHeaders.Authorization را مشاهده می‌کنید که مقدار token خودش را از Local Storage دریافت می‌کند که کلید متناظر با آن‌را در پروژه‌ی BlazorServer.Common به صورت زیر تعریف کرده‌ایم:
namespace BlazorServer.Common
{
    public static class ConstantKeys
    {
        // ...
        public const string LocalToken = "JWT Token";
    }
}
به این ترتیب دیگر نیازی نخواهد بود در تمام سرویس‌های برنامه‌ی WASM که با HttpClient کار می‌کنند، مدام سطر مقدار دهی httpClient.DefaultRequestHeaders.Authorization را تکرار کنیم.
- همچنین در اینجا به کمک متد JwtParser.ParseClaimsFromJwt که در ابتدای بحث تهیه کردیم، لیست Claims دریافتی از JWT ارسالی از سمت سرور را تبدیل به یک AuthenticationState قابل استفاده‌ی در برنامه‌ی Blazor WASM کرده‌ایم.

پس از تعریف یک AuthenticationStateProvider سفارشی، باید آن‌را به همراه Authorization، به سیستم تزریق وابستگی‌های برنامه در فایل Program.cs اضافه کرد:
namespace BlazorWasm.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            // ...

            builder.Services.AddAuthorizationCore();
            builder.Services.AddScoped<AuthenticationStateProvider, AuthStateProvider>();

            // ...
        }
    }
}
و برای سهولت استفاده‌ی از امکانات اعتبارسنجی فوق در کامپوننت‌های برنامه، فضای نام زیر را به فایل BlazorWasm.Client\_Imports.razor اضافه می‌کنیم:
@using Microsoft.AspNetCore.Components.Authorization


تهیه‌ی سرویسی برای کار با AccountController

اکنون می‌خواهیم در برنامه‌ی سمت کلاینت، از AccountController سمت سرور که آن‌را در قسمت 25 این سری تهیه کردیم، استفاده کنیم. بنابراین نیاز است سرویس زیر را تدارک دید که امکان لاگین، ثبت نام و خروج از سیستم را در سمت کلاینت میسر می‌کند:
namespace BlazorWasm.Client.Services
{
    public interface IClientAuthenticationService
    {
        Task<AuthenticationResponseDTO> LoginAsync(AuthenticationDTO userFromAuthentication);
        Task LogoutAsync();
        Task<RegisterationResponseDTO> RegisterUserAsync(UserRequestDTO userForRegisteration);
    }
}
و به صورت زیر پیاده سازی می‌شود:
namespace BlazorWasm.Client.Services
{
    public class ClientAuthenticationService : IClientAuthenticationService
    {
        private readonly HttpClient _client;
        private readonly ILocalStorageService _localStorage;

        public ClientAuthenticationService(HttpClient client, ILocalStorageService localStorage)
        {
            _client = client;
            _localStorage = localStorage;
        }

        public async Task<AuthenticationResponseDTO> LoginAsync(AuthenticationDTO userFromAuthentication)
        {
            var response = await _client.PostAsJsonAsync("api/account/signin", userFromAuthentication);
            var responseContent = await response.Content.ReadAsStringAsync();
            var result = JsonSerializer.Deserialize<AuthenticationResponseDTO>(responseContent);

            if (response.IsSuccessStatusCode)
            {
                await _localStorage.SetItemAsync(ConstantKeys.LocalToken, result.Token);
                await _localStorage.SetItemAsync(ConstantKeys.LocalUserDetails, result.UserDTO);
                _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", result.Token);
                return new AuthenticationResponseDTO { IsAuthSuccessful = true };
            }
            else
            {
                return result;
            }
        }

        public async Task LogoutAsync()
        {
            await _localStorage.RemoveItemAsync(ConstantKeys.LocalToken);
            await _localStorage.RemoveItemAsync(ConstantKeys.LocalUserDetails);
            _client.DefaultRequestHeaders.Authorization = null;
        }

        public async Task<RegisterationResponseDTO> RegisterUserAsync(UserRequestDTO userForRegisteration)
        {
            var response = await _client.PostAsJsonAsync("api/account/signup", userForRegisteration);
            var responseContent = await response.Content.ReadAsStringAsync();
            var result = JsonSerializer.Deserialize<RegisterationResponseDTO>(responseContent);

            if (response.IsSuccessStatusCode)
            {
                return new RegisterationResponseDTO { IsRegisterationSuccessful = true };
            }
            else
            {
                return result;
            }
        }
    }
}
که به نحو زیر به سیستم تزریق وابستگی‌های برنامه معرفی می‌شود:
namespace BlazorWasm.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            // ...
            builder.Services.AddScoped<IClientAuthenticationService, ClientAuthenticationService>();
            // ...
        }
    }
}
توضیحات:
- متد LoginAsync، مشخصات لاگین کاربر را به سمت اکشن متد api/account/signin ارسال کرده و در صورت موفقیت این عملیات، اصل توکن دریافتی را به همراه مشخصاتی از کاربر، در Local Storage ذخیره سازی می‌کند. این مورد سبب خواهد شد تا بتوان به مشخصات کاربر در صفحات دیگر و سرویس‌های دیگری مانند AuthStateProvider ای که تهیه کردیم، دسترسی پیدا کنیم. به علاوه مزیت دیگر کار با Local Storage، مواجه شدن با حالت‌هایی مانند Refresh کامل صفحه و برنامه، توسط کاربر است. در یک چنین حالتی، برنامه از نو بارگذاری مجدد می‌شود و به این ترتیب می‌توان به مشخصات کاربر لاگین کرده، به سادگی دسترسی یافت و مجددا قسمت‌های مختلف برنامه را به او نشان داد. نمونه‌ی دیگر این سناریو، بازگشت از درگاه پرداخت بانکی است. در این حالت نیز از یک سرویس سمت سرور دیگر، کاربر به سمت برنامه‌ی کلاینت، Redirect کامل خواهد شد که در اصل اتفاقی که رخ می‌دهد، با Refresh کامل صفحه یکی است. در این حالت نیز باید بتوان کاربری را که از درگاه بانکی ثالث، به سمت برنامه‌ی کلاینت از نو بارگذاری شده، هدایت شده، بلافاصله تشخیص داد.

- اگر برنامه، Refresh کامل نشود، نیازی به Local Storage نخواهد بود؛ از این لحاظ که در برنامه‌های سمت کلاینت Blazor، طول عمر تمام سرویس‌ها، صرفنظر از نوع طول عمری که برای آن‌ها مشخص می‌کنیم، همواره Singleton هستند (ماخذ).
Blazor WebAssembly apps don't currently have a concept of DI scopes. Scoped-registered services behave like Singleton services.
بنابراین می‌توان یک سرویس سراسری توکن را تهیه و به سادگی آن‌را در تمام قسمت‌های برنامه تزریق کرد. این روش هرچند کار می‌کند، اما همانطور که عنوان شد، به Refresh کامل صفحه حساس است. اگر برنامه در مرورگر کاربر Refresh نشود، تا زمانیکه باز است، سرویس‌های در اصل Singleton تعریف شده‌ی در آن نیز در تمام قسمت‌های برنامه در دسترس هستند؛ اما با Refresh کامل صفحه، به علت بارگذاری مجدد کل برنامه، سرویس‌های آن نیز از نو، وهله سازی خواهند شد که سبب از دست رفتن حالت قبلی آن‌ها می‌شود. بنابراین نیاز به روشی داریم که بتوانیم حالت قبلی برنامه را در زمان راه اندازی اولیه‌ی آن بازیابی کنیم و یکی از روش‌های استاندارد اینکار، استفاده از Local Storage خود مرورگر است که مستقل از برنامه و توسط مرورگر مدیریت می‌شود.

- در متد LoginAsync، علاوه بر ثبت اطلاعات کاربر در Local Storage، مقدار دهی client.DefaultRequestHeaders.Authorization را نیز ملاحظه می‌کنید. همانطور که عنوان شد، سرویس‌های Blazor WASM در اصل دارای طول عمر Singleton هستند. بنابراین تنظیم این هدر در اینجا، بر روی تمام سرویس‌های HttpClient تزریق شده‌ی به سایر سرویس‌های برنامه نیز بلافاصله تاثیرگذار خواهد بود.

- متد LogoutAsync، اطلاعاتی را که در حین لاگین موفق در Local Storage ذخیره کردیم، حذف کرده و همچنین client.DefaultRequestHeaders.Authorization را نیز نال می‌کند تا دیگر اطلاعات لاگین شخص قابل بازیابی نبوده و مورد استفاده قرار نگیرد. همین مقدار برای شکست پردازش درخواست‌های ارسالی به منابع محافظت شده‌ی سمت سرور کفایت می‌کند.

- متد RegisterUserAsync، مشخصات کاربر در حال ثبت نام را به سمت اکشن متد api/account/signup ارسال می‌کند که سبب افزوده شدن کاربر جدیدی به بانک اطلاعاتی برنامه و سیستم ASP.NET Core Identity خواهد شد.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-31.zip
مطالب
فعال سازی قسمت آپلود تصویر و فایل Kendo UI Editor
یکی دیگر از ویجت‌های Kendo UI یک HTML Editor کامل است به همراه امکانات ارسال فایل، تصویر و ... پشتیبانی از راست به چپ. در ادامه قصد داریم نحوه‌ی مدیریت نمایش لیست فایل‌ها، افزودن و حذف آن‌ها را از طریق این ادیتور بررسی کنیم.


تنظیمات ابتدایی Kendo UI Editor

در ذیل کدهای سمت کاربر فعال سازی مقدماتی Kendo UI را مشاهده می‌کنید. در قسمت tools آن، لیست امکانات و نوار ابزار مهیای آن درج شده‌اند.
دو مورد insertImage و insertFile آن نیاز به تنظیمات سمت کاربر و سرور بیشتری دارند.
<!--نحوه‌ی راست به چپ سازی -->
<div class="k-rtl">
    <textarea id="editor" rows="10" cols="30" style="height: 440px"></textarea>
</div>
 
@section JavaScript
{
    <script type="text/javascript">
        $(function () {
            $("#editor").kendoEditor({
                tools: [
                    "bold", "italic", "underline", "strikethrough", "justifyLeft",
                    "justifyCenter", "justifyRight", "justifyFull", "insertUnorderedList",
                    "insertOrderedList", "indent", "outdent", "createLink", "unlink",
                    "insertImage", "insertFile",
                    "subscript", "superscript", "createTable", "addRowAbove", "addRowBelow",
                    "addColumnLeft", "addColumnRight", "deleteRow", "deleteColumn", "viewHtml",
                    "formatting", "cleanFormatting", "fontName", "fontSize", "foreColor",
                    "backColor", "print"
                ],
                imageBrowser: {
                    messages: {
                        dropFilesHere: "فایل‌های خود را به اینجا کشیده و رها کنید"
                    },
                    transport: {
                        read: {
                            url: "@Url.Action("GetFilesList", "KendoEditorImages")",
                            dataType: "json",
                            contentType: 'application/json; charset=utf-8',
                            type: 'GET',
                            cache: false
                        },
                        destroy: {
                            url: "@Url.Action("DestroyFile", "KendoEditorImages")",
                            type: "POST"
                        },
                        create: {
                            url: "@Url.Action("CreateFolder", "KendoEditorImages")",
                            type: "POST"
                        },
                        thumbnailUrl: "@Url.Action("GetThumbnail", "KendoEditorImages")",
                        uploadUrl: "@Url.Action("UploadFile", "KendoEditorImages")",
                        imageUrl: "@Url.Action("GetFile", "KendoEditorImages")?path={0}"
                    }
                },
                fileBrowser: {
                    messages: {
                        dropFilesHere: "فایل‌های خود را به اینجا کشیده و رها کنید"
                    },
                    transport: {
                        read: {
                            url: "@Url.Action("GetFilesList", "KendoEditorFiles")",
                            dataType: "json",
                            contentType: 'application/json; charset=utf-8',
                            type: 'GET',
                            cache: false
                        },
                        destroy: {
                            url: "@Url.Action("DestroyFile", "KendoEditorFiles")",
                            type: "POST"
                        },
                        create: {
                            url: "@Url.Action("CreateFolder", "KendoEditorFiles")",
                            type: "POST"
                        },
                        uploadUrl: "@Url.Action("UploadFile", "KendoEditorFiles")",
                        fileUrl: "@Url.Action("GetFile", "KendoEditorFiles")?path={0}"
                    }
                }
            });
        });
    </script>
}
در اینجا نحوه‌ی تنظیم مسیرهای مختلف ارسال فایل و تصویر Kendo UI Editor را ملاحظه می‌کنید.
منهای قسمت thumbnailUrl، عملکرد قسمت‌های مختلف افزودن فایل و تصویر این ادیتور یکسان هستند. به همین جهت می‌توان برای مثال کنترلی مانند KendoEditorFilesController را ایجاد و سپس در کنترلر KendoEditorImagesController از آن ارث بری کرد و متد دریافت و نمایش بند انگشتی تصاویر را افزود. به این ترتیب دیگر نیازی به تکرار کدهای مشترک بین این دو قسمت نخواهد بود.


نمایش لیست پوشه‌ها و تصویر در ابتدای باز شدن صفحه‌ی درج تصویر

با کلیک بر روی دکمه‌ی نمایش لیست تصاویر، صفحه دیالوگی مانند شکل زیر ظاهر خواهد شد:


تنظیمات خواندن این فایل‌ها، از قسمت read مربوط به imageBrowser دریافت می‌شود که cache آن نیز به false تنظیم شده‌است تا در این بین مرورگر اطلاعات را کش نکند. این مورد در حین حذف فایل‌ها و پوشه‌ها مهم است. زیرا اگر cache:false تنظیم نشده باشد، حذف یک فایل یا پوشه در سمت کاربر تاثیری نخواهد داشت.
imageBrowser: {
    transport: {
        read: {
            url: "@Url.Action("GetFilesList", "KendoEditorImages")",
            dataType: "json",
            contentType: 'application/json; charset=utf-8',
            type: 'GET',
            cache: false
        }
    }
},
در ادامه نیاز است اکشن متد GetFilesList را به نحو ذیل در سمت سرور تهیه کرد:
namespace KendoUI13.Controllers
{
    public class KendoEditorFilesController : Controller
    {
        //مسیر پوشه فایل‌ها
        protected string FilesFolder = "~/files";
 
        protected string KendoFileType = "f";
        protected string KendoDirType = "d";
 
        [HttpGet]
        public ActionResult GetFilesList(string path)
        {
            path = GetSafeDirPath(path);
            var imagesList = new DirectoryInfo(path)
                                .GetFiles()
                                .Select(fileInfo => new KendoFile
                                {
                                    Name = fileInfo.Name,
                                    Size = fileInfo.Length,
                                    Type = KendoFileType
                                }).ToList();
 
            var foldersList = new DirectoryInfo(path)
                                .GetDirectories()
                                .Select(directoryInfo => new KendoFile
                                {
                                    Name = directoryInfo.Name,
                                    Type = KendoDirType
                                }).ToList();
 
            return new ContentResult
            {
                Content = JsonConvert.SerializeObject(imagesList.Union(foldersList), new JsonSerializerSettings
                {
                    ContractResolver = new CamelCasePropertyNamesContractResolver()
                }),
                ContentType = "application/json",
                ContentEncoding = Encoding.UTF8
            };
        }
 
 
        protected string GetSafeDirPath(string path)
        {
            // path = مسیر زیر پوشه‌ی وارد شده
            if (string.IsNullOrWhiteSpace(path))
            {
                return Server.MapPath(FilesFolder);
            }
 
            //تمیز سازی امنیتی
            path = Path.GetDirectoryName(path);
            path = Path.Combine(Server.MapPath(FilesFolder), path);
            return path;
        } 
    }
}
در اینجا کدهای کلاس پایه KendoEditorFilesController را مشاهده می‌کنید. به این جهت فیلد FilesFolder آن protected تعریف شده‌است تا در کلاسی که از آن ارث بری می‌کند نیز قابل دسترسی باشد. سپس لیست فایل‌ها و پوشه‌های path دریافتی با فرمت لیستی از KendoFile تهیه شده و با فرمت JSON بازگشت داده می‌شوند. ساختار KendoFile را در ذیل مشاهده می‌کنید:
namespace KendoUI13.Models
{
    public class KendoFile
    {
        public string Name { set; get; }
        public string Type { set; get; }
        public long Size { set; get; }
    }
}
- در اینجا Type می‌تواند از نوع فایل با مقدار f و یا از نوع پوشه با مقدار d باشد.
- علت استفاده از CamelCasePropertyNamesContractResolver در حین بازگشت JSON نهایی، تبدیل خواص دات نتی، به نام‌های سازگار با JavaScript است. برای مثال به صورت خودکار Name را تبدیل به name می‌کند.
- پارامتر path در ابتدای کار خالی است. اما کاربر می‌تواند در بین پوشه‌های باز شده‌ی توسط مرورگر تصاویر Kendo UI حرکت کند. به همین جهت مقدار آن باید هربار بررسی شده و بر این اساس لیست فایل‌ها و پوشه‌های جاری بازگشت داده شوند.


مدیریت حذف تصاویر و پوشه‌ها

همانطور که در شکل فوق نیز مشخص است، با انتخاب یک پوشه یا فایل، دکمه‌ای با آیکن ضربدر جهت فراهم آوردن امکان حذف، ظاهر می‌شود. این دکمه متصل است به قسمت destroy تنظیمات ادیتور:
imageBrowser: {
    transport: {
        destroy: {
            url: "@Url.Action("DestroyFile", "KendoEditorImages")",
            type: "POST"
        }
    }
},
این تنظیمات سمت کاربر را باید به نحو ذیل در سمت سرور مدیریت کرد:
namespace KendoUI13.Controllers
{
    public class KendoEditorFilesController : Controller
    {
        //مسیر پوشه فایل‌ها
        protected string FilesFolder = "~/files";
 
        protected string KendoFileType = "f";
        protected string KendoDirType = "d";
 
        [HttpPost]
        public ActionResult DestroyFile(string name, string path)
        {
            //تمیز سازی امنیتی
            name = Path.GetFileName(name);
            path = GetSafeDirPath(path);
 
            var pathToDelete = Path.Combine(path, name);
 
            var attr = System.IO.File.GetAttributes(pathToDelete);
            if ((attr & FileAttributes.Directory) == FileAttributes.Directory)
            {
                Directory.Delete(pathToDelete, recursive: true);
            }
            else
            {
                System.IO.File.Delete(pathToDelete);
            }
 
            return Json(new object[0]);
        } 
    }
}
- استفاده از Path.GetFileName جهت دریافت نام فایل‌ها در اینجا بسیار مهم است. زیرا اگر این تمیز سازی امنیتی صورت نگیرد، ممکن است با کمی تغییر در آن، فایل web.config برنامه، دریافت یا حذف شود.
- پارامتر name دریافتی مساوی است با نام فایل انتخاب شده و path مشخص می‌کند که در کدام پوشه قرار داریم.
- چون در اینجا امکان حذف یک پوشه یا فایل وجود دارد، حتما نیاز است بررسی کنیم، مسیر دریافتی پوشه‌است یا فایل و سپس بر این اساس جهت حذف آن‌ها اقدام صورت گیرد.


مدیریت ایجاد یک پوشه‌ی جدید

تنظیمات قسمت create مرورگر تصاویر، مرتبط است به زمانیکه کاربر با کلیک بر روی دکمه‌ی +، درخواست ایجاد یک پوشه‌ی جدید را کرده‌است:
imageBrowser: {
    transport: {
        create: {
            url: "@Url.Action("CreateFolder", "KendoEditorImages")",
            type: "POST"
        }
    }
},
کدهای اکشن متد متناظر با این عمل را در ذیل مشاهده می‌کنید:
namespace KendoUI13.Controllers
{
    public class KendoEditorFilesController : Controller
    {
        //مسیر پوشه فایل‌ها
        protected string FilesFolder = "~/files";
 
        protected string KendoFileType = "f";
        protected string KendoDirType = "d";
 
        [HttpPost]
        public ActionResult CreateFolder(string name, string path)
        {
            //تمیز سازی امنیتی
            name = Path.GetFileName(name);
            path = GetSafeDirPath(path);
            var dirToCreate = Path.Combine(path, name);
 
            Directory.CreateDirectory(dirToCreate);
 
            return KendoFile(new KendoFile
            {
                Name = name,
                Type = KendoDirType
            });
        }
 
        protected ActionResult KendoFile(KendoFile file)
        {
            return new ContentResult
            {
                Content = JsonConvert.SerializeObject(file,
                    new JsonSerializerSettings
                    {
                        ContractResolver = new CamelCasePropertyNamesContractResolver()
                    }),
                ContentType = "application/json",
                ContentEncoding = Encoding.UTF8
            };
        }
    }
}
- در اینجا نیز name مساوی نام پوشه‌ی درخواستی است و path به مسیر تو در توی پوشه‌ی جاری اشاره می‌کند.
- پس از ایجاد پوشه، باید نام آن‌را با فرمت KendoFile به صورت JSON بازگشت داد. همچنین در اینجا Type را نیز باید به d (پوشه) تنظیم کرد.


مدیریت قسمت ارسال فایل و تصویر

زمانیکه کاربر بر روی دکمه‌ی upload file یا بارگذاری تصاویر در اینجا کلیک می‌کند، اطلاعات فایل آپلودی به مسیر uploadUrl ارسال می‌گردد.
imageBrowser: {
    transport: {
        thumbnailUrl: "@Url.Action("GetThumbnail", "KendoEditorImages")",
        uploadUrl: "@Url.Action("UploadFile", "KendoEditorImages")",
        imageUrl: "@Url.Action("GetFile", "KendoEditorImages")?path={0}"
    }
},
دو تنظیم دیگر thumbnailUrl و imageUrl، برای نمایش بند انگشتی و نمایش کامل تصویر کاربرد دارند.
در ادامه کدهای مدیریت سمت سرور قسمت آپلود این ادیتور را مشاهده می‌کنید:
namespace KendoUI13.Controllers
{
    public class KendoEditorFilesController : Controller
    {
        //مسیر پوشه فایل‌ها
        protected string FilesFolder = "~/files";
 
        protected string KendoFileType = "f";
        protected string KendoDirType = "d";

 
        [HttpPost]
        public ActionResult UploadFile(HttpPostedFileBase file, string path)
        {
            //تمیز سازی امنیتی
            var name = Path.GetFileName(file.FileName);
            path = GetSafeDirPath(path);
            var pathToSave = Path.Combine(path, name);
 
            file.SaveAs(pathToSave);
 
            return KendoFile(new KendoFile
            {
                Name = name,
                Size = file.ContentLength,
                Type = KendoFileType
            });
        } 
    }
}
- در اینجا path مشخص می‌کند که در کدام پوشه‌ی تو در تو قرار داریم و file نیز حاوی محتوای ارسالی به سرور است.
- پس از ذخیره سازی اطلاعات فایل، نیاز است اطلاعات فایل نهایی را با فرمت KendoFile به صورت JSON بازگشت دهیم.


ارث بری از KendoEditorFilesController جهت تکمیل قسمت مدیریت تصاویر

تا اینجا کدهایی را که ملاحظه کردید، برای هر دو قسمت ارسال تصویر و فایل کاربرد دارند. قسمت ارسال تصاویر برای تکمیل نیاز به متد دریافت تصاویر به صورت بند انگشتی نیز دارد که به صورت ذیل قابل تعریف است و چون از کلاس پایه KendoEditorFilesController ارث بری کرده‌است، این کنترلر به صورت خودکار حاوی اکشن متدهای کلاس پایه نیز خواهد بود.
using System.Web.Mvc;
 
namespace KendoUI13.Controllers
{
    public class KendoEditorImagesController : KendoEditorFilesController
    {
        public KendoEditorImagesController()
        {
            // بازنویسی مسیر پوشه‌ی فایل‌ها
            FilesFolder = "~/images";
        }
 
        [HttpGet]
        [OutputCache(Duration = 3600, VaryByParam = "path")]
        public ActionResult GetThumbnail(string path)
        {
            //todo: create thumb/ resize image
 
            path = GetSafeFileAndDirPath(path);
            return File(path, "image/png");
        }
    }
}

کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید.
نظرات مطالب
صفحه بندی و مرتب سازی خودکار اطلاعات به کمک jqGrid در ASP.NET MVC
خیلی مچکرم؛ من می‌خواستم بدونم آیا می‌تونم بدون فرستادن لیست ستون‌های پنهان شده به یک اکشن دیگر و در نتیچه ذخیره کردن آن بر روی کوکی و یا دیتابیس و یا روش‌های دیگر مستقیم در اکشن GetProducts بگیرتش، اگر بخوام شفاف‌تر بگم ، من می‌خوام وقتی کاربر دکمه خروجی پی دی اف رو زد ستون‌های گرید رو کاربر قبلش تنظیم کرده باشه و در پی دی اف مورد نظر ستون‌های حذف شده رو دیگر نبینه همچنین با در نظر گرفتن موارد فیلتر شده در گرید .
مطالب
بهبود کارآیی LINQ در دات نت 7
LINQ یا همان Language-Integrated Query، یک زبان ساده‌ی کوئری نوشتن یکپارچه‌ی با دات نت است. به کمک آن می‌توان اعمال پیچیده‌ای را بر روی اشیاء، به زبانی ساده بیان کرد و امروزه تقریبا توسط تمام توسعه دهندگان دات نت مورد استفاده قرار می‌گیرد. اما ... این سادگی، بهایی را نیز به همراه دارد: کمتر بودن سرعت اجرا و همچنین افزایش مصرف حافظه. با توجه به گستردگی استفاده‌ی از LINQ، اگر بهبودی در این زمینه حاصل شود، بر روی کارآیی تمام برنامه‌های دات نتی تاثیر خواهد گذاشت و این امر در دات نت 7 محقق شده‌است. کارآیی متدهای LINQ to Objects در دات نت 7 (مانند متدهای Enumerable.Max, Enumerable.Min, Enumerable.Average, Enumerable.Sum) به شدت افزایش یافته و این افزایش گاهی حتی بیشتر از 10 برابر نسبت به نگارش‌های قبلی دات نت است؛ اما چگونه به چنین کارآیی رسیده‌اند؟


تدارک یک آزمایش برای بررسی میزان افزایش کارآیی متدهای LINQ در دات نت 7

در ادامه یک آزمایش ساده‌ی بررسی کارآیی متدهای Enumerable.Max, Enumerable.Min, Enumerable.Average, Enumerable.Sum را با استفاده از کتابخانه‌ی معروف BenchmarkDotNet مشاهده می‌کنید:
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Collections.Generic;
using System.Linq;


[MemoryDiagnoser(displayGenColumns: false)]
public partial class Program
{
  static void Main(string[] args) =>
    BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);

  [Params (10, 10000)]
  public int Size { get; set; }
  private IEnumerable<int> items;

  [GlobalSetup]
  public void Setup()
  {
    items = Enumerable.Range(1, Size).ToArray();
  }  

  [Benchmark]
  public int Min() => items.Min();

  [Benchmark]
  public int Max() => items.Max();

  [Benchmark]
  public double Average() => items.Average();

  [Benchmark]
  public int Sum() => items.Sum();
}
برای آزمایش آن، یکبار target framework پروژه را بر روی net6.0 و بار دیگر بر روی net7.0 قرار داده و برنامه را اجرا می‌کنیم. خلاصه‌ی مفهومی نتایج حاصل به صورت زیر است که ... شگفت‌انگیز هستند!
در مورد کار با آرایه‌ها:


- زمان اجرای یافتن Min در آرایه‌های کوچک، در دات نت 7، نسبت به دات نت 6، حدودا 10 برابر کاهش یافته و اگر این آرایه بزرگتر شود و برای مثال حاوی 10 هزار المان باشد، این زمان 20 برابر کاهش یافته‌است.
- این کاهش زمان‌ها برای سایر متدهای LINQ نیز تقریبا به همین صورت است؛ منها متد Sum که اندازه‌ی آرایه، تاثیری را بر روی نتیجه‌ی نهایی ندارد.
- همچنین در دات نت 7، با فراخوانی متدهای LINQ، افزایش حافظه‌ای مشاهده نمی‌شود.

در مورد کار با لیست‌ها:


- در دات نت 6، اعمال صورت گرفته‌ی توسط LINQ بر روی آرایه‌ها، نسبت به لیست‌ها، همواره سریعتر است.
- در دات نت 7 هم در مورد مجموعه‌های کوچک، وضعیت همانند دات نت 6 است. اما اگر مجموعه‌ها بزرگتر شوند، تفاوتی بین مجموعه‌ها و آرایه‌ها وجود ندارد و حتی وضعیت مجموعه‌ها بهتر است: کارآیی کار با لیست‌ها 32 برابر بیشتر شده‌است!


اما چگونه در دات نت 7، چنین بهبود کارآیی خیره‌کننده‌ای در متدهای LINQ حاصل شده‌است؟

برای بررسی چگونگی بهبود کارآیی متدهای LINQ در دات نت 7 باید به نحوه‌ی پیاده سازی آن‌ها در نگارش‌های مختلف دات نت مراجعه کرد. برای مثال پیاده سازی متد الحاقی Min تا دات نت 6 به صورت زیر است:
public static int Min(this IEnumerable<int> source)
{
  if (source == null)
  {
    ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
  }

  int value;
  using (IEnumerator<int> e = source.GetEnumerator())
  {
    if (!e.MoveNext())
    {
      ThrowHelper.ThrowNoElementsException();
    }

    value = e.Current;
    while (e.MoveNext())
    {
      int x = e.Current;
      if (x < value)
      {
        value = x;
      }
    }
  }
  return value;
}
این متد نسبتا ساده‌است. یک IEnumerable را دریافت کرده و سپس با استفاده از متد MoveNext، مقدار فعلی را با مقدار بعدی مقایسه می‌کند. در این مقایسه، کوچکترین مقدار ذخیره می‌شود تا در نهایت به انتهای مجموعه برسیم.
اما ... پیاده سازی این متد در دات نت 7 متفاوت است:
public static int Min(this IEnumerable<int> source) => MinInteger(source);

private static T MinInteger<T>(this IEnumerable<T> source)
  where T : struct, IBinaryInteger<T>
{
  T value;

  if (source.TryGetSpan(out ReadOnlySpan<T> span))
  {
    if (Vector.IsHardwareAccelerated && 
        span.Length >= Vector<T>.Count * 2)
    {
      .... // Optimized implementation
      return ....;
    }
  }
  .... //Implementation as in .NET 6
}
در اینجا در ابتدا سعی می‌شود تا یک ReadOnlySpan از مجموعه‌ی ارائه شده، تهیه شود. اگر این کار میسر نشد، کدهای همان روش قبلی دات نت 6 که توضیح داده شد، اجرا می‌شود. البته در آزمایشی که ما تدارک دیدیم، چون از لیست‌ها و آرایه‌ها استفاده شده بود، همواره امکان تهیه‌ی یک ReadOnlySpan از آن‌ها میسر است. بنابراین به قسمت اجرایی همانند دات نت 6 نمی‌رسیم.
اما ... ReadOnlySpan چیست؟ نوع‌های Span و ReadOnlySpan، یک ناحیه‌ی پیوسته‌ی مدیریت شده و مدیریت نشده‌ی حافظه را بیان می‌کنند. یک Span از نوع ref struct است؛ یعنی تنها می‌تواند بر روی stack قرار گیرد که مزیت آن، عدم نیاز به تخصیص حافظه‌ی اضافی و بهبود کارآیی است. همچنین ساختار داخلی Span در سی شارپ 11 اندکی تغییر کرده‌است که در آن از ref fields جهت دسترسی امن به این ناحیه‌ی از حافظه استفاده می‌شود. پیشتر از نوع داخلی ByReference برای اشاره به ابتدای این ناحیه‌ی از حافظه استفاده می‌شد که به همراه بررسی امنیتی در این باره نبود.

پس از دریافت ReadOnlySpan، به سطر زیر می‌رسیم:
if (Vector.IsHardwareAccelerated && span.Length >= Vector<T>.Count * 2)
که بررسی می‌کند آیا سخت افرار فعلی از قابلیت‌های SIMD برخوردار است یا خیر؟ اگر بله، اینبار با استفاده از ریاضیات برداری شتاب یافته‌ی توسط سخت افزار، محاسبات را انجام می‌دهد:
private static T MinInteger<T>(this IEnumerable<T> source)
where T : struct, IBinaryInteger<T>
{
  .... 
  if (Vector.IsHardwareAccelerated && span.Length >= Vector<T>.Count * 2)
  {
    var mins = new Vector<T>(span);
    index = Vector<T>.Count;
    do
    {
      mins = Vector.Min(mins, new Vector<T>(span.Slice(index)));
      index += Vector<T>.Count;
    }
    while (index + Vector<T>.Count <= span.Length);

    value = mins[0];
    for (int i = 1; i < Vector<T>.Count; i++)
    {  
      if (mins[i] < value)
      {
        value = mins[i];
      }
    }
  ....
}
بنابراین به صورت خلاصه در دات نت 7 با استفاده از بکارگیری نوع‌های ویژه‌ی Span و نوع‌های برداری شتاب‌یافته‌ی توسط اکثر سخت افزارهای امروزی، سبب بهبود قابل ملاحظه‌ی کارآیی متدهای LINQ شده‌اند.
مطالب دوره‌ها
نگاهی به SignalR Clients
در قسمت قبل موفق به ایجاد اولین Hub خود شدیم. در ادامه، برای تکمیل برنامه نیاز است تا کلاینتی را نیز برای آن تهیه کنیم.
مصرف کنندگان یک Hub می‌توانند انواع و اقسام برنامه‌های کلاینت مانند jQuery Clients و یا حتی یک برنامه کنسول ساده باشند و همچنین Hubهای دیگر نیز قابلیت استفاده از این امکانات Hubهای موجود را دارند. تیم SignalR امکان استفاده از Hubهای آن‌را در برنامه‌های دات نت 4 به بعد، برنامه‌های WinRT، ویندوز فون 8، سیلورلایت 5، jQuery و همچنین برنامه‌های CPP نیز مهیا کرده‌اند. به علاوه گروه‌های مختلف نیز با توجه به سورس باز بودن این مجموعه، کلاینت‌های iOS Native، iOS via Mono و Android via Mono را نیز به این لیست اضافه کرده‌اند.


بررسی کلاینت‌های jQuery

با توجه به پروتکل مبتنی بر JSON سیگنال‌آر، استفاده از آن در کتابخانه‌های جاوا اسکریپتی همانند jQuery نیز به سادگی مهیا است. برای نصب آن نیاز است در کنسول پاور شل نوگت، دستور زیر را صادر کنید:
 PM> Install-Package Microsoft.AspNet.SignalR.JS
برای نمونه به solution پروژه قبل، یک برنامه وب خالی دیگر را اضافه کرده و سپس دستور فوق را بر روی آن اجرا نمائید. در این حالت فقط باید دقت داشت که فرامین بر روی کدام پروژه اجرا می‌شوند:


با استفاده از افزونه SignalR jQuery، به دو طریق می‌توان به یک Hub اتصال برقرار کرد:
الف) استفاده از فایل proxy تولید شده آن (این فایل، در زمان اجرای برنامه تولید می‌شود و یا امکان استفاده از آن به کمک ابزارهای کمکی نیز وجود دارد)
نمونه‌ای از آن‌را در قسمت قبل ملاحظه کردید؛ همان فایل تولید شده در مسیر /signalr/hubs برنامه. به نوعی به آن Service contract نیز گفته می‌شود (ارائه متادیتا و قراردادهای کار با یک سرویس Hub). این فایل همانطور که عنوان شد به صورت پویا در زمان اجرای برنامه ایجاد می‌شود.
امکان تولید آن توسط برنامه کمکی signalr.exe نیز وجود دارد؛ برای دریافت آن می‌توان از طریق NuGet اقدام کرد (بسته Microsoft.AspNet.SignalR.Utils) که نهایتا در پوشه packages قرار خواهد گرفت. نحوه استفاده از آن نیز به صورت زیر است:
 Signalr.exe ghp http://localhost/
در این دستور ghp مخفف generate hub proxy است و نهایتا فایلی را به نام server.js تولید می‌کند.

ب) بدون استفاده از فایل proxy و به کمک روش late binding (انقیاد دیر هنگام)

برای کار با یک Hub از طریق jQuery مراحل ذیل باید طی شوند:
1) ارجاعی به Hub باید مشخص شود.
2) روال‌های رخدادگردان تنظیم گردند.
3) اتصال به Hub برقرار گردد.
4) متدی فراخوانی شود.

در اینجا باید دقت داشت که امکانات Hub به صورت خواص
 $.connection
در سمت کلاینت جی‌کوئری، در دسترس خواهند بود. برای مثال:
 $.connection.chatHub
و نام‌های بکارگرفته شده در اینجا مطابق روش‌های متداول نام گذاری در جاوا اسکریپت، camel case هستند.

خوب، تا اینجا فرض بر این است که یک پروژه خالی ASP.NET را آغاز و سپس فرمان نصب Microsoft.AspNet.SignalR.JS را نیز همانطور که عنوان شد، صادر کرده‌اید. در ادامه یک فایل ساده html را به نام chat.htm، به این پروژه جدید اضافه کنید (برای استفاده از کتابخانه جاوا اسکریپتی SignalR الزامی به استفاده از صفحات کامل پروژه‌های وب نیست).
<!DOCTYPE>
<html>
<head>
    <title></title>
    <script src="Scripts/jquery-1.6.4.min.js" type="text/javascript"></script>
    <script src="Scripts/jquery.signalR-1.0.1.min.js" type="text/javascript"></script>
    <script src="http://localhost:1072/signalr/hubs" type="text/javascript"></script>
</head>
<body>
    <div>
        <input id="txtMsg" type="text" /><input id="send" type="button" value="send msg" />
        <ul id="messages">
        </ul>
    </div>
    <script type="text/javascript">
        $(function () {
            var chat;
            $.connection.hub.logging = true; //اطلاعات بیشتری را در جاوا اسکریپت کنسول مرورگر لاگ می‌کند
            chat = $.connection.chat; //این نام مستعار پیشتر توسط ویژگی نام هاب تنظیم شده است
            chat.client.hello = function (message) {
                //متدی که در اینجا تعریف شده دقیقا مطابق نام متد پویایی است که در هاب تعریف شده است
                //به این ترتیب سرور می‌تواند کلاینت را فراخوانی کند
                $("#messages").append("<li>" + message + "</li>");
            };
            $.connection.hub.start(/*{ transport: 'longPolling' }*/); // فاز اولیه ارتباط را آغاز می‌کند

            $("#send").click(function () {
                // Hub's `SendMessage` should be camel case here
                chat.server.sendMessage($("#txtMsg").val());
            });
        });
    </script>
</body>
</html>
کدهای آن‌را به نحو فوق تغییر دهید.
توضیحات:
همانطور که ملاحظه می‌کنید ابتدا ارجاعاتی به jquery و jquery.signalR-1.0.1.min.js اضافه شده‌اند. سپس نیاز است مسیر دقیق فایل پروکسی هاب خود را نیز مشخص کنیم. اینکار با تعریف مسیر signalr/hubs انجام شده است.
<script src="http://localhost:1072/signalr/hubs" type="text/javascript"></script>
در ادامه توسط تنظیم connection.hub.logging سبب خواهیم شد تا اطلاعات بیشتری در javascript console مرورگر لاگ شود.
سپس ارجاعی به هاب تعریف شده، تعریف گردیده است. اگر از قسمت قبل به خاطر داشته باشید، توسط ویژگی HubName، نام chat را برگزیدیم. بنابراین connection.chat ذکر شده دقیقا به این هاب اشاره می‌کند.
سپس سطر chat.client.hello مقدار دهی شده است. متد hello، متدی dynamic و تعریف شده در سمت هاب برنامه است. به این ترتیب می‌توان به پیام‌های رسیده از طرف سرور گوش فرا داد. در اینجا، این پیام‌ها، به li ایی با id مساوی messages اضافه می‌شوند.
سپس توسط فراخوانی متد connection.hub.start، فاز negotiation شروع می‌شود. در اینجا حتی می‌توان نوع transport را نیز صریحا انتخاب کرد که نمونه‌ای از آن را به صورت کامنت شده جهت آشنایی با نحوه تعریف آن مشاهده می‌کنید. مقادیر قابل استفاده در آن به شرح زیر هستند:
 - webSockets
- forverFrame
- serverSentEvents
- longPolling
سپس به رویدادهای کلیک دکمه send گوش فرا داده و در این حین، اطلاعات TextBox ایی با id مساوی txtMsg را به متد SendMessage هاب خود ارسال می‌کنیم. همانطور که پیشتر نیز عنوان شد، در سمت کلاینت، تعریف متد SendMessage باید camel case باشد.

اکنون به صورت جداگانه یکبار برنامه hub را در مرورگر باز کنید. سپس بر روی فایل chat.htm کلیک راست کرده و گزینه مشاهده آن را در مرورگر نیز انتخاب نمائید (گزینه View in browser منوی کلیک راست).

خوب! پروژه کار نمی‌کند! برای اینکه مشکلات را بهتر بتوانید مشاهده کنید نیاز است به JavaScript Console مرورگر خود مراجعه نمائید. برای مثال در مرورگر کروم دکمه F12 را فشرده و برگه Console آن‌را باز کنید. در اینجا اعلام می‌کند که فاز negotiation قابل انجام نیست؛ چون مسیر پیش فرضی را که انتخاب کرده است، همین مسیر پروژه دومی است که اضافه کرده‌ایم (کلاینت ما در پروژه دوم قرار دارد و نه در همان پروژه اول هاب).
برای اینکه مسیر دقیق hub را در این حالت مشخص کنیم، سطر زیر را به ابتدای کدهای جاوا اسکریپتی فوق اضافه نمائید:
 $.connection.hub.url = 'http://localhost:1072/signalr'; //چون در یک پروژه دیگر قرار داریم
اکنون اگر مجدا سعی کنید، باز هم برنامه کار نمی‌کند و پیام می‌دهد که امکان دسترسی به این سرویس از خارج از دومین آن میسر نیست. برای اینکه این مجوز را صادر کنیم نیاز است تنظیمات مسیریابی پروژه هاب را به نحو ذیل ویرایش نمائیم:
using System;
using System.Web;
using System.Web.Routing;
using Microsoft.AspNet.SignalR;

namespace SignalR02
{
    public class Global : HttpApplication
    {
        protected void Application_Start(object sender, EventArgs e)
        {
            // Register the default hubs route: ~/signalr
            RouteTable.Routes.MapHubs(new HubConfiguration
            {
                EnableCrossDomain = true
            });
        }
    }
}
با تنظیم EnableCrossDomain به true اینبار فاز‌های آغاز ارتباط با سرور برقرار می‌شوند:
 SignalR: Auto detected cross domain url. jquery.signalR-1.0.1.min.js:10
SignalR: Negotiating with 'http://localhost:1072/signalr/negotiate'. jquery.signalR-1.0.1.min.js:10
SignalR: SignalR: Initializing long polling connection with server. jquery.signalR-1.0.1.min.js:10
SignalR: Attempting to connect to 'http://localhost:1072/signalr/connect?transport=longPolling&connectionToken…NRh72omzsPkKqhKw2&connectionData=%5B%7B%22name%22%3A%22chat%22%7D%5D&tid=3' using longPolling. jquery.signalR-1.0.1.min.js:10
SignalR: Longpolling connected jquery.signalR-1.0.1.min.js:10
مطابق این لاگ‌ها ابتدا فاز negotiation انجام می‌شود. سپس حالت long polling را به صورت خودکار انتخاب می‌کند.

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

یک نکته:
اگر از ویندوز 8 (یعنی IIS8) و VS 2012 استفاده می‌کنید، برای استفاده از حالت Web socket، ابتدا فایل وب کانفیگ برنامه را باز کرده و در قسمت httpRunTime، مقدار ویژگی targetFramework را بر روی 4.5 تنظیم کنید. اینبار اگر مراحل negotiation را بررسی کنید در همان مرحله اول برقراری اتصال، از روش Web socket استفاده گردیده است.


تمرین 1
به پروژه ساده و ابتدایی فوق یک تکست باکس دیگر به نام Room را اضافه کنید؛ به همراه دکمه join. سپس نکات قسمت قبل را در مورد الحاق به یک گروه و سپس ارسال پیام به اعضای گروه را پیاده سازی نمائید. (تمام نکات آن با مطلب فوق پوشش داده شده است و در اینجا باید صرفا فراخوانی متدهای عمومی دیگری در سمت هاب، صورت گیرد)

تمرین 2
در انتهای قسمت دوم به نحوه ارسال پیام از یک هاب به هابی دیگر اشاره شد. این MonitorHub را ایجاد کرده و همچنین یک کلاینت جاوا اسکریپتی را نیز برای آن تهیه کنید تا بتوان اتصال و قطع اتصال کلیه کاربران سیستم را مانیتور و مشاهده کرد.


پیاده سازی کلاینت jQuery بدون استفاده از کلاس Proxy

در مثال قبل، از پروکسی پویای مهیای در آدرس signalr/hubs استفاده کردیم. در اینجا قصد داریم، بدون استفاده از آن نیز کار برپایی کلاینت را بررسی کنیم.
بنابراین یک فایل جدید html را مثلا به نام chat_np.html به پروژه دوم برنامه اضافه کنید. سپس محتویات آن‌را به نحو زیر تغییر دهید:
<!DOCTYPE>
<html>
<head>
    <title></title>
    <script src="Scripts/jquery-1.6.4.min.js" type="text/javascript"></script>
    <script src="Scripts/jquery.signalR-1.0.1.min.js" type="text/javascript"></script>
</head>
<body>
    <div>
        <input id="txtMsg" type="text" /><input id="send" type="button" value="send msg" />
        <ul id="messages">
        </ul>
    </div>
    <script type="text/javascript">
        $(function () {                        
            $.connection.hub.logging = true; //اطلاعات بیشتری را در جاوا اسکریپت کنسول مرورگر لاگ می‌کند

            var connection = $.hubConnection();
            connection.url = 'http://localhost:1072/signalr'; //چون در یک پروژه دیگر قرار داریم
            var proxy = connection.createHubProxy('chat');

            proxy.on('hello', function (message) {
                //متدی که در اینجا تعریف شده دقیقا مطابق نام متد پویایی است که در هاب تعریف شده است
                //به این ترتیب سرور می‌تواند کلاینت را فراخوانی کند
                $("#messages").append("<li>" + message + "</li>");
            });

            $("#send").click(function () {
                // Hub's `SendMessage` should be camel case here
                proxy.invoke('sendMessage', $("#txtMsg").val());
            });

            connection.start();
        });
    </script>
</body>
</html>
در اینجا سطر مرتبط با تعریف مسیر اسکریپت‌های پویای signalr/hubs را دیگر در ابتدای فایل مشاهده نمی‌کنید. کار تشکیل proxy اینبار از طریق کدنویسی صورت گرفته است. پس از ایجاد پروکسی، برای گوش فرا دادن به متدهای فراخوانی شده از طرف سرور از متد proxy.on و نام متد فراخوانی شده سمت سرور استفاده می‌کنیم و یا برای ارسال اطلاعات به سرور از متد proxy.invoke به همراه نام متد سمت سرور استفاده خواهد شد.


کلاینت‌های دات نتی SignalR
تا کنون Solution ما حاوی یک پروژه Hub و یک پروژه وب کلاینت جی‌کوئری است. به همین Solution، یک پروژه کلاینت کنسول ویندوزی را نیز اضافه کنید.
سپس در خط فرمان پاور شل نوگت دستور زیر را صادر نمائید تا فایل‌های مورد نیاز به پروژه کنسول اضافه شوند:
 PM> Install-Package Microsoft.AspNet.SignalR.Client
در اینجا نیز باید دقت داشت تا دستور بر روی default project صحیحی اجرا شود (حالت پیش فرض، اولین پروژه موجود در solution است).
پس از نصب آن اگر به پوشه packages مراجعه کنید، نگارش‌های مختلف آن‌را مخصوص سیلورلایت، دات نت‌های 4 و 4.5، WinRT و ویندوز فون8 نیز می‌توانید در پوشه Microsoft.AspNet.SignalR.Client ملاحظه نمائید. البته در ابتدای نصب، انتخاب نگارش مناسب، بر اساس نوع پروژه جاری به صورت خودکار صورت می‌گیرد.
مدل برنامه نویسی آن نیز بسیار شبیه است به حالت عدم استفاده از پروکسی در حین استفاده از jQuery که در قسمت قبل بررسی گردید و شامل این مراحل است:
1) یک وهله از شیء HubConnection را ایجاد کنید.
2) پروکسی مورد نیاز را جهت اتصال به Hub از طریق متد CreateProxy تهیه کنید.
3) رویدادگردان‌ها را همانند نمونه کدهای جاوا اسکریپتی قسمت قبل، توسط متد On تعریف کنید.
4) به کمک متد Start، اتصال را آغاز نمائید.
5) متدها را به کمک متد Invoke فراخوانی نمائید.

using System;
using Microsoft.AspNet.SignalR.Client.Hubs;

namespace SignalR02.WinClient
{
    class Program
    {
        static void Main(string[] args)
        {
            var hubConnection = new HubConnection(url: "http://localhost:1072/signalr");
            var chat = hubConnection.CreateHubProxy(hubName: "chat");

            chat.On<string>("hello", msg => {
                Console.WriteLine(msg);
            });

            hubConnection.Start().Wait();

            chat.Invoke<string>("sendMessage", "Hello!");

            Console.WriteLine("Press a key to terminate the client...");
            Console.Read();
        }
    }
}
نمونه‌ای از این پیاده سازی را در کدهای فوق ملاحظه می‌کنید که از لحاظ طراحی آنچنان تفاوتی با نمونه ذهنی جاوا اسکریپتی ندارد.

نکته مهم
کلیه فراخوانی‌هایی که در اینجا ملاحظه می‌کنید غیرهمزمان هستند.
به همین جهت پس از متد Start، متد Wait ذکر شده‌است تا در این برنامه ساده، پس از برقراری کامل اتصال، کار invoke صورت گیرد و یا زمانیکه callback تعریف شده توسط متد chat.On فراخوانی می‌شود نیز این فراخوانی غیرهمزمان است و خصوصا اگر نیاز است رابط کاربری برنامه را در این بین به روز کنید باید به نکات به روز رسانی رابط کاربری از طریق یک ترد دیگر دقت داشت.
مطالب
نکات امنیتی OWASP مخصوص برنامه نویس‌های دات نت
OWSAP ارگانی است غیرانتفاعی که هدف آن ترویج طراحی برنامه‌های امن وب است. در این راه هم مطالب آموزشی بسیار ارزشمندی را منتشر کرده است [+]. در لینک‌های زیر این مطالب از دیدگاه برنامه نویس‌های دات نت مورد بررسی قرار گرفته‌اند. هر چند مطابق آخرین گزارش WhiteHat که اخیرا منتشر شده [+]، تعداد Exploits مربوط به ASP.NET در مقایسه با PHP و جاوا بسیار کمتر بوده اما نیاز است تا با مشکلات عمومی موجود و راه‌حل‌های مرتبط بیشتر آشنا شد:



مطالب دوره‌ها
مدل سازی داده‌ها در RavenDB
در مطلب جاری، به صورت اختصاصی، مبحث مدل سازی اطلاعات و رسیدن به مدل ذهنی مرسوم در طراحی‌های NoSQL سندگرا را در مقایسه با دنیای Relational، بررسی خواهیم کرد.


تفاوت‌های دوره ما با زمانیکه بانک‌های اطلاعاتی رابطه‌ای پدیدار شدند

- دنیای بانک‌های اطلاعاتی رابطه‌ای برای Write بهینه سازی شده‌اند؛ از این جهت که تاریخچه پیدایش آن‌ها به دهه 70 میلادی بر می‌گردد، زمانیکه برای تهیه سخت دیسک‌ها باید هزینه‌های گزافی پرداخت می‌شد. به همین جهت الگوریتم‌ها و روش‌های بسیاری در آن دوره ابداع شدند تا ذخیره سازی اطلاعات، حجم کمتری را به خود اختصاص دهند. اینجا است که مباحثی مانند Normalization بوجود آمدند تا تضمین شود که داده‌ها تنها یکبار ذخیره شده و دوبار در جاهای مختلفی ذخیره نگردند. جهت اطلاع در سال 1980 میلادی، یک سخت دیسک 10 مگابایتی حدود 4000 دلار قیمت داشته است.
- تفاوت مهم دیگر دوره ما با دهه‌های 70 و 80 میلادی، پدیدار شدن UI و روابط کاربری بسیار پیچیده، در مقایسه با برنامه‌های خط فرمان یا حداکثر فرم‌های بسیار ساده ورود اطلاعات در آن زمان است. برای مثال در دهه 70 میلادی تصور UI ایی مانند صفحه ابتدایی سایت Stack overflow احتمالا به ذهن هم خطور نمی‌کرده است.


تهیه چنین UI ایی نه تنها از لحاظ طراحی، بلکه از لحاظ تامین داده‌ها از جداول مختلف نیز بسیار پیچیده است. برای مثال برای رندر صفحه اول سایت استک اورفلو ابتدا باید تعدادی سؤال از جدول سؤالات واکشی شوند. در اینجا در ذیل هر سؤال نام شخص مرتبط را هم مشاهده می‌کنید. بنابراین اطلاعات نام او، از جدول کاربران نیز باید دریافت گردد. یا در اینجا تعداد رای‌های هر سؤال را نیز مشاهده می‌کنید که به طور قطع اطلاعات آن در جدول دیگری نگه داری می‌شود. در گوشه‌ای از صفحه، برچسب‌های مورد علاقه و در ذیل هر سؤال، برچسب‌های اختصاصی هر مطلب نمایش داده شده‌اند. تگ‌ها نیز در جدولی جداگانه قرار دارند. تمام این قسمت‌های مختلف، نیاز به واکشی و رندر حجم بالایی از اطلاعات را دارند.
- تعداد کاربران برنامه‌ها در دهه‌های 70 و 80 میلادی نیز با دوره ما متفاوت بوده‌اند. اغلب برنامه‌های آن دوران تک کاربره طراحی می‌شدند؛ با بانک‌های اطلاعاتی که صرفا جهت کار بر روی یک سیستم طراحی شده بودند. اما برای نمونه سایت استک اور فلویی که مثال زده شده، توسط هزاران و یا شاید میلیون‌ها نفر مورد استفاده قرار می‌گیرد؛ با توزیع و تقسیم اطلاعات آن بر روی سرورها مختلف.


معرفی مفهوم Unit of change

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


محدود‌ه‌های تراکنشی یا Transactional boundaries

محدوده‌های تراکنشی در Domain driven design به Aggregate root نیز معروف است. هر محدود تراکنشی حاوی یک Unit of change قرار گرفته داخل یک سند است. ابتدا بررسی می‌کنیم که در یک Read به چه نوع اطلاعاتی نیاز داریم و سپس کل اطلاعات مورد نیاز را بدون نوشتن JOIN ایی از جداول دیگر، داخل یک سند قرار می‌دهیم.
هر محدوده تراکنشی می‌تواند به محدوده تراکنشی دیگری نیز ارجاع داده باشد. برای مثال در RavenDB شماره‌های اسناد، یک سری رشته هستند؛ برخلاف بانک‌های اطلاعاتی رابطه‌ای که بیشتر از اعداد برای مشخص سازی Id استفاده می‌کنند. در این حالت برای ارجاع به یک کاربر فقط کافی است برای مثال مقدار خاصیت کاربر یک سند به "users/1" تنظیم شود. "users/1" نیز یک Id تعریف شده در RavenDB است.
مزیت این روش، سرعت واکشی بسیار بالای دریافت اطلاعات آن است؛ دیگر در اینجا نیازی به JOINهای سنگین به جداول دیگر برای تامین اطلاعات مورد نیاز نیست و همچنین در ساختار‌های پیچیده‌تری مانند ساختارهای تو در تو، دیگر نیازی به تهیه کوئری‌های بازگشتی و استفاده از روش‌های پیچیده مرتبط با آن‌ها نیز وجود ندارد و کلیه اطلاعات مورد نظر، به شکل یک شیء JSON داخل یک سند حاضر و آماده برای واکشی در طی یک Read هستند.
به این ترتیب می‌توان به سیستم‌های مقیاس پذیری رسید. سیستم‌هایی که با بالا رفتن حجم اطلاعات در حین واکشی‌های داده‌های مورد نیاز، کند نبوده و بسیار سریع پاسخ می‌دهند.


Denormalization داده‌ها

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


ملاحظات اندازه‌های داده‌ها

زمانیکه سند‌ها بسیار بزرگ می‌شوند چه رخ خواهد داد؟ از لحاظ اندازه داده‌ها سه نوع سند را می‌توان متصور بود:
الف) سندهای محدود، مانند اغلب اطلاعاتی که تعداد فیلدهای مشخصی دارند با تعداد اشیاء مشخصی.
ب) سندهای نامحدود اما با محدودیت طبیعی. برای مثال اطلاعات فرزندان یک شخص را درنظر بگیرید. هرچند این اطلاعات نامحدود هستند، اما به صورت طبیعی می‌توان فرض کرد که سقف بالایی آن عموما به 20 نمی‌رسد!
ج) سندهای نامحدود، مانند سندهایی که آرایه‌ای از اطلاعات را ذخیره می‌کنند. برای مثال در یک سایت فروشگاه، اطلاعات فروش یک گروه از اجناس خاص را درنظر بگیرید که عموما نامحدود است. اینجا است که باید به اندازه اسناد نیز دقت داشت. برای مدیریت این مساله حداقل از دو روش استفاده می‌شود:
- محدود کردن تعداد اشیاء. برای مثال در هر سند حداکثر 100 اطلاعات فروش یک محصول بیشتر ثبت نشود. زمانیکه به این حد رسیدیم، یک سند جدید ایجاد شده و Id سند قبلی مثلا "products/1" در سند دوم ذکر خواهد شد.
- محدود کردن تعداد اطلاعات ذخیره شده بر اساس زمان
RavenDB برای مدیریت این مساله، مفهوم Includes را معرفی کرده است. در اینجا با استفاده از متد الحاقی Include، کار زنجیر کردن سندهای مرتبط صورت خواهد گرفت.



یک مثال عملی: مدل سازی داده‌های یک بلاگ در RavenDB

پس از این بحث مقدماتی که جهت معرفی ذهنیت مدل سازی داده‌ها در دنیای غیر رابطه‌ای NoSQL ضروری بود، در ادامه قصد داریم مدل‌های داده‌های یک بلاگ را سازگار با ساختار بانک اطلاعاتی NoSQL سندگرای RavenDB طراحی کنیم.
در یک بلاگ، تعدادی مطلب، نظر، برچسب (گروه‌های مطالب) و امثال آن وجود دارند. اگر بخواهیم این اطلاعات را به صورت رابطه‌ای مدل کنیم، به ازای هر کدام از این موجودیت‌ها یک جدول نیاز خواهد بود و برای رندر صفحه اصلی بلاگ، چندین و چند کوئری برای نمایش اطلاعات مطالب، نویسنده(ها)، برچسب‌ها و غیره باید به بانک اطلاعاتی ارسال گردد، که تعدادی از آن‌ها مستقیما بر روی یک جدول اجرا می‌شوند و تعدادی دیگر نیاز به JOIN دارند.
مشکلاتی که روش رابطه‌ای دارد:
- تعداد اعمالی که باید برای نمایش صفحه اول سایت صورت گیرد، بسیار زیاد است و این مساله با تعداد بالای کاربران از دید مقیاس پذیری سیستم مشکل ساز است.
- داده‌های مرتبط در جداول مختلفی پراکنده‌اند.
- این سیستم برای Write بهینه سازی شده است و نه برای Read. (همان بحث گران بودن سخت دیسک‌ها در دهه‌های قبل که در ابتدای بحث به آن اشاره شد)

مدل سازی سازگار با دنیای NoSQL یک بلاگ

در اینجا چند کلاس مقدماتی را مشاهده می‌کنید که تعریف آن‌ها به همین نحو صحیح است و نیاز به جزئیات و یا روابط بیشتری ندارند.
namespace RavenDBSample01.BlogModels
{
    public class BlogConfig
    {
        public string Id { set; get; }
        public string Title { set; get; }
        public string Description { set; get; }
        // ... more items here
    }

    public class User
    {
        public string Id { set; get; }
        public string FullName { set; get; }
        public string Email { set; get; }
        // ... more items here
    }
}
اما کلاس مطالب بلاگ را به چه صورتی طراحی کنیم؟ هر مطلب، دارای تعدادی نظر خواهد بود. اینجا است که بحث unit of change مطرح می‌شود و درج اطلاعاتی که در طی یک read نیاز است از بانک اطلاعاتی جهت رندر UI واکشی شوند. به این ترتیب به این نتیجه می‌رسیم که بهتر است کلیه کامنت‌های یک مطلب را داخل همان شیء مطلب مرتبط قرار دهیم. از این جهت که یک نظر، خارج از یک مطلب بلاگ دارای مفهوم نیست.
اما این طراحی نیز یک مشکل دارد. درست است که ساختار یک صفحه مطلب، از مطالب وبلاگ به همین نحوی است که توضیح داده شد؛ اما در صفحه اول سایت، هیچگاه کامنت‌های مطالب درج نمی‌شوند. بنابراین نیازی نیست تا تمام کامنت‌ها را داخل یک مطلب ذخیره کرد. به این ترتیب برای نمایش صفحه اول سایت، حجم کمتری از اطلاعات واکشی خواهند شد.
    public class Post
    {
        public string Id { set; get; }
        public string Title { set; get; }
        public string Body { set; get; }

        public ICollection<string> Tags { set; get; }

        public string AuthorId { set; get; }

        public string PostCommentsId { set; get; }
        public int CommentsCount { set; get; }
    }

    public class Comment
    {
        public string Id { set; get; }
        public string Body { set; get; }
        public string AuthorName { set; get; }
        public DateTime CreatedAt { set; get; }
    }

    public class PostComments
    {
        public List<Comment> Comments { set; get; }
        public string LastCommentId { set; get; }
    }
در اینجا ساختار Post و Commentهای بلاگ را مشاهده می‌کنید. جایی که ذخیره سازی اصلی کامنت‌ها صورت می‌گیرد در شیء PostComments است. یعنی PostCommentsId شیء Post به یک وهله از شیء PostComments که حاوی کلیه کامنت‌های آن مطلب است، اشاره می‌کند.
به این ترتیب برای نمایش صفحه اول سایت، فقط یک کوئری صادر می‌شود. برای نمایش یک مطلب و کلیه کامنت‌های متناظر با آن دو کوئری صادر خواهند شد.

بنابراین همانطور که مشاهده می‌کنید، در دنیای NoSQL، طراحی مدل‌های داده‌ای بر اساس «سناریوهای Read» صورت می‌گیرد و نه صرفا طراحی یک مدل رابطه‌ای بهینه سازی شده برای حالت Write.

سورس کامل ASP.NET MVC این بلاگ‌را که «راکن بلاگ» نام دارد، از GitHub نویسندگان اصلی RavenDB می‌توانید دریافت کنید.
مسیرراه‌ها
React 16x
پیش نیاز ها
کامپوننت ها
ترکیب کامپوننت ها
طراحی یک گرید
مسیریابی 
کار با فرم ها
ارتباط با سرور
احراز هویت و اعتبارسنجی کاربران 
React Hooks  
توزیع برنامه

مدیریت پیشرفته‌ی حالت در React با Redux و Mobx   

       Redux
       MobX  

مطالب تکمیلی 
    مطالب
    فرم‌های مبتنی بر قالب‌ها در Angular - قسمت سوم - Data binding
    در قسمت قبل، ساختار فرم ثبت اطلاعات کارمندان را تکمیل کردیم. در این قسمت قصد داریم این اطلاعات را در کامپوننت آن توسط data binding دریافت کنیم.


    نقش ngModel در data binding

    ngModel دایرکتیوی است که وجود آن سبب می‌شود تا Angular آن المان ورودی خاص را تحت نظر قرار دهد:
    <!--no binding -->
    <input name="firstname" ngModel>
    در حالت تعریفی فوق، هیچگونه عملیات data binding ایی صورت نمی‌گیرد؛ اما Angular به علت وجود ngModel، از وجود این فیلد مطلع شده‌است. اما کامپوننت برنامه اطلاعات خاصی را دریافت نخواهد کرد.
    برای رفع این مشکل می‌توان با data binding یک طرفه شروع کرد:
    <!--one way binding -->
    <input name="firstname" [ngModel]="firstName">
    در اینجا از syntax ویژه‌ی property binding استفاده شده و ngModel داخل [] قرار گرفته‌است و به firstName تنظیم شده‌است. در این حالت Angular در کامپوننت متناظر با این قالب HTML ایی، به دنبال یک خاصیت عمومی به نام firstName می‌گردد و مقدار اولیه‌ی این فیلد را از آن دریافت می‌کند.
    در حالت data binding یک طرفه، اگر کاربر اطلاعات فیلد firstname را در فرم برنامه تغییر دهد، این اطلاعات به خاصیت عمومی firstName منعکس نخواهد شد.
    برای رفع این مشکل (در صورت نیاز)، می‌توان از data binding دو طرفه استفاده کرد:
    <!--two way binding -->
    <input name="firstname" [ngModel]="firstName"
    (ngModelChange)="firstName=$event">
    این حالت شبیه به حالت data binding یک طرفه است؛ با این تفاوت که رویدادگردانی ngModelChange نیز به آن اضافه شده‌است. در اینجا event$ به مقدار فیلد تغییر یافته اشاره می‌کند و آن‌را به firstName انتساب می‌دهد.
    البته این حالت دو طرفه، syntax ساده شده‌ی زیر را که به banana in the box نیز معروف شده‌است (موز همان () است و جعبه به [] اشاره می‌کند)، نیز می‌تواند داشته باشد که بیشتر مورد استفاده قرار می‌گیرد:
    <!--two way binding -->
    <input name="firstname" [(ngModel)]="firstName">


    تعریف مدل فرم ثبت اطلاعات کارمندان

    برای نگهداری اطلاعات فرم کارمندان، کلاس Employee را به ماژول Employee اضافه می‌کنیم:
     > ng g cl Employee/Employee
    با این خروجی:
     installing class
      create src\app\Employee\employee.ts
    سپس ساختار این کلاس را به نحو ذیل تکمیل خواهیم کرد که هر کدام از خواص آن، معادل یکی از المان‌های فرم است:
    export class Employee {
      constructor(
        public firstName: string,
        public lastName: string,
        public isFullTime: boolean,
        public paymentType: string,
        public primaryLanguage: string
      ) {}
    }
    TypeScript این امکان را می‌دهد تا بتوان خواص عمومی را مستقیما در سازنده‌ی کلاس تعریف کرد. بنابراین در اینجا برای نمونه firstName هم یکی از آرگومان‌های سازنده‌ی کلاس کارمند است و هم یک خاصیت عمومی تعریف شده‌ی در آن. به علاوه در اینجا می‌توان به این خواص، مقادیر پیش فرضی را نیز انتساب داد تا در حین وهله سازی آن بتوان از تعریف اجباری یک سری از پارامترها صرفنظر کرد.

    پس از آن، به فایل employee-register.component.ts مراجعه کرده و وهله‌ای از کلاس را به صورت یک خاصیت عمومی در اختیار قالب HTML ایی آن که فرم جاری را تشکیل می‌دهد، قرار می‌دهیم:
    import { Employee } from "app/employee/employee";
    
    export class EmployeeRegisterComponent implements OnInit {
      languages = ["Persian", "English", "Spanish", "Other"];
      model = new Employee("Vahid", "N", true, "FullTime", "Persian");
    ابتدا کلاس کارمند import شده و سپس وهله‌ای از آن به نام model، به صورت یک خاصیت عمومی در اختیار قالب آن قرار گرفته‌است.


    تغییر قالب فرم ثبت اطلاعات کارمندان برای اتصال به model

    در ادامه، مرحله به مرحله قالب فرم جاری را جهت اتصال به شیء model فوق تغییر خواهیم داد:

    اتصال به Text boxes

      <form #form="ngForm" novalidate>
        <div class="form-group">
          <label>First Name</label>
          <input type="text" class="form-control" name="firstName" [(ngModel)]="model.firstName">
        </div>
    
        <div class="form-group">
          <label>Last Name</label>
          <input type="text" class="form-control" name="lastName" [(ngModel)]="model.lastName">
        </div>
    همانطور که مشاهده می‌کنید، اینبار ngModel خالی قسمت قبل را توسط syntax تکمیلی banana in the box به data binding دو طرفه تغییر داده‌ایم. به این ترتیب در ابتدای نمایش فرم، این دو فیلد، مقادیر اولیه نام و نام خانوادگی را از شیء model دریافت کرده و نمایش می‌دهند. به علاوه اگر فرم نیز تغییر کند، این اطلاعات به شیء model و خواص آن نیز منعکس می‌شوند.

    برای بررسی این مورد، در پایان فرم جهت دیباگ data binding، اطلاعاتی را که در مدل داریم و همچنین اطلاعاتی را که Angular در حال نظارت بر آن‌ها است، به صورت json در صفحه درج می‌کنیم:
        <button class="btn btn-primary" type="submit">Ok</button>
      </form>
      Model: {{ model | json }}
      <br> Angular: {{ form.value | json }}
      <br> form.pristine: {{ form.pristine }}
    برای مثال یکبار [()] را به [] تبدیل کنید و سپس سعی در تغییر مقادیر فرم نمائید. مشاهده می‌کنید هرچند این اطلاعات تحت نظارت Angular هستند، اما چون data binding به حالت یک طرفه تغییر کرده‌است، دیگر انعکاس آن‌ها، در Model مشاهده نمی‌شوند.


    اتصال به Check boxes

        <div class="checkbox">
          <label>
                <input type="checkbox" name="is-full-time"
                       [(ngModel)]="model.isFullTime"> Full Time Employee
                </label>
        </div>
    روش کار در اینجا نیز همانند قبل است. با استفاده از data binding دو طرفه، مقدار checkbox را به یک خاصیت عمومی boolean انتساب داده‌ایم و برعکس (زمانیکه فرم برای بار اول نمایش داده می‌شود، مقدار اولیه‌ی خود را از شیء model دریافت می‌کند).

    اتصال به Radio buttons

        <label>Payment Type</label>
        <div class="radio">
          <label>
                <input type="radio" name="paymentType" value="FullTime" checked
                       [(ngModel)]="model.paymentType">
                    Full Time
                </label>
        </div>
        <div class="radio">
          <label>
                <input type="radio" name="paymentType" value="PartTime"
                       [(ngModel)]="model.paymentType">
                    Part Time
                </label>
        </div>
    روش اتصال به radio buttons نیز بر اساس data binding دو طرفه‌است. فقط در اینجا دقیقا یک خاصیت مشخص، به چندین radio button متصل شده‌است و در نهایت در این گروه که بر اساس name هایی یکسان تشکیل شده‌است، یک مقدار انتخاب می‌شود و مقدار آن از ویژگی value المان متناظر دریافت می‌گردد.

    اتصال به Drop downs

        <div class="form-group">
          <label>Primary Language</label>
          <select class="form-control" name="primaryLanguage" [(ngModel)]="model.primaryLanguage">
              <option *ngFor="let lang of languages">
                 {{ lang }}
              </option>
          </select>
        </div>
    در اینجا نیز ابتدا نامی به این المان انتساب داده شده‌است و سپس توسط data binding دو طرفه، خاصیت متناظری از مدل را به این المان متصل کرده‌ایم یا برعکس؛ زمانیکه این فرم برای اولین بار نمایش داده می‌شود، مقدار اولیه‌ی این فیلد بر اساس مقدار آن در شیء model تعیین می‌شود:



    نحوه‌ی فراخوانی یک متد در حین data binding دو طرفه

    همانطور که در ابتدای بحث نیز عنوان شد، data binding دو طرفه را به نحو دیگری نیز می‌توان تعریف کرد:

    <div class="form-group">
                <label>First Name</label>
                <input type="text" class="form-control" name="firstName" 
                    [ngModel]="model.firstName" 
                    (ngModelChange)="firstNameToUpperCase($event)">
    </div>
    در اینجا بجای استفاده‌ی از syntax معروف banana in the box، از روش اتصال یک طرفه و سپس دریافت تغییرات از طریق یک رخدادگردان استفاده شده‌است. مزیت این روش امکان دسترسی همزمان به مقدار وارد شده‌ی توسط کاربر، در کامپوننت متناظر می‌باشد:
      firstNameToUpperCase(value: string) {
        if (value.length > 0)
          this.model.firstName = value.charAt(0).toUpperCase() + value.slice(1);
        else
          this.model.firstName = value;
      }
    برای مثال در اینجا اگر کاربر حرف اول یک نام را با حروف کوچک وارد کند، توسط این متد به حرف بزرگ تبدیل شده و جایگزین می‌شود. این جایگزینی نیز بلافاصله در فرم منعکس خواهد شد.

    در قسمت بعد مباحث اعتبارسنجی فرم‌های مبتنی بر قالب‌ها را بررسی می‌کنیم.


    کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: angular-template-driven-forms-lab-03.zip
    برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
    مطالب دوره‌ها
    تزریق وابستگی‌ها
    خوب! بالاخره بعد از دو قسمت قبل، به بحث اصلی تزریق وابستگی‌ها رسیدیم. بحثی که بسیاری از برنامه نویس‌های تصور می‌کنند همان IoC است که پیشتر در مورد آن بحث شد.

    تزریق وابستگی‌ها یا DI چیست؟

    تزریق وابستگی‌ها یکی از انواع IoC بوده که در آن ایجاد و انقیاد (binding) یک وابستگی، در خارج از کلاسی که به آن نیاز دارد صورت می‌گیرد. روش‌های متفاوتی برای ارائه این وابستگی وهله سازی شده در خارج از کلاس به آن وجود دارد که در ادامه مورد بررسی قرار خواهند گرفت.
    یک مثال: بسته غذایی را که با خود به سر کار می‌برید درنظر بگیرید. تمام اجزای مورد نیاز تشکیل دهنده یک بسته غذا عموما داخل آن قرار گرفته و حمل می‌شوند. حالت عکس آن زمانی است که در محل کار به شما غذا می‌دهند. این دقیقا همان حالتی است که تزریق وابستگی‌ها کار می‌کند؛ یک سری سرویس‌های خارجی، نیازهای کلاس جاری را برآورده می‌سازند.


    در تصویر فوق یک طراحی مبتنی بر معکوس سازی کنترل‌ها را مشاهده می‌کنید. وابستگی‌های یک کلاس توسط اینترفیسی مشخص شده و سپس کلاسی وجود دارد که این وابستگی را پیاده سازی کرده است.
    همچنین در این تصویر یک خط منقطع از کلاس MyClass به کلاس Dependency رسم شده است. این خط، مربوط به حالتی است که خود کلاس، مستقیما کار وهله سازی وابستگی مورد نیاز خود را انجام دهد؛ هر چند اینترفیسی نیز در این بین تعریف شده باشد. بنابراین اگر در بین کدهای این کلاس، چنین کدی مشاهده شد:
     IDependency dependency= new Dependency();
    تعریف dependency از نوع IDependency به معنای معکوس سازی کنترل نبوده و عملا همان معماری سابق و متداول بکارگرفته شده است. برای بهبود این وضعیت، از تزریق وابستگی‌ها کمک خواهیم گرفت:


    در اینجا یک کلاس دیگر به نام Injector اضافه شده است که قابلیت تزریق وابستگی‌ها را به کلاس نیازمند آن به روش‌های مختلفی دارا است. کار کلاس Injector، وهله سازی MyClass و همچنین وابستگی‌های آن می‌باشد؛ سپس وابستگی را دراختیار MyClass قرار می‌دهد.


    تزریق وابستگی‌ها در سازنده کلاس

    یکی از انواع روش‌های تزریق وابستگی‌ها، Constructor Injection و یا تزریق وابستگی‌ها در سازنده کلاس می‌باشد که متداول‌ترین نوع آن‌ها نیز به‌شمار می‌رود. در این حالت، وابستگی پس از وهله سازی، از طریق پارامترهای سازنده یک کلاس، در اختیار آن قرار می‌گیرند.
    یک مثال:
    public class Shopper
    {
       private readonly ICreditCard _creditCard;
       public Shopper(ICreditCard creditCard)
       {
          _creditCard = creditCard;
       }
    }
    در اینجا شما یک کلاس خریدار را مشاهده می‌کنید که وابستگی مورد نیاز خود را به شکل یک اینترفیس از طریق سازنده کلاس دریافت می‌کند.
     ICreditCard creditCard = new MasterCard();
    Shopper shopper = new Shopper(creditCard);
    برای نمونه، وهله‌ای از مستر کارتی که  ICreditCard را پیاده سازی کرده باشد، می‌توان به سازنده این کلاس ارسال کرد. کار وهله سازی وابستگی و واژه کلیدی new به خارج از کلاس استفاده کننده از وابستگی منتقل شده است. بنابراین همانطور که ملاحظه می‌کنید، این مفاهیم آنچنان پیچیده نبوده و به همین سادگی قابل تعریف و اعمال هستند.


    تزریق در خواص عمومی کلاس

    Setter Injection و یا تزریق در خواص عمومی یک کلاس، یکی دیگر از روش‌های تزریق وابستگی‌ها است (setter در اینجا منظور همان get و set یک خاصیت می‌باشد).
    در حالت تزریق وابستگی‌ها در سازنده کلاس، امکان وهله سازی آن کلاس بدون ارسال وابستگی‌ها به سازنده آن ممکن نیست؛ اما در اینجا خیر و امکان وهله سازی و استفاده از یک کلاس، پیش از اعمال وابستگی‌ها نیز وجود دارد. بنابراین امکان تغییر و تعویض وابستگی‌ها پس از وهله سازی کلاس نیز میسر است.
    public class Shopper
    {
       public ICreditCard CreditCard { get; set; }
    }
    
    ICreditCard creditCard = new MasterCard();
    Shopper shopper = new Shopper();
    shopper.CreditCard = creditCard;
    نمونه‌ای از این روش را در مثال فوق مشاهده می‌کنید. در این کلاس، وابستگی مورد نیاز از طریق یک خاصیت عمومی دریافت شده است.


    تزریق اینترفیس‌ها

    تزریق اینترفیس‌ها نیز یکی دیگر از روش‌های تزریق وابستگی‌ها است؛ اما آنچنان استفاده گسترده‌ای برخلاف دو روش قبل نیافته است.
    در این روش نه از سازنده کلاس استفاده می‌شود و نه از یک خاصیت عمومی. ابتدا یک اینترفیس که بیانگر ساختار کلاسی که قرار است تزریق شود ایجاد می‌گردد. سپس این اینترفیس را در کلاس وابستگی مورد نظر پیاده سازی خواهیم کرد. در این اینترفیس نیاز است متد خاصی تعریف شود تا کار تزریق وابستگی را انجام دهد.
    یک مثال:
    public interface IDependOnCreditCard
    {
       void Inject(ICreditCard creditCard);
    }
    
    public class Shopper : IDependOnCreditCard
    {
      private ICreditCard creditCard;
      public void Inject(ICreditCard creditCard)
      {
         this.creditCard = creditCard;
      }
    }  
    
    ICreditCard creditCard = new MasterCard();
    Shopper shopper = new Shopper();
    ((IDependOnCreditCard)shopper).Inject(creditCard);
    در اینجا یک اینترفیس به نام IDependOnCreditCard  تعریف گردیده و متد خاصی را به نام Inject تدارک دیده است که نوعی از کردیت کارد را دریافت می‌کند.
    در ادامه کلاس خریدار، اینترفیس IDependOnCreditCard را پیاده سازی کرده است. به این ترتیب کلاس Injector تنها کافی است بداند تا این متد خاص را باید جهت تنظیم و تزریق وابستگی‌ها فراخوانی نماید. این روش نسبتا شبیه به روش Setter injection است.
    این روش بیشتر می‌تواند جهت فریم ورک‌هایی که قابلیت یافتن کلیه کلاس‌های مشتق شده از IDependOnCreditCard  را داشته و سپس می‌دانند که باید متد Inject آن‌ها را فراخوانی کنند، مناسب است.


    نکاتی که باید حین کار با تزریق وابستگی‌ها درنظر داشت

    الف) حین استفاده از تزریق وابستگی‌ها، وهله سازی به تاخیر افتاده وابستگی‌ها میسر نیست. برای مثال زمانیکه یک وابستگی قرار است در سازنده کلاسی تزریق شود، وهله سازی آن باید پیش از نیاز واقعی به آن صورت گیرد. البته امکان استفاده از اشیاء Lazy دات نت 4 برای مدیریت این مساله وجود دارد اما در حالت کلی، دیگر همانند قبل و روش‌های متداول، وهله سازی تنها زمانیکه نیاز به آن وابستگی خاص باشد، میسر نیست. به همین جهت باید به تعداد وابستگی‌هایی که قرار است در یک کلاس استفاده شوند نیز جهت کاهش مصرف حافظه دقت داشت.
    ب) یکی از مزایای دیگر تزریق وابستگی‌ها، ساده‌تر شدن نوشتن آزمون‌های واحد است. زیرا تهیه Mocks در این حالت کار با اینترفیس‌ها بسیار ساده‌تر است. اما باید دقت داشت، کلاسی که در سازنده آن حداقل 10 اینترفیس را به عنوان وابستگی دریافت می‌کند، احتمالا دچار مشکلاتی در طراحی و همچنین مصرف حافظه می‌باشد.