مطالب
نمایش در حال بارگذاری بودن صفحه در برنامه‌های Angular
پیشتر در مورد HTTP Client جدید Angular، مطلب «ارتقاء به HTTP Client در Angular 4.3» را بررسی کردیم. یکی دیگر از قابلیت‌های HttpClient که روش اصلی دسترسی به اطلاعات از راه دور در Angular 5 نیز می‌باشد، امکان تعریف Interceptors سفارشی برای آن است. به کمک Interceptors می‌توان به تمامی درخواست‌های HTTP ارسالی و پاسخ‌های دریافتی از سرور گوش فرا داد و در این بین منطق ویژه‌ای را پیاده سازی کرد؛ مانندcaching ،logging و غیره. در مطلب جاری قصد داریم از این قابلیت برای نمایش یک loading bar مشخص کننده‌ی شروع و پایان تمامی درخواست‌های HTTP استفاده کنیم.


دریافت و نصب پیشنیازها

برای نمایش loading bar در بالای صفحه، از کامپوننت ng2-slim-loading-bar استفاده خواهیم کرد. دمویی از آن‌را در اینجا می‌توانید مشاهده کنید.
برای نصب آن، ابتدا دستور ذیل را در ریشه‌ی پروژه اجرا کنید:
 npm install ng2-slim-loading-bar --save
پس از آن فایل angular-cli.json. را گشوده و فایل css آن‌را تعریف نمائید:
"styles": [
   "../node_modules/ng2-slim-loading-bar/style.css",
   "styles.css"
],
در ادامه به فایل app.module.ts مراجعه کرد و ماژول آن‌را نیز به نحو ذیل به برنامه معرفی کنید:
import {SlimLoadingBarModule} from 'ng2-slim-loading-bar';

@NgModule({
imports: [
     //...
     SlimLoadingBarModule.forRoot()
  ]
})
export class AppModule {
}

پس از این مقدمات، هرجایی که نیاز به استفاده‌ی از آن با کدنویسی باشد، ابتدا سرویس SlimLoadingBarService آن‌را به سازنده‌ی کلاس کامپوننت مدنظر تزریق می‌کنیم و سپس از متدهای start ،stop و complete آن می‌توان استفاده کرد.
 import {SlimLoadingBarService} from 'ng2-slim-loading-bar';
همچنین برای نمایش این کامپوننت باید selector آن را در فایل app.component.html در بالای صفحه اضافه کرد:
  <ng2-slim-loading-bar></ng2-slim-loading-bar>


تدارک یک LoaderInterceptor برای استفاده از ng2-slim-loading-bar

در ادامه می‌خواهیم هر زمانیکه در سراسر برنامه، درخواست HTTP ایی شروع شد، این کامپوننت نمایش داده شود و در پایان درخواست و یا درصورت بروز خطایی، پایان یابد و مخفی شود. برای این منظور یک Interceptor جدید را به صورت ذیل به پوشه‌ی Core برنامه اضافه می‌کنیم:
 ng g s core/interceptors/LoaderInterceptor --spec false
با این کدها:
import { Injectable } from "@angular/core";
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from "@angular/common/http";
import { Observable } from "rxjs/Observable";
import "rxjs/add/operator/do";
import { SlimLoadingBarService } from "ng2-slim-loading-bar";


@Injectable()
export class LoaderInterceptorService implements HttpInterceptor {

  constructor(private loadingBar: SlimLoadingBarService) {
  }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // start our loader here
    this.loadingBar.start();
    return next.handle(req).do(
      (event: HttpEvent<any>) => {
        if (event instanceof HttpResponse) {
          this.loadingBar.complete();
        }
      },
      (err: any) => {
        this.loadingBar.complete();
      });
  }
}
توضیحات:
- برای پیاده سازی یک interceptor جدید، نیاز است کلاس سرویسی را که HttpInterceptor را پیاده سازی می‌کند، ایجاد کنیم. برای تکمیل این پیاده سازی نیاز است متد intercept را با امضایی که مشاهده می‌کنید، تعریف کنیم:
 intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
- در اینجا پارامتر req به خود درخواست Http اشاره می‌کند.
- کار پارامتر next تنظیم و بازگشت یک HttpEvent observable توسط متد handle آن است.

برای نمونه در اینجا ابتدا سرویس SlimLoadingBarService به سازنده‌ی کلاس interceptor‌  تزریق شده‌است. سپس توسط آن می‌توان به متدهای start و complete این کامپوننت دسترسی یافت. برای مثال در ابتدای کار گوش فرادادن به درخواست جاری، متد start فراخوانی شده‌است و سپس زمانیکه پاسخی از سرور دریافت شده و یا خطایی رخ‌داده، متد complete آن فراخوانی شده‌است.


ثبت و معرفی LoaderInterceptorService به سیستم

برای معرفی interceptor تهیه شده، به فایل app.module.ts مراجعه کرد و قسمت providers آن‌را به نحو ذیل تکمیل می‌کنیم: 
 @NgModule({
  providers: [
   { provide: HTTP_INTERCEPTORS, useClass: LoaderInterceptorService, multi: true }
  ]
})
export class AppModule {}


آزمایش برنامه

اکنون اگر قسمت‌های مختلف برنامه را که با HttpClient جدید کار می‌کنند بررسی کنید، متوجه خواهید شد که با شروع هر درخواست، loading bar قرمزی در بالای صفحه ظاهر می‌شود و در پایان درخواست، به صورت خودکار مخفی می‌گردد. نکته‌ی مهم این روش عدم نیاز به تغییری در قسمت‌های مختلف برنامه است. این interceptor سراسری است و به صورت یکسانی بر روی کل برنامه تاثیر می‌گذارد.
مطالب
Blazor 5x - قسمت 23 - احراز هویت و اعتبارسنجی کاربران Blazor Server - بخش 3 - کار با نقش‌های کاربران
در قسمت قبل، روش یکپارچه سازی context مربوط به ASP.NET Core Identity را با یک برنامه‌ی Blazor Server، بررسی کردیم. در این قسمت می‌خواهیم محدود کردن دسترسی‌ها را بر اساس نقش‌های کاربران و همچنین کدنویسی مستقیم، بررسی کنیم.


کار با Authentication State از طریق کدنویسی

فرض کنید در کامپوننت HotelRoomUpsert.razor نمی‌خواهیم دسترسی‌ها را به کمک اعمال ویژگی attribute [Authorize]@ محدود کنیم؛ می‌خواهیم اینکار را از طریق کدنویسی مستقیم انجام دهیم:
// ...

@*@attribute [Authorize]*@


@code
{
    [CascadingParameter] public Task<AuthenticationState> AuthenticationState { get; set; }

    protected override async Task OnInitializedAsync()
    {
        var authenticationState = await AuthenticationState;
        if (!authenticationState.User.Identity.IsAuthenticated)
        {
            var uri = new Uri(NavigationManager.Uri);
            NavigationManager.NavigateTo($"/identity/account/login?returnUrl={uri.LocalPath}");
        }
        // ...
- در اینجا در ابتدا اعمال ویژگی Authorize را کامنت کردیم.
- سپس یک پارامتر ویژه را از نوع CascadingParameter، به نام AuthenticationState تعریف کردیم. این خاصیت از طریق کامپوننت CascadingAuthenticationState که در قسمت قبل به فایل BlazorServer.App\App.razor اضافه کردیم، تامین می‌شود.
- در آخر در روال رویدادگردان OnInitializedAsync، بر اساس آن می‌توان به اطلاعات User جاری وارد شده‌ی به سیستم دسترسی یافت و برای مثال اگر اعتبارسنجی نشده بود، با استفاده از NavigationManager، او را به صفحه‌ی لاگین هدایت می‌کنیم.
- در اینجا روش ارسال آدرس صفحه‌ی فعلی را نیز مشاهده می‌کنید. این امر سبب می‌شود تا پس از لاگین، کاربر مجددا به همین صفحه هدایت شود.

authenticationState، امکانات بیشتری را نیز در اختیار ما قرار می‌دهد؛ برای مثال با استفاده از متد ()authenticationState.User.IsInRole آن می‌توان دسترسی به قسمتی را بر اساس نقش‌های خاصی محدود کرد.


ثبت کاربر ادمین Identity

در ادامه می‌خواهیم دسترسی به کامپوننت‌های مختلف را بر اساس نقش‌ها، محدود کنیم. به همین جهت نیاز است تعدادی نقش و یک کاربر ادمین را به بانک اطلاعاتی برنامه اضافه کنیم. برای اینکار به پروژه‌ی BlazorServer.Common مراجعه کرده و تعدادی نقش ثابت را تعریف می‌کنیم:
namespace BlazorServer.Common
{
    public static class ConstantRoles
    {
        public const string Admin = nameof(Admin);
        public const string Customer = nameof(Customer);
        public const string Employee = nameof(Employee);
    }
}
علت قرار دادن این کلاس در پروژه‌ی Common، نیاز به دسترسی به آن در پروژه‌ی اصلی Blazor Server و همچنین در پروژه‌ی سرویس‌های برنامه‌است. فضای نام این کلاس را نیز در فایل imports.razor_ قرار می‌دهیم.

سپس در فایل BlazorServer.App\appsettings.json، مشخصات ابتدایی کاربر ادمین را ثبت می‌کنیم:
{
  "AdminUserSeed": {
    "UserName": "vahid@dntips.ir",
    "Password": "123@456#Pass",
    "Email": "vahid@dntips.ir"
  }
}
جهت دریافت strongly typed این تنظیمات در برنامه، کلاس معادل AdminUserSeed را به پروژه‌ی Models اضافه می‌کنیم:
namespace BlazorServer.Models
{
    public class AdminUserSeed
    {
        public string UserName { get; set; }
        public string Password { get; set; }
        public string Email { get; set; }
    }
}
که به صورت زیر در فایل BlazorServer.App\Startup.cs به سیستم تزریق وابستگی‌های برنامه معرفی می‌شود:
namespace BlazorServer.App
{
    public class Startup
    {

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddOptions<AdminUserSeed>().Bind(Configuration.GetSection("AdminUserSeed"));
            // ...
اکنون می‌توان سرویس افزودن نقش‌ها و کاربر ادمین را در پروژه‌ی BlazorServer.Services تکمیل کرد:
using System;
using System.Linq;
using System.Threading.Tasks;
using BlazorServer.Common;
using BlazorServer.DataAccess;
using BlazorServer.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;

namespace BlazorServer.Services
{
    public class IdentityDbInitializer : IIdentityDbInitializer
    {
        private readonly ApplicationDbContext _dbContext;
        private readonly UserManager<IdentityUser> _userManager;
        private readonly RoleManager<IdentityRole> _roleManager;
        private readonly IOptions<AdminUserSeed> _adminUserSeedOptions;

        public IdentityDbInitializer(
            ApplicationDbContext dbContext,
            UserManager<IdentityUser> userManager,
            RoleManager<IdentityRole> roleManager,
            IOptions<AdminUserSeed> adminUserSeedOptions)
        {
            _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
            _roleManager = roleManager ?? throw new ArgumentNullException(nameof(roleManager));
            _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager));
            _adminUserSeedOptions = adminUserSeedOptions ?? throw new ArgumentNullException(nameof(adminUserSeedOptions));
        }

        public async Task SeedDatabaseWithAdminUserAsync()
        {
            if (_dbContext.Roles.Any(role => role.Name == ConstantRoles.Admin))
            {
                return;
            }

            await _roleManager.CreateAsync(new IdentityRole(ConstantRoles.Admin));
            await _roleManager.CreateAsync(new IdentityRole(ConstantRoles.Customer));
            await _roleManager.CreateAsync(new IdentityRole(ConstantRoles.Employee));

            await _userManager.CreateAsync(
                new IdentityUser
                {
                    UserName = _adminUserSeedOptions.Value.UserName,
                    Email = _adminUserSeedOptions.Value.Email,
                    EmailConfirmed = true
                },
                _adminUserSeedOptions.Value.Password);

            var user = await _dbContext.Users.FirstAsync(u => u.Email == _adminUserSeedOptions.Value.Email);
            await _userManager.AddToRoleAsync(user, ConstantRoles.Admin);
        }
    }
}
این سرویس، با استفاده از دو سرویس توکار UserManager و RoleManager کتابخانه‌ی Identity، ابتدا سه نقش ادمین، مشتری و کارمند را ثبت می‌کند. سپس بر اساس اطلاعات AdminUserSeed تعریف شده، کاربر ادمین را ثبت می‌کند. البته این کاربر در این مرحله، یک کاربر معمولی بیشتر نیست. در مرحله‌ی بعد است که با انتساب نقش ادمین به او، می‌توان کاربر او را بر اساس این نقش ویژه، شناسایی کرد. کلاس‌های IdentityRole و IdentityUser، کلاس‌های پایه‌ی نقش‌ها و کاربران کتابخانه‌ی Identity هستند.

پس از تعریف این سرویس، نیاز است آن‌را به سیستم تزریق وابستگی‌های برنامه اضافه کرد:
namespace BlazorServer.App
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IIdentityDbInitializer, IdentityDbInitializer>();
            // ...
مرحله‌ی آخر، اعمال و اجرای سرویس IIdentityDbInitializer، در زمان آغاز برنامه‌است و محل توصیه شده‌ی آن، در متد Main برنامه‌ی اصلی، پیش از اجرای برنامه‌است. به همین جهت، نیاز است BlazorServer.DataAccess\Utils\MigrationHelpers.cs را به صورت زیر ایجاد کرد:
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Polly;

namespace BlazorServer.DataAccess.Utils
{
    public static class MigrationHelpers
    {
        public static void MigrateDbContext<TContext>(
                this IServiceProvider serviceProvider,
                Action<IServiceProvider> postMigrationAction
                ) where TContext : DbContext
        {
            using var scope = serviceProvider.CreateScope();
            var scopedServiceProvider = scope.ServiceProvider;
            var logger = scopedServiceProvider.GetRequiredService<ILogger<TContext>>();
            using var context = scopedServiceProvider.GetService<TContext>();

            logger.LogInformation($"Migrating the DB associated with the context {typeof(TContext).Name}");

            var retry = Policy.Handle<Exception>().WaitAndRetry(new[]
                {
                    TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(15)
                });

            retry.Execute(() =>
                {
                    context.Database.Migrate();
                    postMigrationAction(scopedServiceProvider);
                });

            logger.LogInformation($"Migrated the DB associated with the context {typeof(TContext).Name}");
        }
    }
}
در مورد این متد و استفاده از Polly جهت تکرار عملیات شکست خورده پیشتر در مطلب «اضافه کردن سعی مجدد به اجرای عملیات Migration در EF Core» بحث شده‌است.
کار متد الحاقی فوق، دریافت یک IServiceProvider است که به سرویس‌های اصلی برنامه اشاره می‌کند. سپس بر اساس آن، یک Scoped ServiceProvider را ایجاد می‌کند تا درون آن بتوان با Context برنامه در طی مدت کوتاهی کار کرد و در پایان آن، سرویس‌های ایجاد شده را Dispose کرد.
در این متد ابتدا Database.Migrate فراخوانی می‌شود تا اگر مرحله‌ای از Migrations برنامه هنوز به بانک اطلاعاتی اعمال نشده، کار اجرا و اعمال آن انجام شود. سپس یک متد سفارشی را از فراخوان دریافت کرده و اجرا می‌کند. برای مثال توسط آن می‌توان IIdentityDbInitializer در فایل BlazorServer.App\Program.cs به صوت زیر فراخوانی کرد:
public static void Main(string[] args)
{
    var host = CreateHostBuilder(args).Build();
    host.Services.MigrateDbContext<ApplicationDbContext>(
     scopedServiceProvider =>
            scopedServiceProvider.GetRequiredService<IIdentityDbInitializer>()
                                 .SeedDatabaseWithAdminUserAsync()
                                 .GetAwaiter()
                                 .GetResult()
    );
    host.Run();
}
تا اینجا اگر برنامه را اجرا کنیم، سه نقش پیش‌فرض، به بانک اطلاعاتی برنامه اضافه شده‌اند:


و همچنین کاربر پیش‌فرض سیستم را نیز می‌توان مشاهده کرد:


که نقش ادمین و کاربر پیش‌فرض، به این صورت به هم مرتبط شده‌اند (یک رابطه‌ی many-to-many برقرار است):



محدود کردن دسترسی کاربران بر اساس نقش‌ها

پس از ایجاد کاربر ادمین و تعریف نقش‌های پیش‌فرض، اکنون محدود کردن دسترسی به کامپوننت‌های برنامه بر اساس نقش‌ها، ساده‌است. برای این منظور فقط کافی است لیست نقش‌های مدنظر را که می‌توانند توسط کاما از هم جدا شوند، به ویژگی Authorize کامپوننت‌ها معرفی کرد:
@attribute [Authorize(Roles = ConstantRoles.Admin)]
و یا از طریق کدنویسی به صورت زیر نیز قابل اعمال است:
    protected override async Task OnInitializedAsync()
    {
        var authenticationState = await AuthenticationState;
        if (!authenticationState.User.Identity.IsAuthenticated ||
            !authenticationState.User.IsInRole(ConstantRoles.Admin))
        {
            var uri = new Uri(NavigationManager.Uri);
            NavigationManager.NavigateTo($"/identity/account/login?returnUrl={uri.LocalPath}");
        }


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-23.zip
پاسخ به بازخورد‌های پروژه‌ها
مشکل با نوشتن تابع تجمعی سفارشی(از طریق پیاده سازی IAggregateFunction)
متد printSummary فایل MainTableCellsEvent.cs باید به صورت زیر اصلاح شود:
// Lib/Core/PdfTable/MainTableCellsEvent.cs
private void printSummary(Rectangle position, PdfContentByte[] canvases)
        {
            var rowSummaryData = getRowSummaryData();
            if (string.IsNullOrEmpty(rowSummaryData)) return;

            var pdfContentByte = canvases[PdfPTable.TEXTCANVAS];            

            if (!_pdfRptCell.BasicProperties.RunDirection.HasValue)
                _pdfRptCell.BasicProperties.RunDirection = PdfRunDirection.LeftToRight;

            if (_pdfRptCell.BasicProperties.RunDirection == PdfRunDirection.RightToLeft)
                rowSummaryData = rowSummaryData.FixWeakCharacters();

            var phrase = _pdfRptCell.BasicProperties.PdfFont.FontSelector.Process(rowSummaryData.ToSafeString());
            ColumnText.ShowTextAligned(
                                    canvas: pdfContentByte,
                                    alignment: Element.ALIGN_CENTER,
                                    phrase: phrase,
                                    x: (position.Left + position.Right) / 2,
                                    y: ((position.Bottom + position.Top) / 2) - 4,
                                    rotation: 0,
                                    runDirection: (int)_pdfRptCell.BasicProperties.RunDirection,
                                    arabicOptions: 0);
        }
نظرات مطالب
شروع به کار با بوت استرپ 4
یک نکته‌ی تکمیلی: بررسی نگارش‌های ثالث راست به چپ بوت استرپ 4


1) MahdiMajidzadeh/bootstrap-v4-rtl
متاسفانه اصلا برای یک کار رسمی مناسب نیست و منوهای آن به هم ریخته‌است. list-group آن در حالت flush، کل عرض یک card را پر نمی‌کند و جداول آن نیز به همین صورت است. کامپوننت bread-crumb آن محل قرارگیری /‌های نامناسبی دارد. همچنین با آخرین نگارش بوت استرپ 4.1.3 سازگار نیست و از آن کمی عقب است و برای کار با آن، باید دقیقا همین بسته‌ی ثالث را دریافت و اضافه کنید و مستقل از خود بوت استرپ اصلی نیست. اما به همراه یک بسته‌ی npm مخصوص به خود است که یک مزیت به شمار می‌رود. مجوز آن، در مخزن کد Github آن ذکر نشده، اما در صفحه‌ی npm آن MIT ذکر شده‌است.
یک نمونه خروجی آن:


2) DediData/Bootstrap-RTL
به نظر یک پروژه‌ی خاتمه یافته‌است. با نگارش بوت استرپ 4.1.3 سازگار نیست و برای نگارش بتای آن تهیه شده‌است.


3) GhalamborM/bootstrap4-rtl
این پروژه، روش بهتری را نسبت به بسته‌های راست به چپ موجود، انتخاب کرده‌است. در اینجا شما بوت استرپ اصلی را با آخرین نگارش آن به صورت مستقل دریافت، نصب و تنظیم می‌کنید. سپس ذیل آن کلاس‌های راست به چپ این بسته‌ی ثالث را اضافه می‌کنید.
مجوز GPL، برای اینکار انتخاب شده‌است. متاسفانه یک چنین مجوزی در تضاد با مجوز MIT بوت استرپ اصلی است. مجوز GPL یعنی کار مشتق شده‌ی از آن نیز باید سورس باز شود و قابل استفاده‌ی در پروژه‌های تجاری غیر سورس باز نیست.
همچنین متاسفانه به صورت یک بسته‌ی npm نیز ارائه نشده‌است و باید آخرین نگارش آن‌را از GitHub به صورت مستقیم دریافت کنید.

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

 



4) PerseusTheGreat/bootstrap-4-rtl
روش راست به چپ سازی این نگارش نیز مانند حالت اولی است که بررسی شد و باید بسته‌ی مستقل آن‌را دریافت و استفاده کنید و به عنوان یک مکمل مطرح نیست. همچنین به همراه بسته‌ی npm نیز ارائه نشده‌است و تا این تاریخ، باید آخرین به روز رسانی‌های آن‌را از همان آدرس GitHub آن مستقیما دریافت کنید. البته مزیت آن، به روز رسانی هفتگی آن است. همچنین مجوز MIT این بسته را نیز تغییر نداده‌است.
خروجی آن با خروجی بسته‌ی سومی که معرفی شد، تقریبا یکی و مناسب است:



در کل اگر از مجوز GPL مورد سوم صرف نظر کنیم، به علت استقلال فایل CSS راست به چپ کننده‌ی آن از بسته‌ی اصلی بوت استرپ، انتخاب مناسب‌تری به نظر می‌رسد و خروجی قابل قبولی را نیز به همراه دارد. فقط ایکاش بسته‌ی npm ای نیز به پروژه‌ی آن اضافه شود.
به روز رسانی: تغییر مجوز به MIT و همچنین افزوده شدن بسته‌ی npm به مورد سوم صورت گرفته‌است:
npm install @ghalamborm/bootstrap4-rtl


پ.ن.
این روزها ارائه‌ی یک کتابخانه‌ی جاوا اسکریپتی و یا CSS ای بدون بسته‌ی npm متناظر با آن، ناقص به شمار می‌رود.
مطالب
چرا به الگوهای طراحی JavaScript نیاز داریم ؟
امروزه اهمیت یادگیری JavaScript بر هیچ کس پوشیده نیست ، API‌های جدید HTML 5 و امکانات جدید وب مثل Geo Location ، Offline storage ، Canvas ، Offline Web applications و ... همه نیاز به دانش JavaScript دارند. در نتیجه نیاز است علاوه بر یادگیری JavaScript به تمیز کد نوشتن جهت سهولت نگهداری آگاه بود.  همانطور که در کدهای سمت سرور مثل C# و یا PHP نیاز به استفاده از الگوهای طراحی (Design Patterns)است در JavaScript هم اوضاع به همین منوال است.
الگوی طراحی یک راه حل قابل استفاده مجدد است که برای حل مشکلات متداول در طراحی نرم افزار به کار می‌رود.
چرا به الگوی‌های طراحی JavaScript نیازمندیم ؟ 
  1. می خواهیم کد هایی با قابلیت استفاده‌ی مجدد بنویسیم ، استفاده از عملکرد‌های مشابه در سطح صفحات یک Web application یا چند Web Application.
  2. می خواهیم کد هایی با قابلیت نگهداری بنویسیم ، هر چه قدر در فاز توسعه کد‌های با کیفیت بنویسیم در فاز نگهداری از آن بهره می‌بریم. باید کد هایی بنویسیم که قابل Debug و خواندن توسط دیگر افراد تیم باشند.
  3. کدهای ما نباید با توابع و متغیر‌های دیگر پلاگین‌ها تداخل نامگزاری داشته باشند. در برنامه‌های امروزی بسیار مرسوم است که از پلاگین‌های Third party استفاده شود. می‌خواهیم با رعایت Encapsulation and modularization در کدهایمان از این تداخل جلوگیری کنیم.
معمولا کد‌های JavaScript که توسط اکثر ما نوشته می‌شود یک سری تابع پشت سرهم هست ، بدون هیچ کپسوله سازی : 
function getDate() {
    var now = new Date();
    var utc = now.getTime() + (now.getTimezoneOffset() * 60000);
    var est;
    est = new Date(utc + (3600000 * -4));
    return dateFormat(est, "dddd, mmmm dS, yyyy, h:MM:ss TT") + " EST";

}
function initiate_geolocationToTextbox() {
    navigator.geolocation.getCurrentPosition(handle_geolocation_queryToTextBox);
}

function handle_geolocation_queryToTextBox(position) {
    var longitude = position.coords.longitude;
    var latitude = position.coords.latitude;
    $("#IncidentLocation").val(latitude + " " + longitude);
}
به این روش کدنویسی Function Spaghetti Code گفته می‌شود که معایبی دارد : 
  • توابع و متغیر‌ها به Global scope برنامه افزوده می‌شوند.
  • کد Modular نیست.
  • احتمال رخ دادن Conflict در اسامی متغیر‌ها و توابع بالا می‌رود.
  • نگهداری کد به مرور زمان سخت می‌شود.
با شبیه سازی یک مثال مشکلات احتمالی را بررسی می‌کنیم : 
// file1.js
function saveState(obj) {
    // write code here to saveState of some object
    alert('file1 saveState');
}
// file2.js (remote team or some third party scripts)
function saveState(obj, obj2) {
     // further code...
    alert('file2 saveState");
}
همانطور که می‌بینید در این مثال در 2 فایل متفاوت در برنامه مان از 2 تابع با اسامی یکسان و امضای متفاوت استفاده کرده ایم . اگر فایل‌ها را اینگونه در برنامه آدرس دهی کنیم : 
<script src="file1.js" type="text/javascript"></script>
<script src="file2.js" type="text/javascript"></script>
متد saveState در فایلی که دیرتر آدرس داده شده (file2.js) ، متد saveState در file1.js را Override می‌کند ، در نتیجه عملکردی که از متد saveState در فایل اول انتظار داریم اتفاق نمی‌افتد.
 در پست بعدی به راه حل این مشکلات و کپسوله سازی خواهم پرداخت.
برای مطالعه‌ی بیشتر کتاب (Learning JavaScript Design Patterns) را از دست ندهید.
نظرات مطالب
بهبود صفحه‌‌ی بارگذاری اولیه در Blazor WASM
نمایش درصد بارگذاری اولیه‌ی یک برنامه‌ی Blazor WASM در دات نت 7

اگر یک برنامه‌ی جدید blazor wasm را در دات نت 7، برای مثال با دستور dotnet new blazorwasm --hosted ایجاد کنیم، با اجرای آن، یک progress-bar حلقوی نمایش میزان درصد بارگذاری اولیه‌ی برنامه ظاهر می‌شود که به نوعی پیاده سازی توکار نکات مطلب جاری است. این پیاده سازی از اجزای زیر تشکیل شده‌است:
الف) تغییرات فایل index.html برنامه
برای این منظور، فایل Client\wwwroot\index.html به صورت زیر تغییر کرده‌است:
<body>
    <div id="app">
        <svg class="loading-progress">
            <circle r="40%" cx="50%" cy="50%" />
            <circle r="40%" cx="50%" cy="50%" />
        </svg>
        <div class="loading-progress-text"></div>
    </div>
که در اینجا progress-bar حلقوی را با یک طرح SVG ایجاد کرده‌اند.

ب) تغییرات فایل app.css برنامه
کلاس‌های progress-bar را به این صورت در فایل Client\wwwroot\css\app.css اضافه کرده‌اند:
.loading-progress {
    position: relative;
    display: block;
    width: 8rem;
    height: 8rem;
    margin: 20vh auto 1rem auto;
}
    .loading-progress circle {
        fill: none;
        stroke: #e0e0e0;
        stroke-width: 0.6rem;
        transform-origin: 50% 50%;
        transform: rotate(-90deg);
    }
        .loading-progress circle:last-child {
            stroke: #1b6ec2;
            stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
            transition: stroke-dasharray 0.05s ease-in-out;
        }
.loading-progress-text {
    position: absolute;
    text-align: center;
    font-weight: bold;
    inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
}
    .loading-progress-text:after {
        content: var(--blazor-load-percentage-text, "Loading");
    }

روش سفارشی سازی این progress-bar بر اساس دو CSS variable فوق صورت می‌گیرد:
--blazor-load-percentage: درصد بارگذاری جاری را مقدار دهی می‌کند.
--blazor-load-percentage-text: متن Loading نمایش داده شده را مشخص می‌کند.

برای مثال اگر علاقمند باشیم بجای SVG پیش‌فرض از progress-bar توکار خود فریم‌ورک بوت‌استرپ استفاده کنیم، روش کار به صورت زیر خواهد بود:
<body>
    <div id="app">
      <div class="progress">
        <div class="progress-bar progress-bar-striped progress-bar-animated" 
            role="progressbar" aria-valuenow="75" aria-valuemin="0" aria-valuemax="100" 
            style="width: var(--blazor-load-percentage, 0%)">
            <div class="loading-text"></div>
        </div>        
      </div>
که در اینجا همان CSS variable معادل درصد بارگذاری، بجای width استفاده شده تا به صورت خودکار سبب پیشرفت progress-bar شود. همچنین کلاس جدید loading-text را هم همانند loading-progress-text:after موجود به صورت زیر به فایل app.css اضافه می‌کنیم تا سبب نمایش متن درصد پیشرفت جاری نیز شود:
.loading-text:after {
        content: var(--blazor-load-percentage-text, "Loading");
    }

مطالب
استفاده از قالب مخصوص Redux Toolkit جهت ایجاد پروژه‌های React/Redux
استفاده از Redux درون پروژه‌های React، به روش‌های مختلفی قابل انجام است؛ یعنی محدودیتی از لحاظ نحوه چیدمان فایل‌ها، تغییر state و نحوه‌ی dispatch کردن action وجود ندارد. به عبارتی این آزادی عمل را خواهیم داشت تا خودمان سیم‌کشی پروژه را انجام دهیم؛ ولی مشکل اصلی اینجاست که نمی‌توانیم مطمئن شویم روشی که پروژه را با آن ستاپ کرده‌ایم آیا به عنوان یک best-practice محسوب می‌شود یا خیر. در نهایت خروجی را که خواهیم داشت، حجم انبوهی از کدهای boilerplate و پکیج‌های زیادی است که در حین توسعه‌ی پروژه، به همراه Redux اضافه شده‌اند. همچنین در نهایت یک store پیچیده را خواهیم داشت که مدیریت آن به مراتب سخت خواهد شد. یک مشکل دیگر این است که روال گفته شده را باید به ازای هر پروژه‌ی جدید تکرار کنیم. برای حل این مشکل، یکی از maintainerهای اصلی تیم ریداکس، یک پروژه را تحت عنوان Redux Toolkit، مدتها قبل برای حل مشکلات عنوان شده شروع کرده است و این پکیج، جدیداً به قالب رسمی create-react-app اضافه شده است. که در واقع یک روش استاندارد و به اصطلاح opinionated برای ایجاد پروژه‌های ریداکسی می‌باشد و شامل تمامی وابستگی‌های موردنیاز برای کار با Redux از قبیل redux-thunk و همچنین Redux DevTools است. 

 ایجاد یک برنامه‌ی خالی React با قالب redux
در ادامه برای بررسی این قالب جدید، یک پروژه‌ی جدید React را ایجاد خواهیم کرد:
> npx create-react-app redux-template --template redux
> cd redux-template
> yarn start


بررسی ساختار پروژه‌ی ایجاد شده
ساختار پروژه‌ی ایجاد شده به صورت زیر است:


این ساختار خیلی شبیه به قالب پیش‌فرض create-react-app می‌باشد. همانطور که در تصویر فوق نیز مشاهده میکنید، پروژه‌ی ایجاد شده‌ی با قالب redux (تصویر سمت چپ)، یک فایل با نام store و همچنین یک دایرکتوری را به نام features دارد. اگر به فایل store.js مراجعه کنید، خواهید دید که تنظیمات اولیه‌ی ایجاد store را در قالب یک مثال Counter ایجاد کرده‌است:
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './features/counter/counterSlice';

export default configureStore({
  reducer: {
    counter: counterReducer,
  },
});
در کد فوق، نحوه‌ی ایجاد store، نسبت به حالت معمول، خیلی تمیزتر است. نکته‌ی جالب این است که به همراه کد فوق، Redux DevTools و همچنین redux-thunk هم از قبل تنظیم شده‌اند و در نتیجه، نیازی به تنظیم و نصب آنها نیست. تغییر مهم دیگر، پوشه‌ی features می‌باشد که یک روش رایج برای گروه‌بندی کامپوننت‌ها، همراه با فایل‌های وابسته‌ی آن‌ها است. درون این پوشه، یک پوشه جدید را تحت عنوان counter داریم که حاوی فایل‌های زیر می‌باشد: 
Counter
Counter.module.css
counterSlice.js
Counter.js، کامپوننتی است که در نهایت درون صفحه رندر خواهد شد. درون این فایل با استفاده از Redux Hooks کار اتصال به store و همچنین dispatch کردن اکشن‌ها انجام گرفته است:
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
  decrement,
  increment,
  incrementByAmount,
  selectCount,
} from './counterSlice';
import styles from './Counter.module.css';

export function Counter() {
  const count = useSelector(selectCount);
  const dispatch = useDispatch();
  const [incrementAmount, setIncrementAmount] = useState(2);

  return (
    <div>
      <div className={styles.row}>
        <button
          className={styles.button}
          aria-label="Increment value"
          onClick={() => dispatch(increment())}
        >
          +
        </button>
        <span className={styles.value}>{count}</span>
        <button
          className={styles.button}
          aria-label="Decrement value"
          onClick={() => dispatch(decrement())}
        >
          -
        </button>
      </div>
      <div className={styles.row}>
        <input
          className={styles.textbox}
          value={incrementAmount}
          onChange={e => setIncrementAmount(e.target.value)}
        />
        <button
          className={styles.button}
          onClick={() =>
            dispatch(
              incrementByAmount({ amount: Number(incrementAmount) || 0 })
            )
          }
        >
          Add Amount
        </button>
      </div>
    </div>
  );
}

فایل Counter.module.css نیز در واقع استایل‌های مربوط به کامپوننت فوق میباشد که به صورت CSS module اضافه شده‌است. در نهایت فایل counterSlice.js را داریم که  کار همان reducer سابق را برایمان انجام خواهد داد؛ اما اینبار با یک ساختار جدید و تحت عنوان slice. اگر به فایل عنوان شده مراجعه کنید، کدهای زیر را خواهید دید:
import { createSlice } from '@reduxjs/toolkit';

export const slice = createSlice({
  name: 'counter',
  initialState: {
    value: 0,
  },
  reducers: {
    increment: state => {
      // Redux Toolkit allows us to 'mutate' the state. It doesn't actually
      // mutate the state because it uses the immer library, which detects
      // changes to a "draft state" and produces a brand new immutable state
      // based off those changes
      state.value += 1;
    },
    decrement: state => {
      state.value -= 1;
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload.amount;
    },
  },
});

export const selectCount = state => state.counter.value;
export const { increment, decrement, incrementByAmount } = slice.actions;

export default slice.reducer;
در این قالب جدید، ترکیب این قطعات هستند که شیء اصلی یا در واقع همان state کلی پروژه را تشکیل خواهند داد. همانطور که مشاهده میکنید، برای ایجاد یک قطعه جدید، از تابع createSlice استفاده شده است. این تابع، تعدادی پارامتر را از ورودی دریافت می‌کند:
  • name: برای هر بخش از state، می‌توانیم یک نام را تعیین کنیم و این همان عنوانی خواهد بود که می‌توانید توسط Redux DevTools مشاهده کنید.
  • initialValue: در اینجا می‌توانیم مقادیر اولیه‌ای را برای این بخش از state، تعیین کنیم که در مثال فوق، value به مقدار صفر تنظیم شده‌است.
  • reducers: این قسمت محل تعریف actionهایی هستند که قرار است state را تغییر دهند. نکته جالب توجه این است که state در هر کدام از متدهای فوق، به ظاهر mutate شده است؛ اما همانطور که به صورت کامنت نیز نوشته‌است، در پشت صحنه از کتابخانه‌ای با عنوان immer استفاده می‌کند که در عمل بجای تغییر state اصلی، یک کپی از state جدید را جایگزین state قبلی خواهد کرد.
توسط selectCount نیز کار انتخاب مقدار موردنظر از state انجام شده‌است که معادل همان mapPropsToState است و در اینجا امکان دسترسی به state ذخیره شده در مخزن redux فراهم شده است. همچنین در خطوط پایانی فایل نیز اکشن‌ها برای دسترسی ساده‌تر به درون کامپوننت، به صورت Object Destructuring به بیرون export شده‌اند. در نهایت reducer نهایی را از slice ایجاد شده استخراج کرده‌ایم. این پراپرتی برای ایجاد store مورداستفاده قرار می‌گیرد.

چرا قالب Redux Toolkit از immer برای تغییر state استفاده میکند؟
همانطور که در این قسمت از سری Redux توضیح داده شد، اعمال تغییرات بر روی آرایه‌ها و اشیاء، باعث ایجاد ناخالصی خواهد شد؛ بنابراین به جای تغییر شیء اصلی، باید توسط یکی از روش‌های Object.assign و یا spread operator، یک clone از state اصلی را ایجاد کرده و آن را به عنوان state نهایی لحاظ کنیم. اما در حین کار با اشیاء nested، انجام اینکار سخت خواهد شد و همچنین خوانایی کد را نیز کاهش می‌دهد:
return {
    ...state,
    models: state.models.map(c =>
        c.model === action.payload.model
          ? {
              ...c,
              on: action.payload.toggle
            }
          : c
      )
  };
اما با کمک immer می‌توانیم به صورت مستقیم state را تغییر دهیم:
state.models.forEach(item => {
    if (item.model === action.payload.model) {
      item.on = action.payload.toggle;
    }
 });
کاری که immer انجام می‌دهد، تغییر یک شیء، به صورت مستقیم نیست؛ در واقع یک draftState را ایجاد خواهد کرد که در عمل یک proxy برای state فعلی می‌باشد. یعنی با mutate کردن state، یک شیء جدید را در نهایت clone خواهد کرد و به عنوان state نهایی برمی‌گرداند.
مطالب
Ajax.BeginForm و ارسال فایل به سرور در ASP.NET MVC
Ajax.BeginForm در ASP.NET MVC از jQuery Ajax برای ارسال مقادیر فرم، به سرور استفاده می‌کند. در این بین اگر یکی از عناصر فرم، المان ارسال فایل به سرور باشد، مقدار دریافتی در سمت سرور نال خواهد بود. مشکل اینجا است که نمی‌توان به کمک Ajax معمولی (یا به عبارتی XMLHttpRequest) فایلی را به سرور ارسال کرد. یا باید از سیلورلایت یا فلش استفاده نمود و یا از مرورگرهایی که XMLHttpRequest Level 2 را پشتیبانی می‌کنند (از IE 10 به بعد مثلا) که امکان Ajax upload توکار به همراه گزارش درصد آپلود را بدون نیاز به فلش یا سیلورلایت، دارند.
در این بین راه حل دیگری نیز وجود دارد که با تمام مرورگرها سازگار است؛ اما تنها گزارش درصد آپلود را توسط آن نخواهیم داشت. در اینجا به صورت پویا یک IFrame مخفی در صفحه تشکیل می‌شود، مقادیر معمولی فرم (تمام المان‌ها، منهای file) به صورت Ajax ایی به سرور ارسال خواهند شد. المان file آن در این IFrame مخفی، به صورت معمولی به سرور Postback می‌شود. البته کاربر در این بین چیزی را مشاهده یا احساس نخواهد کرد و تمام عملیات از دیدگاه او Ajax ایی به نظر می‌رسد. برای انجام اینکار تنها کافی است از افزونه‌ی AjaxFileUpload استفاده کنیم که در ادامه نحوه‌ی استفاده از آن‌را بررسی خواهیم کرد.


پیشنیازها

در ادامه فرض بر این است که افزونه‌ی AjaxFileUpload را دریافت کرده و به فایل Layout برنامه افزوده‌اید:
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title - My ASP.NET Application</title>
    <link href="~/Content/Site.css" rel="stylesheet" type="text/css" />
</head>
<body>
    <div>
        @RenderBody()
    </div>

    <script src="~/Scripts/jquery-1.11.1.min.js"></script>
    <script src="~/Scripts/jquery.unobtrusive-ajax.js"></script>
    <script src="~/Scripts/ajaxfileupload.js"></script>
    @RenderSection("Scripts", required: false)
</body>
</html>


مدل، کنترلر و View برنامه

مدل برنامه مشخصات یک محصول است:
namespace MVCAjaxFormUpload.Models
{
    public class Product
    {
        public int Id { set; get; }
        public string Name { set; get; }
    }
}

کنترلر آن از سه متد تشکیل شده‌است:
using System.Threading;
using System.Web;
using System.Web.Mvc;
using MVCAjaxFormUpload.Models;

namespace MVCAjaxFormUpload.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }

        [HttpPost]
        public ActionResult Index(Product product)
        {
            var isAjax = this.Request.IsAjaxRequest();

            return Json(new { result = "ok" }, JsonRequestBehavior.AllowGet);
        }

        [HttpPost]
        public ActionResult UploadFiles(HttpPostedFileBase image1, int id)
        {
            var isAjax = this.Request.IsAjaxRequest();
            Thread.Sleep(3000); //شبیه سازی عملیات طولانی
            return Json(new { FileName = "/Uploads/filename.ext" }, "text/html", JsonRequestBehavior.AllowGet);
        }
    }
}
Index اول، کار نمایش صفحه‌ی ارسال اطلاعات را انجام خواهد داد.
Index دوم کار پردازش Ajax ایی اطلاعات ارسالی به سرور را به عهده دارد. HttpPost آن Ajax ایی است.
متد UploadFiles، کار پردازش اطلاعات ارسالی از طرف IFrame مخفی را انجام می‌دهد. HttpPost آن معمولی است.

و کدهای View این مثال نیز به شرح زیر است:
@model MVCAjaxFormUpload.Models.Product
@{
    ViewBag.Title = "Index";
}

<h2>Ajax Form Upload</h2>

@using (Ajax.BeginForm(actionName: "Index",
                       controllerName: "Home",
                       ajaxOptions: new AjaxOptions { HttpMethod = "POST" },
                       routeValues: null,
                       htmlAttributes: new { id = "uploadForm" }))
{
    <label>Name:</label>
    @Html.TextBoxFor(model => model.Name)
    <br />
    <label>Image:</label>
    <br />
    <input type="file" name="Image1" id="Image1" />
    <br />
    <input type="submit" value="Submit" />
    <img id="loading" src="~/Content/Images/loading.gif" style="display:none;">
}

@section Scripts
{
    <script type="text/javascript">
        $(function () {
            $('#uploadForm').submit(function () {
                $("#loading").show();
                $.ajaxFileUpload({
                    url: "@Url.Action("UploadFiles", "Home")", // مسیری که باید فایل به آن ارسال شود
                    secureuri: false,
                    fileElementId: 'Image1', // آی دی المان ورودی فایل
                    dataType: 'json',
                    data: { id: 1, data: 'test' }, // اطلاعات اضافی در صورت نیاز
                    success: function (data, status) {
                        $("#loading").hide();
                        if (typeof (data.FileName) != 'undefined') {
                            alert(data.FileName);
                        }
                    },
                    error: function (data, status, e) {
                        $("#loading").hide();
                        alert(e);
                    }
                });
            });
        });
    </script>
}
فرمی که توسط Ajax.BeginForm تشکیل شده‌است، یک فرم معمولی Ajax ایی است و نکته‌ی جدیدی ندارد. تنها در آن یک المان ارسال فایل قرار گرفته‌است و همچنین Id آن‌را نیز جهت استفاده توسط jQuery مشخص کرده‌ایم.
در ادامه نحوه‌ی فعال سازی ajaxFileUpload را دقیقا در زمان submit فرم، مشاهده می‌کنید. در اینجا url آن به اکشن متدی که اطلاعات المان file را باید دریافت کند، اشاره می‌کند. fileElementId آن مساوی Id المان فایل فرم Ajax ایی صفحه‌است. از قسمت data جهت ارسال اطلاعات اضافه‌تری به اکشن متد UploadFiles استفاده می‌شود. سایر قسمت‌های آن نیز مشخص هستند. اگر عملیات موفقیت آمیز بود، success آن و اگر خیر، error آن اجرا می‌شوند.
فقط باید دقت داشت که content type دریافتی توسط آن باید text/html باشد، که این مورد در اکشن متدهای کنترلر مشخص هستند.
به این ترتیب دیگر کاربر نیازی ندارد ابتدا یکبار بر روی دکمه‌ی دومی کلیک کرده و فایل را ارسال کند و سپس بار دیگر بر روی دکمه‌ی submit فرم کلیک نماید. هر دو کار توسط یک دکمه انجام می‌شوند.

کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید
MVCAjaxFormUpload.zip
مطالب دوره‌ها
بوت استرپ (نگارش 3) چیست؟
بوت استرپ یک فریم ورک CSS واکنشگرا (responsive) است، که جهت ساخت سریع برنامه‌های استاتیک و همچنین پویای وب کاربرد دارد. در حال حاضر این پروژه جزو محبوب‌ترین و فعال‌ترین پروژه‌های سایت Github است. اگر علاقمند هستید که لیستی از سایت‌های استفاده کننده از بوت استرپ را مشاهده کنید، به آدرس‌های ذیل مراجعه نمائید:


تازه‌های بوت استرپ 3 کدامند؟

- بوت استرپ 3 جهت کار با صفحه‌های نمایش کوچک دستگاه‌های موبایل به شدت بهینه سازی شده است و به همین جهت به آن mobile-first CSS framework نیز می‌گویند.
- در نگارش 2 بوت استرپ، حداقل دو نوع گرید واکنشگرا و غیر واکنشگرا قابل تعریف بودند. در نگارش سوم آن، تنها یک نوع گرید جدید واکنشگرا در این فریم ورک وجود دارد که می‌تواند چهار نوع سایز از بزرگ تا کوچک را شامل شود.
- بوت استرپ 3 با IE7 به قبل و همچنین فایرفاکس 3.6 و پایین‌تر دیگر سازگار نیست. البته برای پشتیبانی از IE8، نیاز به اندکی تغییرات نیز وجود خواهد داشت که در قسمت‌های بعد این جزئیات را بیشتر بررسی خواهیم کرد. به عبارت دیگر بدون این تغییرات، بوت استرپ 3 در حالت پیش فرض با IE9 به بعد سازگار است.
- در بوت استرپ 3 برخلاف نگارش قبلی آن که لیستی از آیکن‌های خود را در قالب چند فایل PNG image sprite که آیکن‌ها را به صورت فشرده در کنار هم قرار داده بود، اینبار تنها از Font icons استفاده می‌کند. به این ترتیب تغییر اندازه این آیکن‌ها با توجه به برداری بودن نمایش قلم‌ها و همچنین قابلیت اعمال رنگ به آن‌ها نیز بسیار ساده‌تر می‌گردد.


سؤال: آیا نیاز است از یک فریم ورک CSS واکنشگرا استفاده شود؟

در سال‌های قبل، عموما طراحی وب بر اساس تهیه یا خرید یک سری قالب‌های از پیش آماده شده، شکیل صورت می‌گرفته‌است. این قالب‌ها به سرعت با برنامه، یکپارچه شده و حداکثر قلم یا رنگ‌های آن‌ها‌را اندکی تغییر می‌دادیم و یا اینکه خودمان کل این مسیر را از صفر طی می‌کردیم. این پروسه سفارشی، بسیار سنگین بوده و مشکل مهم آن، عدم امکان استفاده مجدد از طراحی‌های انجام شده می‌باشد که نهایتا در دراز مدت هزینه‌ی بالایی را برای ما به همراه خواهند داشت. اما با استفاده از فریم ورک‌های CSS واکنشگرا به این مزایا خواهیم رسید:
- قسمت عمده‌ای از کار پیشتر برای شما انجام شده است.
برای مثال نیازی نیست تا حتما برای طرحبندی صفحه، سیستم گرید خاص خودتان را طراحی کنید و یا اینکه مانند سال‌های دور، به استفاده از HTML tables پناه ببرید.
- قابلیت سفارشی سازی بسیار بالایی دارند.
برای مثال با استفاده از فناوری‌هایی مانند less می‌توان بوت استرپ را تا حد بسیار زیادی سفارشی سازی کرد. به این ترتیب دیگر یک سایت بوت استرپ، شبیه به بوت استرپ به نظر نخواهد رسید! شاید عده‌ای عنوان کنند که تمام سایت‌های بوت استرپ یک شکل هستند، اما واقعیت این است که این سایت‌ها تنها از قابلیت‌های سفارشی سازی بوت استرپ و less استفاده نکرده‌اند.
 

دریافت بوت استرپ 3

سایت رسمی دریافت بوت استرپ، آدرس ذیل می‌باشد:

البته ما از این نگارش خام استفاده نخواهیم کرد و نیاز است برای کارهای سایت‌های فارسی، از نگارش راست به چپ آن استفاده کنیم. بنابراین اگر از ویژوال استودیو استفاده می‌کنید، می‌توانید به یکی از دو بسته نیوگت ذیل مراجعه نمائید:
و اگر می‌خواهید صرفا به فایل‌های درون این بسته‌ها دسترسی پیدا کنید، از دو آدرس ذیل استفاده کنید:
فایل‌های دریافت شده با پسوند nupkg، در حقیقت یک فایل zip استاندارد هستند.

اگر بوت استرپ اصل را از سایت اصلی آن دریافت کنید، شامل تعداد فایل‌ها و پوشه‌های بسیار بیشتری است نسبت به نمونه RTL فوق. اما فایل‌های نهایی آن که مورد استفاده قرار خواهند گرفت، درون پوشه dist یا توزیع آن قرار گرفته‌اند و آنچنان تفاوتی با نگارش RTL ندارند. فقط در نگارش اصل، فایل‌های min و فشرده شده نیز همراه این بسته هستند که در نگارش RTL لحاظ نشده‌اند. این موضوع در آینده به نفع ما خواهد بود. از این لحاظ که اگر از سیستم bundling & minification مربوط بهASP.NET  استفاده کنید (جهت تولید خودکار فایل‌های min در زمان اجرا)، این سیستم به صورت پیش فرض از فایل‌های min موجود استفاده می‌کند و ممکن است مدتی سردرگم باشید که چرا تغییراتی را که به فایل CSS بوت استرپ اعمال کرده‌ام، در سایت اعمال نمی‌شوند. به علاوه امکان اعمال تغییرات و حتی دیباگ فایل‌های غیرفشرده خصوصا جاوا اسکریپتی آن نیز بسیار ساده‌تر و مفهوم‌تر است.

جهت مطالعه مباحث تکمیلی در مورد نحوه فشرده سازی فایل‌های CSS یا JS می‌توانید به مقالات ذیل، در سایت جاری مراجعه نمائید:

علاوه بر این‌ها در نگارش سوم بوت استرپ، تعدادی فایل CSS جدید به نام قالب یا theme نیز اضافه شده‌اند که همراه نسخه RTL نیست. برای مثال اگر به پوشه bootstrap-3.0.0.zip\bootstrap-3.0.0\dist\css مراجعه کنید، فایل bootstrap-theme.css نیز قابل مشاهده است. به این ترتیب قالبی و لایه‌ای بر روی مقادیر پیش فرض موجود در فایل bootstrap.css اعمال خواهند شد؛ برای مثال اعمال طراحی تخت یا flat مدرن آن به دکمه‌ها و عناصر دیگر این مجموعه.


شروع یک فایل HTML با بوت استرپ

تا اینجا فرض بر این است که فایل‌های بوت استرپ را دریافت کرده‌اید. در ادامه قصد داریم، نحوه معرفی این فایل‌ها را در یک فایل ساده HTML بررسی کنیم.
<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My Website</title>
    <link href="Content/css/bootstrap-rtl.css" rel="stylesheet">    
<link href="Content/css/custom.css" rel="stylesheet">    
</head>
<body>


</body>
</html>
صفحه آغازین کار با بوت استرپ 3 یک چنین شکلی را خواهد داشت و می‌تواند پایه تشکیل فایل masterpage یا layout برنامه‌های ASP.NET قرار گیرد. متا تگ viewport اضافه شده، جهت طراحی‌های واکنشگرا اضافه شده است و در ادامه لینک شدن فایل CSS بوت استرپ 3 را ملاحظه می‌کنید.
اگر سایت شما از تعاریف CSS سفارشی دیگری نیز استفاده می‌کند، تعاریف آن‌ها باید پس از بوت استرپ، ذکر گردند.


افزودن اسکریپت‌های بوت استرپ 3

برای کار با اسکریپت‌های بوت استرپ 3 نیاز است ابتدا jQuery را به صورت جداگانه دریافت کنیم. در حال حاضر اگر به سایت جی‌کوئری مراجعه کنید با دو نگارش 1.x و 2.x این کتابخانه مواجه خواهید شد. اگر نیاز به پشتیبانی از IE 8 را در محل کار خود دارید، باید از نگارش 1.x استفاده کنید. نگارش آخر 1.x کتابخانه جی‌کوئری را از طریق CDN آن همواره می‌توان مورد استفاده قرار داد:
 <script src="http://code.jquery.com/jquery-latest.min.js"></script>
بهتر است تعاریف فایل‌های جاوا اسکریپت را پیش از بسته شدن تگ body قرار دهید. یکی از مزایای مهم آن مشاهده نشدن یک فلش کوتاه مدت سفید رنگ در ابتدای بارگذاری صفحاتی با پس زمینه غیر روشن است. از این جهت که هر المانی که در head صفحه تعریف شود، حتما باید پیش از بارگذاری کل صفحه دریافت گردد. به این ترتیب با سرعت‌های دریافت کمتر، این مساله سبب خالی ماندن صفحه برای مدتی کوتاه خواهد شد و همان فلش سفید رنگ عنوان شده را پدید می‌آورد؛ چون هنوز مابقی صفحه بارگذاری نشده و خالی است.
پس از تعریف جی‌کوئری، تعریف اسکریپت‌های بوت استرپ قرار می‌گیرد (چون وابسته است به جی‌کوئری). فایل bootstrap-rtl.js شامل تمام زیر فایل‌های مورد نیاز نیز می‌باشد:
 <script src="Scripts/bootstrap-rtl.js"></script>
برای سازگار سازی بوت استرپ 3 با IE8 نیاز به یک فایل اسکریپت دیگر نیز داریم. این فایل را از آدرس ذیل دریافت نمائید:
این فایل 4 کیلوبایتی را نیز باید به تعاریف اسکریپت‌های مورد نیاز، اضافه کرد:
 <script src="Scripts/respond.min.js"></script>
البته این اسکریپت خاص، مطابق توضیحات آن باید به head صفحه اضافه شود تا با IE8 بهتر کار کند.
تا اینجا ساختار صفحه HTML تهیه شده جهت استفاده از امکانات بوت استرپ 3، شکل زیر را خواهد داشت:
<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My Website</title>

    <link href="Content/css/bootstrap-rtl.css" rel="stylesheet">    
<link href="Content/css/custom.css" rel="stylesheet">       
<script src="Scripts/respond.min.js"></script>
</head>
<body>


<script src="http://code.jquery.com/jquery-latest.min.js"></script>
<script src="Scripts/bootstrap-rtl.js"></script>
</body>
</html>

فایل‌های نهایی این قسمت را از اینجا نیز می‌توانید دریافت کنید:
bs3-sample01.zip
 
مطالب
تخته وایت برد آنلاین توسط SignalR
همانطور که در دوره SignalR سایت نیز مطرح شده‌است : "یکی از کاربردهای جالب SignalR می‌تواند به روز رسانی مداوم صفحه نمایش کاربران، توسط اطلاعات ارسالی از طرف سرور باشد." در ادامه میخواهیم به طراحی یک "تخته وایت برد" آنلاین بپردازیم.
در این پروژه برای ترسم خطوط بر روی صفحه از Canvas در  HTML5 استفاده میشود.

پیشنیازها :
پیشنیازهای این مطلب با مطلب «مثال - نمایش درصد پیشرفت عملیات توسط SignalR» یکی است. برای مثال، نحوه دریافت وابستگی‌ها، تنظیمات فایل global.asax و افزودن اسکریپت‌ها، تفاوتی با مقاله‌ی ذکر شده ندارد و تنها تعدادی اسکریپت و CSS جهت زیبایی کار افزوده شده است :
<link href="Styles/bootstrap.min.css" rel="stylesheet" />

//برای نمایش و استفاده از جعبه‌ی رنگ در بوت استراپ
<link href="Styles/bootstrap-colorpalette.css" rel="stylesheet" />
<script src="Scripts/jquery-1.8.2.js"></script>
<script type="text/javascript" src='Scripts/jquery.signalR-1.1.3.js'></script>
<script src="Scripts/bootstrap.min.js"></script>
<script src="Scripts/bootstrap-colorpalette.js"></script>
<script type="text/javascript" src='<%: ResolveClientUrl("~/signalr/hubs") %>'></script>

//حاوی متدهایی برای رسم خط با استفاده از HTML5
<script src="Scripts/draw.js" type="text/javascript"></script>

//تعریف کلاینت‌های متصل به هاب
<script src="Scripts/Whiteboard.js"></script>

  تعریف کلاس Hub برنامه :

using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
using OnlineSignalRWhiteboard.Model;

namespace OnlineSignalRWhiteboard.Hubs
{
    [HubName("onWhiteboard")]
    public class Whiteboard : Hub
    {
        public void OnDrawPen(Point prev, Point current, string color, int width)
        {
            Clients.All.drawPen(prev, current, color, width);
        }

        public void ClearBoard()
        {
            Clients.All.clear();
        }

    }
}
در ابتدا توسط ویژگی HubName یک نام مشخص برای هاب خود انتخاب کرده ایم. سپس متدی به نام OnDrawPen تعریف شده است که پس از فراخوانی از سمت کلاینت، مقادیر دریافتی خود را با صدا زدن متدی در سمت کلاینت به نام drawPen  ، به تمام کلاینت‌ها ارسال و نقاط مربوطه در سمت کلاینت، بر روی صفحه رسم میشوند.
متد دیگری به نام ClearBoard نیز تعریف شده است که روال رخدادگردان clear در سمت کلاینت را فراخوانی میکند و باعث پاک شدن صفحه نمایش میشود.

کدهای کلاینت‌های متصل به هاب در فایل Whiteboard.js 
 :
$(function () {
    $.connection.hub.logging = true;
    var whiteboard = $.connection.onWhiteboard;

    $("#clear").click(function () {
        //پاک کردن صفحه نمایش
        whiteboard.server.clearBoard();
    });

    var color = function (colors) {
        //تغییر رنگ قلم
        draw.colour = colors;
    };

    $(".size").click(function () {
        //تغییر سایز قلم
        draw.lineWidth = $(this).height();
        $('#size').css('height', $(this).height());
    });

    $.connection.hub.start().done(function () {
    })
    .fail(function () {
        alert("Could not Connect!");
     });

    draw.onDraw = function (prev, current, color, width) {
        //ارسال پارامترها به سمت سرور تا سرور بتواند داده‌ها را به سمت سایر کلاینت‌ها نیز ارسال کند
        whiteboard.server.onDrawPen(prev, current, color, width);
    };

    whiteboard.client.drawPen = function (prev, current, color, width) {
        draw.drawPen(prev, current, color, width);
    };

    whiteboard.client.clear = function () {
        draw.clear();
    };

    $('#colorpalette3').colorPalette()
      .on('selectColor', function (e) {
          $('#color').css('background-color', e.color);
          color(e.color);
      });

    var canvas = document.getElementById('draw');
    //تغییر سایر کانواس متناسب با پنجره‌ی مرورگر
    window.addEventListener('resize', resizeCanvas, false);

    function resizeCanvas() {
        canvas.width = window.innerWidth - 20;
        canvas.height = window.innerHeight - 70;
    }
    resizeCanvas();

});
در این قطعه کد ابتدا ارجاعی به هاب مشخص شده است و سپس روال‌های رخداد گردانی مانند whiteboard.client.drawPen و whiteboard.client.clear تعریف شده اند که به ترتیب متصل هستند به فراخوانی های Clients.All.drawPen و  Clients.All.clear در سمت سرور.
همچنین یک متد به نام draw.onDraw تعریف شده است که وظیفه‌ی آن ارسال اطاعاتی نظیر موقعیت موس در صفحه، رنگ، اندازه و ...  به سمت متدی به نام onDrawPen در هاب است.

کدهای کامل سمت کلاینت :
<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
    <link href="Styles/bootstrap.min.css" rel="stylesheet" />
    <link href="Styles/bootstrap-colorpalette.css" rel="stylesheet" />
    <script src="Scripts/jquery-1.8.2.js"></script>
    <script type="text/javascript" src='Scripts/jquery.signalR-1.1.3.js'></script>
    <script src="Scripts/bootstrap.min.js"></script>
    <script src="Scripts/bootstrap-colorpalette.js"></script>
    <script type="text/javascript" src='<%: ResolveClientUrl("~/signalr/hubs") %>'></script>
    <script src="Scripts/draw.js" type="text/javascript"></script>
    <script src="Scripts/Whiteboard.js"></script>
</head>
<body>
    <form id="form1" runat="server">
        <div>
            <div style="position: static;">
              <div>
                <div>
                  <a data-toggle="collapse" data-target=".navbar-inverse-collapse">
                    <span></span>
                    <span></span>
                    <span></span>
                  </a>
                  <a href="#">تخته وایت برد آنلاین توسط SignalR و HTML5</a>
                  <div>
                    <ul>
                      <li>
                          <div>
                              <a id="selected-color2" data-toggle="dropdown">
                                  <div style="width: 20px; height: 20px; background: black" id="color">
                                  </div>
                              </a>
                              <ul style="width:293px;">
                                <li style="display:inline-block;">
                                  <div>رنگ پس زمینه</div>
                                  <div id="colorpalette3"></div>
                                </li>
                              </ul>
                          </div>
                      </li>
                      <li></li>
                      <li>
                          <div>
                              <a data-toggle="dropdown" style="width: 120px; height: 20px" href="#">
                                <hr style="width: 120px; height: 1px; background-color: black;margin: 0px; display: table-cell" id="size">
                              </a>
                              <ul style="width: 120px">
                                <li><hr style="width: 140px; height: 1px; background-color: black"></li>
                                  <li></li>
                                <li><hr style="width: 140px; height: 3px; background-color: black"></li>
                                  <li></li>
                                <li><hr style="width: 140px; height: 7px; background-color: black"></li>
                                  <li></li>
                                <li><hr style="width: 140px; height: 10px; background-color: black"></li>
                                  <li></li>
                                <li><hr style="width: 140px; height: 20px; background-color: black"></li>
                              </ul>
                            </div>
                      </li>
                      <li></li>
                      <li>
                          <div>
                            <a href="#" id="clear"><i></i>جدید</a>
                          </div>
                      </li>
                    </ul>
                  </div><!-- /.nav-collapse -->
                </div>
              </div><!-- /navbar-inner -->
            </div>
        </div>
        
        <div>
            <div>
                 <canvas id="draw" style="cursor: crosshair;border: 1px solid black;"></canvas>
            </div>
       </div>
    </form>
</body>
</html>
در ابتدا یک دکمه برای تغییر رنگ قلم و یک لیست بازشو برای تعیین اندازه‌ی قلم و همچنین یک دکمه برای پاک کردن صفحه نمایش قرار داده شده است. سپس یک Canvas به نام draw ایجاد کرده ایم که بتوانیم خطوط خود را بر روی آن رسم کنیم.

مشاهده نسخه نمایشی 
کدهای کامل این مثال را میتوانید از اینجا دانلود کنید : OnlineSignalRWhiteboard.zip  
و همچنین میتوانید نمونه‌ی بهینه شده‌ی آن را نیز از این قسمت دانلود کنید :  OnlineCustomSignalRWhiteboard.zip